@williamthorsen/nmr 0.43.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.
@@ -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', 'lint:check', 'test'],
7
- 'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
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 const rootScripts = {
29
- audit: ['audit:prod', 'audit:dev'],
30
- 'audit:dev': 'pnpm exec v11y --dev',
31
- 'audit:prod': 'pnpm exec v11y --prod',
32
- build: ['-R build'],
33
- check: [TYPECHECK_STEP, 'fmt:check', 'lint:check', 'test'],
34
- 'check:strict': [TYPECHECK_STEP, 'fmt:check', 'lint:strict', 'test'],
35
- ci: [{ run: 'build', shouldDeclineArguments: true }, 'check:strict'],
36
- clean: 'nmr-clean',
37
- fix: ['lint', 'fmt'],
38
- 'fix:check': ['fmt:check', 'lint:check'],
39
- fmt: 'nmr-fmt --write',
40
- 'fmt:check': 'nmr-fmt --check',
41
- lint: 'eslint --fix .',
42
- 'lint:check': 'eslint .',
43
- 'lint:strict': 'strict-lint',
44
- prepush: [{ run: 'audit', shouldDeclineArguments: true }, 'ci'],
45
- 'report-overrides': 'nmr-report-overrides',
46
- 'root:check': [ROOT_TYPECHECK_STEP, 'fmt:check', 'root:lint:check', 'root:test'],
47
- 'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
48
- 'root:lint:check': "eslint --ignore-pattern 'packages/**' .",
49
- 'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
50
- 'root:test': `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
51
- 'root:test:all': 'vitest --config ./vitest.root.config.ts',
52
- 'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
53
- 'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
54
- 'root:typecheck': 'tsgo --noEmit',
55
- 'root:upgrade': 'nmr-report-overrides && nmr-taze',
56
- test: ['root:test', '-R test'],
57
- 'test:all': ['root:test:all', '-R test:all'],
58
- 'test:coverage': ['root:test', '-R test:coverage'],
59
- 'test:tool': ['root:test:tool', '-R test:tool'],
60
- 'test:unit': ['root:test:unit', '-R test:unit'],
61
- 'test:watch': `vitest ${GATE_PROJECTS} --watch`,
62
- typecheck: [ROOT_TYPECHECK_STEP, { run: '-R typecheck', shouldDeclineArguments: true }],
63
- upgrade: 'nmr-report-overrides && nmr-taze --recursive',
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
  *
@@ -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 a shallow copy of the default root scripts, whose entries a caller may replace without changing the
10
- * defaults.
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 { rootScripts, workspaceScripts } from "./default-scripts.js";
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
- return { ...rootScripts };
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
  }
@@ -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's only addition is the
39
- * declaration of what the step does with the invocation's trailing arguments, which position cannot express.
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.
@@ -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, element.shouldDeclineArguments));
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
- return element.shouldDeclineArguments === true ? `${element.run} (no args)` : element.run;
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
  }
@@ -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 ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
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
  }
@@ -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.
@@ -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
- for (const step of steps) {
26
- const result = await runStep(step, cwd, resolvedOptions);
27
- if (step.kind === 'opaque') {
28
- if (result.stderr === undefined || result.stdout === undefined) {
29
- isCaptureWhole = false;
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
- else {
32
- parts.push({ stderr: result.stderr, stdout: result.stdout });
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
- if (result.exitCode !== 0) {
36
- return { exitCode: result.exitCode, ...composeRetainedOutput(parts, isCaptureWhole) };
82
+ for (const run of backgroundRuns) {
83
+ await run.settled;
84
+ flushEndedBackgroundRuns();
37
85
  }
38
86
  }
39
- return { exitCode: 0, ...composeRetainedOutput(parts, isCaptureWhole) };
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
- async function runSpawned({ args, file, input, shouldUseShell }, cwd, options) {
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 runStep(step, cwd, options) {
137
+ function startStep(step, cwd, options) {
79
138
  const { quiet, stderr, stdout } = options;
80
139
  if (step.kind === 'opaque') {
81
- return runSpawned({ args: [], file: step.command, input: 'inherit', shouldUseShell: true }, cwd, {
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 runSpawned({ args, file, input, shouldUseShell: false }, cwd, {
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: SIGNAL_EXIT_BASE + os.constants.signals[signal], signal };
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);
@@ -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` is read by the runner alone, which gives the step's child the null device as stdin rather
12
- * than nmr's own. Like `shouldDeclineArguments`, it is set only when it holds and leaves the rendered chain unchanged.
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 `shouldDeclineArguments` is set only when it holds, a step that takes the trailing arguments compares
48
- * equal to one composed without the option.
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, shouldDeclineArguments?: boolean): Step;
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, shouldDeclineArguments = false) {
15
- const flags = isWorkspaceRoot ? ['-w'] : [];
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', ...flags, ...tokenize(element)],
19
+ argv: ['nmr', ...nmrFlags, ...tokenize(element)],
19
20
  ...(shouldDeclineArguments && { shouldDeclineArguments }),
21
+ ...(shouldRunInBackground && { shouldRunInBackground }),
20
22
  };
21
23
  }
22
24
  export function dropRecursiveSteps(steps) {
@@ -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`. The latter restores
16
- * taze's inheritance of pnpm's `minimumReleaseAge`, which only applies while `maturityPeriod` is unset.
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
- return { ...SHARED_POLICY, ...config };
7
+ const merged = { ...SHARED_POLICY, ...config };
8
+ if ('mode' in config && config.mode === undefined)
9
+ merged.mode = 'default';
10
+ return merged;
8
11
  }
@@ -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
  }