Skip to content

Commit 5d5f6a8

Browse files
committed
feat: add devEngines field
This field is only looked at when in the top-level project, and takes precedence over `engines` when present See openjs-foundation/package-metadata-interoperability-collab-space#15 (comment) for the schema
1 parent 9214be9 commit 5d5f6a8

File tree

9 files changed

+114
-14
lines changed

9 files changed

+114
-14
lines changed

docs/lib/content/commands/npm-query.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ npm query ":type(git)" | jq 'map(.name)' | xargs -I {} npm why {}
125125
"peerDependencies": {},
126126
"peerDependenciesMeta": {},
127127
"engines": {},
128+
"devEngines": {},
128129
"os": [],
129130
"cpu": [],
130131
"workspaces": {},
@@ -158,7 +159,7 @@ $ npm query ':root>:outdated(in-range).prod' --no-expect-results
158159

159160
### Package lock only mode
160161

161-
If package-lock-only is enabled, only the information in the package lock (or shrinkwrap) is loaded. This means that information from the package.json files of your dependencies will not be included in the result set (e.g. description, homepage, engines).
162+
If package-lock-only is enabled, only the information in the package lock (or shrinkwrap) is loaded. This means that information from the package.json files of your dependencies will not be included in the result set (e.g. description, homepage, engines, devEngines).
162163

163164
### Configuration
164165

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

+24
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,30 @@ Unless the user has set the
10661066
advisory only and will only produce warnings when your package is installed as a
10671067
dependency.
10681068

1069+
### devEngines
1070+
1071+
This field works similar to the `engines` field. It is only respected at the project root.
1072+
1073+
All properties of `devEngines` are optional. Here is a TypeScript interface describing the schema of the object:
1074+
```ts
1075+
interface DevEngines {
1076+
os?: DevEngineDependency | DevEngineDependency[];
1077+
cpu?: DevEngineDependency | DevEngineDependency[];
1078+
runtime?: DevEngineDependency | DevEngineDependency[];
1079+
packageManager?: DevEngineDependency | DevEngineDependency[];
1080+
}
1081+
1082+
interface DevEngineDependency {
1083+
name: string;
1084+
version?: string;
1085+
onFail?: 'ignore' | 'warn' | 'error';
1086+
}
1087+
```
1088+
1089+
`onFail` defaults to `error`. When an unknown `onFail` value is provided, it will error.
1090+
1091+
When present, the `engines` field is ignored.
1092+
10691093
### os
10701094

10711095
You can specify which operating systems your

docs/lib/content/using-npm/developers.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ goes in that file. At the very least, you need:
6161
* name: This should be a string that identifies your project. Please do
6262
not use the name to specify that it runs on node, or is in JavaScript.
6363
You can use the "engines" field to explicitly state the versions of node
64-
(or whatever else) that your program requires, and it's pretty well
65-
assumed that it's JavaScript.
64+
(or whatever else) that your program requires (and "devEngines" when the
65+
requirements for developers differ from the requirements for users), and
66+
it's pretty well assumed that it's JavaScript.
6667

6768
It does not necessarily need to match your github repository name.
6869

@@ -71,8 +72,12 @@ goes in that file. At the very least, you need:
7172
* version: A semver-compatible version.
7273

7374
* engines: Specify the versions of node (or whatever else) that your
74-
program runs on. The node API changes a lot, and there may be bugs or
75-
new functionality that you depend on. Be explicit.
75+
program runs on. The node API changes a lot, and there may be bugs or
76+
new functionality that you depend on. Be explicit.
77+
78+
* devEngines: Specify the versions of node (or whatever else) that your
79+
program needs for development. The node API changes a lot, and there may
80+
be bugs or new functionality that you depend on. Be explicit.
7681

7782
* author: Take some credit.
7883

node_modules/npm-install-checks/lib/index.js

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
const semver = require('semver')
22

