Skip to content

Commit 4d57928

Browse files
authoredOct 3, 2024··
feat: devEngines (#7766)
This PR adds a check for `devEngines` in the current projects `package.json` as defined in the spec here: openjs-foundation/package-metadata-interoperability-collab-space#15 This PR utilizes a `checkDevEngines` function defined within `npm-install-checks` open here: npm/npm-install-checks#116 The goal of this pr is to have a check for specific npm commands `install`, and `run` consult the `devEngines` property before execution and check if the current system / environment. For `npm ` the runtime will always be `node` and the `packageManager` will always be `npm`, if a project is defined as not those two envs and it's required we'll throw. > Note the current `engines` property is checked when you install your dependencies. Each packages `engines` are checked with your environment. However, `devEngines` operates on commands for maintainers of a package, service, project when install and run commands are executed and is meant to enforce / guide maintainers to all be using the same engine / env and or versions.
1 parent 95e2cb1 commit 4d57928

File tree

13 files changed

+765
-3
lines changed

13 files changed

+765
-3
lines changed
 

‎docs/lib/content/configuring-npm/package-json.md

+26
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,32 @@ Like the `os` option, you can also block architectures:
11291129
11301130
The host architecture is determined by `process.arch`
11311131
1132+
### devEngines
1133+
1134+
The `devEngines` field aids engineers working on a codebase to all be using the same tooling.
1135+
1136+
You can specify a `devEngines` property in your `package.json` which will run before `install`, `ci`, and `run` commands.
1137+
1138+
> 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.
1139+
1140+
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`.
1141+
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.
1142+
1143+
```json
1144+
{
1145+
"devEngines": {
1146+
"runtime": {
1147+
"name": "node",
1148+
"onFail": "error"
1149+
},
1150+
"packageManager": {
1151+
"name": "npm",
1152+
"onFail": "error"
1153+
}
1154+
}
1155+
}
1156+
```
1157+
11321158
### private
11331159

11341160
If you set `"private": true` in your package.json, then npm will refuse to

‎lib/arborist-cmd.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class ArboristCmd extends BaseCommand {
1818

1919
static workspaces = true
2020
static ignoreImplicitWorkspace = false
21+
static checkDevEngines = true
2122

2223
constructor (npm) {
2324
super(npm)

‎lib/base-cmd.js

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
const { log } = require('proc-log')
22

33
class BaseCommand {
4+
// these defaults can be overridden by individual commands
45
static workspaces = false
56
static ignoreImplicitWorkspace = true
7+
static checkDevEngines = false
68

7-
// these are all overridden by individual commands
9+
// these should always be overridden by individual commands
810
static name = null
911
static description = null
1012
static params = null
@@ -129,6 +131,63 @@ class BaseCommand {
129131
}
130132
}
131133

134+
// Checks the devEngines entry in the package.json at this.localPrefix
135+
async checkDevEngines () {
136+
const force = this.npm.flatOptions.force
137+
138+
const { devEngines } = await require('@npmcli/package-json')
139+
.normalize(this.npm.config.localPrefix)
140+
.then(p => p.content)
141+
.catch(() => ({}))
142+
143+
if (typeof devEngines === 'undefined') {
144+
return
145+
}
146+
147+
const { checkDevEngines, currentEnv } = require('npm-install-checks')
148+
const current = currentEnv.devEngines({
149+
nodeVersion: this.npm.nodeVersion,
150+
npmVersion: this.npm.version,
151+
})
152+
153+
const failures = checkDevEngines(devEngines, current)
154+
const warnings = failures.filter(f => f.isWarn)
155+
const errors = failures.filter(f => f.isError)
156+
157+
const genMsg = (failure, i = 0) => {
158+
return [...new Set([
159+
// eslint-disable-next-line
160+
i === 0 ? 'The developer of this package has specified the following through devEngines' : '',
161+
`${failure.message}`,
162+
`${failure.errors.map(e => e.message).join('\n')}`,
163+
])].filter(v => v).join('\n')
164+
}
165+
166+
[...warnings, ...(force ? errors : [])].forEach((failure, i) => {
167+
const message = genMsg(failure, i)
168+
log.warn('EBADDEVENGINES', message)
169+
log.warn('EBADDEVENGINES', {
170+
current: failure.current,
171+
required: failure.required,
172+
})
173+
})
174+
175+
if (force) {
176+
return
177+
}
178+
179+
if (errors.length) {
180+
const failure = errors[0]
181+
const message = genMsg(failure)
182+
throw Object.assign(new Error(message), {
183+
engine: failure.engine,
184+
code: 'EBADDEVENGINES',
185+
current: failure.current,
186+
required: failure.required,
187+
})
188+
}
189+
}
190+
132191
async setWorkspaces () {
133192
const { relative } = require('node:path')
134193

‎lib/commands/run-script.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class RunScript extends BaseCommand {
2121
static workspaces = true
2222
static ignoreImplicitWorkspace = false
2323
static isShellout = true
24+
static checkDevEngines = true
2425

2526
static async completion (opts, npm) {
2627
const argv = opts.conf.argv.remain

‎lib/npm.js

+4
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ class Npm {
247247
execWorkspaces = true
248248
}
249249

250+
if (command.checkDevEngines && !this.global) {
251+
await command.checkDevEngines()
252+
}
253+
250254
return time.start(`command:${cmd}`, () =>
251255
execWorkspaces ? command.execWorkspaces(args) : command.exec(args))
252256
}

‎lib/utils/error-message.js

+7
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ const errorMessage = (er, npm) => {
200200
].join('\n')])
201201
break
202202

203+
case 'EBADDEVENGINES': {
204+
const { current, required } = er
205+
summary.push(['EBADDEVENGINES', er.message])
206+
detail.push(['EBADDEVENGINES', { current, required }])
207+
break
208+
}
209+
203210
case 'EBADPLATFORM': {
204211
const actual = er.current
205212
const expected = { ...er.required }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/* IMPORTANT
2+
* This snapshot file is auto-generated, but designed for humans.
3+
* It should be checked into source control and tracked carefully.
4+
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
5+
* Make sure to inspect the output below. Do not ignore changes!
6+
*/
7+
'use strict'
8+
exports[`test/lib/commands/install.js TAP devEngines should not utilize engines in root if devEngines is provided > must match snapshot 1`] = `
9+
silly config load:file:{CWD}/npmrc
10+
silly config load:file:{CWD}/prefix/.npmrc
11+
silly config load:file:{CWD}/home/.npmrc
12+
silly config load:file:{CWD}/global/etc/npmrc
13+
verbose title npm
14+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
15+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
16+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
17+
silly logfile done cleaning log files
18+
warn EBADDEVENGINES The developer of this package has specified the following through devEngines
19+
warn EBADDEVENGINES Invalid engine "runtime"
20+
warn EBADDEVENGINES Invalid semver version "0.0.1" does not match "v1337.0.0" for "runtime"
21+
warn EBADDEVENGINES {
22+
warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
23+
warn EBADDEVENGINES required: { name: 'node', version: '0.0.1', onFail: 'warn' }
24+
warn EBADDEVENGINES }
25+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
26+
silly idealTree buildDeps
27+
silly reify moves {}
28+
silly audit report null
29+
30+
up to date, audited 1 package in {TIME}
31+
found 0 vulnerabilities
32+
`
33+
34+
exports[`test/lib/commands/install.js TAP devEngines should show devEngines doesnt break engines > must match snapshot 1`] = `
35+
silly config load:file:{CWD}/npmrc
36+
silly config load:file:{CWD}/home/.npmrc
37+
silly config load:file:{CWD}/global/etc/npmrc
38+
verbose title npm
39+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--global" "true"
40+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
41+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
42+
silly logfile done cleaning log files
43+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
44+
silly idealTree buildDeps
45+
silly placeDep ROOT alpha@ OK for: want: file:../../prefix/alpha
46+
warn EBADENGINE Unsupported engine {
47+
warn EBADENGINE package: undefined,
48+
warn EBADENGINE required: { node: '1.0.0' },
49+
warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' }
50+
warn EBADENGINE }
51+
warn EBADENGINE Unsupported engine {
52+
warn EBADENGINE package: undefined,
53+
warn EBADENGINE required: { node: '1.0.0' },
54+
warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' }
55+
warn EBADENGINE }
56+
silly reify moves {}
57+
silly ADD node_modules/alpha
58+
59+
added 1 package in {TIME}
60+
`
61+
62+
exports[`test/lib/commands/install.js TAP devEngines should show devEngines has no effect on dev package install > must match snapshot 1`] = `
63+
silly config load:file:{CWD}/npmrc
64+
silly config load:file:{CWD}/prefix/.npmrc
65+
silly config load:file:{CWD}/home/.npmrc
66+
silly config load:file:{CWD}/global/etc/npmrc
67+
verbose title npm
68+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--save-dev" "true"
69+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
70+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
71+
silly logfile done cleaning log files
72+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
73+
silly idealTree buildDeps
74+
silly placeDep ROOT alpha@ OK for: want: file:alpha
75+
silly reify moves {}
76+
silly audit bulk request {}
77+
silly audit report null
78+
silly ADD node_modules/alpha
79+
80+
added 1 package, and audited 3 packages in {TIME}
81+
found 0 vulnerabilities
82+
`
83+
84+
exports[`test/lib/commands/install.js TAP devEngines should show devEngines has no effect on global package install > must match snapshot 1`] = `
85+
silly config load:file:{CWD}/npmrc
86+
silly config load:file:{CWD}/home/.npmrc
87+
silly config load:file:{CWD}/global/etc/npmrc
88+
verbose title npm
89+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--global" "true"
90+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
91+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
92+
silly logfile done cleaning log files
93+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
94+
silly idealTree buildDeps
95+
silly placeDep ROOT alpha@ OK for: want: file:../../prefix
96+
silly reify moves {}
97+
silly ADD node_modules/alpha
98+
99+
added 1 package in {TIME}
100+
`
101+
102+
exports[`test/lib/commands/install.js TAP devEngines should show devEngines has no effect on package install > must match snapshot 1`] = `
103+
silly config load:file:{CWD}/npmrc
104+
silly config load:file:{CWD}/prefix/.npmrc
105+
silly config load:file:{CWD}/home/.npmrc
106+
silly config load:file:{CWD}/global/etc/npmrc
107+
verbose title npm
108+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
109+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
110+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
111+
silly logfile done cleaning log files
112+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
113+
silly idealTree buildDeps
114+
silly placeDep ROOT alpha@ OK for: want: file:alpha
115+
silly reify moves {}
116+
silly audit bulk request {}
117+
silly audit report null
118+
silly ADD node_modules/alpha
119+
120+
added 1 package, and audited 3 packages in {TIME}
121+
found 0 vulnerabilities
122+
`
123+
124+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines 2x error case > must match snapshot 1`] = `
125+
silly config load:file:{CWD}/npmrc
126+
silly config load:file:{CWD}/prefix/.npmrc
127+
silly config load:file:{CWD}/home/.npmrc
128+
silly config load:file:{CWD}/global/etc/npmrc
129+
verbose title npm
130+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
131+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
132+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
133+
silly logfile done cleaning log files
134+
verbose stack Error: The developer of this package has specified the following through devEngines
135+
verbose stack Invalid engine "runtime"
136+
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
137+
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27)
138+
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7)
139+
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9)
140+
error code EBADDEVENGINES
141+
error EBADDEVENGINES The developer of this package has specified the following through devEngines
142+
error EBADDEVENGINES Invalid engine "runtime"
143+
error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime"
144+
error EBADDEVENGINES {
145+
error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
146+
error EBADDEVENGINES required: { name: 'nondescript', onFail: 'error' }
147+
error EBADDEVENGINES }
148+
`
149+
150+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines 2x warning case > must match snapshot 1`] = `
151+
silly config load:file:{CWD}/npmrc
152+
silly config load:file:{CWD}/prefix/.npmrc
153+
silly config load:file:{CWD}/home/.npmrc
154+
silly config load:file:{CWD}/global/etc/npmrc
155+
verbose title npm
156+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
157+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
158+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
159+
silly logfile done cleaning log files
160+
warn EBADDEVENGINES The developer of this package has specified the following through devEngines
161+
warn EBADDEVENGINES Invalid engine "runtime"
162+
warn EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime"
163+
warn EBADDEVENGINES {
164+
warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
165+
warn EBADDEVENGINES required: { name: 'nondescript', onFail: 'warn' }
166+
warn EBADDEVENGINES }
167+
warn EBADDEVENGINES Invalid engine "cpu"
168+
warn EBADDEVENGINES Invalid name "risv" does not match "x86" for "cpu"
169+
warn EBADDEVENGINES {
170+
warn EBADDEVENGINES current: { name: 'x86' },
171+
warn EBADDEVENGINES required: { name: 'risv', onFail: 'warn' }
172+
warn EBADDEVENGINES }
173+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
174+
silly idealTree buildDeps
175+
silly reify moves {}
176+
silly audit report null
177+
178+
up to date, audited 1 package in {TIME}
179+
found 0 vulnerabilities
180+
`
181+
182+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure and warning case > must match snapshot 1`] = `
183+
silly config load:file:{CWD}/npmrc
184+
silly config load:file:{CWD}/prefix/.npmrc
185+
silly config load:file:{CWD}/home/.npmrc
186+
silly config load:file:{CWD}/global/etc/npmrc
187+
verbose title npm
188+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
189+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
190+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
191+
silly logfile done cleaning log files
192+
warn EBADDEVENGINES The developer of this package has specified the following through devEngines
193+
warn EBADDEVENGINES Invalid engine "cpu"
194+
warn EBADDEVENGINES Invalid name "risv" does not match "x86" for "cpu"
195+
warn EBADDEVENGINES {
196+
warn EBADDEVENGINES current: { name: 'x86' },
197+
warn EBADDEVENGINES required: { name: 'risv', onFail: 'warn' }
198+
warn EBADDEVENGINES }
199+
verbose stack Error: The developer of this package has specified the following through devEngines
200+
verbose stack Invalid engine "runtime"
201+
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
202+
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27)
203+
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7)
204+
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9)
205+
error code EBADDEVENGINES
206+
error EBADDEVENGINES The developer of this package has specified the following through devEngines
207+
error EBADDEVENGINES Invalid engine "runtime"
208+
error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime"
209+
error EBADDEVENGINES {
210+
error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
211+
error EBADDEVENGINES required: { name: 'nondescript' }
212+
error EBADDEVENGINES }
213+
`
214+
215+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure case > must match snapshot 1`] = `
216+
silly config load:file:{CWD}/npmrc
217+
silly config load:file:{CWD}/prefix/.npmrc
218+
silly config load:file:{CWD}/home/.npmrc
219+
silly config load:file:{CWD}/global/etc/npmrc
220+
verbose title npm
221+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
222+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
223+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
224+
silly logfile done cleaning log files
225+
verbose stack Error: The developer of this package has specified the following through devEngines
226+
verbose stack Invalid engine "runtime"
227+
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
228+
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:182:27)
229+
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:251:7)
230+
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:207:9)
231+
error code EBADDEVENGINES
232+
error EBADDEVENGINES The developer of this package has specified the following through devEngines
233+
error EBADDEVENGINES Invalid engine "runtime"
234+
error EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime"
235+
error EBADDEVENGINES {
236+
error EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
237+
error EBADDEVENGINES required: { name: 'nondescript' }
238+
error EBADDEVENGINES }
239+
`
240+
241+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines failure force case > must match snapshot 1`] = `
242+
silly config load:file:{CWD}/npmrc
243+
silly config load:file:{CWD}/prefix/.npmrc
244+
silly config load:file:{CWD}/home/.npmrc
245+
silly config load:file:{CWD}/global/etc/npmrc
246+
verbose title npm
247+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false" "--force" "true"
248+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
249+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
250+
warn using --force Recommended protections disabled.
251+
silly logfile done cleaning log files
252+
warn EBADDEVENGINES The developer of this package has specified the following through devEngines
253+
warn EBADDEVENGINES Invalid engine "runtime"
254+
warn EBADDEVENGINES Invalid name "nondescript" does not match "node" for "runtime"
255+
warn EBADDEVENGINES {
256+
warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
257+
warn EBADDEVENGINES required: { name: 'nondescript' }
258+
warn EBADDEVENGINES }
259+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
260+
silly idealTree buildDeps
261+
silly reify moves {}
262+
silly audit report null
263+
264+
up to date, audited 1 package in {TIME}
265+
found 0 vulnerabilities
266+
`
267+
268+
exports[`test/lib/commands/install.js TAP devEngines should utilize devEngines success case > must match snapshot 1`] = `
269+
silly config load:file:{CWD}/npmrc
270+
silly config load:file:{CWD}/prefix/.npmrc
271+
silly config load:file:{CWD}/home/.npmrc
272+
silly config load:file:{CWD}/global/etc/npmrc
273+
verbose title npm
274+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
275+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
276+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
277+
silly logfile done cleaning log files
278+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
279+
silly idealTree buildDeps
280+
silly reify moves {}
281+
silly audit report null
282+
283+
up to date, audited 1 package in {TIME}
284+
found 0 vulnerabilities
285+
`
286+
287+
exports[`test/lib/commands/install.js TAP devEngines should utilize engines in root if devEngines is not provided > must match snapshot 1`] = `
288+
silly config load:file:{CWD}/npmrc
289+
silly config load:file:{CWD}/prefix/.npmrc
290+
silly config load:file:{CWD}/home/.npmrc
291+
silly config load:file:{CWD}/global/etc/npmrc
292+
verbose title npm
293+
verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly" "--color" "false"
294+
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
295+
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
296+
silly logfile done cleaning log files
297+
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
298+
silly idealTree buildDeps
299+
warn EBADENGINE Unsupported engine {
300+
warn EBADENGINE package: undefined,
301+
warn EBADENGINE required: { node: '0.0.1' },
302+
warn EBADENGINE current: { node: 'v1337.0.0', npm: '42.0.0' }
303+
warn EBADENGINE }
304+
silly reify moves {}
305+
silly audit report null
306+
307+
up to date, audited 1 package in {TIME}
308+
found 0 vulnerabilities
309+
`

