@williamthorsen/nmr 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +105 -0
- package/.readyup/kits/default.js +1042 -137
- package/.readyup/manifest.json +151 -8
- package/CHANGELOG.md +33 -0
- package/README.md +20 -11
- package/dist/esm/commands/build-output.js +1 -1
- package/dist/esm/commands/build.js +3 -2
- package/dist/esm/commands/fmt-cache.d.ts +26 -0
- package/dist/esm/commands/fmt-cache.js +71 -0
- package/dist/esm/commands/fmt.d.ts +6 -1
- package/dist/esm/commands/fmt.js +24 -4
- package/dist/esm/config.js +28 -6
- package/dist/esm/default-scripts.d.ts +10 -1
- package/dist/esm/default-scripts.js +45 -39
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +3 -3
- package/dist/esm/helpers/createOutputRecorder.d.ts +17 -0
- package/dist/esm/helpers/createOutputRecorder.js +22 -0
- package/dist/esm/resolve-scripts.d.ts +5 -3
- package/dist/esm/resolve-scripts.js +7 -3
- package/dist/esm/resolver.d.ts +5 -4
- package/dist/esm/resolver.js +11 -4
- package/dist/esm/runCli.js +5 -3
- package/dist/esm/runner.d.ts +7 -0
- package/dist/esm/runner.js +105 -15
- package/dist/esm/steps.d.ts +12 -5
- package/dist/esm/steps.js +5 -3
- package/dist/esm/taze.d.ts +5 -2
- package/dist/esm/taze.js +4 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/vitest.js +9 -5
- package/dist/esm/workspace-globs.d.ts +5 -0
- package/dist/esm/workspace-globs.js +8 -0
- package/docs/check-cache.md +2 -0
- package/docs/scripts.md +26 -8
- package/docs/upgrades.md +1 -1
- package/docs/utilities.md +17 -3
- package/docs/vitest.md +8 -0
- package/package.json +6 -6
|
@@ -10,6 +10,11 @@ export interface StepSpec {
|
|
|
10
10
|
* narrowed steps run, or the tool that it invokes would be misled by them. A declining step runs unnarrowed.
|
|
11
11
|
*/
|
|
12
12
|
shouldDeclineArguments?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Set to start this step and run the steps after it without waiting for it. The composite waits for it before
|
|
15
|
+
* ending, and prints its output whole at the first step boundary after it ends.
|
|
16
|
+
*/
|
|
17
|
+
shouldRunInBackground?: boolean;
|
|
13
18
|
}
|
|
14
19
|
export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
|
|
15
20
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
@@ -19,4 +24,8 @@ export type ScriptRegistry = Record<string, ScriptValue>;
|
|
|
19
24
|
* collects every file that does not name any other tier.
|
|
20
25
|
*/
|
|
21
26
|
export declare const workspaceScripts: ScriptRegistry;
|
|
22
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Builds the root scripts. Each root-only lint command excludes the workspaces, given as globs relative to the root,
|
|
29
|
+
* so that the root ESLint config never lints a package.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildRootScripts(workspaceGlobs: readonly string[]): ScriptRegistry;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { quoteToken } from "./steps.js";
|
|
1
2
|
const GATE_PROJECTS = '--project unit --project tool';
|
|
2
3
|
const TYPECHECK_STEP = { run: 'typecheck', shouldDeclineArguments: true };
|
|
3
4
|
const ROOT_TYPECHECK_STEP = { run: 'root:typecheck', shouldDeclineArguments: true };
|
|
5
|
+
const LINT_CHECK_STEP = { run: 'lint:check', shouldRunInBackground: true };
|
|
6
|
+
const LINT_STRICT_STEP = { run: 'lint:strict', shouldRunInBackground: true };
|
|
4
7
|
export const workspaceScripts = {
|
|
5
8
|
build: ['compile'],
|
|
6
|
-
check: [TYPECHECK_STEP, 'fmt:check', '
|
|
7
|
-
'check:strict': [TYPECHECK_STEP, 'fmt:check', '
|
|
9
|
+
check: [LINT_CHECK_STEP, TYPECHECK_STEP, 'fmt:check', 'test'],
|
|
10
|
+
'check:strict': [LINT_STRICT_STEP, TYPECHECK_STEP, 'fmt:check', 'test'],
|
|
8
11
|
clean: 'nmr-clean',
|
|
9
12
|
compile: 'nmr-compile',
|
|
10
13
|
fix: ['lint', 'fmt'],
|
|
@@ -25,40 +28,43 @@ export const workspaceScripts = {
|
|
|
25
28
|
upgrade: 'nmr-report-catalog && nmr-taze',
|
|
26
29
|
'view-coverage': 'open coverage/index.html',
|
|
27
30
|
};
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
31
|
+
export function buildRootScripts(workspaceGlobs) {
|
|
32
|
+
const ignorePatterns = workspaceGlobs.map((glob) => `--ignore-pattern ${quoteToken(glob)} `).join('');
|
|
33
|
+
return {
|
|
34
|
+
audit: ['audit:prod', 'audit:dev'],
|
|
35
|
+
'audit:dev': 'pnpm exec v11y --dev',
|
|
36
|
+
'audit:prod': 'pnpm exec v11y --prod',
|
|
37
|
+
build: ['-R build'],
|
|
38
|
+
check: [LINT_CHECK_STEP, TYPECHECK_STEP, 'fmt:check', 'test'],
|
|
39
|
+
'check:strict': [LINT_STRICT_STEP, TYPECHECK_STEP, 'fmt:check', 'test'],
|
|
40
|
+
ci: [{ run: 'build', shouldDeclineArguments: true }, 'check:strict'],
|
|
41
|
+
clean: 'nmr-clean',
|
|
42
|
+
fix: ['lint', 'fmt'],
|
|
43
|
+
'fix:check': ['fmt:check', 'lint:check'],
|
|
44
|
+
fmt: 'nmr-fmt --write',
|
|
45
|
+
'fmt:check': 'nmr-fmt --check',
|
|
46
|
+
lint: 'eslint --fix .',
|
|
47
|
+
'lint:check': 'eslint .',
|
|
48
|
+
'lint:strict': 'strict-lint',
|
|
49
|
+
prepush: [{ run: 'audit', shouldDeclineArguments: true }, 'ci'],
|
|
50
|
+
'report-overrides': 'nmr-report-overrides',
|
|
51
|
+
'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
|
|
52
|
+
'root:lint': `eslint --fix ${ignorePatterns}.`,
|
|
53
|
+
'root:lint:check': `eslint ${ignorePatterns}.`,
|
|
54
|
+
'root:lint:strict': `strict-lint ${ignorePatterns}.`,
|
|
55
|
+
'root:test': `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
|
|
56
|
+
'root:test:all': 'vitest --config ./vitest.root.config.ts',
|
|
57
|
+
'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
|
|
58
|
+
'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
|
|
59
|
+
'root:typecheck': 'tsgo --noEmit',
|
|
60
|
+
'root:upgrade': 'nmr-report-overrides && nmr-taze',
|
|
61
|
+
test: ['root:test', '-R test'],
|
|
62
|
+
'test:all': ['root:test:all', '-R test:all'],
|
|
63
|
+
'test:coverage': ['root:test', '-R test:coverage'],
|
|
64
|
+
'test:tool': ['root:test:tool', '-R test:tool'],
|
|
65
|
+
'test:unit': ['root:test:unit', '-R test:unit'],
|
|
66
|
+
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
67
|
+
typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', shouldDeclineArguments: true }],
|
|
68
|
+
upgrade: 'nmr-report-overrides && nmr-taze --recursive',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
|
-
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, StepSpec, } from './types.js';
|
|
2
|
+
export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, SelfHostedRunnerConfig, StepSpec, } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Returns `config` unchanged, typing a configuration file's default export as `NmrConfig`.
|
|
5
5
|
*
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -16,4 +16,4 @@ import type { NmrConfig } from './types.js';
|
|
|
16
16
|
* unless it is an npm or pnpm lifecycle script or the active registry
|
|
17
17
|
* contains its name.
|
|
18
18
|
*/
|
|
19
|
-
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, shouldUseRoot: boolean): string;
|
|
19
|
+
export declare function generateHelp(config: NmrConfig, monorepoRoot: string, packageDir: string | undefined, shouldUseRoot: boolean): string;
|
package/dist/esm/help.js
CHANGED
|
@@ -23,7 +23,7 @@ const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
|
23
23
|
'uninstall',
|
|
24
24
|
'version',
|
|
25
25
|
]);
|
|
26
|
-
export function generateHelp(config, packageDir, shouldUseRoot) {
|
|
26
|
+
export function generateHelp(config, monorepoRoot, packageDir, shouldUseRoot) {
|
|
27
27
|
const lines = [
|
|
28
28
|
'Usage: nmr [flags] <command> [args...]',
|
|
29
29
|
'',
|
|
@@ -54,12 +54,12 @@ export function generateHelp(config, packageDir, shouldUseRoot) {
|
|
|
54
54
|
hadOverride = true;
|
|
55
55
|
formatRegistry(workspaceRegistry, workspaceMarkedNames, lines);
|
|
56
56
|
lines.push('', 'Root commands:');
|
|
57
|
-
const rootRegistry = filterHooks(buildRootRegistry(config));
|
|
57
|
+
const rootRegistry = filterHooks(buildRootRegistry(config, monorepoRoot));
|
|
58
58
|
const rootMarkedNames = shouldUseRoot ? applyOverrides(rootRegistry, overrides) : new Set();
|
|
59
59
|
if (rootMarkedNames.size > 0)
|
|
60
60
|
hadOverride = true;
|
|
61
61
|
formatRegistry(rootRegistry, rootMarkedNames, lines);
|
|
62
|
-
const activeRegistry = shouldUseRoot ? buildRootRegistry(config) : buildWorkspaceRegistry(config);
|
|
62
|
+
const activeRegistry = shouldUseRoot ? buildRootRegistry(config, monorepoRoot) : buildWorkspaceRegistry(config);
|
|
63
63
|
const unregisteredScripts = collectUnregisteredScripts(packageScripts, activeRegistry);
|
|
64
64
|
if (Object.keys(unregisteredScripts).length > 0) {
|
|
65
65
|
lines.push('', 'Package scripts:');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
/** Which of a child's output streams a recorded chunk arrived on. */
|
|
3
|
+
type StreamName = 'stderr' | 'stdout';
|
|
4
|
+
export interface OutputRecorder {
|
|
5
|
+
/** Destination for the child's stderr. It does not have a descriptor, so a child writing to it is given a pipe. */
|
|
6
|
+
stderr: Writable;
|
|
7
|
+
/** Destination for the child's stdout. It does not have a descriptor, so a child writing to it is given a pipe. */
|
|
8
|
+
stdout: Writable;
|
|
9
|
+
/** Writes every recorded chunk to the destination matching its stream, in the order in which the chunks arrived. */
|
|
10
|
+
replay: (destinations: Record<StreamName, Writable>) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a pair of in-memory destinations that hold everything written to them, unbounded, so that output
|
|
14
|
+
* produced while other output is streaming can be printed whole afterwards.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createOutputRecorder(): OutputRecorder;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
export function createOutputRecorder() {
|
|
3
|
+
const entries = [];
|
|
4
|
+
function createSink(stream) {
|
|
5
|
+
return new Writable({
|
|
6
|
+
write(chunk, encoding, callback) {
|
|
7
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), encoding);
|
|
8
|
+
entries.push({ chunk: buffer, stream });
|
|
9
|
+
callback();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
stderr: createSink('stderr'),
|
|
15
|
+
stdout: createSink('stdout'),
|
|
16
|
+
replay: (destinations) => {
|
|
17
|
+
for (const { chunk, stream } of entries) {
|
|
18
|
+
destinations[stream].write(chunk);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -6,7 +6,9 @@ export type { ScriptRegistry, ScriptValue, StepSpec } from './default-scripts.js
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function getDefaultWorkspaceScripts(): ScriptRegistry;
|
|
8
8
|
/**
|
|
9
|
-
* Returns
|
|
10
|
-
*
|
|
9
|
+
* Returns the default root scripts for the monorepo at `monorepoRoot`, whose root-only lint commands exclude every
|
|
10
|
+
* workspace package that its `pnpm-workspace.yaml` declares. A directory without that manifest yields commands
|
|
11
|
+
* without exclusions rather than an error, so that a caller reading only the command names need not stand in a
|
|
12
|
+
* workspace.
|
|
11
13
|
*/
|
|
12
|
-
export declare function getDefaultRootScripts(): ScriptRegistry;
|
|
14
|
+
export declare function getDefaultRootScripts(monorepoRoot: string): ScriptRegistry;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildRootScripts, workspaceScripts } from "./default-scripts.js";
|
|
2
|
+
import { resolveWorkspace } from "./workspace.js";
|
|
3
|
+
import { toWorkspacePackageGlobs } from "./workspace-globs.js";
|
|
2
4
|
export function getDefaultWorkspaceScripts() {
|
|
3
5
|
return { ...workspaceScripts };
|
|
4
6
|
}
|
|
5
|
-
export function getDefaultRootScripts() {
|
|
6
|
-
|
|
7
|
+
export function getDefaultRootScripts(monorepoRoot) {
|
|
8
|
+
const resolution = resolveWorkspace(monorepoRoot);
|
|
9
|
+
const packageDirs = resolution.kind === 'packages' ? resolution.packageDirs : [];
|
|
10
|
+
return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
|
|
7
11
|
}
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -35,8 +35,9 @@ export interface ResolvedScript {
|
|
|
35
35
|
* Expands a script value into the ordered steps that it runs as: a string is one opaque step, and an array is one
|
|
36
36
|
* structural step per element.
|
|
37
37
|
*
|
|
38
|
-
* A bare string element and the `{ run }` spec compose the same step. The spec
|
|
39
|
-
*
|
|
38
|
+
* A bare string element and the `{ run }` spec compose the same step. The spec adds only declarations that
|
|
39
|
+
* position cannot express: what the step does with the invocation's trailing arguments, and whether it runs in
|
|
40
|
+
* the background.
|
|
40
41
|
*/
|
|
41
42
|
export declare function expandScript(script: ScriptValue, isWorkspaceRoot: boolean): readonly Step[];
|
|
42
43
|
/**
|
|
@@ -56,10 +57,10 @@ export declare function readPackageJsonScripts(packageDir: string): Record<strin
|
|
|
56
57
|
*/
|
|
57
58
|
export declare function buildWorkspaceRegistry(config: NmrConfig): ScriptRegistry;
|
|
58
59
|
/**
|
|
59
|
-
* Builds the merged root script registry
|
|
60
|
+
* Builds the merged root script registry for the monorepo at `monorepoRoot`:
|
|
60
61
|
* tier 1 (defaults) + tier 2 (config overrides)
|
|
61
62
|
*/
|
|
62
|
-
export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
|
|
63
|
+
export declare function buildRootRegistry(config: NmrConfig, monorepoRoot: string): ScriptRegistry;
|
|
63
64
|
/**
|
|
64
65
|
* Returns a `package.json` entry that re-invokes the command under which it is declared alongside other steps,
|
|
65
66
|
* or `undefined` when the entry does not.
|
package/dist/esm/resolver.js
CHANGED
|
@@ -46,7 +46,10 @@ export function expandScript(script, isWorkspaceRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
return script.map((element) => typeof element === 'string'
|
|
48
48
|
? composeNmrStep(element, isWorkspaceRoot)
|
|
49
|
-
: composeNmrStep(element.run, isWorkspaceRoot,
|
|
49
|
+
: composeNmrStep(element.run, isWorkspaceRoot, {
|
|
50
|
+
shouldDeclineArguments: element.shouldDeclineArguments,
|
|
51
|
+
shouldRunInBackground: element.shouldRunInBackground,
|
|
52
|
+
}));
|
|
50
53
|
}
|
|
51
54
|
export function describeScript(script) {
|
|
52
55
|
return typeof script === 'string' ? script : `[${script.map(describeElement).join(', ')}]`;
|
|
@@ -55,7 +58,11 @@ function describeElement(element) {
|
|
|
55
58
|
if (typeof element === 'string') {
|
|
56
59
|
return element;
|
|
57
60
|
}
|
|
58
|
-
|
|
61
|
+
const notes = [
|
|
62
|
+
...(element.shouldRunInBackground === true ? ['background'] : []),
|
|
63
|
+
...(element.shouldDeclineArguments === true ? ['no args'] : []),
|
|
64
|
+
];
|
|
65
|
+
return notes.length === 0 ? element.run : `${element.run} (${notes.join(', ')})`;
|
|
59
66
|
}
|
|
60
67
|
export function readPackageJsonScripts(packageDir) {
|
|
61
68
|
const file = resolvePackageJsonPath(packageDir);
|
|
@@ -83,9 +90,9 @@ export function buildWorkspaceRegistry(config) {
|
|
|
83
90
|
...config.workspaceScripts,
|
|
84
91
|
};
|
|
85
92
|
}
|
|
86
|
-
export function buildRootRegistry(config) {
|
|
93
|
+
export function buildRootRegistry(config, monorepoRoot) {
|
|
87
94
|
return {
|
|
88
|
-
...getDefaultRootScripts(),
|
|
95
|
+
...getDefaultRootScripts(monorepoRoot),
|
|
89
96
|
...config.rootScripts,
|
|
90
97
|
};
|
|
91
98
|
}
|
package/dist/esm/runCli.js
CHANGED
|
@@ -81,7 +81,7 @@ async function runInvocation(options) {
|
|
|
81
81
|
const shouldUseRoot = parsedArgs.isWorkspaceRoot || context.isRoot;
|
|
82
82
|
const anchorDir = shouldUseRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
83
83
|
if (parsedArgs.shouldShowHelp || !parsedArgs.command) {
|
|
84
|
-
stdout.write(`${generateHelp(context.config, anchorDir, shouldUseRoot)}\n`);
|
|
84
|
+
stdout.write(`${generateHelp(context.config, context.monorepoRoot, anchorDir, shouldUseRoot)}\n`);
|
|
85
85
|
return { exitCode: 0 };
|
|
86
86
|
}
|
|
87
87
|
const { command } = parsedArgs;
|
|
@@ -118,7 +118,9 @@ async function runInvocation(options) {
|
|
|
118
118
|
const result = await runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
|
|
119
119
|
return { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) };
|
|
120
120
|
}
|
|
121
|
-
const registry = shouldUseRoot
|
|
121
|
+
const registry = shouldUseRoot
|
|
122
|
+
? buildRootRegistry(context.config, context.monorepoRoot)
|
|
123
|
+
: buildWorkspaceRegistry(context.config);
|
|
122
124
|
assertNoSelfReference({
|
|
123
125
|
anchorDir,
|
|
124
126
|
command,
|
|
@@ -894,7 +896,7 @@ function formatMisplacedFlagHint(args, passthrough, style) {
|
|
|
894
896
|
`nmr reads its flags only before the command: \`${correctedTokens.map(quoteToken).join(' ')}\``);
|
|
895
897
|
}
|
|
896
898
|
function isDefinedInAnyScope(command, context) {
|
|
897
|
-
const registries = [buildRootRegistry(context.config), buildWorkspaceRegistry(context.config)];
|
|
899
|
+
const registries = [buildRootRegistry(context.config, context.monorepoRoot), buildWorkspaceRegistry(context.config)];
|
|
898
900
|
if (registries.some((registry) => Object.hasOwn(registry, command))) {
|
|
899
901
|
return true;
|
|
900
902
|
}
|
package/dist/esm/runner.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare function resolveInheritedChannel(stream: Writable): OutputChannel
|
|
|
75
75
|
* makes a signal to nmr end the run: The steps after it are never reached, whereas a shell would have gone on
|
|
76
76
|
* running them unsupervised.
|
|
77
77
|
*
|
|
78
|
+
* A background step is started and left running while the steps after it run. Its output is recorded rather
|
|
79
|
+
* than streamed, and printed whole at the first step boundary after it ends, so that it never interleaves with
|
|
80
|
+
* another step's. A failing foreground step still ends the sequence, but the list waits for every background step
|
|
81
|
+
* that it started and reports the first failure among them when the foreground did not fail. While a background
|
|
82
|
+
* step runs, a SIGINT or SIGTERM to nmr is forwarded to every running child, so that a signal sent to nmr alone
|
|
83
|
+
* still ends the children.
|
|
84
|
+
*
|
|
78
85
|
* Returns a bounded copy of what the list's own commands wrote, so that a caller can retain it. Only opaque
|
|
79
86
|
* steps contribute: A structural step is another nmr process reporting for the subtree beneath it, and its
|
|
80
87
|
* output is that subtree's to account for.
|
package/dist/esm/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
6
|
+
import { createOutputRecorder } from "./helpers/createOutputRecorder.js";
|
|
6
7
|
const SIGNAL_EXIT_BASE = 128;
|
|
7
8
|
const PIPE_DRAIN_GRACE_MS = 2_000;
|
|
8
9
|
export function resolveChannel(stream, quiet) {
|
|
@@ -22,26 +23,81 @@ export async function runSteps(steps, cwd, options) {
|
|
|
22
23
|
};
|
|
23
24
|
const parts = [];
|
|
24
25
|
let isCaptureWhole = true;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const backgroundRuns = [];
|
|
27
|
+
const runningChildren = new Set();
|
|
28
|
+
let receivedSignal;
|
|
29
|
+
let foregroundExitCode = 0;
|
|
30
|
+
function forwardSignal(signal) {
|
|
31
|
+
receivedSignal ??= signal;
|
|
32
|
+
for (const child of runningChildren)
|
|
33
|
+
child.kill(signal);
|
|
34
|
+
}
|
|
35
|
+
function launch(step, launchOptions) {
|
|
36
|
+
const started = startStep(step, cwd, launchOptions);
|
|
37
|
+
runningChildren.add(started.child);
|
|
38
|
+
void started.result.finally(() => runningChildren.delete(started.child));
|
|
39
|
+
return started;
|
|
40
|
+
}
|
|
41
|
+
function flushEndedBackgroundRuns() {
|
|
42
|
+
for (const run of backgroundRuns) {
|
|
43
|
+
if (run.result === undefined || run.isFlushed)
|
|
44
|
+
continue;
|
|
45
|
+
run.recorder.replay({ stderr: resolvedOptions.stderr, stdout: resolvedOptions.stdout });
|
|
46
|
+
run.isFlushed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
for (const step of steps) {
|
|
51
|
+
if (receivedSignal !== undefined) {
|
|
52
|
+
foregroundExitCode = describeSignalExitCode(receivedSignal);
|
|
53
|
+
break;
|
|
30
54
|
}
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
if (step.kind === 'structural' && step.shouldRunInBackground === true) {
|
|
56
|
+
if (backgroundRuns.length === 0) {
|
|
57
|
+
process.on('SIGINT', forwardSignal);
|
|
58
|
+
process.on('SIGTERM', forwardSignal);
|
|
59
|
+
}
|
|
60
|
+
backgroundRuns.push(startBackgroundRun(step, resolvedOptions, launch));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const result = await launch(step, resolvedOptions).result;
|
|
64
|
+
flushEndedBackgroundRuns();
|
|
65
|
+
if (step.kind === 'opaque') {
|
|
66
|
+
if (result.stderr === undefined || result.stdout === undefined) {
|
|
67
|
+
isCaptureWhole = false;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
parts.push({ stderr: result.stderr, stdout: result.stdout });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (result.exitCode !== 0) {
|
|
74
|
+
foregroundExitCode = result.exitCode;
|
|
75
|
+
if (result.outcome === 'signaled') {
|
|
76
|
+
for (const run of backgroundRuns)
|
|
77
|
+
run.child.kill(result.signal);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
33
80
|
}
|
|
34
81
|
}
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
for (const run of backgroundRuns) {
|
|
83
|
+
await run.settled;
|
|
84
|
+
flushEndedBackgroundRuns();
|
|
37
85
|
}
|
|
38
86
|
}
|
|
39
|
-
|
|
87
|
+
finally {
|
|
88
|
+
process.off('SIGINT', forwardSignal);
|
|
89
|
+
process.off('SIGTERM', forwardSignal);
|
|
90
|
+
}
|
|
91
|
+
const exitCode = selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns);
|
|
92
|
+
return { exitCode, ...composeRetainedOutput(parts, isCaptureWhole) };
|
|
40
93
|
}
|
|
41
94
|
export async function runCommand(command, cwd, options) {
|
|
42
95
|
return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
|
|
43
96
|
}
|
|
44
|
-
|
|
97
|
+
function runSpawned(spec, cwd, options) {
|
|
98
|
+
return startSpawned(spec, cwd, options).result;
|
|
99
|
+
}
|
|
100
|
+
function startSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
45
101
|
const quiet = options.quiet === true;
|
|
46
102
|
const stdout = options.stdout ?? process.stdout;
|
|
47
103
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -52,6 +108,9 @@ async function runSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
|
52
108
|
cwd,
|
|
53
109
|
env,
|
|
54
110
|
});
|
|
111
|
+
return { child, result: collectResult(child, { quiet, stderr, stdout }) };
|
|
112
|
+
}
|
|
113
|
+
async function collectResult(child, { quiet, stderr, stdout }) {
|
|
55
114
|
const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
|
|
56
115
|
const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
|
|
57
116
|
const commandCompletion = await awaitCompletion(child, () => {
|
|
@@ -75,17 +134,17 @@ async function runSpawned({ args, file, input, shouldUseShell }, cwd, options) {
|
|
|
75
134
|
}
|
|
76
135
|
return result;
|
|
77
136
|
}
|
|
78
|
-
function
|
|
137
|
+
function startStep(step, cwd, options) {
|
|
79
138
|
const { quiet, stderr, stdout } = options;
|
|
80
139
|
if (step.kind === 'opaque') {
|
|
81
|
-
return
|
|
140
|
+
return startSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: true }, cwd, {
|
|
82
141
|
...options,
|
|
83
142
|
channels: { stderr: resolveChannel(stderr, quiet), stdout: resolveChannel(stdout, quiet) },
|
|
84
143
|
});
|
|
85
144
|
}
|
|
86
145
|
const [file, ...args] = step.argv;
|
|
87
146
|
const input = step.shouldWithholdInput === true ? 'ignore' : 'inherit';
|
|
88
|
-
return
|
|
147
|
+
return startSpawned({ args, file, input, shouldUseShell: false }, cwd, {
|
|
89
148
|
...options,
|
|
90
149
|
quiet: false,
|
|
91
150
|
channels: { stderr: resolveInheritedChannel(stderr), stdout: resolveInheritedChannel(stdout) },
|
|
@@ -157,18 +216,49 @@ function awaitCompletion(child, stopReading) {
|
|
|
157
216
|
});
|
|
158
217
|
});
|
|
159
218
|
}
|
|
219
|
+
function buildBackgroundEnv(options) {
|
|
220
|
+
const env = options.env ?? process.env;
|
|
221
|
+
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
222
|
+
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
223
|
+
}
|
|
160
224
|
function describeExit(code, signal) {
|
|
161
225
|
if (signal !== null) {
|
|
162
|
-
return { outcome: 'signaled', exitCode:
|
|
226
|
+
return { outcome: 'signaled', exitCode: describeSignalExitCode(signal), signal };
|
|
163
227
|
}
|
|
164
228
|
return { outcome: 'exited', exitCode: code ?? 1 };
|
|
165
229
|
}
|
|
230
|
+
function describeSignalExitCode(signal) {
|
|
231
|
+
return SIGNAL_EXIT_BASE + os.constants.signals[signal];
|
|
232
|
+
}
|
|
166
233
|
function hasDescriptor(stream) {
|
|
167
234
|
return 'fd' in stream && typeof stream.fd === 'number';
|
|
168
235
|
}
|
|
169
236
|
function isTerminal(stream) {
|
|
170
237
|
return 'isTTY' in stream && stream.isTTY === true && hasDescriptor(stream);
|
|
171
238
|
}
|
|
239
|
+
async function recordResult(run, result) {
|
|
240
|
+
run.result = await result;
|
|
241
|
+
}
|
|
242
|
+
function selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns) {
|
|
243
|
+
if (foregroundExitCode !== 0)
|
|
244
|
+
return foregroundExitCode;
|
|
245
|
+
if (receivedSignal !== undefined)
|
|
246
|
+
return describeSignalExitCode(receivedSignal);
|
|
247
|
+
return backgroundRuns.find((run) => run.result !== undefined && run.result.exitCode !== 0)?.result?.exitCode ?? 0;
|
|
248
|
+
}
|
|
249
|
+
function startBackgroundRun(step, options, launch) {
|
|
250
|
+
const recorder = createOutputRecorder();
|
|
251
|
+
const started = launch({ ...step, shouldWithholdInput: true }, { ...options, env: buildBackgroundEnv(options), stderr: recorder.stderr, stdout: recorder.stdout });
|
|
252
|
+
const run = {
|
|
253
|
+
child: started.child,
|
|
254
|
+
isFlushed: false,
|
|
255
|
+
recorder,
|
|
256
|
+
result: undefined,
|
|
257
|
+
settled: Promise.resolve(),
|
|
258
|
+
};
|
|
259
|
+
run.settled = recordResult(run, started.result);
|
|
260
|
+
return run;
|
|
261
|
+
}
|
|
172
262
|
function writeBuffer(buffer, destination) {
|
|
173
263
|
if (buffer !== undefined && buffer.length > 0)
|
|
174
264
|
destination.write(buffer);
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* `shouldDeclineArguments` is passed from the composite element that composed the step to the one reader that acts
|
|
9
9
|
* on it, the binding of the invocation's trailing arguments. Every stage between the two passes it through unread.
|
|
10
10
|
*
|
|
11
|
-
* `shouldWithholdInput`
|
|
12
|
-
*
|
|
11
|
+
* `shouldRunInBackground` and `shouldWithholdInput` are read by the runner alone. The first starts the step without
|
|
12
|
+
* waiting for it; the second gives the step's child the null device as stdin rather than nmr's own. Like
|
|
13
|
+
* `shouldDeclineArguments`, each is set only when it holds and leaves the rendered chain unchanged.
|
|
13
14
|
*/
|
|
14
15
|
export type Step = {
|
|
15
16
|
kind: 'opaque';
|
|
@@ -18,8 +19,14 @@ export type Step = {
|
|
|
18
19
|
kind: 'structural';
|
|
19
20
|
argv: readonly [string, ...(readonly string[])];
|
|
20
21
|
shouldDeclineArguments?: boolean;
|
|
22
|
+
shouldRunInBackground?: boolean;
|
|
21
23
|
shouldWithholdInput?: boolean;
|
|
22
24
|
};
|
|
25
|
+
/** The declarations that a composite element adds to the step that it composes. */
|
|
26
|
+
export interface StepFlags {
|
|
27
|
+
shouldDeclineArguments?: boolean | undefined;
|
|
28
|
+
shouldRunInBackground?: boolean | undefined;
|
|
29
|
+
}
|
|
23
30
|
/** What a structural step asks of the nmr process that it spawns. */
|
|
24
31
|
export interface NmrStepTarget {
|
|
25
32
|
command: string;
|
|
@@ -44,10 +51,10 @@ export type SelfReference = 'chained' | 'sole';
|
|
|
44
51
|
* The element tokenizes on whitespace, so it may contain nmr's own flags but cannot contain a space-bearing
|
|
45
52
|
* token. `-w` is prepended as its own token: The child selects the root registry on its own.
|
|
46
53
|
*
|
|
47
|
-
* Because
|
|
48
|
-
*
|
|
54
|
+
* Because each flag is set only when it holds, a step composed with a flag at `false` compares equal to one
|
|
55
|
+
* composed without it.
|
|
49
56
|
*/
|
|
50
|
-
export declare function composeNmrStep(element: string, isWorkspaceRoot: boolean,
|
|
57
|
+
export declare function composeNmrStep(element: string, isWorkspaceRoot: boolean, flags?: StepFlags): Step;
|
|
51
58
|
/**
|
|
52
59
|
* Returns the steps that still run in a workspace without any package: every step but the ones that a `-R` fans
|
|
53
60
|
* out.
|
package/dist/esm/steps.js
CHANGED
|
@@ -11,12 +11,14 @@ const QUOTES = new Set(['"', "'"]);
|
|
|
11
11
|
const SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
|
12
12
|
const SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
13
13
|
const TOKEN_SEPARATORS = new Set([' ', '\t', '\n', '\r']);
|
|
14
|
-
export function composeNmrStep(element, isWorkspaceRoot,
|
|
15
|
-
const
|
|
14
|
+
export function composeNmrStep(element, isWorkspaceRoot, flags = {}) {
|
|
15
|
+
const { shouldDeclineArguments = false, shouldRunInBackground = false } = flags;
|
|
16
|
+
const nmrFlags = isWorkspaceRoot ? ['-w'] : [];
|
|
16
17
|
return {
|
|
17
18
|
kind: 'structural',
|
|
18
|
-
argv: ['nmr', ...
|
|
19
|
+
argv: ['nmr', ...nmrFlags, ...tokenize(element)],
|
|
19
20
|
...(shouldDeclineArguments && { shouldDeclineArguments }),
|
|
21
|
+
...(shouldRunInBackground && { shouldRunInBackground }),
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
export function dropRecursiveSteps(steps) {
|
package/dist/esm/taze.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export type TazeConfig = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Builds a taze configuration from nmr's shared upgrade policy and a repo's own settings.
|
|
14
14
|
*
|
|
15
|
-
* Any property that the caller supplies wins, including `0` and an explicit `undefined
|
|
16
|
-
*
|
|
15
|
+
* Any property that the caller supplies wins, including `0` and an explicit `undefined`, which clears nmr's
|
|
16
|
+
* default and hands the setting back to taze. Clearing `maturityPeriod` restores taze's inheritance of pnpm's
|
|
17
|
+
* `minimumReleaseAge`, which only applies while it is unset. Clearing `mode` yields taze's own `'default'`, which
|
|
18
|
+
* follows each declared range; passed through as `undefined`, it would overwrite taze's default and leave no
|
|
19
|
+
* valid range to search.
|
|
17
20
|
*
|
|
18
21
|
* The merge is shallow, which suits a policy of scalar defaults. A nested default would clobber rather
|
|
19
22
|
* than merge, so adding one means revisiting this.
|
package/dist/esm/taze.js
CHANGED
|
@@ -4,5 +4,8 @@ const SHARED_POLICY = {
|
|
|
4
4
|
mode: 'minor',
|
|
5
5
|
};
|
|
6
6
|
export function defineConfig(config) {
|
|
7
|
-
|
|
7
|
+
const merged = { ...SHARED_POLICY, ...config };
|
|
8
|
+
if ('mode' in config && config.mode === undefined)
|
|
9
|
+
merged.mode = 'default';
|
|
10
|
+
return merged;
|
|
8
11
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -28,6 +28,15 @@ export interface CheckCacheConfig {
|
|
|
28
28
|
*/
|
|
29
29
|
extraCommands?: string[];
|
|
30
30
|
}
|
|
31
|
+
/** Self-hosted runner settings honored at the monorepo root, read by nmr's readyup kit. */
|
|
32
|
+
export interface SelfHostedRunnerConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Why the repo keeps its CI off the self-hosted runners although it qualifies, such as a circular dependency on
|
|
35
|
+
* the runners themselves. Declaring it stops the kit from recommending `CI_RUNS_ON`. A missing tool is not a
|
|
36
|
+
* reason: A workflow step installs it.
|
|
37
|
+
*/
|
|
38
|
+
exempt: string;
|
|
39
|
+
}
|
|
31
40
|
/** Output settings honored at the monorepo root. */
|
|
32
41
|
export interface OutputConfig {
|
|
33
42
|
/**
|
|
@@ -49,4 +58,5 @@ export interface NmrConfig {
|
|
|
49
58
|
output?: OutputConfig;
|
|
50
59
|
workspaceScripts?: Record<string, ScriptValue>;
|
|
51
60
|
rootScripts?: Record<string, ScriptValue>;
|
|
61
|
+
selfHostedRunner?: SelfHostedRunnerConfig;
|
|
52
62
|
}
|