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