Skip to content

Commit 4d57928

Browse files
authored
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 }

0 commit comments

Comments
 (0)