3-
const checkEngine = (target, npmVer, nodeVer, force = false) => {
3+
const checkEngine = ({ devEngines, engines, _id: pkgid }, npmVer, nodeVer, force = false, isProjectRoot = false) => {
44
const nodev = force ? null : nodeVer
5-
const eng = target.engines
65
const opt = { includePrerelease: true }
7-
if (!eng) {
8-
return
6+
7+
let npmEngine = engines?.npm ?? '*';
8+
let nodeEngine = engines?.node ?? '*';
9+
10+
if (isProjectRoot && devEngines?.packageManager) {
11+
npmEngine ??= [].concat(devEngines?.packageManager ?? []).find(({ name }) => name === 'npm')?.version;
12+
nodeEngine ??= [].concat(devEngines?.runtime ?? []).find(({ name }) => name === 'node')?.version;
913
}
1014

11-
const nodeFail = nodev && eng.node && !semver.satisfies(nodev, eng.node, opt)
12-
const npmFail = npmVer && eng.npm && !semver.satisfies(npmVer, eng.npm, opt)
15+
if (
16+
(!npmEngine || npmEngine === '*')
17+
|| (!nodeEngine || nodeEngine === '*')
18+
) {
19+
return;
20+
}
21+
22+
const nodeFail = nodev && !semver.satisfies(nodev, nodeEngine, opt)
23+
const npmFail = npmVer && !semver.satisfies(npmVer, npmEngine, opt)
1324
if (nodeFail || npmFail) {
1425
throw Object.assign(new Error('Unsupported engine'), {
15-
pkgid: target._id,
26+
pkgid,
1627
current: { node: nodeVer, npm: npmVer },
1728
required: eng,
1829
code: 'EBADENGINE',

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ 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+
checkEngine(node.package, npmVersion, nodeVersion, this.options.force, node.isProjectRoot)
199199
} catch (err) {
200200
if (engineStrict) {
201201
throw err

workspaces/arborist/lib/shrinkwrap.js

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const pkgMetaKeys = [
9393
'acceptDependencies',
9494
'funding',
9595
'engines',
96+
'devEngines',
9697
'os',
9798
'cpu',
9899
'_integrity',

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ t.teardown(stop)
2222

2323
const cache = t.testdir()
2424

25-
// track the warnings that are emitted. returns a function that removes
25+
// track the warnings that are emitted. returns a function that removes
2626
// the listener and provides the list of what it saw.
2727
const warningTracker = () => {
2828
const list = []
@@ -126,6 +126,18 @@ t.test('fail on mismatched engine when engineStrict is set', async t => {
126126
}), { code: 'EBADENGINE' })
127127
})
128128

129+
t.test('fail on mismatched devEngine when engineStrict is set', async t => {
130+
const path = resolve(fixtures, 'dev-engine-specification')
131+
132+
t.rejects(buildIdeal(path, {
133+
...OPT,
134+
nodeVersion: '12.18.4',
135+
engineStrict: true,
136+
}).then(() => {
137+
throw new Error('failed to fail')
138+
}), { code: 'EBADENGINE' })
139+
})
140+
129141
t.test('fail on malformed package.json', t => {
130142
const path = resolve(fixtures, 'malformed-json')
131143

@@ -157,6 +169,18 @@ t.test('warn on mismatched engine when engineStrict is false', t => {
157169
]))
158170
})
159171

172+
t.test('warn on mismatched devEngine when engineStrict is false', t => {
173+
const path = resolve(fixtures, 'dev-engine-specification')
174+
const check = warningTracker()
175+
return buildIdeal(path, {
176+
...OPT,
177+
nodeVersion: '12.18.4',
178+
engineStrict: false,
179+
}).then(() => t.match(check(), [
180+
['warn', 'EBADENGINE'],
181+
]))
182+
})
183+
160184
t.test('fail on mismatched platform', async t => {
161185
const path = resolve(fixtures, 'platform-specification')
162186
t.rejects(buildIdeal(path, {

workspaces/arborist/test/fixtures/dev-engine-specification/package-lock.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "dev-engine-platform-test",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
},
13+
"devEngines": {
14+
"runtime": {
15+
"node": "< 0.1"
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)