1const t = require('tap') 2const { resolve, dirname, join } = require('path') 3const fs = require('fs') 4const { load: loadMockNpm } = require('../fixtures/mock-npm.js') 5const mockGlobals = require('@npmcli/mock-globals') 6const { commands } = require('../../lib/utils/cmd-list.js') 7 8t.test('not yet loaded', async t => { 9 const { npm, logs } = await loadMockNpm(t, { load: false }) 10 t.match(npm, { 11 started: Number, 12 command: null, 13 config: { 14 loaded: false, 15 get: Function, 16 set: Function, 17 }, 18 version: String, 19 }) 20 t.throws(() => npm.config.set('foo', 'bar')) 21 t.throws(() => npm.config.get('foo')) 22 t.same(logs, []) 23}) 24 25t.test('npm.load', async t => { 26 await t.test('load error', async t => { 27 const { npm } = await loadMockNpm(t, { load: false }) 28 const loadError = new Error('load error') 29 npm.config.load = async () => { 30 throw loadError 31 } 32 await t.rejects( 33 () => npm.load(), 34 /load error/ 35 ) 36 37 t.equal(npm.loadErr, loadError) 38 npm.config.load = async () => { 39 throw new Error('different error') 40 } 41 await t.rejects( 42 () => npm.load(), 43 /load error/, 44 'loading again returns the original error' 45 ) 46 t.equal(npm.loadErr, loadError) 47 }) 48 49 await t.test('basic loading', async t => { 50 const { npm, logs, prefix: dir, cache, other } = await loadMockNpm(t, { 51 prefixDir: { node_modules: {} }, 52 otherDirs: { 53 newCache: {}, 54 }, 55 }) 56 57 t.equal(npm.loaded, true) 58 t.equal(npm.config.loaded, true) 59 t.equal(npm.config.get('force'), false) 60 t.ok(npm.usage, 'has usage') 61 62 t.match(npm, { 63 flatOptions: {}, 64 }) 65 t.match(logs.timing.filter(([p]) => p === 'npm:load'), [ 66 ['npm:load', /Completed in [0-9.]+ms/], 67 ]) 68 69 mockGlobals(t, { process: { platform: 'posix' } }) 70 t.equal(resolve(npm.cache), resolve(cache), 'cache is cache') 71 npm.cache = other.newCache 72 t.equal(npm.config.get('cache'), other.newCache, 'cache setter sets config') 73 t.equal(npm.cache, other.newCache, 'cache getter gets new config') 74 t.equal(npm.lockfileVersion, 2, 'lockfileVersion getter') 75 t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix') 76 t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix') 77 npm.globalPrefix = npm.prefix 78 t.equal(npm.prefix, npm.globalPrefix, 'globalPrefix setter') 79 npm.localPrefix = dir + '/extra/prefix' 80 t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix after localPrefix setter') 81 t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after localPrefix setter') 82 83 npm.prefix = dir + '/some/prefix' 84 t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix after prefix setter') 85 t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix after prefix setter') 86 t.equal(npm.bin, npm.localBin, 'bin is local bin after prefix setter') 87 t.not(npm.bin, npm.globalBin, 'bin is not global bin after prefix setter') 88 t.equal(npm.dir, npm.localDir, 'dir is local dir after prefix setter') 89 t.not(npm.dir, npm.globalDir, 'dir is not global dir after prefix setter') 90 91 npm.config.set('global', true) 92 t.equal(npm.prefix, npm.globalPrefix, 'prefix is global prefix after setting global') 93 t.not(npm.prefix, npm.localPrefix, 'prefix is not local prefix after setting global') 94 t.equal(npm.bin, npm.globalBin, 'bin is global bin after setting global') 95 t.not(npm.bin, npm.localBin, 'bin is not local bin after setting global') 96 t.equal(npm.dir, npm.globalDir, 'dir is global dir after setting global') 97 t.not(npm.dir, npm.localDir, 'dir is not local dir after setting global') 98 99 npm.prefix = dir + '/new/global/prefix' 100 t.equal(npm.prefix, npm.globalPrefix, 'prefix is global prefix after prefix setter') 101 t.not(npm.prefix, npm.localPrefix, 'prefix is not local prefix after prefix setter') 102 t.equal(npm.bin, npm.globalBin, 'bin is global bin after prefix setter') 103 t.not(npm.bin, npm.localBin, 'bin is not local bin after prefix setter') 104 105 mockGlobals(t, { process: { platform: 'win32' } }) 106 t.equal(npm.bin, npm.globalBin, 'bin is global bin in windows mode') 107 t.equal(npm.dir, npm.globalDir, 'dir is global dir in windows mode') 108 }) 109 110 await t.test('forceful loading', async t => { 111 const { logs } = await loadMockNpm(t, { 112 globals: { 113 'process.argv': [...process.argv, '--force', '--color', 'always'], 114 }, 115 }) 116 t.match(logs.warn, [ 117 [ 118 'using --force', 119 'Recommended protections disabled.', 120 ], 121 ]) 122 }) 123 124 await t.test('node is a symlink', async t => { 125 const node = process.platform === 'win32' ? 'node.exe' : 'node' 126 const { Npm, npm, logs, outputs, prefix } = await loadMockNpm(t, { 127 prefixDir: { 128 bin: t.fixture('symlink', dirname(process.execPath)), 129 }, 130 globals: (dirs) => ({ 131 'process.env.PATH': resolve(dirs.prefix, 'bin'), 132 'process.argv': [ 133 node, 134 process.argv[1], 135 '--usage', 136 '--scope=foo', 137 'token', 138 'revoke', 139 'blergggg', 140 ], 141 }), 142 }) 143 144 t.equal(npm.config.get('scope'), '@foo', 'added the @ sign to scope') 145 t.match([ 146 ...logs.timing.filter(([p]) => p === 'npm:load:whichnode'), 147 ...logs.verbose, 148 ...logs.timing.filter(([p]) => p === 'npm:load'), 149 ], [ 150 ['npm:load:whichnode', /Completed in [0-9.]+ms/], 151 ['node symlink', resolve(prefix, 'bin', node)], 152 ['title', 'npm token revoke blergggg'], 153 ['argv', '"--usage" "--scope" "foo" "token" "revoke" "blergggg"'], 154 ['logfile', /logs-max:\d+ dir:.*/], 155 ['logfile', /.*-debug-0.log/], 156 ['npm:load', /Completed in [0-9.]+ms/], 157 ]) 158 t.equal(process.execPath, resolve(prefix, 'bin', node)) 159 160 outputs.length = 0 161 logs.length = 0 162 await npm.exec('ll', []) 163 164 t.equal(npm.command, 'll', 'command set to first npm command') 165 t.equal(npm.flatOptions.npmCommand, 'll', 'npmCommand flatOption set') 166 167 const ll = Npm.cmd('ll') 168 t.same(outputs, [[ll.describeUsage]], 'print usage') 169 npm.config.set('usage', false) 170 171 outputs.length = 0 172 logs.length = 0 173 await npm.exec('get', ['scope', '\u2010not-a-dash']) 174 175 t.strictSame([npm.command, npm.flatOptions.npmCommand], ['ll', 'll'], 176 'does not change npm.command when another command is called') 177 178 t.match(logs, [ 179 [ 180 'error', 181 'arg', 182 'Argument starts with non-ascii dash, this is probably invalid:', 183 '\u2010not-a-dash', 184 ], 185 [ 186 'timing', 187 'command:config', 188 /Completed in [0-9.]+ms/, 189 ], 190 [ 191 'timing', 192 'command:get', 193 /Completed in [0-9.]+ms/, 194 ], 195 ]) 196 t.same(outputs, [['scope=@foo\n\u2010not-a-dash=undefined']]) 197 }) 198 199 await t.test('--no-workspaces with --workspace', async t => { 200 const { npm } = await loadMockNpm(t, { 201 prefixDir: { 202 packages: { 203 a: { 204 'package.json': JSON.stringify({ 205 name: 'a', 206 version: '1.0.0', 207 scripts: { test: 'echo test a' }, 208 }), 209 }, 210 }, 211 'package.json': JSON.stringify({ 212 name: 'root', 213 version: '1.0.0', 214 workspaces: ['./packages/*'], 215 }), 216 }, 217 globals: { 218 'process.argv': [ 219 process.execPath, 220 process.argv[1], 221 '--color', 'false', 222 '--workspaces', 'false', 223 '--workspace', 'a', 224 ], 225 }, 226 }) 227 await t.rejects( 228 npm.exec('run', []), 229 /Can not use --no-workspaces and --workspace at the same time/ 230 ) 231 }) 232 233 await t.test('workspace-aware configs and commands', async t => { 234 const { npm, outputs } = await loadMockNpm(t, { 235 prefixDir: { 236 packages: { 237 a: { 238 'package.json': JSON.stringify({ 239 name: 'a', 240 version: '1.0.0', 241 scripts: { test: 'echo test a' }, 242 }), 243 }, 244 b: { 245 'package.json': JSON.stringify({ 246 name: 'b', 247 version: '1.0.0', 248 scripts: { test: 'echo test b' }, 249 }), 250 }, 251 }, 252 'package.json': JSON.stringify({ 253 name: 'root', 254 version: '1.0.0', 255 workspaces: ['./packages/*'], 256 }), 257 }, 258 globals: { 259 'process.argv': [ 260 process.execPath, 261 process.argv[1], 262 '--color', 'false', 263 '--workspaces', 'true', 264 ], 265 }, 266 }) 267 268 await npm.exec('run', []) 269 270 t.equal(npm.command, 'run-script', 'npm.command set to canonical name') 271 272 t.match( 273 outputs, 274 [ 275 ['Lifecycle scripts included in a@1.0.0:'], 276 [' test\n echo test a'], 277 [''], 278 ['Lifecycle scripts included in b@1.0.0:'], 279 [' test\n echo test b'], 280 [''], 281 ], 282 'should exec workspaces version of commands' 283 ) 284 }) 285 286 await t.test('workspaces in global mode', async t => { 287 const { npm } = await loadMockNpm(t, { 288 prefixDir: { 289 packages: { 290 a: { 291 'package.json': JSON.stringify({ 292 name: 'a', 293 version: '1.0.0', 294 scripts: { test: 'echo test a' }, 295 }), 296 }, 297 b: { 298 'package.json': JSON.stringify({ 299 name: 'b', 300 version: '1.0.0', 301 scripts: { test: 'echo test b' }, 302 }), 303 }, 304 }, 305 'package.json': JSON.stringify({ 306 name: 'root', 307 version: '1.0.0', 308 workspaces: ['./packages/*'], 309 }), 310 }, 311 globals: { 312 'process.argv': [ 313 process.execPath, 314 process.argv[1], 315 '--color', 316 'false', 317 '--workspaces', 318 '--global', 319 'true', 320 ], 321 }, 322 }) 323 324 await t.rejects( 325 npm.exec('run', []), 326 /Workspaces not supported for global packages/ 327 ) 328 }) 329}) 330 331t.test('set process.title', async t => { 332 t.test('basic title setting', async t => { 333 const { npm } = await loadMockNpm(t, { 334 globals: { 335 'process.argv': [ 336 process.execPath, 337 process.argv[1], 338 '--usage', 339 '--scope=foo', 340 'ls', 341 ], 342 }, 343 }) 344 t.equal(npm.title, 'npm ls') 345 t.equal(process.title, 'npm ls') 346 }) 347 348 t.test('do not expose token being revoked', async t => { 349 const { npm } = await loadMockNpm(t, { 350 globals: { 351 'process.argv': [ 352 process.execPath, 353 process.argv[1], 354 '--usage', 355 '--scope=foo', 356 'token', 357 'revoke', 358 `npm_${'a'.repeat(36)}`, 359 ], 360 }, 361 }) 362 t.equal(npm.title, 'npm token revoke npm_***') 363 t.equal(process.title, 'npm token revoke npm_***') 364 }) 365 366 t.test('do show *** unless a token is actually being revoked', async t => { 367 const { npm } = await loadMockNpm(t, { 368 globals: { 369 'process.argv': [ 370 process.execPath, 371 process.argv[1], 372 '--usage', 373 '--scope=foo', 374 'token', 375 'revoke', 376 'notatoken', 377 ], 378 }, 379 }) 380 t.equal(npm.title, 'npm token revoke notatoken') 381 t.equal(process.title, 'npm token revoke notatoken') 382 }) 383}) 384 385t.test('debug log', async t => { 386 t.test('writes log file', async t => { 387 const { npm, debugFile } = await loadMockNpm(t, { load: false }) 388 389 const log1 = ['silly', 'test', 'before load'] 390 const log2 = ['silly', 'test', 'after load'] 391 const log3 = ['silly', 'test', 'hello\x00world'] 392 393 process.emit('log', ...log1) 394 await npm.load() 395 process.emit('log', ...log2) 396 process.emit('log', ...log3) 397 398 const debug = await debugFile() 399 t.equal(npm.logFiles.length, 1, 'one debug file') 400 t.match(debug, log1.join(' '), 'before load appears') 401 t.match(debug, log2.join(' '), 'after load log appears') 402 t.match(debug, 'hello^@world') 403 }) 404 405 t.test('can load with bad dir', async t => { 406 const { npm, testdir } = await loadMockNpm(t, { 407 load: false, 408 config: (dirs) => ({ 409 'logs-dir': join(dirs.testdir, 'my_logs_dir'), 410 }), 411 }) 412 const logsDir = join(testdir, 'my_logs_dir') 413 414 // make logs dir a file before load so it files 415 fs.writeFileSync(logsDir, 'A_TEXT_FILE') 416 await t.resolves(npm.load(), 'loads with invalid logs dir') 417 418 t.equal(npm.logFiles.length, 0, 'no log files array') 419 t.strictSame(fs.readFileSync(logsDir, 'utf-8'), 'A_TEXT_FILE') 420 }) 421}) 422 423t.test('cache dir', async t => { 424 t.test('creates a cache dir', async t => { 425 const { npm } = await loadMockNpm(t) 426 427 t.ok(fs.existsSync(npm.cache), 'cache dir exists') 428 }) 429 430 t.test('can load with a bad cache dir', async t => { 431 const { npm, cache } = await loadMockNpm(t, { 432 load: false, 433 // The easiest way to make mkdir(cache) fail is to make it a file. 434 // This will have the same effect as if its read only or inaccessible. 435 cacheDir: 'A_TEXT_FILE', 436 }) 437 438 await t.resolves(npm.load(), 'loads with cache dir as a file') 439 440 t.equal(fs.readFileSync(cache, 'utf-8'), 'A_TEXT_FILE') 441 }) 442}) 443 444t.test('timings', async t => { 445 t.test('gets/sets timers', async t => { 446 const { npm, logs } = await loadMockNpm(t, { load: false }) 447 process.emit('time', 'foo') 448 process.emit('time', 'bar') 449 t.match(npm.unfinishedTimers.get('foo'), Number, 'foo timer is a number') 450 t.match(npm.unfinishedTimers.get('bar'), Number, 'foo timer is a number') 451 process.emit('timeEnd', 'foo') 452 process.emit('timeEnd', 'bar') 453 process.emit('timeEnd', 'baz') 454 // npm timer is started by default 455 process.emit('timeEnd', 'npm') 456 t.match(logs.timing, [ 457 ['foo', /Completed in [0-9]+ms/], 458 ['bar', /Completed in [0-9]+ms/], 459 ['npm', /Completed in [0-9]+ms/], 460 ]) 461 t.match(logs.silly, [[ 462 'timing', 463 "Tried to end timer that doesn't exist:", 464 'baz', 465 ]]) 466 t.notOk(npm.unfinishedTimers.has('foo'), 'foo timer is gone') 467 t.notOk(npm.unfinishedTimers.has('bar'), 'bar timer is gone') 468 t.match(npm.finishedTimers, { foo: Number, bar: Number, npm: Number }) 469 }) 470 471 t.test('writes timings file', async t => { 472 const { npm, cache, timingFile } = await loadMockNpm(t, { 473 config: { timing: true }, 474 }) 475 process.emit('time', 'foo') 476 process.emit('timeEnd', 'foo') 477 process.emit('time', 'bar') 478 npm.writeTimingFile() 479 t.match(npm.timingFile, cache) 480 t.match(npm.timingFile, /-timing.json$/) 481 const timings = await timingFile() 482 t.match(timings, { 483 metadata: { 484 command: [], 485 logfiles: [String], 486 version: String, 487 }, 488 unfinishedTimers: { 489 bar: [Number, Number], 490 npm: [Number, Number], 491 }, 492 timers: { 493 foo: Number, 494 'npm:load': Number, 495 }, 496 }) 497 }) 498 499 t.test('does not write timings file with timers:false', async t => { 500 const { npm, timingFile } = await loadMockNpm(t, { 501 config: { timing: false }, 502 }) 503 npm.writeTimingFile() 504 await t.rejects(() => timingFile()) 505 }) 506 507 const timingDisplay = [ 508 [{ loglevel: 'silly' }, true, false], 509 [{ loglevel: 'silly', timing: true }, true, true], 510 [{ loglevel: 'silent', timing: true }, false, false], 511 ] 512 513 for (const [config, expectedDisplay, expectedTiming] of timingDisplay) { 514 const msg = `${JSON.stringify(config)}, display:${expectedDisplay}, timing:${expectedTiming}` 515 await t.test(`timing display: ${msg}`, async t => { 516 const { display } = await loadMockNpm(t, { config }) 517 t.equal(!!display.length, expectedDisplay, 'display') 518 t.equal(!!display.timing.length, expectedTiming, 'timing display') 519 }) 520 } 521}) 522 523t.test('output clears progress and console.logs cleaned messages', async t => { 524 t.plan(4) 525 let showingProgress = true 526 const logs = [] 527 const errors = [] 528 const { npm } = await loadMockNpm(t, { 529 load: false, 530 mocks: { 531 npmlog: { 532 clearProgress: () => showingProgress = false, 533 showProgress: () => showingProgress = true, 534 }, 535 }, 536 globals: { 537 'console.log': (...args) => { 538 t.equal(showingProgress, false, 'should not be showing progress right now') 539 logs.push(args) 540 }, 541 'console.error': (...args) => { 542 t.equal(showingProgress, false, 'should not be showing progress right now') 543 errors.push(args) 544 }, 545 }, 546 }) 547 npm.originalOutput('hello\x00world') 548 npm.originalOutputError('error\x00world') 549 550 t.match(logs, [['hello^@world']]) 551 t.match(errors, [['error^@world']]) 552}) 553 554t.test('aliases and typos', async t => { 555 const { Npm } = await loadMockNpm(t, { init: false }) 556 t.throws(() => Npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) 557 t.throws(() => Npm.cmd(''), { code: 'EUNKNOWNCOMMAND' }) 558 t.throws(() => Npm.cmd('birthday'), { code: 'EUNKNOWNCOMMAND' }) 559 t.match(Npm.cmd('it').name, 'install-test') 560 t.match(Npm.cmd('installTe').name, 'install-test') 561 t.match(Npm.cmd('access').name, 'access') 562 t.match(Npm.cmd('auth').name, 'owner') 563}) 564 565t.test('explicit workspace rejection', async t => { 566 const mock = await loadMockNpm(t, { 567 prefixDir: { 568 packages: { 569 a: { 570 'package.json': JSON.stringify({ 571 name: 'a', 572 version: '1.0.0', 573 scripts: { test: 'echo test a' }, 574 }), 575 }, 576 }, 577 'package.json': JSON.stringify({ 578 name: 'root', 579 version: '1.0.0', 580 workspaces: ['./packages/a'], 581 }), 582 }, 583 globals: { 584 'process.argv': [ 585 process.execPath, 586 process.argv[1], 587 '--color', 'false', 588 '--workspace', './packages/a', 589 ], 590 }, 591 }) 592 await t.rejects( 593 mock.npm.exec('ping', []), 594 /This command does not support workspaces/ 595 ) 596}) 597 598t.test('implicit workspace rejection', async t => { 599 const mock = await loadMockNpm(t, { 600 prefixDir: { 601 packages: { 602 a: { 603 'package.json': JSON.stringify({ 604 name: 'a', 605 version: '1.0.0', 606 scripts: { test: 'echo test a' }, 607 }), 608 }, 609 }, 610 'package.json': JSON.stringify({ 611 name: 'root', 612 version: '1.0.0', 613 workspaces: ['./packages/a'], 614 }), 615 }, 616 chdir: ({ prefix }) => join(prefix, 'packages', 'a'), 617 globals: { 618 'process.argv': [ 619 process.execPath, 620 process.argv[1], 621 '--color', 'false', 622 '--workspace', './packages/a', 623 ], 624 }, 625 }) 626 await t.rejects( 627 mock.npm.exec('team', []), 628 /This command does not support workspaces/ 629 ) 630}) 631 632t.test('implicit workspace accept', async t => { 633 const mock = await loadMockNpm(t, { 634 prefixDir: { 635 packages: { 636 a: { 637 'package.json': JSON.stringify({ 638 name: 'a', 639 version: '1.0.0', 640 scripts: { test: 'echo test a' }, 641 }), 642 }, 643 }, 644 'package.json': JSON.stringify({ 645 name: 'root', 646 version: '1.0.0', 647 workspaces: ['./packages/a'], 648 }), 649 }, 650 chdir: ({ prefix }) => join(prefix, 'packages', 'a'), 651 globals: { 652 'process.argv': [ 653 process.execPath, 654 process.argv[1], 655 '--color', 'false', 656 ], 657 }, 658 }) 659 await t.rejects(mock.npm.exec('org', []), /.*Usage/) 660}) 661 662t.test('usage', async t => { 663 t.test('with browser', async t => { 664 const { npm } = await loadMockNpm(t, { globals: { process: { platform: 'posix' } } }) 665 const usage = npm.usage 666 npm.config.set('viewer', 'browser') 667 const browserUsage = npm.usage 668 t.notMatch(usage, '(in a browser)') 669 t.match(browserUsage, '(in a browser)') 670 }) 671 672 t.test('windows always uses browser', async t => { 673 const { npm } = await loadMockNpm(t, { globals: { process: { platform: 'win32' } } }) 674 const usage = npm.usage 675 npm.config.set('viewer', 'browser') 676 const browserUsage = npm.usage 677 t.match(usage, '(in a browser)') 678 t.match(browserUsage, '(in a browser)') 679 }) 680 681 t.test('includes commands', async t => { 682 const { npm } = await loadMockNpm(t) 683 const usage = npm.usage 684 npm.config.set('long', true) 685 const longUsage = npm.usage 686 687 const lastCmd = commands[commands.length - 1] 688 for (const cmd of commands) { 689 const isLast = cmd === lastCmd 690 const shortCmd = new RegExp(`\\s${cmd}${isLast ? '\\n' : ',[\\s\\n]'}`) 691 const longCmd = new RegExp(`^\\s+${cmd}\\s+\\w.*\n\\s+Usage:\\n`, 'm') 692 693 t.match(usage, shortCmd, `usage includes ${cmd}`) 694 t.notMatch(usage, longCmd, `usage does not include long ${cmd}`) 695 696 t.match(longUsage, longCmd, `long usage includes ${cmd}`) 697 if (!isLast) { 698 // long usage includes false positives for the last command since it is 699 // not followed by a comma 700 t.notMatch(longUsage, shortCmd, `long usage does not include short ${cmd}`) 701 } 702 } 703 }) 704 705 t.test('set process.stdout.columns', async t => { 706 const { npm } = await loadMockNpm(t, { 707 config: { viewer: 'man' }, 708 }) 709 t.cleanSnapshot = str => 710 str.replace(npm.config.get('userconfig'), '{USERCONFIG}') 711 .replace(npm.npmRoot, '{NPMROOT}') 712 .replace(`npm@${npm.version}`, 'npm@{VERSION}') 713 714 const widths = [0, 1, 10, 24, 40, 41, 75, 76, 90, 100] 715 for (const width of widths) { 716 t.test(`column width ${width}`, async t => { 717 mockGlobals(t, { 'process.stdout.columns': width }) 718 const usage = npm.usage 719 t.matchSnapshot(usage) 720 }) 721 } 722 }) 723}) 724