‎test/fixtures/clean-snapshot.js

+6
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ const cleanZlib = str => str
4242
.replace(/"integrity": ".*",/g, '"integrity": "{integrity}",')
4343
.replace(/"size": [0-9]*,/g, '"size": "{size}",')
4444

45+
const cleanPackumentCache = str => str
46+
.replace(/heap:[0-9]*/g, 'heap:{heap}')
47+
.replace(/maxSize:[0-9]*/g, 'maxSize:{maxSize}')
48+
.replace(/maxEntrySize:[0-9]*/g, 'maxEntrySize:{maxEntrySize}')
49+
4550
module.exports = {
4651
cleanCwd,
4752
cleanDate,
4853
cleanNewlines,
4954
cleanTime,
5055
cleanZlib,
56+
cleanPackumentCache,
5157
normalizePath,
5258
pathRegex,
5359
}

‎test/lib/commands/install.js

+317
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
const tspawk = require('../../fixtures/tspawk')
2+
const {
3+
cleanCwd,
4+
cleanTime,
5+
cleanDate,
6+
cleanPackumentCache,
7+
} = require('../../fixtures/clean-snapshot.js')
28

39
const path = require('node:path')
410
const t = require('tap')
511

12+
t.cleanSnapshot = (str) => cleanPackumentCache(cleanDate(cleanTime(cleanCwd(str))))
13+
614
const {
715
loadNpmWithRegistry: loadMockNpm,
816
workspaceMock,
@@ -400,3 +408,312 @@ t.test('should show install keeps dirty --workspace flag', async t => {
400408
assert.packageDirty('node_modules/abbrev@1.1.0')
401409
assert.packageInstalled('node_modules/lodash@1.1.1')
402410
})
411+
412+
t.test('devEngines', async t => {
413+
const mockArguments = {
414+
globals: {
415+
'process.platform': 'linux',
416+
'process.arch': 'x86',
417+
'process.version': 'v1337.0.0',
418+
},
419+
mocks: {
420+
'{ROOT}/package.json': { version: '42.0.0' },
421+
},
422+
}
423+
424+
t.test('should utilize devEngines success case', async t => {
425+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
426+
...mockArguments,
427+
prefixDir: {
428+
'package.json': JSON.stringify({
429+
name: 'test-package',
430+
version: '1.0.0',
431+
devEngines: {
432+
runtime: {
433+
name: 'node',
434+
},
435+
},
436+
}),
437+
},
438+
})
439+
await npm.exec('install', [])
440+
const output = joinedFullOutput()
441+
t.matchSnapshot(output)
442+
t.ok(!output.includes('EBADDEVENGINES'))
443+
})
444+
445+
t.test('should utilize devEngines failure case', async t => {
446+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
447+
...mockArguments,
448+
prefixDir: {
449+
'package.json': JSON.stringify({
450+
name: 'test-package',
451+
version: '1.0.0',
452+
devEngines: {
453+
runtime: {
454+
name: 'nondescript',
455+
},
456+
},
457+
}),
458+
},
459+
})
460+
await t.rejects(
461+
npm.exec('install', [])
462+
)
463+
const output = joinedFullOutput()
464+
t.matchSnapshot(output)
465+
t.ok(output.includes('error EBADDEVENGINES'))
466+
})
467+
468+
t.test('should utilize devEngines failure force case', async t => {
469+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
470+
...mockArguments,
471+
config: {
472+
force: true,
473+
},
474+
prefixDir: {
475+
'package.json': JSON.stringify({
476+
name: 'test-package',
477+
version: '1.0.0',
478+
devEngines: {
479+
runtime: {
480+
name: 'nondescript',
481+
},
482+
},
483+
}),
484+
},
485+
})
486+
await npm.exec('install', [])
487+
const output = joinedFullOutput()
488+
t.matchSnapshot(output)
489+
t.ok(output.includes('warn EBADDEVENGINES'))
490+
})
491+
492+
t.test('should utilize devEngines 2x warning case', async t => {
493+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
494+
...mockArguments,
495+
prefixDir: {
496+
'package.json': JSON.stringify({
497+
name: 'test-package',
498+
version: '1.0.0',
499+
devEngines: {
500+
runtime: {
501+
name: 'nondescript',
502+
onFail: 'warn',
503+
},
504+
cpu: {
505+
name: 'risv',
506+
onFail: 'warn',
507+
},
508+
},
509+
}),
510+
},
511+
})
512+
await npm.exec('install', [])
513+
const output = joinedFullOutput()
514+
t.matchSnapshot(output)
515+
t.ok(output.includes('warn EBADDEVENGINES'))
516+
})
517+
518+
t.test('should utilize devEngines 2x error case', async t => {
519+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
520+
...mockArguments,
521+
prefixDir: {
522+
'package.json': JSON.stringify({
523+
name: 'test-package',
524+
version: '1.0.0',
525+
devEngines: {
526+
runtime: {
527+
name: 'nondescript',
528+
onFail: 'error',
529+
},
530+
cpu: {
531+
name: 'risv',
532+
onFail: 'error',
533+
},
534+
},
535+
}),
536+
},
537+
})
538+
await t.rejects(
539+
npm.exec('install', [])
540+
)
541+
const output = joinedFullOutput()
542+
t.matchSnapshot(output)
543+
t.ok(output.includes('error EBADDEVENGINES'))
544+
})
545+
546+
t.test('should utilize devEngines failure and warning case', async t => {
547+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
548+
...mockArguments,
549+
prefixDir: {
550+
'package.json': JSON.stringify({
551+
name: 'test-package',
552+
version: '1.0.0',
553+
devEngines: {
554+
runtime: {
555+
name: 'nondescript',
556+
},
557+
cpu: {
558+
name: 'risv',
559+
onFail: 'warn',
560+
},
561+
},
562+
}),
563+
},
564+
})
565+
await t.rejects(
566+
npm.exec('install', [])
567+
)
568+
const output = joinedFullOutput()
569+
t.matchSnapshot(output)
570+
t.ok(output.includes('EBADDEVENGINES'))
571+
})
572+
573+
t.test('should show devEngines has no effect on package install', async t => {
574+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
575+
...mockArguments,
576+
prefixDir: {
577+
alpha: {
578+
'package.json': JSON.stringify({
579+
name: 'alpha',
580+
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
581+
}),
582+
'index.js': 'console.log("this is alpha index")',
583+
},
584+
'package.json': JSON.stringify({
585+
name: 'project',
586+
}),
587+
},
588+
})
589+
await npm.exec('install', ['./alpha'])
590+
const output = joinedFullOutput()
591+
t.matchSnapshot(output)
592+
t.ok(!output.includes('EBADDEVENGINES'))
593+
})
594+
595+
t.test('should show devEngines has no effect on dev package install', async t => {
596+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
597+
...mockArguments,
598+
prefixDir: {
599+
alpha: {
600+
'package.json': JSON.stringify({
601+
name: 'alpha',
602+
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
603+
}),
604+
'index.js': 'console.log("this is alpha index")',
605+
},
606+
'package.json': JSON.stringify({
607+
name: 'project',
608+
}),
609+
},
610+
config: {
611+
'save-dev': true,
612+
},
613+
})
614+
await npm.exec('install', ['./alpha'])
615+
const output = joinedFullOutput()
616+
t.matchSnapshot(output)
617+
t.ok(!output.includes('EBADDEVENGINES'))
618+
})
619+
620+
t.test('should show devEngines doesnt break engines', async t => {
621+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
622+
...mockArguments,
623+
prefixDir: {
624+
alpha: {
625+
'package.json': JSON.stringify({
626+
name: 'alpha',
627+
devEngines: { runtime: { name: 'node', version: '1.0.0' } },
628+
engines: { node: '1.0.0' },
629+
}),
630+
'index.js': 'console.log("this is alpha index")',
631+
},
632+
'package.json': JSON.stringify({
633+
name: 'project',
634+
}),
635+
},
636+
config: { global: true },
637+
})
638+
await npm.exec('install', ['./alpha'])
639+
const output = joinedFullOutput()
640+
t.matchSnapshot(output)
641+
t.ok(output.includes('warn EBADENGINE'))
642+
})
643+
644+
t.test('should not utilize engines in root if devEngines is provided', async t => {
645+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
646+
...mockArguments,
647+
prefixDir: {
648+
'package.json': JSON.stringify({
649+
name: 'alpha',
650+
engines: {
651+
node: '0.0.1',
652+
},
653+
devEngines: {
654+
runtime: {
655+
name: 'node',
656+
version: '0.0.1',
657+
onFail: 'warn',
658+
},
659+
},
660+
}),
661+
'index.js': 'console.log("this is alpha index")',
662+
},
663+
})
664+
await npm.exec('install')
665+
const output = joinedFullOutput()
666+
t.matchSnapshot(output)
667+
t.ok(!output.includes('EBADENGINE'))
668+
t.ok(output.includes('warn EBADDEVENGINES'))
669+
})
670+
671+
t.test('should utilize engines in root if devEngines is not provided', async t => {
672+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
673+
...mockArguments,
674+
prefixDir: {
675+
'package.json': JSON.stringify({
676+
name: 'alpha',
677+
engines: {
678+
node: '0.0.1',
679+
},
680+
}),
681+
'index.js': 'console.log("this is alpha index")',
682+
},
683+
})
684+
await npm.exec('install')
685+
const output = joinedFullOutput()
686+
t.matchSnapshot(output)
687+
t.ok(output.includes('EBADENGINE'))
688+
t.ok(!output.includes('EBADDEVENGINES'))
689+
})
690+
691+
t.test('should show devEngines has no effect on global package install', async t => {
692+
const { npm, joinedFullOutput } = await loadMockNpm(t, {
693+
...mockArguments,
694+
prefixDir: {
695+
'package.json': JSON.stringify({
696+
name: 'alpha',
697+
bin: {
698+
alpha: 'index.js',
699+
},
700+
devEngines: {
701+
runtime: {
702+
name: 'node',
703+
version: '0.0.1',
704+
},
705+
},
706+
}),
707+
'index.js': 'console.log("this is alpha index")',
708+
},
709+
config: {
710+
global: true,
711+
},
712+
})
713+
await npm.exec('install', ['.'])
714+
const output = joinedFullOutput()
715+
t.matchSnapshot(output)
716+
t.ok(!output.includes('EBADENGINE'))
717+
t.ok(!output.includes('EBADDEVENGINES'))
718+
})
719+
})

