PR-URL: https://github.com/nodejs/node/pull/63463 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
1166 lines
35 KiB
JavaScript
1166 lines
35 KiB
JavaScript
const fs = require('node:fs')
|
|
const tspawk = require('../../fixtures/tspawk')
|
|
const {
|
|
cleanCwd,
|
|
cleanTime,
|
|
cleanDate,
|
|
cleanPackumentCache,
|
|
} = require('../../fixtures/clean-snapshot.js')
|
|
|
|
const path = require('node:path')
|
|
const t = require('tap')
|
|
|
|
t.cleanSnapshot = (str) => cleanPackumentCache(cleanDate(cleanTime(cleanCwd(str))))
|
|
|
|
const {
|
|
loadNpmWithRegistry: loadMockNpm,
|
|
workspaceMock,
|
|
} = require('../../fixtures/mock-npm')
|
|
|
|
// tspawk calls preventUnmatched which assures that no scripts run if we don't mock any
|
|
const spawk = tspawk(t)
|
|
|
|
const abbrev = {
|
|
'package.json': '{"name": "abbrev", "version": "1.0.0"}',
|
|
test: 'test file',
|
|
}
|
|
|
|
const packageJson = {
|
|
name: '@npmcli/test-package',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
abbrev: '^1.0.0',
|
|
},
|
|
}
|
|
|
|
t.test('exec commands', async t => {
|
|
await t.test('with args does not run lifecycle scripts', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: {
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
...packageJson,
|
|
scripts: {
|
|
install: 'echo install',
|
|
},
|
|
}),
|
|
abbrev,
|
|
},
|
|
})
|
|
const manifest = registry.manifest({ name: 'abbrev' })
|
|
await registry.package({ manifest })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'abbrev'),
|
|
})
|
|
|
|
await npm.exec('install', ['abbrev'])
|
|
})
|
|
|
|
await t.test('without args runs lifecycle scripts', async t => {
|
|
const lifecycleScripts = [
|
|
'preinstall',
|
|
'install',
|
|
'postinstall',
|
|
'prepublish',
|
|
'preprepare',
|
|
'prepare',
|
|
'postprepare',
|
|
]
|
|
const scripts = {}
|
|
for (const script of lifecycleScripts) {
|
|
spawk.spawn(/.*/, a => {
|
|
runOrder.push(script)
|
|
return a.includes(`${script} lifecycle script`)
|
|
})
|
|
scripts[script] = `${script} lifecycle script`
|
|
}
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: {
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
...packageJson,
|
|
scripts,
|
|
}),
|
|
abbrev,
|
|
},
|
|
})
|
|
const runOrder = []
|
|
const manifest = registry.manifest({ name: 'abbrev' })
|
|
await registry.package({ manifest })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'abbrev'),
|
|
})
|
|
|
|
await npm.exec('install')
|
|
t.strictSame(lifecycleScripts, runOrder, 'all script ran in the correct order')
|
|
})
|
|
|
|
await t.test('should ignore scripts with --ignore-scripts', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: {
|
|
'ignore-scripts': true,
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
...packageJson,
|
|
scripts: {
|
|
install: 'echo install',
|
|
},
|
|
}),
|
|
abbrev,
|
|
},
|
|
})
|
|
const manifest = registry.manifest({ name: 'abbrev' })
|
|
await registry.package({ manifest })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'abbrev'),
|
|
})
|
|
|
|
await npm.exec('install')
|
|
})
|
|
|
|
await t.test('should not self-install package if prefix is the same as CWD', async t => {
|
|
let REIFY_CALLED_WITH = null
|
|
const { npm } = await loadMockNpm(t, {
|
|
mocks: {
|
|
'{LIB}/utils/reify-finish.js': async () => {},
|
|
'@npmcli/run-script': () => {},
|
|
'@npmcli/arborist': function () {
|
|
this.reify = (opts) => {
|
|
REIFY_CALLED_WITH = opts
|
|
}
|
|
},
|
|
},
|
|
prefixOverride: process.cwd(),
|
|
})
|
|
|
|
await npm.exec('install')
|
|
t.equal(REIFY_CALLED_WITH.add.length, 0, 'did not install current directory as a dependency')
|
|
})
|
|
|
|
await t.test('should not install invalid global package name', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
global: true,
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', ['']),
|
|
/Usage:/,
|
|
'should not install invalid package name'
|
|
)
|
|
})
|
|
|
|
await t.test('npm i -g npm engines check success', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
prefixDir: {
|
|
npm: {
|
|
'package.json': JSON.stringify({ name: 'npm', version: '1.0.0' }),
|
|
'index.js': 'console.log("this is npm")',
|
|
},
|
|
},
|
|
config: { global: true },
|
|
})
|
|
const manifest = registry.manifest({
|
|
name: 'npm',
|
|
packuments: [{ version: '1.0.0', engines: { node: '>1' } }],
|
|
})
|
|
await registry.package({ manifest, times: 2 })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.localPrefix, 'npm'),
|
|
})
|
|
await npm.exec('install', ['npm'])
|
|
t.ok('No exceptions happen')
|
|
})
|
|
|
|
await t.test('npm i -g npm engines check failure', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
prefixDir: {
|
|
npm: {
|
|
'package.json': JSON.stringify({ name: 'npm', version: '1.0.0' }),
|
|
'index.js': 'console.log("this is the npm we are installing")',
|
|
},
|
|
},
|
|
config: { global: true },
|
|
})
|
|
|
|
const manifest = registry.manifest({
|
|
name: 'npm',
|
|
packuments: [{ version: '1.0.0', engines: { node: '~1' } }],
|
|
})
|
|
await registry.package({ manifest })
|
|
await t.rejects(
|
|
npm.exec('install', ['npm']),
|
|
{
|
|
message: 'Unsupported engine',
|
|
pkgid: 'npm@1.0.0',
|
|
current: {
|
|
node: process.version,
|
|
npm: '1.0.0',
|
|
},
|
|
required: {
|
|
node: '~1',
|
|
},
|
|
code: 'EBADENGINE',
|
|
}
|
|
)
|
|
})
|
|
|
|
await t.test('npm i -g npm engines check failure forced override', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
prefixDir: {
|
|
npm: {
|
|
'package.json': JSON.stringify({ name: 'npm', version: '1.0.0' }),
|
|
'index.js': 'console.log("this is npm")',
|
|
},
|
|
},
|
|
config: { global: true, force: true },
|
|
})
|
|
const manifest = registry.manifest({
|
|
name: 'npm',
|
|
packuments: [{ version: '1.0.0', engines: { node: '~1' } }],
|
|
})
|
|
await registry.package({ manifest, times: 2 })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.localPrefix, 'npm'),
|
|
})
|
|
await npm.exec('install', ['npm'])
|
|
t.ok('No exceptions happen')
|
|
})
|
|
|
|
t.test('allow-git=none', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-git': 'none',
|
|
audit: false,
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', ['npm/npm']),
|
|
{
|
|
code: 'EALLOWGIT',
|
|
message: 'Fetching packages of type "git" have been disabled',
|
|
package: 'github:npm/npm',
|
|
}
|
|
)
|
|
})
|
|
|
|
t.test('allow-git=root refuses non-root git dependency', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-git': 'root',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({ name: '@npmcli/test-package', version: '1.0.0' }),
|
|
abbrev: {
|
|
'package.json': JSON.stringify({ name: 'abbrev', version: '1.0.0', dependencies: { npm: 'npm/npm' } }),
|
|
},
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', ['./abbrev']),
|
|
/Fetching non-root packages of type "git" have been disabled/
|
|
)
|
|
})
|
|
|
|
t.test('allow-directory=none blocks default symlink install', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-directory': 'none',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: '@npmcli/test-package',
|
|
version: '1.0.0',
|
|
dependencies: { 'dir-dep': 'file:./dir-dep' },
|
|
}),
|
|
'dir-dep': {
|
|
'package.json': JSON.stringify({ name: 'dir-dep', version: '1.0.0' }),
|
|
},
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', []),
|
|
{
|
|
code: 'EALLOWDIRECTORY',
|
|
message: 'Fetching packages of type "directory" have been disabled',
|
|
}
|
|
)
|
|
})
|
|
|
|
t.test('allow-directory=root permits top-level directory dependency', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-directory': 'root',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: '@npmcli/test-package',
|
|
version: '1.0.0',
|
|
dependencies: { 'dir-dep': 'file:./dir-dep' },
|
|
}),
|
|
'dir-dep': {
|
|
'package.json': JSON.stringify({ name: 'dir-dep', version: '1.0.0' }),
|
|
},
|
|
},
|
|
})
|
|
await npm.exec('install', [])
|
|
const installedPkg = require(path.join(npm.prefix, 'node_modules', 'dir-dep', 'package.json'))
|
|
t.equal(installedPkg.name, 'dir-dep', 'dir-dep is installed and readable through node_modules')
|
|
})
|
|
|
|
t.test('allow-git=root soft-skips transitive optional git dependency', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-git': 'root',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({ name: '@npmcli/test-package', version: '1.0.0' }),
|
|
abbrev: {
|
|
'package.json': JSON.stringify({
|
|
name: 'abbrev',
|
|
version: '1.0.0',
|
|
optionalDependencies: { npm: 'npm/npm' },
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
await npm.exec('install', ['./abbrev'])
|
|
t.ok(
|
|
fs.existsSync(path.join(npm.prefix, 'node_modules', 'abbrev', 'package.json')),
|
|
'abbrev (the legitimate parent) is installed'
|
|
)
|
|
t.notOk(
|
|
fs.existsSync(path.join(npm.prefix, 'node_modules', 'npm')),
|
|
'optional transitive git dep is silently skipped'
|
|
)
|
|
})
|
|
|
|
t.test('allow-remote=none does not block registry tarballs', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-remote': 'none',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
...packageJson,
|
|
dependencies: { abbrev: '^1.0.0' },
|
|
}),
|
|
abbrev,
|
|
},
|
|
})
|
|
const manifest = registry.manifest({ name: 'abbrev' })
|
|
await registry.package({ manifest })
|
|
await registry.tarball({
|
|
manifest: manifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'abbrev'),
|
|
})
|
|
await npm.exec('install', [])
|
|
const installed = require(path.join(npm.prefix, 'node_modules', 'abbrev', 'package.json'))
|
|
t.equal(installed.name, 'abbrev', 'registry dep is installed despite allow-remote=none')
|
|
})
|
|
|
|
t.test('allow-remote=none still blocks a user-supplied remote URL', async t => {
|
|
const { npm } = await loadMockNpm(t, {
|
|
config: {
|
|
'allow-remote': 'none',
|
|
audit: false,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: '@npmcli/test-package',
|
|
version: '1.0.0',
|
|
dependencies: { abbrev: 'https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz' },
|
|
}),
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', []),
|
|
{ code: 'EALLOWREMOTE' },
|
|
'user-supplied remote URL is still blocked'
|
|
)
|
|
})
|
|
})
|
|
|
|
t.test('completion', async t => {
|
|
const mockComp = async (t, { noChdir } = {}) => loadMockNpm(t, {
|
|
command: 'install',
|
|
prefixDir: {
|
|
arborist: {
|
|
'package.json': '{}',
|
|
},
|
|
'arborist.txt': 'just a file',
|
|
'other-dir': { a: 'a' },
|
|
},
|
|
...(noChdir ? { chdir: false } : {}),
|
|
})
|
|
|
|
await t.test('completion to folder - has a match', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: './ar' })
|
|
t.strictSame(res, ['arborist'], 'package dir match')
|
|
})
|
|
|
|
await t.test('completion to folder - invalid dir', async t => {
|
|
const { install } = await mockComp(t, { noChdir: true })
|
|
const res = await install.completion({ partialWord: '/does/not/exist' })
|
|
t.strictSame(res, [], 'invalid dir: no matching')
|
|
})
|
|
|
|
await t.test('completion to folder - no matches', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: './pa' })
|
|
t.strictSame(res, [], 'no name match')
|
|
})
|
|
|
|
await t.test('completion to folder - match is not a package', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: './othe' })
|
|
t.strictSame(res, [], 'no name match')
|
|
})
|
|
|
|
await t.test('completion to url', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: 'http://path/to/url' })
|
|
t.strictSame(res, [])
|
|
})
|
|
|
|
await t.test('no /', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: 'toto' })
|
|
t.notOk(res)
|
|
})
|
|
|
|
await t.test('only /', async t => {
|
|
const { install } = await mockComp(t)
|
|
const res = await install.completion({ partialWord: '/' })
|
|
t.strictSame(res, [])
|
|
})
|
|
})
|
|
|
|
t.test('should install in workspace with unhoisted module', async t => {
|
|
const { npm, registry, assert } = await loadMockNpm(t, {
|
|
prefixDir: workspaceMock(t, {
|
|
clean: true,
|
|
workspaces: {
|
|
'workspace-a': {
|
|
'abbrev@1.1.0': { hoist: true },
|
|
},
|
|
'workspace-b': {
|
|
'abbrev@1.1.1': { hoist: false },
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
await registry.setup({
|
|
'abbrev@1.1.0': path.join(npm.prefix, 'tarballs/abbrev@1.1.0'),
|
|
'abbrev@1.1.1': path.join(npm.prefix, 'tarballs/abbrev@1.1.1'),
|
|
})
|
|
registry.nock.post('/-/npm/v1/security/advisories/bulk').reply(200, {})
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageMissing('workspace-b/node_modules/abbrev@1.1.1')
|
|
await npm.exec('install', [])
|
|
assert.packageInstalled('node_modules/abbrev@1.1.0')
|
|
assert.packageInstalled('workspace-b/node_modules/abbrev@1.1.1')
|
|
})
|
|
|
|
t.test('should install in workspace with hoisted modules', async t => {
|
|
const prefixDir = workspaceMock(t, {
|
|
clean: true,
|
|
workspaces: {
|
|
'workspace-a': {
|
|
'abbrev@1.1.0': { hoist: true },
|
|
},
|
|
'workspace-b': {
|
|
'lodash@1.1.1': { hoist: true },
|
|
},
|
|
},
|
|
})
|
|
const { npm, registry, assert } = await loadMockNpm(t, { prefixDir })
|
|
await registry.setup({
|
|
'abbrev@1.1.0': path.join(npm.prefix, 'tarballs/abbrev@1.1.0'),
|
|
'lodash@1.1.1': path.join(npm.prefix, 'tarballs/lodash@1.1.1'),
|
|
})
|
|
registry.nock.post('/-/npm/v1/security/advisories/bulk').reply(200, {})
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageMissing('node_modules/lodash@1.1.1')
|
|
await npm.exec('install', [])
|
|
assert.packageInstalled('node_modules/abbrev@1.1.0')
|
|
assert.packageInstalled('node_modules/lodash@1.1.1')
|
|
})
|
|
|
|
t.test('should install unhoisted module with --workspace flag', async t => {
|
|
const { npm, registry, assert } = await loadMockNpm(t, {
|
|
config: {
|
|
workspace: 'workspace-b',
|
|
},
|
|
prefixDir: workspaceMock(t, {
|
|
clean: true,
|
|
workspaces: {
|
|
'workspace-a': {
|
|
'abbrev@1.1.0': { hoist: true },
|
|
},
|
|
'workspace-b': {
|
|
'abbrev@1.1.1': { hoist: false },
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
await registry.setup({
|
|
'abbrev@1.1.1': path.join(npm.prefix, 'tarballs/abbrev@1.1.1'),
|
|
})
|
|
registry.nock.post('/-/npm/v1/security/advisories/bulk').reply(200, {})
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageMissing('workspace-b/node_modules/abbrev@1.1.1')
|
|
await npm.exec('install', [])
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageInstalled('workspace-b/node_modules/abbrev@1.1.1')
|
|
})
|
|
|
|
t.test('should install hoisted module with --workspace flag', async t => {
|
|
const { npm, registry, assert } = await loadMockNpm(t, {
|
|
config: {
|
|
workspace: 'workspace-b',
|
|
},
|
|
prefixDir: workspaceMock(t, {
|
|
clean: true,
|
|
workspaces: {
|
|
'workspace-a': {
|
|
'abbrev@1.1.0': { hoist: true },
|
|
},
|
|
'workspace-b': {
|
|
'lodash@1.1.1': { hoist: true },
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
await registry.setup({
|
|
'lodash@1.1.1': path.join(npm.prefix, 'tarballs/lodash@1.1.1'),
|
|
})
|
|
registry.nock.post('/-/npm/v1/security/advisories/bulk').reply(200, {})
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageMissing('node_modules/lodash@1.1.1')
|
|
await npm.exec('install', [])
|
|
assert.packageMissing('node_modules/abbrev@1.1.0')
|
|
assert.packageInstalled('node_modules/lodash@1.1.1')
|
|
})
|
|
|
|
t.test('should show install keeps dirty --workspace flag', async t => {
|
|
const { npm, registry, assert } = await loadMockNpm(t, {
|
|
config: {
|
|
workspace: 'workspace-b',
|
|
},
|
|
prefixDir: workspaceMock(t, {
|
|
workspaces: {
|
|
'workspace-a': {
|
|
'abbrev@1.1.0': { clean: false, hoist: true },
|
|
},
|
|
'workspace-b': {
|
|
'lodash@1.1.1': { clean: true, hoist: true },
|
|
},
|
|
},
|
|
}),
|
|
})
|
|
await registry.setup({
|
|
'lodash@1.1.1': path.join(npm.prefix, 'tarballs/lodash@1.1.1'),
|
|
})
|
|
registry.nock.post('/-/npm/v1/security/advisories/bulk').reply(200, {})
|
|
assert.packageDirty('node_modules/abbrev@1.1.0')
|
|
assert.packageMissing('node_modules/lodash@1.1.1')
|
|
await npm.exec('install', [])
|
|
assert.packageDirty('node_modules/abbrev@1.1.0')
|
|
assert.packageInstalled('node_modules/lodash@1.1.1')
|
|
})
|
|
|
|
t.test('devEngines', async t => {
|
|
const mockArguments = {
|
|
globals: {
|
|
'process.platform': 'linux',
|
|
'process.arch': 'x86',
|
|
'process.version': 'v1337.0.0',
|
|
},
|
|
mocks: {
|
|
'{ROOT}/package.json': { version: '42.0.0' },
|
|
},
|
|
}
|
|
|
|
t.test('should utilize devEngines success case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'node',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await npm.exec('install', [])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(!output.includes('EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize devEngines failure case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'nondescript',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', [])
|
|
)
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('error EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize devEngines failure force case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
config: {
|
|
force: true,
|
|
},
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'nondescript',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await npm.exec('install', [])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('warn EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize devEngines 2x warning case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'nondescript',
|
|
onFail: 'warn',
|
|
},
|
|
cpu: {
|
|
name: 'risv',
|
|
onFail: 'warn',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await npm.exec('install', [])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('warn EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize devEngines 2x error case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'nondescript',
|
|
onFail: 'error',
|
|
},
|
|
cpu: {
|
|
name: 'risv',
|
|
onFail: 'error',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', [])
|
|
)
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('error EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize devEngines failure and warning case', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'nondescript',
|
|
},
|
|
cpu: {
|
|
name: 'risv',
|
|
onFail: 'warn',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await t.rejects(
|
|
npm.exec('install', [])
|
|
)
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should show devEngines has no effect on package install', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
alpha: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
'package.json': JSON.stringify({
|
|
name: 'project',
|
|
}),
|
|
},
|
|
})
|
|
await npm.exec('install', ['./alpha'])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(!output.includes('EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should show devEngines has no effect on dev package install', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
alpha: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
'package.json': JSON.stringify({
|
|
name: 'project',
|
|
}),
|
|
},
|
|
config: {
|
|
'save-dev': true,
|
|
},
|
|
})
|
|
await npm.exec('install', ['./alpha'])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(!output.includes('EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should show devEngines doesnt break engines', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
alpha: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
|
|
engines: { node: '1.0.0' },
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
'package.json': JSON.stringify({
|
|
name: 'project',
|
|
}),
|
|
},
|
|
config: { global: true },
|
|
})
|
|
await npm.exec('install', ['./alpha'])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('warn EBADENGINE'))
|
|
})
|
|
|
|
t.test('should not utilize engines in root if devEngines is provided', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
engines: {
|
|
node: '0.0.1',
|
|
},
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'node',
|
|
version: '0.0.1',
|
|
onFail: 'warn',
|
|
},
|
|
},
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
})
|
|
await npm.exec('install')
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(!output.includes('EBADENGINE'))
|
|
t.ok(output.includes('warn EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should utilize engines in root if devEngines is not provided', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
engines: {
|
|
node: '0.0.1',
|
|
},
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
})
|
|
await npm.exec('install')
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(output.includes('EBADENGINE'))
|
|
t.ok(!output.includes('EBADDEVENGINES'))
|
|
})
|
|
|
|
t.test('should show devEngines has no effect on global package install', async t => {
|
|
const { npm, joinedFullOutput } = await loadMockNpm(t, {
|
|
...mockArguments,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'alpha',
|
|
bin: {
|
|
alpha: 'index.js',
|
|
},
|
|
devEngines: {
|
|
runtime: {
|
|
name: 'node',
|
|
version: '0.0.1',
|
|
},
|
|
},
|
|
}),
|
|
'index.js': 'console.log("this is alpha index")',
|
|
},
|
|
config: {
|
|
global: true,
|
|
},
|
|
})
|
|
await npm.exec('install', ['.'])
|
|
const output = joinedFullOutput()
|
|
t.matchSnapshot(output)
|
|
t.ok(!output.includes('EBADENGINE'))
|
|
t.ok(!output.includes('EBADDEVENGINES'))
|
|
})
|
|
})
|
|
|
|
// Issue #8726 - npm install should re-resolve to satisfy peerOptional constraints
|
|
// https://github.com/npm/cli/issues/8726
|
|
//
|
|
// When a lockfile has fetcher@1.1.0 but a peerOptional wants fetcher@1.0.0 (exact), npm install (save: true) should re-resolve fetcher to 1.0.0 to satisfy both the regular dep range (^1.0.0) and the exact peerOptional constraint.
|
|
t.test('issue-8726: npm install re-resolves to satisfy peerOptional constraint', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: { audit: false, 'ignore-scripts': true },
|
|
prefixDir: {
|
|
'linter-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'linter',
|
|
version: '1.0.0',
|
|
dependencies: { scanner: '1.0.0' },
|
|
}),
|
|
},
|
|
'scanner-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'scanner',
|
|
version: '1.0.0',
|
|
peerDependencies: { fetcher: '1.0.0' },
|
|
peerDependenciesMeta: { fetcher: { optional: true } },
|
|
}),
|
|
},
|
|
'hint-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'hint',
|
|
version: '1.0.0',
|
|
dependencies: { fetcher: '^1.0.0' },
|
|
}),
|
|
},
|
|
'fetcher-1.0.0-tarball': {
|
|
'package.json': JSON.stringify({ name: 'fetcher', version: '1.0.0' }),
|
|
},
|
|
'fetcher-1.1.0-tarball': {
|
|
'package.json': JSON.stringify({ name: 'fetcher', version: '1.1.0' }),
|
|
},
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devDependencies: {
|
|
linter: '1.0.0',
|
|
hint: '1.0.0',
|
|
},
|
|
}),
|
|
'package-lock.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
lockfileVersion: 3,
|
|
requires: true,
|
|
packages: {
|
|
'': {
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devDependencies: { linter: '1.0.0', hint: '1.0.0' },
|
|
},
|
|
'node_modules/linter': {
|
|
version: '1.0.0',
|
|
resolved: 'https://registry.npmjs.org/linter/-/linter-1.0.0.tgz',
|
|
dev: true,
|
|
dependencies: { scanner: '1.0.0' },
|
|
},
|
|
'node_modules/scanner': {
|
|
version: '1.0.0',
|
|
resolved: 'https://registry.npmjs.org/scanner/-/scanner-1.0.0.tgz',
|
|
dev: true,
|
|
peerDependencies: { fetcher: '1.0.0' },
|
|
peerDependenciesMeta: { fetcher: { optional: true } },
|
|
},
|
|
'node_modules/hint': {
|
|
version: '1.0.0',
|
|
resolved: 'https://registry.npmjs.org/hint/-/hint-1.0.0.tgz',
|
|
dev: true,
|
|
dependencies: { fetcher: '^1.0.0' },
|
|
},
|
|
'node_modules/fetcher': {
|
|
version: '1.1.0',
|
|
resolved: 'https://registry.npmjs.org/fetcher/-/fetcher-1.1.0.tgz',
|
|
dev: true,
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
|
|
// Only set up mocks that npm install actually needs: tarballs for all installed packages (linter, scanner, hint, fetcher@1.0.0) and the fetcher packument (needed for re-resolution via #problemEdges).
|
|
// Packuments for linter/scanner/hint are NOT needed (already in lockfile).
|
|
// Fetcher@1.1.0 tarball is NOT needed (gets replaced by 1.0.0).
|
|
const linterManifest = registry.manifest({ name: 'linter' })
|
|
await registry.tarball({
|
|
manifest: linterManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'linter-tarball'),
|
|
})
|
|
|
|
const scannerManifest = registry.manifest({ name: 'scanner' })
|
|
await registry.tarball({
|
|
manifest: scannerManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'scanner-tarball'),
|
|
})
|
|
|
|
const hintManifest = registry.manifest({ name: 'hint' })
|
|
await registry.tarball({
|
|
manifest: hintManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'hint-tarball'),
|
|
})
|
|
|
|
const fetcherManifest = registry.manifest({
|
|
name: 'fetcher',
|
|
versions: ['1.0.0', '1.1.0'],
|
|
})
|
|
await registry.package({ manifest: fetcherManifest })
|
|
await registry.tarball({
|
|
manifest: fetcherManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'fetcher-1.0.0-tarball'),
|
|
})
|
|
|
|
await npm.exec('install', [])
|
|
|
|
// Read the updated lockfile and verify fetcher was re-resolved to 1.0.0
|
|
const lockfile = JSON.parse(
|
|
fs.readFileSync(path.join(npm.prefix, 'package-lock.json'), 'utf8')
|
|
)
|
|
t.equal(
|
|
lockfile.packages['node_modules/fetcher'].version,
|
|
'1.0.0',
|
|
'lockfile updated fetcher to satisfy peerOptional constraint'
|
|
)
|
|
|
|
// Also verify the installed package
|
|
const installedFetcher = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(npm.prefix, 'node_modules', 'fetcher', 'package.json'), 'utf8'
|
|
)
|
|
)
|
|
t.equal(
|
|
installedFetcher.version,
|
|
'1.0.0',
|
|
'installed fetcher version satisfies peerOptional constraint'
|
|
)
|
|
})
|
|
|
|
// Issue #8726 - fresh npm install (no lockfile) should pick a version that satisfies both the regular dep range AND the exact peerOptional constraint, even when the peerOptional holder is processed BEFORE the dep is placed.
|
|
// https://github.com/npm/cli/issues/8726
|
|
//
|
|
// This test uses package names that reproduce the real-world alphabetical ordering from the original issue (addons-linter < htmlhint), which causes addons-scanner to be processed from the queue BEFORE htmlhint places node-fetcher.
|
|
// At that point the peerOptional edge has no destination (MISSING, valid for peerOptional).
|
|
// Later, htmlhint places node-fetcher@1.1.0 and the edge becomes INVALID.
|
|
// The fix re-queues addons-scanner so #problemEdges can trigger re-resolution of node-fetcher to 1.0.0.
|
|
//
|
|
// Dependency graph:
|
|
// root -> addons-linter@1.0.0 -> addons-scanner@1.0.0 -> PEER_OPTIONAL node-fetcher@1.0.0
|
|
// root -> htmlhint@1.0.0 -> node-fetcher@^1.0.0
|
|
//
|
|
// Processing order (alphabetical): addons-linter, then addons-scanner (dep of addons-linter), THEN htmlhint (which places node-fetcher@1.1.0)
|
|
t.test('issue-8726: fresh install re-queues scanner when dep placed later', async t => {
|
|
const { npm, registry } = await loadMockNpm(t, {
|
|
config: { audit: false, 'ignore-scripts': true },
|
|
prefixDir: {
|
|
'addons-linter-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'addons-linter',
|
|
version: '1.0.0',
|
|
dependencies: { 'addons-scanner': '1.0.0' },
|
|
}),
|
|
},
|
|
'addons-scanner-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'addons-scanner',
|
|
version: '1.0.0',
|
|
peerDependencies: { 'node-fetcher': '1.0.0' },
|
|
peerDependenciesMeta: { 'node-fetcher': { optional: true } },
|
|
}),
|
|
},
|
|
'htmlhint-tarball': {
|
|
'package.json': JSON.stringify({
|
|
name: 'htmlhint',
|
|
version: '1.0.0',
|
|
dependencies: { 'node-fetcher': '^1.0.0' },
|
|
}),
|
|
},
|
|
'node-fetcher-1.0.0-tarball': {
|
|
'package.json': JSON.stringify({ name: 'node-fetcher', version: '1.0.0' }),
|
|
},
|
|
'node-fetcher-1.1.0-tarball': {
|
|
'package.json': JSON.stringify({ name: 'node-fetcher', version: '1.1.0' }),
|
|
},
|
|
'package.json': JSON.stringify({
|
|
name: 'test-package',
|
|
version: '1.0.0',
|
|
devDependencies: {
|
|
'addons-linter': '1.0.0',
|
|
htmlhint: '1.0.0',
|
|
},
|
|
}),
|
|
// NO package-lock.json — this is a fresh install
|
|
},
|
|
})
|
|
|
|
// Fresh install needs packuments for all packages
|
|
const linterManifest = registry.manifest({
|
|
name: 'addons-linter',
|
|
packuments: [{ version: '1.0.0', dependencies: { 'addons-scanner': '1.0.0' } }],
|
|
})
|
|
await registry.package({ manifest: linterManifest })
|
|
await registry.tarball({
|
|
manifest: linterManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'addons-linter-tarball'),
|
|
})
|
|
|
|
const scannerManifest = registry.manifest({
|
|
name: 'addons-scanner',
|
|
packuments: [{
|
|
version: '1.0.0',
|
|
peerDependencies: { 'node-fetcher': '1.0.0' },
|
|
peerDependenciesMeta: { 'node-fetcher': { optional: true } },
|
|
}],
|
|
})
|
|
await registry.package({ manifest: scannerManifest })
|
|
await registry.tarball({
|
|
manifest: scannerManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'addons-scanner-tarball'),
|
|
})
|
|
|
|
const hintManifest = registry.manifest({
|
|
name: 'htmlhint',
|
|
packuments: [{ version: '1.0.0', dependencies: { 'node-fetcher': '^1.0.0' } }],
|
|
})
|
|
await registry.package({ manifest: hintManifest })
|
|
await registry.tarball({
|
|
manifest: hintManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'htmlhint-tarball'),
|
|
})
|
|
|
|
const fetcherManifest = registry.manifest({
|
|
name: 'node-fetcher',
|
|
packuments: [{ version: '1.0.0' }, { version: '1.1.0' }],
|
|
})
|
|
// Packument is fetched twice: once when htmlhint resolves node-fetcher@^1.0.0 (picking 1.1.0), and again when addons-scanner is re-queued and re-resolves node-fetcher (picking 1.0.0 to satisfy the exact peerOptional spec).
|
|
await registry.package({ manifest: fetcherManifest, times: 2 })
|
|
await registry.tarball({
|
|
manifest: fetcherManifest.versions['1.0.0'],
|
|
tarball: path.join(npm.prefix, 'node-fetcher-1.0.0-tarball'),
|
|
})
|
|
// node-fetcher@1.1.0 tarball is NOT needed: it's replaced by 1.0.0 during tree building (before reification), so it's never downloaded.
|
|
|
|
await npm.exec('install', [])
|
|
|
|
// Verify the lockfile has node-fetcher@1.0.0
|
|
const lockfile = JSON.parse(
|
|
fs.readFileSync(path.join(npm.prefix, 'package-lock.json'), 'utf8')
|
|
)
|
|
t.equal(
|
|
lockfile.packages['node_modules/node-fetcher'].version,
|
|
'1.0.0',
|
|
'fresh install picks node-fetcher@1.0.0 satisfying peerOptional constraint'
|
|
)
|
|
|
|
// Also verify the installed package
|
|
const installedFetcher = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(npm.prefix, 'node_modules', 'node-fetcher', 'package.json'), 'utf8'
|
|
)
|
|
)
|
|
t.equal(
|
|
installedFetcher.version,
|
|
'1.0.0',
|
|
'installed node-fetcher version satisfies peerOptional constraint'
|
|
)
|
|
})
|