Skip to content

Commit

Permalink
Support node --run (#1169)
Browse files Browse the repository at this point in the history
Adds support for the new node --run feature which is available starting in Node 22 (basically npm run but faster, see https://nodejs.org/en/blog/announcements/v22-release-announce#running-packagejson-scripts and https://www.yagiz.co/developing-fast-builtin-task-runner/).

Note that node --run is stricter than the other runners when it comes to distinguishing between arguments for the runner vs the script, so an additional -- is needed to set wireit flags and script flags (explained in the README).

Thank you very much to @anonrig for adding the environment variables this required (nodejs/node#53032, nodejs/node#53058) and @justinfagnani for filing the issue (nodejs/node#52673)!

There is a problem with recursive invocations on Windows that I believe is a Node bug but need to double-check, tracking at #1168.

Fixes #1094
  • Loading branch information
aomarks authored Aug 22, 2024
1 parent e7f501d commit 66338d2
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 195 deletions.
19 changes: 13 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,33 @@ jobs:
tests:
strategy:
# We support Node Current, LTS, and Maintenance. See
# https://nodejs.org/en/about/releases/ for release schedule.
# https://github.com/nodejs/release#release-schedule for release schedule
#
# We test all supported Node versions on Linux, and the oldest and newest
# on macOS/Windows.
# on macOS/Windows. See
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
# for the latest available images.
matrix:
include:
# Maintenance
- node: 18
os: ubuntu-22.04
- node: 18
os: macos-13
- node: 18
os: windows-2022

# LTS
- node: 20
os: ubuntu-22.04
- node: 20

# Current
- node: 22
os: ubuntu-22.04
- node: 22
os: macos-13
- node: 20
- node: 22
os: windows-2022
- node: 21
os: ubuntu-22.04

# Allow all matrix configurations to complete, instead of cancelling as
# soon as one fails. Useful because we often have different kinds of
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Added support for `node --run`, available in Node 22 and above (see
https://nodejs.org/en/blog/announcements/v22-release-announce#running-packagejson-scripts).

## [0.14.7] - 2024-08-05

- When GitHub caching fails to initialize, more information is now shown about
Expand Down
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ and replace the original script with the `wireit` command.
</table>

Now when you run `npm run build`, Wireit upgrades the script to be smarter and
more efficient. Wireit works with [yarn](https://yarnpkg.com/)
(both 1.X "[Classic](https://classic.yarnpkg.com/)" and its successor "Berry")
and [pnpm](https://pnpm.io/), too.
more efficient. Wireit also works with [`node --run`](https://nodejs.org/en/blog/announcements/v22-release-announce#running-packagejson-scripts), [yarn](https://yarnpkg.com/), and [pnpm](https://pnpm.io/).

You should also add `.wireit` to your `.gitignore` file. Wireit uses the
`.wireit` directory to store caches and other data for your scripts.
Expand Down Expand Up @@ -238,6 +236,19 @@ npm or Wireit:
npm run build -- --verbose
```

Or in general:

```sh
npm run {script} {npm args} {wireit args} -- {script args}
```

An additional `--` is required when using `node --run` in order to distinguish
between arguments intended for `node`, `wireit`, and the script itself:

```sh
node --run {script} {node args} -- {wireit args} -- {script args}
```

## Input and output files

The `files` and `output` properties of `wireit.<script>` tell Wireit what your
Expand Down Expand Up @@ -420,7 +431,15 @@ flag:
npm run <script> --watch
```

The benefit of Wireit's watch mode over built-in watch modes are:
An additional `--` is required when using `node --run`, otherwise Node's
built-in [watch](https://nodejs.org/docs/v20.17.0/api/cli.html#--watch) feature
will be triggered instead of Wireit's:

```sh
node --run <script> -- --watch
```

The benefit of Wireit's watch mode over the built-in watch modes of Node and other programs are:

- Wireit watches the entire dependency graph, so a single watch command replaces
many built-in ones.
Expand Down Expand Up @@ -719,7 +738,7 @@ WIREIT_FAILURES=kill
By default, Wireit automatically treats package manager lock files as input
files
([`package-lock.json`](https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json)
for npm,
for npm and `node --run`,
[`yarn.lock`](https://yarnpkg.com/configuration/yarnrc#lockfileFilename) for
yarn, and [`pnpm-lock.yaml`](https://pnpm.io/git#lockfiles) for pnpm). Wireit
will look for these lock files in the script's package, and all parent
Expand Down Expand Up @@ -921,13 +940,11 @@ input also affects the fingerprint:

Wireit is supported on Linux, macOS, and Windows.

Wireit is supported on Node Current (21), Active LTS (20), and Maintenance LTS
Wireit is supported on Node Current (22), Active LTS (20), and Maintenance LTS
(18). See [Node releases](https://nodejs.org/en/about/releases/) for the
schedule.

Wireit is supported on the npm versions that ship with the latest versions of
the above supported Node versions (6 and 8), Yarn Classic (1), Yarn Berry (3),
and pnpm (7).
Wireit scripts can be launched via `npm`, `node --run`, `pnpm`, and `yarn`.

## Related tools

Expand Down
23 changes: 12 additions & 11 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@
*/

import * as pathlib from 'path';
import {Dependency, scriptReferenceToString, ServiceConfig} from './config.js';
import {findNodeAtLocation, JsonFile} from './util/ast.js';
import * as fs from './util/fs.js';
import {
CachingPackageJsonReader,
FileSystem,
} from './util/package-json-reader.js';
import {Dependency, scriptReferenceToString, ServiceConfig} from './config.js';
import {findNodeAtLocation, JsonFile} from './util/ast.js';
import {IS_WINDOWS} from './util/windows.js';

import type {Agent} from './cli-options.js';
import type {
ScriptConfig,
ScriptReference,
ScriptReferenceString,
} from './config.js';
import type {Diagnostic, MessageLocation, Result} from './error.js';
import type {Cycle, DependencyOnMissingPackageJson, Failure} from './event.js';
import {Logger} from './logging/logger.js';
import type {
ArrayNode,
JsonAstNode,
NamedAstNode,
ValueTypes,
} from './util/ast.js';
import type {Diagnostic, MessageLocation, Result} from './error.js';
import type {Cycle, DependencyOnMissingPackageJson, Failure} from './event.js';
import type {PackageJson, ScriptSyntaxInfo} from './util/package-json.js';
import type {
ScriptConfig,
ScriptReference,
ScriptReferenceString,
} from './config.js';
import type {Agent} from './cli-options.js';
import {Logger} from './logging/logger.js';

export interface AnalyzeResult {
config: Result<ScriptConfig, Failure[]>;
Expand Down Expand Up @@ -118,6 +118,7 @@ const DEFAULT_EXCLUDE_PATHS = [

const DEFAULT_LOCKFILES: Record<Agent, string[]> = {
npm: ['package-lock.json'],
nodeRun: ['package-lock.json'],
yarnClassic: ['yarn.lock'],
yarnBerry: ['yarn.lock'],
pnpm: ['pnpm-lock.yaml'],
Expand Down
29 changes: 18 additions & 11 deletions src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@
*/

import * as os from 'os';
import * as fs from './util/fs.js';
import * as pathlib from 'path';
import {Result} from './error.js';
import {MetricsLogger} from './logging/metrics-logger.js';
import {ScriptReference} from './config.js';
import {Result} from './error.js';
import {FailureMode} from './executor.js';
import {unreachable} from './util/unreachable.js';
import {DefaultLogger} from './logging/default-logger.js';
import {Console, Logger} from './logging/logger.js';
import {MetricsLogger} from './logging/metrics-logger.js';
import {QuietCiLogger, QuietLogger} from './logging/quiet-logger.js';
import {DefaultLogger} from './logging/default-logger.js';
import * as fs from './util/fs.js';
import {unreachable} from './util/unreachable.js';

export const packageDir = await (async (): Promise<string | undefined> => {
// Recent versions of npm set this environment variable that tells us the
// package.
const packageJsonPath = process.env.npm_package_json;
// Recent versions of npm, and node --run, set environment variables to tell
// us the current package.json.
const packageJsonPath =
process.env.npm_package_json ?? process.env.NODE_RUN_PACKAGE_JSON_PATH;
if (packageJsonPath) {
return pathlib.dirname(packageJsonPath);
}
Expand All @@ -45,7 +46,7 @@ export const packageDir = await (async (): Promise<string | undefined> => {
}
})();

export type Agent = 'npm' | 'pnpm' | 'yarnClassic' | 'yarnBerry';
export type Agent = 'npm' | 'nodeRun' | 'pnpm' | 'yarnClassic' | 'yarnBerry';

export interface Options {
script: ScriptReference;
Expand All @@ -61,7 +62,8 @@ export interface Options {
export const getOptions = async (): Promise<Result<Options>> => {
// This environment variable is set by npm, yarn, and pnpm, and tells us which
// script is running.
const scriptName = process.env.npm_lifecycle_event;
const scriptName =
process.env.npm_lifecycle_event ?? process.env['NODE_RUN_SCRIPT_NAME'];
// We need to handle "npx wireit" as a special case, because it sets
// "npm_lifecycle_event" to "npx". The "npm_execpath" will be "npx-cli.js",
// though, so we use that combination to detect this special case.
Expand Down Expand Up @@ -279,6 +281,9 @@ function getArgvOptions(
extraArgs: process.argv.slice(2),
};
}
case 'nodeRun': {
return parseRemainingArgs(process.argv.slice(2));
}
case 'yarnClassic': {
// yarn 1.22.18
// - If there is no "--", all arguments go to argv.
Expand Down Expand Up @@ -313,14 +318,16 @@ function getArgvOptions(
* Try to find the npm user agent being used. If we can't detect it, assume npm.
*/
function getNpmUserAgent(): Agent {
if (process.env['NODE_RUN_SCRIPT_NAME'] !== undefined) {
return 'nodeRun';
}
const userAgent = process.env['npm_config_user_agent'];
if (userAgent !== undefined) {
const match = userAgent.match(/^(npm|yarn|pnpm)\//);
if (match !== null) {
if (match[1] === 'yarn') {
return /^yarn\/[01]\./.test(userAgent) ? 'yarnClassic' : 'yarnBerry';
}

return match[1] as 'npm' | 'pnpm';
}
}
Expand Down
60 changes: 53 additions & 7 deletions src/test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import {suite} from 'uvu';
import * as assert from 'uvu/assert';
import {rigTest} from './util/rig-test.js';
import {IS_WINDOWS} from '../util/windows.js';
import {NODE_MAJOR_VERSION} from './util/node-version.js';
import {checkScriptOutput} from './util/check-script-output.js';
import {NODE_MAJOR_VERSION} from './util/node-version.js';
import {rigTest} from './util/rig-test.js';

const test = suite<object>();

Expand Down Expand Up @@ -766,6 +766,34 @@ test(
}),
);

if (NODE_MAJOR_VERSION >= 22) {
test(
'runs a script with node --run',
rigTest(async ({rig}) => {
const cmdA = await rig.newCommand();
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: cmdA.command,
},
},
},
});
const exec = rig.exec('node --run a');
await exec.waitForLog(/0% \[0 \/ 1\] \[1 running\] a/);
(await cmdA.nextInvocation()).exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 1);
assert.match(res.stdout, /Ran 1 script and skipped 0/s);
}),
);
}

test(
'runs a script with yarn',
rigTest(async ({rig}) => {
Expand Down Expand Up @@ -1062,9 +1090,21 @@ test(
}),
);

for (const agent of ['npm', 'yarn', 'pnpm']) {
for (const command of [
'npm run',
'node --run',
'yarn run',
'pnpm run',
] as const) {
if (command === 'node --run' && NODE_MAJOR_VERSION < 22) {
// node --run was added in Node 22.
continue;
}
// node --run needs an extra set of "--" before arguments will be passed down
// to Wireit.
const extraDashes = command === 'node --run' ? '--' : '';
test(
`can pass extra args with using "${agent} run --"`,
`can pass extra args with using "${command} run --"`,
rigTest(async ({rig}) => {
const cmdA = await rig.newCommand();
await rig.write({
Expand All @@ -1085,7 +1125,9 @@ for (const agent of ['npm', 'yarn', 'pnpm']) {

// Initially stale.
{
const wireit = rig.exec(`${agent} run a -- foo -bar --baz`);
const wireit = rig.exec(
`${command} a -- ${extraDashes} foo -bar --baz`,
);
const inv = await cmdA.nextInvocation();
assert.equal((await inv.environment()).argv.slice(3), [
'foo',
Expand All @@ -1099,15 +1141,19 @@ for (const agent of ['npm', 'yarn', 'pnpm']) {

// Nothing changed, fresh.
{
const wireit = rig.exec(`${agent} run a -- foo -bar --baz`);
const wireit = rig.exec(
`${command} a -- ${extraDashes} foo -bar --baz`,
);
assert.equal((await wireit.exit).code, 0);
await wireit.waitForLog(/Ran 0 scripts and skipped 1/s); //
}

// Changing the extra args should change the fingerprint so that we're
// stale.
{
const wireit = rig.exec(`${agent} run a -- FOO -BAR --BAZ`);
const wireit = rig.exec(
`${command} a -- ${extraDashes} FOO -BAR --BAZ`,
);
const inv = await cmdA.nextInvocation();
assert.equal((await inv.environment()).argv.slice(3), [
'FOO',
Expand Down
Loading

0 comments on commit 66338d2

Please sign in to comment.