current commit
347dbb2e3 · fix: accept supported Node.js prereleases at startup (#10175)
const t = require('tap')
const { readdirSync } = require('node:fs')
const { dirname } = require('node:path')
const { load: loadMockNpm } = require('../../fixtures/mock-npm.js')
const tmock = require('../../fixtures/tmock.js')
const validateEngines = require('../../../lib/cli/validate-engines.js')

const cliMock = async (t, opts) => {
  let exitHandlerArgs = null
  let npm = null

  const { Npm, ...mock } = await loadMockNpm(t, { ...opts, init: false })
  const cli = tmock(t, '{LIB}/cli/entry.js', {
    '{LIB}/npm.js': Npm,
    '{LIB}/cli/exit-handler.js': class MockExitHandler {
      exit (...args) {
        exitHandlerArgs = args
        npm.unload()
      }

      registerUncaughtHandlers () {}

      setNpm (_npm) {
        npm = _npm
      }
    },
  })

  return {
    ...mock,
    Npm,
    cli: (p) => validateEngines(p, () => cli),
    exitHandlerCalled: () => exitHandlerArgs,
    exitHandlerNpm: () => npm,
  }
}

t.test('print the version ', async t => {
  const { logs, cli, Npm, outputs, exitHandlerCalled } = await cliMock(t, {
    globals: { 'process.argv': ['node', 'npm', 'root'] },
  })
  await cli(process)

  t.strictSame(process.argv, ['node', 'npm', 'root'], 'system process.argv was rewritten')
  t.strictSame(logs.verbose.byTitle('cli'), ['cli node npm'])
  t.strictSame(logs.verbose.byTitle('title'), ['title npm root'])
  t.match(logs.verbose.byTitle('argv'), ['argv "root"'])
  t.strictSame(logs.info, [`using npm@${Npm.version}`, `using node@${process.version}`])
  t.equal(outputs.length, 1)
  t.match(outputs[0], dirname(process.cwd()))
  t.strictSame(exitHandlerCalled(), [])
})

t.test('calling with --versions calls npm version with no args', async t => {
  const { logs, cli, outputs, exitHandlerCalled } = await cliMock(t, {
    globals: {
      'process.argv': ['node', 'npm', 'install', 'or', 'whatever', '--versions', '--json'],
    },
  })
  await cli(process)

  t.equal(process.title, 'npm install or whatever')
  t.strictSame(logs.verbose.byTitle('cli'), ['cli node npm'])
  t.strictSame(logs.verbose.byTitle('title'), ['title npm install or whatever'])
  t.match(logs.verbose.byTitle('argv'), ['argv "install" "or" "whatever" "--versions"'])
  t.equal(outputs.length, 1)
  t.match(JSON.parse(outputs[0]), { npm: String, node: String, v8: String })
  t.strictSame(exitHandlerCalled(), [])
})

t.test('logged argv is sanitized', async t => {
  const { logs, cli } = await cliMock(t, {
    globals: {
      'process.argv': [
        'node',
        'npm',
        'version',
        '--registry',
        'https://u:password@npmjs.org/password',
      ],
    },
  })

  await cli(process)
  t.equal(process.title, 'npm version')
  t.strictSame(logs.verbose.byTitle('cli'), ['cli node npm'])
  t.strictSame(logs.verbose.byTitle('title'), ['title npm version'])
  t.match(logs.verbose.byTitle('argv'),
    ['argv "version" "--registry" "https://u:***@npmjs.org/password"'])
})

t.test('logged argv is sanitized with equals', async t => {
  const { logs, cli } = await cliMock(t, {
    globals: {
      'process.argv': [
        'node',
        'npm',
        'version',
        '--registry=https://u:password@npmjs.org',
      ],
    },
  })
  await cli(process)

  t.match(logs.verbose.byTitle('argv'), ['argv "version" "--registry" "https://u:***@npmjs.org"'])
})

t.test('print usage if no params provided', async t => {
  const { cli, outputs, exitHandlerCalled, exitHandlerNpm } = await cliMock(t, {
    globals: {
      'process.argv': ['node', 'npm'],
    },
  })
  await cli(process)

  t.match(outputs[0], 'Usage:', 'outputs npm usage')
  t.match(exitHandlerCalled(), [], 'should call exitHandler with no args')
  t.ok(exitHandlerNpm(), 'exitHandler npm is set')
  t.match(process.exitCode, 1)
})

t.test('load error calls error handler', async t => {
  const err = new Error('test load error')
  const { cli, exitHandlerCalled } = await cliMock(t, {
    mocks: {
      '@npmcli/config': class BadConfig {
        async load () {
          throw err
        }
      },
    },
    globals: {
      'process.argv': ['node', 'npm', 'asdf'],
    },
  })
  await cli(process)
  t.strictSame(exitHandlerCalled(), [err])
})

t.test('unsupported node version', async t => {
  const { cli, logs } = await cliMock(t, {
    globals: {
      'process.version': '12.6.0',
    },
  })
  await cli(process)
  t.match(
    logs.warn[0],
    /npm v.* does not support Node\.js 12\.6\.0\./
  )
})

t.test('prerelease node versions', async t => {
  for (const [version, supported] of [
    ['v27.0.0-alpha.0.0.0', true],
    ['v28.0.0-alpha.0.0.0', true],
    ['v27.0.0-rc.0', true],
    ['v26.0.0-alpha.0', false],
    ['v23.0.0-alpha.0', false],
  ]) {
    await t.test(version, async t => {
      const { cli, logs } = await cliMock(t, {
        globals: {
          'process.argv': ['node', 'npm', '--version'],
          'process.version': version,
        },
      })
      await cli(process)
      if (supported) {
        t.strictSame(logs.warn, [])
      } else {
        t.match(logs.warn[0], `does not support Node.js ${version}.`)
      }
    })
  }
})

t.test('non-ascii dash', async t => {
  const { cli, logs } = await cliMock(t, {
    globals: {
      'process.argv': ['node', 'npm', 'scope', '\u2010not-a-dash'],
    },
  })
  await cli(process)
  t.equal(
    logs.error[0],
    'arg Argument starts with non-ascii dash, this is probably invalid: \u2010not-a-dash'
  )
})

t.test('exit early for --version', async t => {
  const { cli, outputs, Npm, cache } = await cliMock(t, {
    globals: {
      'process.argv': ['node', 'npm', '-v'],
    },
  })
  await cli(process)
  t.strictSame(readdirSync(cache), [], 'nothing created in cache')
  t.equal(outputs[0], Npm.version)
})