1const t = require('tap')
2const fs = require('fs/promises')
3const { resolve, basename } = require('path')
4const _mockNpm = require('../../fixtures/mock-npm')
5const { cleanTime } = require('../../fixtures/clean-snapshot')
6
7t.cleanSnapshot = cleanTime
8
9const mockNpm = async (t, { noLog, libnpmexec, initPackageJson, ...opts } = {}) => {
10  const res = await _mockNpm(t, {
11    ...opts,
12    mocks: {
13      ...(libnpmexec ? { libnpmexec } : {}),
14      ...(initPackageJson ? { 'init-package-json': initPackageJson } : {}),
15    },
16    globals: {
17      // init-package-json prints directly to console.log
18      // this avoids poluting test output with those logs
19      ...(noLog ? { 'console.log': () => {} } : {}),
20    },
21  })
22
23  return res
24}
25
26t.test('displays output', async t => {
27  const { npm, joinedOutput } = await mockNpm(t, {
28    initPackageJson: async () => {},
29  })
30
31  await npm.exec('init', [])
32  t.matchSnapshot(joinedOutput(), 'displays helper info')
33})
34
35t.test('classic npm init -y', async t => {
36  const { npm, prefix } = await mockNpm(t, {
37    config: { yes: true },
38    noLog: true,
39  })
40
41  await npm.exec('init', [])
42
43  const pkg = require(resolve(prefix, 'package.json'))
44  t.equal(pkg.version, '1.0.0')
45  t.equal(pkg.license, 'ISC')
46})
47
48t.test('classic interactive npm init', async t => {
49  t.plan(1)
50
51  const { npm } = await mockNpm(t, {
52    initPackageJson: async (path) => {
53      t.equal(
54        path,
55        resolve(npm.localPrefix),
56        'should start init package.json in expected path'
57      )
58    },
59  })
60
61  await npm.exec('init', [])
62})
63
64t.test('npm init <arg>', async t => {
65  t.plan(1)
66
67  const { npm } = await mockNpm(t, {
68    libnpmexec: ({ args }) => {
69      t.same(
70        args,
71        ['create-react-app@*'],
72        'should npx with listed packages'
73      )
74    },
75  })
76
77  await npm.exec('init', ['react-app'])
78})
79
80t.test('npm init <arg> -- other-args', async t => {
81  t.plan(1)
82
83  const { npm } = await mockNpm(t, {
84    libnpmexec: ({ args }) => {
85      t.same(
86        args,
87        ['create-react-app@*', 'my-path', '--some-option', 'some-value'],
88        'should npm exec with expected args'
89      )
90    },
91
92  })
93
94  await npm.exec('init', ['react-app', 'my-path', '--some-option', 'some-value'])
95})
96
97t.test('npm init @scope/name', async t => {
98  t.plan(1)
99
100  const { npm } = await mockNpm(t, {
101    libnpmexec: ({ args }) => {
102      t.same(
103        args,
104        ['@npmcli/create-something@*'],
105        'should npx with scoped packages'
106      )
107    },
108  })
109
110  await npm.exec('init', ['@npmcli/something'])
111})
112
113t.test('npm init @scope@spec', async t => {
114  t.plan(1)
115
116  const { npm } = await mockNpm(t, {
117    libnpmexec: ({ args }) => {
118      t.same(
119        args,
120        ['@npmcli/create@foo'],
121        'should npx with scoped packages'
122      )
123    },
124  })
125
126  await npm.exec('init', ['@npmcli@foo'])
127})
128
129t.test('npm init @scope/name@spec', async t => {
130  t.plan(1)
131
132  const { npm } = await mockNpm(t, {
133    libnpmexec: ({ args }) => {
134      t.same(
135        args,
136        ['@npmcli/create-something@foo'],
137        'should npx with scoped packages'
138      )
139    },
140  })
141
142  await npm.exec('init', ['@npmcli/something@foo'])
143})
144
145t.test('npm init git spec', async t => {
146  t.plan(1)
147  const { npm } = await mockNpm(t, {
148    libnpmexec: ({ args }) => {
149      t.same(
150        args,
151        ['npm/create-something'],
152        'should npx with git-spec packages'
153      )
154    },
155  })
156
157  await npm.exec('init', ['npm/something'])
158})
159
160t.test('npm init @scope', async t => {
161  t.plan(1)
162
163  const { npm } = await mockNpm(t, {
164    libnpmexec: ({ args }) => {
165      t.same(
166        args,
167        ['@npmcli/create'],
168        'should npx with @scope/create pkgs'
169      )
170    },
171  })
172
173  await npm.exec('init', ['@npmcli'])
174})
175
176t.test('npm init tgz', async t => {
177  const { npm } = await mockNpm(t)
178
179  await t.rejects(
180    npm.exec('init', ['something.tgz']),
181    /Unrecognized initializer: something.tgz/,
182    'should throw error when using an unsupported spec'
183  )
184})
185
186t.test('npm init <arg>@next', async t => {
187  t.plan(1)
188
189  const { npm } = await mockNpm(t, {
190    libnpmexec: ({ args }) => {
191      t.same(
192        args,
193        ['create-something@next'],
194        'should npx with something@next'
195      )
196    },
197  })
198
199  await npm.exec('init', ['something@next'])
200})
201
202t.test('npm init exec error', async t => {
203  const { npm } = await mockNpm(t, {
204    libnpmexec: async () => {
205      throw new Error('ERROR')
206    },
207  })
208
209  await t.rejects(
210    npm.exec('init', ['something@next']),
211    /ERROR/,
212    'should exit with exec error'
213  )
214})
215
216t.test('should not rewrite flatOptions', async t => {
217  t.plan(1)
218
219  const { npm } = await mockNpm(t, {
220    libnpmexec: async ({ args }) => {
221      t.same(
222        args,
223        ['create-react-app@*', 'my-app'],
224        'should npx with extra args'
225      )
226    },
227  })
228
229  await npm.exec('init', ['react-app', 'my-app'])
230})
231
232t.test('npm init cancel', async t => {
233  const { npm, logs } = await mockNpm(t, {
234    initPackageJson: async () => {
235      throw new Error('canceled')
236    },
237  })
238
239  await npm.exec('init', [])
240
241  t.equal(logs.warn[0][0], 'init', 'should have init title')
242  t.equal(logs.warn[0][1], 'canceled', 'should log canceled')
243})
244
245t.test('npm init error', async t => {
246  const { npm } = await mockNpm(t, {
247    initPackageJson: async () => {
248      throw new Error('Unknown Error')
249    },
250  })
251
252  await t.rejects(
253    npm.exec('init', []),
254    /Unknown Error/,
255    'should throw error'
256  )
257})
258
259t.test('workspaces', async t => {
260  await t.test('no args -- yes', async t => {
261    const { npm, prefix, joinedOutput } = await mockNpm(t, {
262      prefixDir: {
263        'package.json': JSON.stringify({
264          name: 'top-level',
265        }),
266      },
267      config: { workspace: 'a', yes: true },
268      noLog: true,
269    })
270
271    await npm.exec('init', [])
272
273    const pkg = require(resolve(prefix, 'a/package.json'))
274    t.equal(pkg.name, 'a')
275    t.equal(pkg.version, '1.0.0')
276    t.equal(pkg.license, 'ISC')
277
278    t.matchSnapshot(joinedOutput(), 'should print helper info')
279
280    const lock = require(resolve(prefix, 'package-lock.json'))
281    t.ok(lock.packages.a)
282  })
283
284  await t.test('no args, existing folder', async t => {
285    const { npm, prefix } = await mockNpm(t, {
286      prefixDir: {
287        packages: {
288          a: {
289            'package.json': JSON.stringify({
290              name: 'a',
291              version: '2.0.0',
292            }),
293          },
294        },
295        'package.json': JSON.stringify({
296          name: 'top-level',
297          workspaces: ['packages/a'],
298        }),
299      },
300      config: { workspace: 'packages/a', yes: true },
301      noLog: true,
302    })
303
304    await npm.exec('init', [])
305
306    const pkg = require(resolve(prefix, 'packages/a/package.json'))
307    t.equal(pkg.name, 'a')
308    t.equal(pkg.version, '2.0.0')
309    t.equal(pkg.license, 'ISC')
310  })
311
312  await t.test('fail parsing top-level package.json to set workspace', async t => {
313    const { npm } = await mockNpm(t, {
314      prefixDir: {
315        'package.json': 'not json[',
316      },
317      config: { workspace: 'a', yes: true },
318      noLog: true,
319    })
320
321    await t.rejects(
322      npm.exec('init', []),
323      { code: 'EJSONPARSE' }
324    )
325  })
326
327  await t.test('missing top-level package.json when settting workspace', async t => {
328    const { npm, logs } = await mockNpm(t, {
329      config: { workspace: 'a' },
330    })
331
332    await t.rejects(
333      npm.exec('init', []),
334      { code: 'ENOENT' },
335      'should exit with missing package.json file error'
336    )
337
338    t.equal(logs.warn[0][0], 'Missing package.json. Try with `--include-workspace-root`.')
339  })
340
341  await t.test('bad package.json when settting workspace', async t => {
342    const { npm, logs } = await mockNpm(t, {
343      prefixDir: {
344        'package.json': '{{{{',
345      },
346      config: { workspace: 'a' },
347    })
348
349    await t.rejects(
350      npm.exec('init', []),
351      { code: 'EJSONPARSE' },
352      'should exit with parse file error'
353    )
354
355    t.strictSame(logs.warn, [])
356  })
357
358  await t.test('using args - no package.json', async t => {
359    const { npm, prefix } = await mockNpm(t, {
360      prefixDir: {
361        b: {
362          'package.json': JSON.stringify({
363            name: 'b',
364          }),
365        },
366        'package.json': JSON.stringify({
367          name: 'top-level',
368          workspaces: ['b'],
369        }),
370      },
371      // Important: exec did not write a package.json here
372      libnpmexec: async () => {},
373      config: { workspace: 'a', yes: true },
374    })
375
376    await npm.exec('init', ['react-app'])
377
378    const pkg = require(resolve(prefix, 'package.json'))
379    t.strictSame(pkg.workspaces, ['b'], 'pkg workspaces did not get updated')
380  })
381
382  await t.test('init template - bad package.json', async t => {
383    const { npm, prefix } = await mockNpm(t, {
384      prefixDir: {
385        b: {
386          'package.json': JSON.stringify({
387            name: 'b',
388          }),
389        },
390        'package.json': JSON.stringify({
391          name: 'top-level',
392          workspaces: ['b'],
393        }),
394      },
395      initPackageJson: async (...args) => {
396        const [dir] = args
397        if (dir.endsWith('c')) {
398          await fs.writeFile(resolve(dir, 'package.json'), JSON.stringify({
399            name: basename(dir),
400          }), 'utf-8')
401        }
402      },
403      config: { yes: true, workspace: ['a', 'c'] },
404    })
405
406    await npm.exec('init', [])
407
408    const pkg = require(resolve(prefix, 'package.json'))
409    t.strictSame(pkg.workspaces, ['b', 'c'])
410
411    const lock = require(resolve(prefix, 'package-lock.json'))
412    t.notOk(lock.packages.a)
413  })
414
415  t.test('workspace root', async t => {
416    const { npm } = await mockNpm(t, {
417      config: { workspace: 'packages/a', 'include-workspace-root': true, yes: true },
418      noLog: true,
419    })
420
421    await npm.exec('init', [])
422
423    const pkg = require(resolve(npm.localPrefix, 'package.json'))
424    t.equal(pkg.version, '1.0.0')
425    t.equal(pkg.license, 'ISC')
426    t.strictSame(pkg.workspaces, ['packages/a'])
427
428    const ws = require(resolve(npm.localPrefix, 'packages/a/package.json'))
429    t.equal(ws.version, '1.0.0')
430    t.equal(ws.license, 'ISC')
431  })
432})
433