1const t = require('tap')
2const { resolve } = require('path')
3const mockNpm = require('../../fixtures/mock-npm.js')
4
5const mockExplain = async (t, opts) => {
6  const mock = await mockNpm(t, {
7    command: 'explain',
8    mocks: {
9      // keep the snapshots pared down a bit, since this has its own tests.
10      '{LIB}/utils/explain-dep.js': {
11        explainNode: (expl, depth, chalk) => {
12          const color = chalk.level !== 0
13          return `${expl.name}@${expl.version} depth=${depth} color=${color}`
14        },
15      },
16    },
17    ...opts,
18  })
19
20  return mock
21}
22
23t.test('no args throws usage', async t => {
24  const { explain } = await mockExplain(t)
25  await t.rejects(
26    explain.exec([]),
27    explain.usage
28  )
29})
30
31t.test('no match throws not found', async t => {
32  const { explain } = await mockExplain(t)
33  await t.rejects(
34    explain.exec(['foo@1.2.3', 'node_modules/baz']),
35    'No dependencies found matching foo@1.2.3, node_modules/baz'
36  )
37})
38
39t.test('invalid package name throws not found', async t => {
40  const { explain } = await mockExplain(t)
41  const badName = ' not a valid package name '
42  await t.rejects(
43    explain.exec([`${badName}@1.2.3`]),
44    `No dependencies found matching ${badName}@1.2.3`
45  )
46})
47
48t.test('explain some nodes', async t => {
49  const mockNodes = async (t, config = {}) => {
50    const mock = await mockExplain(t, {
51      prefixDir: {
52        node_modules: {
53          foo: {
54            'package.json': JSON.stringify({
55              name: 'foo',
56              version: '1.2.3',
57              dependencies: {
58                bar: '*',
59              },
60            }),
61          },
62          bar: {
63            'package.json': JSON.stringify({
64              name: 'bar',
65              version: '1.2.3',
66            }),
67          },
68          baz: {
69            'package.json': JSON.stringify({
70              name: 'baz',
71              version: '1.2.3',
72              dependencies: {
73                foo: '*',
74                bar: '2',
75              },
76            }),
77            node_modules: {
78              bar: {
79                'package.json': JSON.stringify({
80                  name: 'bar',
81                  version: '2.3.4',
82                }),
83              },
84              extra: {
85                'package.json': JSON.stringify({
86                  name: 'extra',
87                  version: '99.9999.999999',
88                  description: 'extraneous package',
89                }),
90              },
91            },
92          },
93        },
94        'package.json': JSON.stringify({
95          dependencies: {
96            baz: '1',
97          },
98        }),
99      },
100      config: {
101        color: 'always',
102        ...config,
103      },
104    })
105
106    return mock
107  }
108
109  t.test('works with the location', async t => {
110    const path = 'node_modules/foo'
111    const { explain, joinedOutput } = await mockNodes(t)
112    await explain.exec([path])
113    t.strictSame(joinedOutput(), 'foo@1.2.3 depth=Infinity color=true')
114  })
115
116  t.test('works with a full actual path', async t => {
117    const { npm, explain, joinedOutput } = await mockNodes(t)
118    const path = resolve(npm.prefix, 'node_modules/foo')
119    await explain.exec([path])
120    t.strictSame(joinedOutput(), 'foo@1.2.3 depth=Infinity color=true')
121  })
122
123  t.test('finds all nodes by name', async t => {
124    const { explain, joinedOutput } = await mockNodes(t)
125    await explain.exec(['bar'])
126    t.strictSame(joinedOutput(),
127      'bar@1.2.3 depth=Infinity color=true\n\n' +
128      'bar@2.3.4 depth=Infinity color=true'
129    )
130  })
131
132  t.test('finds only nodes that match the spec', async t => {
133    const { explain, joinedOutput } = await mockNodes(t)
134    await explain.exec(['bar@1'])
135    t.strictSame(joinedOutput(), 'bar@1.2.3 depth=Infinity color=true')
136  })
137
138  t.test('finds extraneous nodes', async t => {
139    const { explain, joinedOutput } = await mockNodes(t)
140    await explain.exec(['extra'])
141    t.strictSame(joinedOutput(), 'extra@99.9999.999999 depth=Infinity color=true')
142  })
143
144  t.test('json output', async t => {
145    const { explain, joinedOutput } = await mockNodes(t, { json: true })
146    await explain.exec(['node_modules/foo'])
147    t.match(JSON.parse(joinedOutput()), [{
148      name: 'foo',
149      version: '1.2.3',
150      dependents: Array,
151    }])
152  })
153
154  t.test('report if no nodes found', async t => {
155    const { explain } = await mockNodes(t)
156    await t.rejects(
157      explain.exec(['asdf/foo/bar', 'quux@1.x']),
158      'No dependencies found matching asdf/foo/bar, quux@1.x'
159    )
160  })
161})
162
163t.test('workspaces', async t => {
164  const mockWorkspaces = async (t, exec = [], workspaces = true) => {
165    const mock = await mockExplain(t, {
166      prefixDir: {
167        'package.json': JSON.stringify({
168          name: 'workspaces-project',
169          version: '1.0.0',
170          workspaces: ['packages/*'],
171          dependencies: {
172            abbrev: '^1.0.0',
173          },
174        }),
175        node_modules: {
176          a: t.fixture('symlink', '../packages/a'),
177          b: t.fixture('symlink', '../packages/b'),
178          c: t.fixture('symlink', '../packages/c'),
179          once: {
180            'package.json': JSON.stringify({
181              name: 'once',
182              version: '1.0.0',
183              dependencies: {
184                wrappy: '2.0.0',
185              },
186            }),
187          },
188          abbrev: {
189            'package.json': JSON.stringify({
190              name: 'abbrev',
191              version: '1.0.0',
192            }),
193          },
194          wrappy: {
195            'package.json': JSON.stringify({
196              name: 'wrappy',
197              version: '2.0.0',
198            }),
199          },
200        },
201        packages: {
202          a: {
203            'package.json': JSON.stringify({
204              name: 'a',
205              version: '1.0.0',
206              dependencies: {
207                once: '1.0.0',
208              },
209            }),
210          },
211          b: {
212            'package.json': JSON.stringify({
213              name: 'b',
214              version: '1.0.0',
215              dependencies: {
216                abbrev: '^1.0.0',
217              },
218            }),
219          },
220          c: {
221            'package.json': JSON.stringify({
222              name: 'c',
223              version: '1.0.0',
224            }),
225          },
226        },
227      },
228      config: {
229        ...(typeof workspaces === 'boolean' ? { workspaces } : { workspace: workspaces }),
230        color: 'always',
231      },
232    })
233
234    await mock.explain.exec(exec)
235
236    return mock.joinedOutput()
237  }
238
239  t.test('should explain workspaces deps', async t => {
240    const OUTPUT = await mockWorkspaces(t, ['wrappy'])
241    t.strictSame(
242      OUTPUT,
243      'wrappy@2.0.0 depth=Infinity color=true'
244    )
245  })
246
247  t.test('should explain deps when filtering to a single ws', async t => {
248    const OUTPUT = await mockWorkspaces(t, ['wrappy'], ['a'])
249    t.strictSame(
250      OUTPUT,
251      'wrappy@2.0.0 depth=Infinity color=true'
252    )
253  })
254
255  t.test('should explain deps of workspaces only', async t => {
256    const OUTPUT = await mockWorkspaces(t, ['abbrev'])
257    t.strictSame(
258      OUTPUT,
259      'abbrev@1.0.0 depth=Infinity color=true'
260    )
261  })
262
263  t.test('should throw usage if dep not found within filtered ws', async t => {
264    await t.rejects(
265      mockWorkspaces(t, ['abbrev'], ['a']),
266      'No dependencies found matching abbrev'
267    )
268  })
269
270  t.test('workspaces disabled', async t => {
271    await t.rejects(
272      mockWorkspaces(t, ['once'], false),
273      'No dependencies found matching once',
274      'should throw usage if dep not found when excluding ws'
275    )
276  })
277})
278