Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: devEngines #7766

Merged
merged 3 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/lib/content/configuring-npm/package-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,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
Expand Down
1 change: 1 addition & 0 deletions lib/arborist-cmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ArboristCmd extends BaseCommand {

static workspaces = true
static ignoreImplicitWorkspace = false
static checkDevEngines = true

constructor (npm) {
super(npm)
Expand Down
61 changes: 60 additions & 1 deletion lib/base-cmd.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(() => ({}))
hashtagchris marked this conversation as resolved.
Show resolved Hide resolved

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')

Expand Down
1 change: 1 addition & 0 deletions lib/commands/run-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ class Npm {
execWorkspaces = true
}

if (command.checkDevEngines && !this.global) {
reggi marked this conversation as resolved.
Show resolved Hide resolved
await command.checkDevEngines()
}

return time.start(`command:${cmd}`, () =>
execWorkspaces ? command.execWorkspaces(args) : command.exec(args))
}
Expand Down
7 changes: 7 additions & 0 deletions lib/utils/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading
Loading