‎test/lib/npm.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ t.test('npm.load', async t => {
149149
'does not change npm.command when another command is called')
150150

151151
t.match(logs, [
152+
/timing config:load:flatten Completed in [0-9.]+ms/,
152153
/timing command:config Completed in [0-9.]+ms/,
153-
/timing command:get Completed in [0-9.]+ms/,
154154
])
155155
t.same(outputs, ['scope=@foo\nusage=false'])
156156
})

‎workspaces/arborist/lib/arborist/build-ideal-tree.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
195195
for (const node of this.idealTree.inventory.values()) {
196196
if (!node.optional) {
197197
try {
198-
checkEngine(node.package, npmVersion, nodeVersion, this.options.force)
198+
// if devEngines is present in the root node we ignore the engines check
199+
if (!(node.isRoot && node.package.devEngines)) {
200+
checkEngine(node.package, npmVersion, nodeVersion, this.options.force)
201+
}
199202
} catch (err) {
200203
if (engineStrict) {
201204
throw err

‎workspaces/arborist/tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs

+14
Original file line numberDiff line numberDiff line change
@@ -97921,6 +97921,20 @@ ArboristNode {
9792197921
}
9792297922
`
9792397923

97924+
exports[`test/arborist/build-ideal-tree.js TAP should take devEngines in account > must match snapshot 1`] = `
97925+
{
97926+
"name": "empty-update",
97927+
"lockfileVersion": 3,
97928+
"requires": true,
97929+
"packages": {
97930+
"": {
97931+
"name": "empty-update"
97932+
}
97933+
}
97934+
}
97935+
97936+
`
97937+
9792497938
exports[`test/arborist/build-ideal-tree.js TAP store files with a custom indenting > must match snapshot 1`] = `
9792597939
{
9792697940
"name": "tab-indented-package-json",

‎workspaces/arborist/test/arborist/build-ideal-tree.js

+15
Original file line numberDiff line numberDiff line change
@@ -3979,3 +3979,18 @@ t.test('store files with a custom indenting', async t => {
39793979
const tree = await buildIdeal(path)
39803980
t.matchSnapshot(String(tree.meta))
39813981
})
3982+
3983+
t.test('should take devEngines in account', async t => {
3984+
const path = t.testdir({
3985+
'package.json': JSON.stringify({
3986+
name: 'empty-update',
3987+
devEngines: {
3988+
runtime: {
3989+
name: 'node',
3990+
},
3991+
},
3992+
}),
3993+
})
3994+
const tree = await buildIdeal(path)
3995+
t.matchSnapshot(String(tree.meta))
3996+
})

0 commit comments

Comments
 (0)
Please sign in to comment.