1const t = require('tap') 2const MockRegistry = require('@npmcli/mock-registry') 3const _mockNpm = require('../../fixtures/mock-npm') 4const { cleanCwd } = require('../../fixtures/clean-snapshot') 5 6t.cleanSnapshot = (str) => cleanCwd(str) 7 8const packument = spec => { 9 const mocks = { 10 cat: { 11 name: 'cat', 12 'dist-tags': { 13 latest: '1.0.1', 14 }, 15 versions: { 16 '1.0.1': { 17 version: '1.0.1', 18 dependencies: { 19 dog: '2.0.0', 20 }, 21 }, 22 }, 23 }, 24 chai: { 25 name: 'chai', 26 'dist-tags': { 27 latest: '1.0.1', 28 }, 29 versions: { 30 '1.0.1': { 31 version: '1.0.1', 32 }, 33 }, 34 }, 35 dog: { 36 name: 'dog', 37 'dist-tags': { 38 latest: '2.0.0', 39 }, 40 versions: { 41 '1.0.1': { 42 version: '1.0.1', 43 }, 44 '2.0.0': { 45 version: '2.0.0', 46 }, 47 }, 48 }, 49 theta: { 50 name: 'theta', 51 'dist-tags': { 52 latest: '1.0.1', 53 }, 54 versions: { 55 '1.0.1': { 56 version: '1.0.1', 57 }, 58 }, 59 }, 60 } 61 62 if (spec.name === 'eta') { 63 throw new Error('There is an error with this package.') 64 } 65 66 if (!mocks[spec.name]) { 67 const err = new Error() 68 err.code = 'E404' 69 throw err 70 } 71 72 return mocks[spec.name] 73} 74 75const fixtures = { 76 global: { 77 node_modules: { 78 cat: { 79 'package.json': JSON.stringify({ 80 name: 'cat', 81 version: '1.0.0', 82 }, null, 2), 83 }, 84 }, 85 }, 86 local: { 87 'package.json': JSON.stringify({ 88 name: 'delta', 89 version: '1.0.0', 90 dependencies: { 91 cat: '^1.0.0', 92 dog: '^1.0.0', 93 theta: '^1.0.0', 94 }, 95 devDependencies: { 96 zeta: '^1.0.0', 97 }, 98 optionalDependencies: { 99 lorem: '^1.0.0', 100 }, 101 peerDependencies: { 102 chai: '^1.0.0', 103 }, 104 }, null, 2), 105 node_modules: { 106 cat: { 107 'package.json': JSON.stringify({ 108 name: 'cat', 109 version: '1.0.0', 110 dependencies: { 111 dog: '2.0.0', 112 }, 113 }, null, 2), 114 node_modules: { 115 dog: { 116 'package.json': JSON.stringify({ 117 name: 'dog', 118 version: '2.0.0', 119 }, null, 2), 120 }, 121 }, 122 }, 123 chai: { 124 'package.json': JSON.stringify({ 125 name: 'chai', 126 version: '1.0.0', 127 }, null, 2), 128 }, 129 dog: { 130 'package.json': JSON.stringify({ 131 name: 'dog', 132 version: '1.0.1', 133 }, null, 2), 134 }, 135 zeta: { 136 'package.json': JSON.stringify({ 137 name: 'zeta', 138 version: '1.0.0', 139 }, null, 2), 140 }, 141 }, 142 }, 143 workspaces: { 144 'package.json': JSON.stringify({ 145 name: 'workspaces-project', 146 version: '1.0.0', 147 workspaces: ['packages/*'], 148 dependencies: { 149 dog: '^1.0.0', 150 }, 151 }), 152 node_modules: { 153 a: t.fixture('symlink', '../packages/a'), 154 b: t.fixture('symlink', '../packages/b'), 155 c: t.fixture('symlink', '../packages/c'), 156 cat: { 157 'package.json': JSON.stringify({ 158 name: 'cat', 159 version: '1.0.0', 160 dependencies: { 161 dog: '2.0.0', 162 }, 163 }), 164 node_modules: { 165 dog: { 166 'package.json': JSON.stringify({ 167 name: 'dog', 168 version: '2.0.0', 169 }), 170 }, 171 }, 172 }, 173 chai: { 174 'package.json': JSON.stringify({ 175 name: 'chai', 176 version: '1.0.0', 177 }), 178 }, 179 dog: { 180 'package.json': JSON.stringify({ 181 name: 'dog', 182 version: '1.0.1', 183 }), 184 }, 185 foo: { 186 'package.json': JSON.stringify({ 187 name: 'foo', 188 version: '1.0.0', 189 dependencies: { 190 chai: '^1.0.0', 191 }, 192 }), 193 }, 194 zeta: { 195 'package.json': JSON.stringify({ 196 name: 'zeta', 197 version: '1.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 b: '^1.0.0', 208 cat: '^1.0.0', 209 foo: '^1.0.0', 210 }, 211 }), 212 }, 213 b: { 214 'package.json': JSON.stringify({ 215 name: 'b', 216 version: '1.0.0', 217 dependencies: { 218 zeta: '^1.0.0', 219 }, 220 }), 221 }, 222 c: { 223 'package.json': JSON.stringify({ 224 name: 'c', 225 version: '1.0.0', 226 dependencies: { 227 theta: '^1.0.0', 228 }, 229 }), 230 }, 231 }, 232 }, 233} 234 235const mockNpm = async (t, { prefixDir, ...opts } = {}) => { 236 const res = await _mockNpm(t, { 237 command: 'outdated', 238 mocks: { 239 pacote: { 240 packument, 241 }, 242 }, 243 ...opts, 244 prefixDir, 245 }) 246 247 // this is not currently used, but ensures that no requests are 248 // hitting the registry. 249 // XXX: the pacote mock should be replaced with mock registry calls 250 const registry = new MockRegistry({ 251 tap: t, 252 registry: res.npm.config.get('registry'), 253 strict: true, 254 }) 255 256 return { 257 ...res, 258 registry, 259 } 260} 261 262t.test('should display outdated deps', async t => { 263 await t.test('outdated global', async t => { 264 const { outdated, joinedOutput } = await mockNpm(t, { 265 globalPrefixDir: fixtures.global, 266 config: { global: true }, 267 }) 268 await outdated.exec([]) 269 t.equal(process.exitCode, 1) 270 t.matchSnapshot(joinedOutput()) 271 }) 272 273 await t.test('outdated', async t => { 274 const { outdated, joinedOutput } = await mockNpm(t, { 275 prefixDir: fixtures.local, 276 config: { 277 color: 'always', 278 }, 279 }) 280 await outdated.exec([]) 281 t.equal(process.exitCode, 1) 282 t.matchSnapshot(joinedOutput()) 283 }) 284 285 await t.test('outdated --omit=dev', async t => { 286 const { outdated, joinedOutput } = await mockNpm(t, { 287 prefixDir: fixtures.local, 288 config: { 289 omit: ['dev'], 290 color: 'always', 291 }, 292 }) 293 await outdated.exec([]) 294 t.equal(process.exitCode, 1) 295 t.matchSnapshot(joinedOutput()) 296 }) 297 298 await t.test('outdated --omit=dev --omit=peer', async t => { 299 const { outdated, joinedOutput } = await mockNpm(t, { 300 prefixDir: fixtures.local, 301 config: { 302 omit: ['dev', 'peer'], 303 color: 'always', 304 }, 305 }) 306 await outdated.exec([]) 307 t.equal(process.exitCode, 1) 308 t.matchSnapshot(joinedOutput()) 309 }) 310 311 await t.test('outdated --omit=prod', async t => { 312 const { outdated, joinedOutput } = await mockNpm(t, { 313 prefixDir: fixtures.local, 314 config: { 315 omit: ['prod'], 316 color: 'always', 317 }, 318 }) 319 await outdated.exec([]) 320 t.equal(process.exitCode, 1) 321 t.matchSnapshot(joinedOutput()) 322 }) 323 324 await t.test('outdated --long', async t => { 325 const { outdated, joinedOutput } = await mockNpm(t, { 326 prefixDir: fixtures.local, 327 config: { 328 long: true, 329 }, 330 }) 331 await outdated.exec([]) 332 t.equal(process.exitCode, 1) 333 t.matchSnapshot(joinedOutput()) 334 }) 335 336 await t.test('outdated --json', async t => { 337 const { outdated, joinedOutput } = await mockNpm(t, { 338 prefixDir: fixtures.local, 339 config: { 340 json: true, 341 }, 342 }) 343 await outdated.exec([]) 344 t.equal(process.exitCode, 1) 345 t.matchSnapshot(joinedOutput()) 346 }) 347 348 await t.test('outdated --json --long', async t => { 349 const { outdated, joinedOutput } = await mockNpm(t, { 350 prefixDir: fixtures.local, 351 config: { 352 json: true, 353 long: true, 354 }, 355 }) 356 await outdated.exec([]) 357 t.equal(process.exitCode, 1) 358 t.matchSnapshot(joinedOutput()) 359 }) 360 361 await t.test('outdated --parseable', async t => { 362 const { outdated, joinedOutput } = await mockNpm(t, { 363 prefixDir: fixtures.local, 364 config: { 365 parseable: true, 366 }, 367 }) 368 await outdated.exec([]) 369 t.equal(process.exitCode, 1) 370 t.matchSnapshot(joinedOutput()) 371 }) 372 373 await t.test('outdated --parseable --long', async t => { 374 const { outdated, joinedOutput } = await mockNpm(t, { 375 prefixDir: fixtures.local, 376 config: { 377 parseable: true, 378 long: true, 379 }, 380 }) 381 await outdated.exec([]) 382 t.equal(process.exitCode, 1) 383 t.matchSnapshot(joinedOutput()) 384 }) 385 386 await t.test('outdated --all', async t => { 387 const { outdated, joinedOutput } = await mockNpm(t, { 388 prefixDir: fixtures.local, 389 config: { 390 all: true, 391 }, 392 }) 393 await outdated.exec([]) 394 t.equal(process.exitCode, 1) 395 t.matchSnapshot(joinedOutput()) 396 }) 397 398 await t.test('outdated specific dep', async t => { 399 const { outdated, joinedOutput } = await mockNpm(t, { 400 prefixDir: fixtures.local, 401 }) 402 await outdated.exec(['cat']) 403 t.equal(process.exitCode, 1) 404 t.matchSnapshot(joinedOutput()) 405 }) 406}) 407 408t.test('should return if no outdated deps', async t => { 409 const testDir = { 410 'package.json': JSON.stringify({ 411 name: 'delta', 412 version: '1.0.0', 413 dependencies: { 414 cat: '^1.0.0', 415 }, 416 }, null, 2), 417 node_modules: { 418 cat: { 419 'package.json': JSON.stringify({ 420 name: 'cat', 421 version: '1.0.1', 422 }, null, 2), 423 }, 424 }, 425 } 426 427 const { outdated, joinedOutput } = await mockNpm(t, { 428 prefixDir: testDir, 429 430 }) 431 await outdated.exec([]) 432 t.equal(joinedOutput(), '', 'no logs') 433}) 434 435t.test('throws if error with a dep', async t => { 436 const testDir = { 437 'package.json': JSON.stringify({ 438 name: 'delta', 439 version: '1.0.0', 440 dependencies: { 441 eta: '^1.0.0', 442 }, 443 }, null, 2), 444 node_modules: { 445 eta: { 446 'package.json': JSON.stringify({ 447 name: 'eta', 448 version: '1.0.1', 449 }, null, 2), 450 }, 451 }, 452 } 453 454 const { outdated } = await mockNpm(t, { 455 prefixDir: testDir, 456 }) 457 458 await t.rejects(outdated.exec([]), 'There is an error with this package.') 459}) 460 461t.test('should skip missing non-prod deps', async t => { 462 const testDir = { 463 'package.json': JSON.stringify({ 464 name: 'delta', 465 version: '1.0.0', 466 devDependencies: { 467 chai: '^1.0.0', 468 }, 469 }, null, 2), 470 node_modules: {}, 471 } 472 473 const { outdated, joinedOutput } = await mockNpm(t, { 474 prefixDir: testDir, 475 }) 476 477 await outdated.exec([]) 478 479 t.equal(joinedOutput(), '', 'no logs') 480}) 481 482t.test('should skip invalid pkg ranges', async t => { 483 const testDir = { 484 'package.json': JSON.stringify({ 485 name: 'delta', 486 version: '1.0.0', 487 dependencies: { 488 cat: '>=^2', 489 }, 490 }, null, 2), 491 node_modules: { 492 cat: { 493 'package.json': JSON.stringify({ 494 name: 'cat', 495 version: '1.0.0', 496 }, null, 2), 497 }, 498 }, 499 } 500 501 const { outdated, joinedOutput } = await mockNpm(t, { 502 prefixDir: testDir, 503 }) 504 await outdated.exec([]) 505 t.equal(joinedOutput(), '', 'no logs') 506}) 507 508t.test('should skip git specs', async t => { 509 const testDir = { 510 'package.json': JSON.stringify({ 511 name: 'delta', 512 version: '1.0.0', 513 dependencies: { 514 cat: 'github:username/foo', 515 }, 516 }, null, 2), 517 node_modules: { 518 cat: { 519 'package.json': JSON.stringify({ 520 name: 'cat', 521 version: '1.0.0', 522 }, null, 2), 523 }, 524 }, 525 } 526 527 const { outdated, joinedOutput } = await mockNpm(t, { 528 prefixDir: testDir, 529 }) 530 await outdated.exec([]) 531 t.equal(joinedOutput(), '', 'no logs') 532}) 533 534t.test('workspaces', async t => { 535 const mockWorkspaces = async (t, { exitCode = 1, ...config } = {}) => { 536 const { outdated, joinedOutput } = await mockNpm(t, { 537 prefixDir: fixtures.workspaces, 538 config, 539 }) 540 541 await outdated.exec([]) 542 543 t.matchSnapshot(joinedOutput(), 'output') 544 t.equal(process.exitCode, exitCode ?? undefined) 545 } 546 547 await t.test('should display ws outdated deps human output', t => 548 mockWorkspaces(t)) 549 550 // TODO: This should display dog, but doesn't because arborist filters 551 // workspace deps even if they're also root deps 552 // This will be fixed in a future arborist version 553 await t.test('should display only root outdated when ws disabled', t => 554 mockWorkspaces(t, { workspaces: false, exitCode: null })) 555 556 await t.test('should display ws outdated deps json output', t => 557 mockWorkspaces(t, { json: true })) 558 559 await t.test('should display ws outdated deps parseable output', t => 560 mockWorkspaces(t, { parseable: true })) 561 562 await t.test('should display all dependencies', t => 563 mockWorkspaces(t, { all: true })) 564 565 await t.test('should highlight ws in dependend by section', t => 566 mockWorkspaces(t, { color: 'always' })) 567 568 await t.test('should display results filtered by ws', t => 569 mockWorkspaces(t, { workspace: 'a' })) 570 571 await t.test('should display json results filtered by ws', t => 572 mockWorkspaces(t, { json: true, workspace: 'a' })) 573 574 await t.test('should display parseable results filtered by ws', t => 575 mockWorkspaces(t, { parseable: true, workspace: 'a' })) 576 577 await t.test('should display nested deps when filtering by ws and using --all', t => 578 mockWorkspaces(t, { all: true, workspace: 'a' })) 579 580 await t.test('should display no results if ws has no deps to display', t => 581 mockWorkspaces(t, { workspace: 'b', exitCode: null })) 582 583 await t.test('should display missing deps when filtering by ws', t => 584 mockWorkspaces(t, { workspace: 'c', exitCode: 1 })) 585}) 586 587t.test('aliases', async t => { 588 const testDir = { 589 'package.json': JSON.stringify({ 590 name: 'display-aliases', 591 version: '1.0.0', 592 dependencies: { 593 cat: 'npm:dog@latest', 594 }, 595 }), 596 node_modules: { 597 cat: { 598 'package.json': JSON.stringify({ 599 name: 'dog', 600 version: '1.0.0', 601 }), 602 }, 603 }, 604 } 605 606 const { outdated, joinedOutput } = await mockNpm(t, { 607 prefixDir: testDir, 608 }) 609 await outdated.exec([]) 610 611 t.matchSnapshot(joinedOutput(), 'should display aliased outdated dep output') 612 t.equal(process.exitCode, 1) 613}) 614