diff --git a/docs/lib/content/configuring-npm/package-json.md b/docs/lib/content/configuring-npm/package-json.md index a7a0026ebcbbb..0f82ff49a6eb7 100644 --- a/docs/lib/content/configuring-npm/package-json.md +++ b/docs/lib/content/configuring-npm/package-json.md @@ -1125,6 +1125,32 @@ Like the `os` option, you can also block architectures: The host architecture is determined by `process.arch` +### devEngines + +The `devEngines` field aids engineers working on a codebase to all be using the same tooling. + +You can specify a `devEngines` property in your `package.json` which will run before `install`, `ci`, and `run` commands. + +> Note: `engines` and `devEngines` differ in object shape. They also function very differently. `engines` is designed to alert the user when a dependency uses a differening npm or node version that the project it's being used in, whereas `devEngines` is used to alert people interacting with the source code of a project. + +The supported keys under the `devEngines` property are `cpu`, `os`, `libc`, `runtime`, and `packageManager`. Each property can be an object or an array of objects. Objects must contain `name`, and optionally can specify `version`, and `onFail`. `onFail` can be `warn`, `error`, or `ignore`, and if left undefined is of the same value as `error`. `npm` will assume that you're running with `node`. +Here's an example of a project that will fail if the environment is not `node` and `npm`. If you set `runtime.name` or `packageManager.name` to any other string, it will fail within the npm CLI. + +```json +{ + "devEngines": { + "runtime": { + "name": "node", + "onFail": "error" + }, + "packageManager": { + "name": "npm", + "onFail": "error" + } + } +} +``` + ### private If you set `"private": true` in your package.json, then npm will refuse to diff --git a/lib/arborist-cmd.js b/lib/arborist-cmd.js index 9d247d02fa181..f0167887b0699 100644 --- a/lib/arborist-cmd.js +++ b/lib/arborist-cmd.js @@ -18,6 +18,7 @@ class ArboristCmd extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false + static checkDevEngines = true constructor (npm) { super(npm) diff --git a/lib/base-cmd.js b/lib/base-cmd.js index 99ae6d7f43c70..941ffefad2ef4 100644 --- a/lib/base-cmd.js +++ b/lib/base-cmd.js @@ -1,10 +1,12 @@ const { log } = require('proc-log') class BaseCommand { + // these defaults can be overridden by individual commands static workspaces = false static ignoreImplicitWorkspace = true + static checkDevEngines = false - // these are all overridden by individual commands + // these should always be overridden by individual commands static name = null static description = null static params = null @@ -129,6 +131,63 @@ class BaseCommand { } } + // Checks the devEngines entry in the package.json at this.localPrefix + async checkDevEngines () { + const force = this.npm.flatOptions.force + + const { devEngines } = await require('@npmcli/package-json') + .normalize(this.npm.config.localPrefix) + .then(p => p.content) + .catch(() => ({})) + + if (typeof devEngines === 'undefined') { + return + } + + const { checkDevEngines, currentEnv } = require('npm-install-checks') + const current = currentEnv.devEngines({ + nodeVersion: this.npm.nodeVersion, + npmVersion: this.npm.version, + }) + + const failures = checkDevEngines(devEngines, current) + const warnings = failures.filter(f => f.isWarn) + const errors = failures.filter(f => f.isError) + + const genMsg = (failure, i = 0) => { + return [...new Set([ + // eslint-disable-next-line + i === 0 ? 'The developer of this package has specified the following through devEngines' : '', + `${failure.message}`, + `${failure.errors.map(e => e.message).join('\n')}`, + ])].filter(v => v).join('\n') + } + + [...warnings, ...(force ? errors : [])].forEach((failure, i) => { + const message = genMsg(failure, i) + log.warn('EBADDEVENGINES', message) + log.warn('EBADDEVENGINES', { + current: failure.current, + required: failure.required, + }) + }) + + if (force) { + return + } + + if (errors.length) { + const failure = errors[0] + const message = genMsg(failure) + throw Object.assign(new Error(message), { + engine: failure.engine, + code: 'EBADDEVENGINES', + current: failure.current, + required: failure.required, + }) + } + } + async setWorkspaces () { const { relative } = require('node:path') diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 0a139d08af745..50c745d6d9c07 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -21,6 +21,7 @@ class RunScript extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false static isShellout = true + static checkDevEngines = true static async completion (opts, npm) { const argv = opts.conf.argv.remain diff --git a/lib/npm.js b/lib/npm.js index 5563cec21ba4d..893e032f1eced 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -247,6 +247,10 @@ class Npm { execWorkspaces = true } + if (command.checkDevEngines && !this.global) { + await command.checkDevEngines() + } + return time.start(`command:${cmd}`, () => execWorkspaces ? command.execWorkspaces(args) : command.exec(args)) } diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index fc47c909069f0..4b5582ac8e181 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -200,6 +200,13 @@ const errorMessage = (er, npm) => { ].join('\n')]) break + case 'EBADDEVENGINES': { + const { current, required } = er + summary.push(['EBADDEVENGINES', er.message]) + detail.push(['EBADDEVENGINES', { current, required }]) + break + } + case 'EBADPLATFORM': { const actual = er.current const expected = { ...er.required } diff --git a/node_modules/.gitignore b/node_modules/.gitignore index 48a33e86435ce..dcb00e65f4a73 100644 --- a/node_modules/.gitignore +++ b/node_modules/.gitignore @@ -145,6 +145,9 @@ !/npm-package-arg !/npm-packlist !/npm-pick-manifest +!/npm-pick-manifest/node_modules/ +/npm-pick-manifest/node_modules/* +!/npm-pick-manifest/node_modules/npm-install-checks !/npm-profile !/npm-registry-fetch !/npm-user-validate diff --git a/node_modules/npm-install-checks/lib/current-env.js b/node_modules/npm-install-checks/lib/current-env.js new file mode 100644 index 0000000000000..9babde1f277ff --- /dev/null +++ b/node_modules/npm-install-checks/lib/current-env.js @@ -0,0 +1,63 @@ +const process = require('node:process') +const nodeOs = require('node:os') + +function isMusl (file) { + return file.includes('libc.musl-') || file.includes('ld-musl-') +} + +function os () { + return process.platform +} + +function cpu () { + return process.arch +} + +function libc (osName) { + // this is to make it faster on non linux machines + if (osName !== 'linux') { + return undefined + } + let family + const originalExclude = process.report.excludeNetwork + process.report.excludeNetwork = true + const report = process.report.getReport() + process.report.excludeNetwork = originalExclude + if (report.header?.glibcVersionRuntime) { + family = 'glibc' + } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { + family = 'musl' + } + return family +} + +function devEngines (env = {}) { + const osName = env.os || os() + return { + cpu: { + name: env.cpu || cpu(), + }, + libc: { + name: env.libc || libc(osName), + }, + os: { + name: osName, + version: env.osVersion || nodeOs.release(), + }, + packageManager: { + name: 'npm', + version: env.npmVersion, + }, + runtime: { + name: 'node', + version: env.nodeVersion || process.version, + }, + } +} + +module.exports = { + cpu, + libc, + os, + devEngines, +} diff --git a/node_modules/npm-install-checks/lib/dev-engines.js b/node_modules/npm-install-checks/lib/dev-engines.js new file mode 100644 index 0000000000000..ac5a182330d3b --- /dev/null +++ b/node_modules/npm-install-checks/lib/dev-engines.js @@ -0,0 +1,145 @@ +const satisfies = require('semver/functions/satisfies') +const validRange = require('semver/ranges/valid') + +const recognizedOnFail = [ + 'ignore', + 'warn', + 'error', + 'download', +] + +const recognizedProperties = [ + 'name', + 'version', + 'onFail', +] + +const recognizedEngines = [ + 'packageManager', + 'runtime', + 'cpu', + 'libc', + 'os', +] + +/** checks a devEngine dependency */ +function checkDependency (wanted, current, opts) { + const { engine } = opts + + if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) { + throw new Error(`Invalid non-object value for "${engine}"`) + } + + const properties = Object.keys(wanted) + + for (const prop of properties) { + if (!recognizedProperties.includes(prop)) { + throw new Error(`Invalid property "${prop}" for "${engine}"`) + } + } + + if (!properties.includes('name')) { + throw new Error(`Missing "name" property for "${engine}"`) + } + + if (typeof wanted.name !== 'string') { + throw new Error(`Invalid non-string value for "name" within "${engine}"`) + } + + if (typeof current.name !== 'string' || current.name === '') { + throw new Error(`Unable to determine "name" for "${engine}"`) + } + + if (properties.includes('onFail')) { + if (typeof wanted.onFail !== 'string') { + throw new Error(`Invalid non-string value for "onFail" within "${engine}"`) + } + if (!recognizedOnFail.includes(wanted.onFail)) { + throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`) + } + } + + if (wanted.name !== current.name) { + return new Error( + `Invalid name "${wanted.name}" does not match "${current.name}" for "${engine}"` + ) + } + + if (properties.includes('version')) { + if (typeof wanted.version !== 'string') { + throw new Error(`Invalid non-string value for "version" within "${engine}"`) + } + if (typeof current.version !== 'string' || current.version === '') { + throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`) + } + if (validRange(wanted.version)) { + if (!satisfies(current.version, wanted.version, opts.semver)) { + return new Error( + // eslint-disable-next-line max-len + `Invalid semver version "${wanted.version}" does not match "${current.version}" for "${engine}"` + ) + } + } else if (wanted.version !== current.version) { + return new Error( + `Invalid version "${wanted.version}" does not match "${current.version}" for "${engine}"` + ) + } + } +} + +/** checks devEngines package property and returns array of warnings / errors */ +function checkDevEngines (wanted, current = {}, opts = {}) { + if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) { + throw new Error(`Invalid non-object value for devEngines`) + } + + const errors = [] + + for (const engine of Object.keys(wanted)) { + if (!recognizedEngines.includes(engine)) { + throw new Error(`Invalid property "${engine}"`) + } + const dependencyAsAuthored = wanted[engine] + const dependencies = [dependencyAsAuthored].flat() + const currentEngine = current[engine] || {} + + // this accounts for empty array eg { runtime: [] } and ignores it + if (dependencies.length === 0) { + continue + } + + const depErrors = [] + for (const dep of dependencies) { + const result = checkDependency(dep, currentEngine, { ...opts, engine }) + if (result) { + depErrors.push(result) + } + } + + const invalid = depErrors.length === dependencies.length + + if (invalid) { + const lastDependency = dependencies[dependencies.length - 1] + let onFail = lastDependency.onFail || 'error' + if (onFail === 'download') { + onFail = 'error' + } + + const err = Object.assign(new Error(`Invalid engine "${engine}"`), { + errors: depErrors, + engine, + isWarn: onFail === 'warn', + isError: onFail === 'error', + current: currentEngine, + required: dependencyAsAuthored, + }) + + errors.push(err) + } + } + return errors +} + +module.exports = { + checkDevEngines, +} diff --git a/node_modules/npm-install-checks/lib/index.js b/node_modules/npm-install-checks/lib/index.js index 545472b61dc60..7170292087308 100644 --- a/node_modules/npm-install-checks/lib/index.js +++ b/node_modules/npm-install-checks/lib/index.js @@ -1,4 +1,6 @@ const semver = require('semver') +const currentEnv = require('./current-env') +const { checkDevEngines } = require('./dev-engines') const checkEngine = (target, npmVer, nodeVer, force = false) => { const nodev = force ? null : nodeVer @@ -20,44 +22,29 @@ const checkEngine = (target, npmVer, nodeVer, force = false) => { } } -const isMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-') - const checkPlatform = (target, force = false, environment = {}) => { if (force) { return } - const platform = environment.os || process.platform - const arch = environment.cpu || process.arch - const osOk = target.os ? checkList(platform, target.os) : true - const cpuOk = target.cpu ? checkList(arch, target.cpu) : true + const os = environment.os || currentEnv.os() + const cpu = environment.cpu || currentEnv.cpu() + const libc = environment.libc || currentEnv.libc(os) - let libcOk = true - let libcFamily = null - if (target.libc) { - // libc checks only work in linux, any value is a failure if we aren't - if (environment.libc) { - libcOk = checkList(environment.libc, target.libc) - } else if (platform !== 'linux') { - libcOk = false - } else { - const report = process.report.getReport() - if (report.header?.glibcVersionRuntime) { - libcFamily = 'glibc' - } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { - libcFamily = 'musl' - } - libcOk = libcFamily ? checkList(libcFamily, target.libc) : false - } + const osOk = target.os ? checkList(os, target.os) : true + const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true + let libcOk = target.libc ? checkList(libc, target.libc) : true + if (target.libc && !libc) { + libcOk = false } if (!osOk || !cpuOk || !libcOk) { throw Object.assign(new Error('Unsupported platform'), { pkgid: target._id, current: { - os: platform, - cpu: arch, - libc: libcFamily, + os, + cpu, + libc, }, required: { os: target.os, @@ -98,4 +85,6 @@ const checkList = (value, list) => { module.exports = { checkEngine, checkPlatform, + checkDevEngines, + currentEnv, } diff --git a/node_modules/npm-install-checks/package.json b/node_modules/npm-install-checks/package.json index 11a3b87750e25..e9e69575a6dc6 100644 --- a/node_modules/npm-install-checks/package.json +++ b/node_modules/npm-install-checks/package.json @@ -1,28 +1,29 @@ { "name": "npm-install-checks", - "version": "6.3.0", + "version": "7.1.0", "description": "Check the engines and platform fields in package.json", "main": "lib/index.js", "dependencies": { "semver": "^7.1.1" }, "devDependencies": { - "@npmcli/eslint-config": "^4.0.0", - "@npmcli/template-oss": "4.19.0", + "@npmcli/eslint-config": "^5.0.0", + "@npmcli/template-oss": "4.23.3", "tap": "^16.0.1" }, "scripts": { "test": "tap", - "lint": "eslint \"**/*.js\"", + "lint": "npm run eslint", "postlint": "template-oss-check", "template-oss-apply": "template-oss-apply --force", - "lintfix": "npm run lint -- --fix", + "lintfix": "npm run eslint -- --fix", "snap": "tap", - "posttest": "npm run lint" + "posttest": "npm run lint", + "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" }, "repository": { "type": "git", - "url": "https://github.com/npm/npm-install-checks.git" + "url": "git+https://github.com/npm/npm-install-checks.git" }, "keywords": [ "npm,", @@ -34,12 +35,12 @@ "lib/" ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" }, "author": "GitHub Inc.", "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.19.0", + "version": "4.23.3", "publish": "true" }, "tap": { diff --git a/node_modules/npm-pick-manifest/node_modules/npm-install-checks/LICENSE b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/LICENSE new file mode 100644 index 0000000000000..3bed8320c15b2 --- /dev/null +++ b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) Robert Kowalski and Isaac Z. Schlueter ("Authors") +All rights reserved. + +The BSD License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/node_modules/npm-pick-manifest/node_modules/npm-install-checks/lib/index.js b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/lib/index.js new file mode 100644 index 0000000000000..545472b61dc60 --- /dev/null +++ b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/lib/index.js @@ -0,0 +1,101 @@ +const semver = require('semver') + +const checkEngine = (target, npmVer, nodeVer, force = false) => { + const nodev = force ? null : nodeVer + const eng = target.engines + const opt = { includePrerelease: true } + if (!eng) { + return + } + + const nodeFail = nodev && eng.node && !semver.satisfies(nodev, eng.node, opt) + const npmFail = npmVer && eng.npm && !semver.satisfies(npmVer, eng.npm, opt) + if (nodeFail || npmFail) { + throw Object.assign(new Error('Unsupported engine'), { + pkgid: target._id, + current: { node: nodeVer, npm: npmVer }, + required: eng, + code: 'EBADENGINE', + }) + } +} + +const isMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-') + +const checkPlatform = (target, force = false, environment = {}) => { + if (force) { + return + } + + const platform = environment.os || process.platform + const arch = environment.cpu || process.arch + const osOk = target.os ? checkList(platform, target.os) : true + const cpuOk = target.cpu ? checkList(arch, target.cpu) : true + + let libcOk = true + let libcFamily = null + if (target.libc) { + // libc checks only work in linux, any value is a failure if we aren't + if (environment.libc) { + libcOk = checkList(environment.libc, target.libc) + } else if (platform !== 'linux') { + libcOk = false + } else { + const report = process.report.getReport() + if (report.header?.glibcVersionRuntime) { + libcFamily = 'glibc' + } else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) { + libcFamily = 'musl' + } + libcOk = libcFamily ? checkList(libcFamily, target.libc) : false + } + } + + if (!osOk || !cpuOk || !libcOk) { + throw Object.assign(new Error('Unsupported platform'), { + pkgid: target._id, + current: { + os: platform, + cpu: arch, + libc: libcFamily, + }, + required: { + os: target.os, + cpu: target.cpu, + libc: target.libc, + }, + code: 'EBADPLATFORM', + }) + } +} + +const checkList = (value, list) => { + if (typeof list === 'string') { + list = [list] + } + if (list.length === 1 && list[0] === 'any') { + return true + } + // match none of the negated values, and at least one of the + // non-negated values, if any are present. + let negated = 0 + let match = false + for (const entry of list) { + const negate = entry.charAt(0) === '!' + const test = negate ? entry.slice(1) : entry + if (negate) { + negated++ + if (value === test) { + return false + } + } else { + match = match || value === test + } + } + return match || negated === list.length +} + +module.exports = { + checkEngine, + checkPlatform, +} diff --git a/node_modules/npm-pick-manifest/node_modules/npm-install-checks/package.json b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/package.json new file mode 100644 index 0000000000000..11a3b87750e25 --- /dev/null +++ b/node_modules/npm-pick-manifest/node_modules/npm-install-checks/package.json @@ -0,0 +1,51 @@ +{ + "name": "npm-install-checks", + "version": "6.3.0", + "description": "Check the engines and platform fields in package.json", + "main": "lib/index.js", + "dependencies": { + "semver": "^7.1.1" + }, + "devDependencies": { + "@npmcli/eslint-config": "^4.0.0", + "@npmcli/template-oss": "4.19.0", + "tap": "^16.0.1" + }, + "scripts": { + "test": "tap", + "lint": "eslint \"**/*.js\"", + "postlint": "template-oss-check", + "template-oss-apply": "template-oss-apply --force", + "lintfix": "npm run lint -- --fix", + "snap": "tap", + "posttest": "npm run lint" + }, + "repository": { + "type": "git", + "url": "https://github.com/npm/npm-install-checks.git" + }, + "keywords": [ + "npm,", + "install" + ], + "license": "BSD-2-Clause", + "files": [ + "bin/", + "lib/" + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "author": "GitHub Inc.", + "templateOSS": { + "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", + "version": "4.19.0", + "publish": "true" + }, + "tap": { + "nyc-arg": [ + "--exclude", + "tap-snapshots/**" + ] + } +} diff --git a/package-lock.json b/package-lock.json index 5f2d5ce1edae5..52e32045c9618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,7 @@ "nopt": "^7.2.1", "normalize-package-data": "^6.0.2", "npm-audit-report": "^5.0.0", - "npm-install-checks": "^6.3.0", + "npm-install-checks": "^7.1.0", "npm-package-arg": "^11.0.3", "npm-pick-manifest": "^9.1.0", "npm-profile": "^10.0.0", @@ -9430,16 +9430,16 @@ } }, "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.0.tgz", + "integrity": "sha512-bkTildVlofeMX7wiOaWk3PlW7YcBXAuEc7TWpOxwUgalG5ZvgT/ms+6OX9zt7iGLv4+VhKbRZhpOfgQJzk1YAw==", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm-normalize-package-bin": { @@ -9497,6 +9497,19 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/npm-pick-manifest/node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/npm-profile": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/npm-profile/-/npm-profile-10.0.0.tgz", @@ -15810,6 +15823,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "workspaces/arborist/node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "workspaces/config": { "name": "@npmcli/config", "version": "8.3.4", diff --git a/package.json b/package.json index e778411933208..b3f568d03c0e0 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "nopt": "^7.2.1", "normalize-package-data": "^6.0.2", "npm-audit-report": "^5.0.0", - "npm-install-checks": "^6.3.0", + "npm-install-checks": "^7.1.0", "npm-package-arg": "^11.0.3", "npm-pick-manifest": "^9.1.0", "npm-profile": "^10.0.0", diff --git a/tap-snapshots/test/lib/commands/install.js.test.cjs b/tap-snapshots/test/lib/commands/install.js.test.cjs new file mode 100644 index 0000000000000..d5e223dcc846e --- /dev/null +++ b/tap-snapshots/test/lib/commands/install.js.test.cjs @@ -0,0 +1,291 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/commands/install.js TAP devEngines should not utilize engines in root if devEngines is provided > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +warn EBADDEVENGINES The developer of this package has specified the following through devEngines +warn EBADDEVENGINES Invalid engine "runtime" +warn EBADDEVENGINES Invalid semver version "0.0.1" does not match "v1337.0.0" for "runtime" +warn EBADDEVENGINES { +warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +warn EBADDEVENGINES required: { name: 'node', version: '0.0.1', onFail: 'warn' } +warn EBADDEVENGINES } +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly reify moves {} +silly audit report null + +up to date, audited 1 package in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should show devEngines doesnt break engines > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--global" "true" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly placeDep ROOT alpha@ OK for: want: file:../../prefix/alpha +warn EBADENGINE Unsupported engine { +warn EBADENGINE package: undefined, +warn EBADENGINE required: { node: '1.0.0' }, +warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' } +warn EBADENGINE } +warn EBADENGINE Unsupported engine { +warn EBADENGINE package: undefined, +warn EBADENGINE required: { node: '1.0.0' }, +warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' } +warn EBADENGINE } +silly reify moves {} +silly ADD node_modules/alpha + +added 1 package in {TIME} +` + +exports[`test/lib/commands/install.js TAP devEngines should show devEngines has no effect on dev package install > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--save-dev" "true" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly placeDep ROOT alpha@ OK for: want: file:alpha +silly reify moves {} +silly audit bulk request {} +silly audit report null +silly ADD node_modules/alpha + +added 1 package, and audited 3 packages in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should show devEngines has no effect on package install > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly placeDep ROOT alpha@ OK for: want: file:alpha +silly reify moves {} +silly audit bulk request {} +silly audit report null +silly ADD node_modules/alpha + +added 1 package, and audited 3 packages in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines 2x error case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +verbose stack Error: The developer of this package has specified the following through devEngines +verbose stack Invalid engine "runtime" +verbose stack Invalid name "nondescript" does not match "node" for "runtime" +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9) +error code EBADDEVENGINES +error EBADDEVENGINES The developer of this package has specified the following through devEngines +error EBADDEVENGINES Invalid engine "runtime" +error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime" +error EBADDEVENGINES { +error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +error EBADDEVENGINES required: { name: 'nondescript', onFail: 'error' } +error EBADDEVENGINES } +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines 2x warning case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +warn EBADDEVENGINES The developer of this package has specified the following through devEngines +warn EBADDEVENGINES Invalid engine "runtime" +warn EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime" +warn EBADDEVENGINES { +warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +warn EBADDEVENGINES required: { name: 'nondescript', onFail: 'warn' } +warn EBADDEVENGINES } +warn EBADDEVENGINES Invalid engine "cpu" +warn EBADDEVENGINES Invalid name "risv" does not match "x86" for "cpu" +warn EBADDEVENGINES { +warn EBADDEVENGINES current: { name: 'x86' }, +warn EBADDEVENGINES required: { name: 'risv', onFail: 'warn' } +warn EBADDEVENGINES } +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly reify moves {} +silly audit report null + +up to date, audited 1 package in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure and warning case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +warn EBADDEVENGINES The developer of this package has specified the following through devEngines +warn EBADDEVENGINES Invalid engine "cpu" +warn EBADDEVENGINES Invalid name "risv" does not match "x86" for "cpu" +warn EBADDEVENGINES { +warn EBADDEVENGINES current: { name: 'x86' }, +warn EBADDEVENGINES required: { name: 'risv', onFail: 'warn' } +warn EBADDEVENGINES } +verbose stack Error: The developer of this package has specified the following through devEngines +verbose stack Invalid engine "runtime" +verbose stack Invalid name "nondescript" does not match "node" for "runtime" +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9) +error code EBADDEVENGINES +error EBADDEVENGINES The developer of this package has specified the following through devEngines +error EBADDEVENGINES Invalid engine "runtime" +error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime" +error EBADDEVENGINES { +error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +error EBADDEVENGINES required: { name: 'nondescript' } +error EBADDEVENGINES } +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +verbose stack Error: The developer of this package has specified the following through devEngines +verbose stack Invalid engine "runtime" +verbose stack Invalid name "nondescript" does not match "node" for "runtime" +verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27) +verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9) +error code EBADDEVENGINES +error EBADDEVENGINES The developer of this package has specified the following through devEngines +error EBADDEVENGINES Invalid engine "runtime" +error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime" +error EBADDEVENGINES { +error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +error EBADDEVENGINES required: { name: 'nondescript' } +error EBADDEVENGINES } +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure force case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--force" "true" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +warn using --force Recommended protections disabled. +silly logfile done cleaning log files +warn EBADDEVENGINES The developer of this package has specified the following through devEngines +warn EBADDEVENGINES Invalid engine "runtime" +warn EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime" +warn EBADDEVENGINES { +warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' }, +warn EBADDEVENGINES required: { name: 'nondescript' } +warn EBADDEVENGINES } +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly reify moves {} +silly audit report null + +up to date, audited 1 package in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines success case > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +silly reify moves {} +silly audit report null + +up to date, audited 1 package in {TIME} +found 0 vulnerabilities +` + +exports[`test/lib/commands/install.js TAP devEngines should utilize engines in root if devEngines is not provided > must match snapshot 1`] = ` +silly config load:file:{CWD}/npmrc +silly config load:file:{CWD}/prefix/.npmrc +silly config load:file:{CWD}/home/.npmrc +silly config load:file:{CWD}/global/etc/npmrc +verbose title npm +verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" +verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}- +verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log +silly logfile done cleaning log files +silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize} +silly idealTree buildDeps +warn EBADENGINE Unsupported engine { +warn EBADENGINE package: undefined, +warn EBADENGINE required: { node: '0.0.1' }, +warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' } +warn EBADENGINE } +silly reify moves {} +silly audit report null + +up to date, audited 1 package in {TIME} +found 0 vulnerabilities +` diff --git a/test/fixtures/clean-snapshot.js b/test/fixtures/clean-snapshot.js index bcbf699cb81fc..3439400b576ae 100644 --- a/test/fixtures/clean-snapshot.js +++ b/test/fixtures/clean-snapshot.js @@ -42,12 +42,18 @@ const cleanZlib = str => str .replace(/"integrity": ".*",/g, '"integrity": "{integrity}",') .replace(/"size": [0-9]*,/g, '"size": "{size}",') +const cleanPackumentCache = str => str + .replace(/heap:[0-9]*/g, 'heap:{heap}') + .replace(/maxSize:[0-9]*/g, 'maxSize:{maxSize}') + .replace(/maxEntrySize:[0-9]*/g, 'maxEntrySize:{maxEntrySize}') + module.exports = { cleanCwd, cleanDate, cleanNewlines, cleanTime, cleanZlib, + cleanPackumentCache, normalizePath, pathRegex, } diff --git a/test/lib/commands/install.js b/test/lib/commands/install.js index 0273f3deec73e..9a99092b8efed 100644 --- a/test/lib/commands/install.js +++ b/test/lib/commands/install.js @@ -1,8 +1,16 @@ 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, @@ -400,3 +408,283 @@ t.test('should show install keeps dirty --workspace flag', async t => { 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')) + }) +}) diff --git a/test/lib/npm.js b/test/lib/npm.js index 00ef3f79b04c1..739aa28eb0343 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -149,8 +149,8 @@ t.test('npm.load', async t => { 'does not change npm.command when another command is called') t.match(logs, [ + /timing config:load:flatten Completed in [0-9.]+ms/, /timing command:config Completed in [0-9.]+ms/, - /timing command:get Completed in [0-9.]+ms/, ]) t.same(outputs, ['scope=@foo\nusage=false']) }) diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 06d03bbce7a32..6bd4e9407e72d 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -195,7 +195,10 @@ module.exports = cls => class IdealTreeBuilder extends cls { for (const node of this.idealTree.inventory.values()) { if (!node.optional) { try { - checkEngine(node.package, npmVersion, nodeVersion, this.options.force) + // if devEngines is present in the root node we ignore the engines check + if (!(node.isRoot && node.package.devEngines)) { + checkEngine(node.package, npmVersion, nodeVersion, this.options.force) + } } catch (err) { if (engineStrict) { throw err diff --git a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs index fb847598577b9..de205053a2cd4 100644 --- a/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs +++ b/workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs @@ -97921,6 +97921,20 @@ ArboristNode { } ` +exports[`test/arborist/build-ideal-tree.js TAP should take devEngines in account > must match snapshot 1`] = ` +{ + "name": "empty-update", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "empty-update" + } + } +} + +` + exports[`test/arborist/build-ideal-tree.js TAP store files with a custom indenting > must match snapshot 1`] = ` { "name": "tab-indented-package-json", diff --git a/workspaces/arborist/test/arborist/build-ideal-tree.js b/workspaces/arborist/test/arborist/build-ideal-tree.js index 807287c73cf11..2972a00b5580e 100644 --- a/workspaces/arborist/test/arborist/build-ideal-tree.js +++ b/workspaces/arborist/test/arborist/build-ideal-tree.js @@ -3979,3 +3979,18 @@ t.test('store files with a custom indenting', async t => { const tree = await buildIdeal(path) t.matchSnapshot(String(tree.meta)) }) + +t.test('should take devEngines in account', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'empty-update', + devEngines: { + runtime: { + name: 'node', + }, + }, + }), + }) + const tree = await buildIdeal(path) + t.matchSnapshot(String(tree.meta)) +})