@williamthorsen/nmr 0.51.0 → 0.53.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 +97 -0
- package/.readyup/kits/default.js +29 -27
- package/.readyup/manifest.json +31 -23
- package/CHANGELOG.md +32 -0
- package/README.md +13 -6
- package/bin/nmr-actionlint.js +11 -0
- package/dist/esm/check-cache-gate.d.ts +1 -1
- package/dist/esm/check-cache-gate.js +16 -4
- package/dist/esm/check-cache.js +1 -0
- package/dist/esm/cli-actionlint.d.ts +1 -0
- package/dist/esm/cli-actionlint.js +12 -0
- package/dist/esm/commands/actionlint.d.ts +23 -0
- package/dist/esm/commands/actionlint.js +31 -0
- package/dist/esm/commands/build-output.d.ts +13 -0
- package/dist/esm/commands/build-output.js +30 -1
- package/dist/esm/commands/build.js +12 -3
- package/dist/esm/commands/clean.d.ts +1 -1
- package/dist/esm/commands/clean.js +2 -1
- package/dist/esm/commands/fmt.d.ts +2 -1
- package/dist/esm/commands/fmt.js +7 -2
- package/dist/esm/commands/run-vitest.js +2 -0
- package/dist/esm/commands/workflow-tools.d.ts +39 -0
- package/dist/esm/commands/workflow-tools.js +140 -0
- package/dist/esm/default-scripts.js +5 -3
- package/dist/esm/delegate-host.js +3 -3
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/glyphs.js +1 -0
- package/dist/esm/remote-host-session.js +60 -0
- package/dist/esm/remote-run.js +5 -5
- package/dist/esm/remote-snapshot.js +2 -2
- package/dist/esm/runner.d.ts +7 -0
- package/dist/esm/runner.js +37 -3
- package/dist/esm/stated-outcome.d.ts +37 -0
- package/dist/esm/stated-outcome.js +60 -0
- package/dist/esm/taze.d.ts +9 -7
- package/dist/esm/taze.js +0 -1
- package/dist/esm/verdict.d.ts +2 -0
- package/dist/esm/verdict.js +15 -3
- package/docs/check-cache.md +1 -1
- package/docs/delegate.md +4 -4
- package/docs/reporting.md +5 -1
- package/docs/scripts.md +9 -1
- package/docs/upgrades.md +6 -4
- package/docs/utilities.md +14 -3
- package/package.json +8 -7
package/dist/esm/runner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RunCommandOptions, type RunCommandResult } from './child-process.js';
|
|
2
|
+
import { type StatedOutcome } from './stated-outcome.js';
|
|
2
3
|
import type { Step } from './steps.js';
|
|
3
4
|
/** What a step list needs to run. Each step's channels follow its kind, so the caller chooses none of them. */
|
|
4
5
|
export type RunStepsOptions = Omit<RunCommandOptions, 'channels'>;
|
|
@@ -14,6 +15,8 @@ export interface RunStepsResult {
|
|
|
14
15
|
* at least one to capture.
|
|
15
16
|
*/
|
|
16
17
|
retainedOutput?: RetainedOutput;
|
|
18
|
+
/** The outcome that each foreground opaque step stated, absent unless a step that exited 0 stated one. */
|
|
19
|
+
statedOutcomes?: Map<Step, StatedOutcome>;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Runs a step list in order and resolves once one fails or all have run, implementing the `&&` semantics that
|
|
@@ -33,6 +36,10 @@ export interface RunStepsResult {
|
|
|
33
36
|
* Returns a bounded copy of what the list's own commands wrote, so that a caller can retain it. Only opaque
|
|
34
37
|
* steps contribute: A structural step is another nmr process reporting for the subtree beneath it, and its
|
|
35
38
|
* output is that subtree's to account for.
|
|
39
|
+
*
|
|
40
|
+
* Each foreground opaque step is handed a file of its own in `NMR_OUTCOME_FILE`, through which an nmr bin states its
|
|
41
|
+
* outcome. Every other step runs without the variable, as does a step whose value would have been inherited from an
|
|
42
|
+
* ancestor, so that a statement reaches only the list that handed out its file.
|
|
36
43
|
*/
|
|
37
44
|
export declare function runSteps(steps: readonly Step[], cwd: string | undefined, options: RunStepsOptions): Promise<RunStepsResult>;
|
|
38
45
|
/**
|
package/dist/esm/runner.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import process from 'node:process';
|
|
5
|
+
import { writeDebugNote } from "./check-cache.js";
|
|
2
6
|
import { describeSignalExitCode, startSpawned, } from "./child-process.js";
|
|
3
7
|
import { createOutputRecorder } from "./helpers/createOutputRecorder.js";
|
|
4
8
|
import { isTerminal, resolveChannel, resolveInheritedChannel } from "./output-channel.js";
|
|
9
|
+
import { omitOutcomeFile, OUTCOME_FILE_ENV_VAR, readStatedOutcome } from "./stated-outcome.js";
|
|
5
10
|
export async function runSteps(steps, cwd, options) {
|
|
6
11
|
const resolvedOptions = {
|
|
7
12
|
...options,
|
|
13
|
+
env: omitOutcomeFile(options.env ?? process.env),
|
|
8
14
|
quiet: options.quiet === true,
|
|
9
15
|
stdout: options.stdout ?? process.stdout,
|
|
10
16
|
stderr: options.stderr ?? process.stderr,
|
|
@@ -15,6 +21,8 @@ export async function runSteps(steps, cwd, options) {
|
|
|
15
21
|
const runningChildren = new Set();
|
|
16
22
|
let receivedSignal;
|
|
17
23
|
let foregroundExitCode = 0;
|
|
24
|
+
const statedOutcomes = new Map();
|
|
25
|
+
let outcomeDir;
|
|
18
26
|
function forwardSignal(signal) {
|
|
19
27
|
receivedSignal ??= signal;
|
|
20
28
|
for (const child of runningChildren)
|
|
@@ -26,6 +34,24 @@ export async function runSteps(steps, cwd, options) {
|
|
|
26
34
|
void started.result.finally(() => runningChildren.delete(started.child));
|
|
27
35
|
return started;
|
|
28
36
|
}
|
|
37
|
+
async function runForeground(step) {
|
|
38
|
+
if (step.kind !== 'opaque') {
|
|
39
|
+
return { result: await launch(step, resolvedOptions).result };
|
|
40
|
+
}
|
|
41
|
+
outcomeDir ??= await mkdtemp(path.join(os.tmpdir(), 'nmr-outcome-'));
|
|
42
|
+
const outcomeFile = path.join(outcomeDir, `${steps.indexOf(step)}.json`);
|
|
43
|
+
const env = { ...resolvedOptions.env, [OUTCOME_FILE_ENV_VAR]: outcomeFile };
|
|
44
|
+
const started = launch(step, { ...resolvedOptions, env });
|
|
45
|
+
const result = await started.result;
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
return { result };
|
|
48
|
+
}
|
|
49
|
+
const parentPids = [process.pid, ...(started.child.pid === undefined ? [] : [started.child.pid])];
|
|
50
|
+
const statedOutcome = await readStatedOutcome(outcomeFile, parentPids, (reason) => {
|
|
51
|
+
writeDebugNote(`ignoring the outcome that a step stated: ${reason}`, resolvedOptions.env, resolvedOptions.stderr);
|
|
52
|
+
});
|
|
53
|
+
return { result, ...(statedOutcome !== undefined && { statedOutcome }) };
|
|
54
|
+
}
|
|
29
55
|
function flushEndedBackgroundRuns() {
|
|
30
56
|
for (const run of backgroundRuns) {
|
|
31
57
|
if (run.result === undefined || run.isFlushed)
|
|
@@ -48,8 +74,10 @@ export async function runSteps(steps, cwd, options) {
|
|
|
48
74
|
backgroundRuns.push(startBackgroundRun(step, resolvedOptions, launch));
|
|
49
75
|
continue;
|
|
50
76
|
}
|
|
51
|
-
const result = await
|
|
77
|
+
const { result, statedOutcome } = await runForeground(step);
|
|
52
78
|
flushEndedBackgroundRuns();
|
|
79
|
+
if (statedOutcome !== undefined)
|
|
80
|
+
statedOutcomes.set(step, statedOutcome);
|
|
53
81
|
if (step.kind === 'opaque') {
|
|
54
82
|
if (result.stderr === undefined || result.stdout === undefined) {
|
|
55
83
|
isCaptureWhole = false;
|
|
@@ -75,9 +103,15 @@ export async function runSteps(steps, cwd, options) {
|
|
|
75
103
|
finally {
|
|
76
104
|
process.off('SIGINT', forwardSignal);
|
|
77
105
|
process.off('SIGTERM', forwardSignal);
|
|
106
|
+
if (outcomeDir !== undefined)
|
|
107
|
+
await rm(outcomeDir, { force: true, recursive: true });
|
|
78
108
|
}
|
|
79
109
|
const exitCode = selectExitCode(foregroundExitCode, receivedSignal, backgroundRuns);
|
|
80
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
exitCode,
|
|
112
|
+
...composeRetainedOutput(parts, isCaptureWhole),
|
|
113
|
+
...(statedOutcomes.size > 0 && { statedOutcomes }),
|
|
114
|
+
};
|
|
81
115
|
}
|
|
82
116
|
export async function runCommand(command, cwd, options) {
|
|
83
117
|
return runSpawned({ args: [], file: command, input: 'inherit', shouldUseShell: true }, cwd, options);
|
|
@@ -113,7 +147,7 @@ function composeRetainedOutput(parts, isCaptureWhole) {
|
|
|
113
147
|
};
|
|
114
148
|
}
|
|
115
149
|
function buildBackgroundEnv(options) {
|
|
116
|
-
const env = options
|
|
150
|
+
const { env } = options;
|
|
117
151
|
const shouldForceColor = isTerminal(options.stdout) && (env['NO_COLOR'] ?? '') === '' && env['FORCE_COLOR'] === undefined;
|
|
118
152
|
return shouldForceColor ? { ...env, FORCE_COLOR: '1' } : env;
|
|
119
153
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names the file in which an nmr bin states its own outcome to the nmr process that runs it. nmr sets it for each
|
|
3
|
+
* opaque step and removes it from every other process that it starts.
|
|
4
|
+
*/
|
|
5
|
+
export declare const OUTCOME_FILE_ENV_VAR = "NMR_OUTCOME_FILE";
|
|
6
|
+
/**
|
|
7
|
+
* An outcome that a bin states about its own run, which a passing verdict reports in its detail slot.
|
|
8
|
+
*
|
|
9
|
+
* `build-unchanged` is a build-cache hit. Its `savedMs` is the emit time recorded for the current inputs, whatever
|
|
10
|
+
* its size, and is absent when none was recorded; whether a saving is worth naming belongs to rendering.
|
|
11
|
+
*/
|
|
12
|
+
export interface StatedOutcome {
|
|
13
|
+
kind: 'build-unchanged';
|
|
14
|
+
savedMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the environment without `NMR_OUTCOME_FILE`, for a process that is not the one to which the file was handed,
|
|
18
|
+
* so that it cannot state an outcome for the step that runs it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function omitOutcomeFile(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
21
|
+
/**
|
|
22
|
+
* Reads the outcome that a step stated in `filePath`, or `undefined` when it stated none. A record that cannot be
|
|
23
|
+
* read as a stated outcome is reported to `onRejection` and treated as none, so that a bin from another nmr version
|
|
24
|
+
* cannot break the verdict.
|
|
25
|
+
*
|
|
26
|
+
* A record is accepted only from a process whose parent is one of `parentPids`: the process that handed out the file,
|
|
27
|
+
* or the shell that it spawned for the step. A shell running a single command either replaces itself with it or runs
|
|
28
|
+
* it as its child, so the step's own bin passes either way. Where the shell replaces itself, a process that the
|
|
29
|
+
* command starts passes too; a process two levels below the command does not.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readStatedOutcome(filePath: string, parentPids: readonly number[], onRejection: (reason: string) => void): Promise<StatedOutcome | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Writes the outcome into the file that `NMR_OUTCOME_FILE` names, with this process's parent as the one that it is
|
|
34
|
+
* stated to, and does nothing when the variable is unset. The statement is advisory: A write that fails leaves the
|
|
35
|
+
* verdict reading as it would without one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function writeStatedOutcome(env: NodeJS.ProcessEnv, outcome: StatedOutcome): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
5
|
+
export const OUTCOME_FILE_ENV_VAR = 'NMR_OUTCOME_FILE';
|
|
6
|
+
export function omitOutcomeFile(env) {
|
|
7
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => name !== OUTCOME_FILE_ENV_VAR));
|
|
8
|
+
}
|
|
9
|
+
export async function readStatedOutcome(filePath, parentPids, onRejection) {
|
|
10
|
+
let text;
|
|
11
|
+
try {
|
|
12
|
+
text = await readFile(filePath, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissingFileError(error)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
onRejection(`could not read ${filePath}: ${describeError(error)}`);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = JSON.parse(text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
onRejection(`${filePath} does not contain JSON`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!isStatedOutcome(value)) {
|
|
30
|
+
onRejection(`${filePath} does not contain a stated outcome that this nmr knows`);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!parentPids.includes(value.parentPid)) {
|
|
34
|
+
onRejection(`${filePath} was written by a process whose parent is not one of ${parentPids.join(', ')}`);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return value.savedMs === undefined ? { kind: value.kind } : { kind: value.kind, savedMs: value.savedMs };
|
|
38
|
+
}
|
|
39
|
+
export async function writeStatedOutcome(env, outcome) {
|
|
40
|
+
const filePath = env[OUTCOME_FILE_ENV_VAR];
|
|
41
|
+
if (filePath === undefined || filePath === '') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const record = { ...outcome, parentPid: process.ppid };
|
|
46
|
+
await writeFile(filePath, JSON.stringify(record));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isMissingFileError(error) {
|
|
52
|
+
return isObject(error) && error['code'] === 'ENOENT';
|
|
53
|
+
}
|
|
54
|
+
function isStatedOutcome(value) {
|
|
55
|
+
if (!isObject(value) || value['kind'] !== 'build-unchanged' || !Number.isSafeInteger(value['parentPid'])) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const savedMs = value['savedMs'];
|
|
59
|
+
return savedMs === undefined || (typeof savedMs === 'number' && Number.isFinite(savedMs) && savedMs >= 0);
|
|
60
|
+
}
|
package/dist/esm/taze.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { CheckOptions } from 'taze';
|
|
|
3
3
|
* A taze configuration.
|
|
4
4
|
*
|
|
5
5
|
* Every property admits `undefined` explicitly, unlike `Partial<CheckOptions>`, which rejects it under
|
|
6
|
-
* `exactOptionalPropertyTypes`. Passing `undefined` is how a consumer clears `
|
|
7
|
-
*
|
|
6
|
+
* `exactOptionalPropertyTypes`. Passing `undefined` is how a consumer clears `mode`, so the type has to allow
|
|
7
|
+
* it.
|
|
8
8
|
*/
|
|
9
9
|
export type TazeConfig = {
|
|
10
10
|
[K in keyof CheckOptions]?: CheckOptions[K] | undefined;
|
|
@@ -12,7 +12,7 @@ export type TazeConfig = {
|
|
|
12
12
|
/**
|
|
13
13
|
* Maturity period in days for each package pattern, read by `nmr-taze` rather than by taze. A key is a package
|
|
14
14
|
* name in which `*` matches any run of characters, and the first key that matches a package decides its period.
|
|
15
|
-
* Every other dependency keeps `maturityPeriod`.
|
|
15
|
+
* Every other dependency keeps `maturityPeriod` or, while that is unset, pnpm's `minimumReleaseAge`.
|
|
16
16
|
*/
|
|
17
17
|
packageMaturityPeriod?: Record<string, number> | undefined;
|
|
18
18
|
};
|
|
@@ -20,10 +20,12 @@ export type TazeConfig = {
|
|
|
20
20
|
* Builds a taze configuration from nmr's shared upgrade policy and a repo's own settings.
|
|
21
21
|
*
|
|
22
22
|
* Any property that the caller supplies wins, including `0` and an explicit `undefined`, which clears nmr's
|
|
23
|
-
* default and hands the setting back to taze. Clearing `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* default and hands the setting back to taze. Clearing `mode` yields taze's own `'default'`, which follows each
|
|
24
|
+
* declared range; passed through as `undefined`, it would overwrite taze's default and leave no valid range to
|
|
25
|
+
* search.
|
|
26
|
+
*
|
|
27
|
+
* The policy leaves `maturityPeriod` unset, because taze inherits pnpm's `minimumReleaseAge` only while it is
|
|
28
|
+
* unset. A repo that sets it replaces pnpm's period.
|
|
27
29
|
*
|
|
28
30
|
* The merge is shallow, which suits a policy of scalar defaults. A nested default would clobber rather
|
|
29
31
|
* than merge, so adding one means revisiting this.
|
package/dist/esm/taze.js
CHANGED
package/dist/esm/verdict.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Writable } from 'node:stream';
|
|
|
2
2
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
3
3
|
import type { ReplayLine } from './check-cache-entries.js';
|
|
4
4
|
import type { ReportFormat } from './report-format.js';
|
|
5
|
+
import type { StatedOutcome } from './stated-outcome.js';
|
|
5
6
|
/**
|
|
6
7
|
* The ceiling to which a rendered verdict, its newline included, is held.
|
|
7
8
|
*
|
|
@@ -46,6 +47,7 @@ export type VerdictOutcome = {
|
|
|
46
47
|
} & ({
|
|
47
48
|
outcome: 'passed';
|
|
48
49
|
durationMs: number;
|
|
50
|
+
stated?: StatedOutcome;
|
|
49
51
|
} | {
|
|
50
52
|
outcome: 'failed';
|
|
51
53
|
durationMs: number;
|
package/dist/esm/verdict.js
CHANGED
|
@@ -5,7 +5,7 @@ import { formatDuration, formatSaving } from "./helpers/duration.js";
|
|
|
5
5
|
export const VERDICT_LINE_LIMIT_BYTES = 512;
|
|
6
6
|
export function renderVerdict(verdict, style) {
|
|
7
7
|
const { marker, phrase } = describeOutcome(verdict, style);
|
|
8
|
-
const detail = flattenDetail(verdict.detail ?? renderReplay(verdict) ?? '');
|
|
8
|
+
const detail = flattenDetail(verdict.detail ?? renderReplay(verdict) ?? renderStated(verdict) ?? '');
|
|
9
9
|
const detailClause = detail === '' ? '' : ` — ${detail}`;
|
|
10
10
|
return clampToBytes(`${marker} ${verdict.scope}: ${verdict.command}: ${phrase}${detailClause}`, LINE_BUDGET_BYTES);
|
|
11
11
|
}
|
|
@@ -23,7 +23,7 @@ const MIN_CUT_BYTES = Buffer.byteLength(TRUNCATION_MARK);
|
|
|
23
23
|
const REPLAY_MARKER = 'replayed:';
|
|
24
24
|
function describeOutcome(verdict, style) {
|
|
25
25
|
const statuses = STATUS_GLYPHS[style];
|
|
26
|
-
const onHost = verdict.host === undefined ? '' : ` on ${verdict.host}`;
|
|
26
|
+
const onHost = verdict.host === undefined ? '' : ` on ${formatHost(verdict.host, style)}`;
|
|
27
27
|
switch (verdict.outcome) {
|
|
28
28
|
case 'passed':
|
|
29
29
|
return { marker: statuses.passed.text, phrase: `passed${onHost} in ${formatDuration(verdict.durationMs)}` };
|
|
@@ -35,9 +35,10 @@ function describeOutcome(verdict, style) {
|
|
|
35
35
|
case 'recalled': {
|
|
36
36
|
const saving = formatSaving(verdict.savedMs);
|
|
37
37
|
const savingClause = saving === undefined ? '' : `, ${saving}`;
|
|
38
|
+
const source = verdict.host === undefined ? 'this tree' : formatHost(verdict.host, style);
|
|
38
39
|
return {
|
|
39
40
|
marker: statuses.skipped.text,
|
|
40
|
-
phrase: `passed ${formatDuration(verdict.ageMs)} ago on ${
|
|
41
|
+
phrase: `passed ${formatDuration(verdict.ageMs)} ago on ${source}${savingClause}`,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
case 'no-op':
|
|
@@ -73,6 +74,13 @@ function renderReplay(verdict) {
|
|
|
73
74
|
const lines = verdict.replay.map((line) => `${line.scope}: ${line.command}: ${line.excerpt}`);
|
|
74
75
|
return `${REPLAY_MARKER} ${lines.join('; ')}`;
|
|
75
76
|
}
|
|
77
|
+
function renderStated(verdict) {
|
|
78
|
+
if (verdict.outcome !== 'passed' || verdict.stated === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const saving = formatSaving(verdict.stated.savedMs ?? 0);
|
|
82
|
+
return saving === undefined ? 'build skipped as unchanged' : `build skipped as unchanged, ${saving}`;
|
|
83
|
+
}
|
|
76
84
|
function flattenDetail(detail) {
|
|
77
85
|
return detail.replaceAll(/[\r\n]+/gu, ' ').trim();
|
|
78
86
|
}
|
|
@@ -87,6 +95,10 @@ function findCutTarget(sizesBytes, longestSizeBytes, overrunBytes) {
|
|
|
87
95
|
function isWithinBudget(renderedLine) {
|
|
88
96
|
return Buffer.byteLength(renderedLine) <= LINE_BUDGET_BYTES;
|
|
89
97
|
}
|
|
98
|
+
function formatHost(host, style) {
|
|
99
|
+
const glyph = NMR_GLYPHS[style].host.text;
|
|
100
|
+
return glyph === '' ? host : `${glyph} ${host}`;
|
|
101
|
+
}
|
|
90
102
|
function readAttribution(verdict) {
|
|
91
103
|
if (verdict.outcome !== 'recalled' || verdict.replay === undefined) {
|
|
92
104
|
return [];
|
package/docs/check-cache.md
CHANGED
|
@@ -10,7 +10,7 @@ The cache is a working-tree cache, not a build cache: It answers "has this exact
|
|
|
10
10
|
|
|
11
11
|
## What is cached
|
|
12
12
|
|
|
13
|
-
Cacheable by default: `check`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
|
|
13
|
+
Cacheable by default: `check`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `lint:workflows`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
|
|
14
14
|
|
|
15
15
|
Everything else runs every time:
|
|
16
16
|
|
package/docs/delegate.md
CHANGED
|
@@ -5,7 +5,7 @@ nmr runs a command on another machine over SSH, against the working tree on this
|
|
|
5
5
|
```console
|
|
6
6
|
$ nmr --delegate-to imac2 ci
|
|
7
7
|
# ... the host's own output and verdicts ...
|
|
8
|
-
✅ my-repo: ci: passed on imac2 in 1m 48s
|
|
8
|
+
✅ my-repo: ci: passed on 📡 imac2 in 1m 48s
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
A machine that runs several worktrees' checks at once runs out of memory, and the [machine lock](machine-lock.md) protects it by making the checks wait for one another. Delegation moves a check to a machine that has the room for it instead. Which hosts a machine can reach is a fact about the machine, so a user config on the machine, not the repo, names them and turns delegation on.
|
|
@@ -61,7 +61,7 @@ With several hosts, nmr asks every host at once, with one `ssh` call each, wheth
|
|
|
61
61
|
|
|
62
62
|
```console
|
|
63
63
|
$ NMR_DELEGATE=imac2,imac3 nmr ci
|
|
64
|
-
✅ my-repo: ci: passed on imac3 in 1m 52s
|
|
64
|
+
✅ my-repo: ci: passed on 📡 imac3 in 1m 52s
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
- **A single host is not asked.** The delegation goes to it directly.
|
|
@@ -98,7 +98,7 @@ $ nmr --delegate-to imac2 ci
|
|
|
98
98
|
|
|
99
99
|
nmr records the working tree as it is on disk as a commit: tracked changes, staged or not, and untracked files that git does not ignore. This is the tree that a local run checks. The commit is built in a temporary index, so the repository's index, refs, and stash do not change.
|
|
100
100
|
|
|
101
|
-
nmr pushes the commit to a checkout on the host under `~/.cache/nmr/delegate/`, without running the repository's `pre-push` hook, then runs `pnpm install --frozen-lockfile` and the command from the directory that corresponds to the local one, with nmr's own flags as they were given. The host keeps one checkout for each repository on each machine that delegates to it, so concurrent delegations from different worktrees do not share one, and `node_modules` and build output persist between runs. nmr does not remove these checkouts.
|
|
101
|
+
nmr pushes the commit to a checkout on the host under `~/.cache/nmr/delegate/`, without running the repository's `pre-push` hook, then runs `pnpm install --frozen-lockfile` and the command from the directory that corresponds to the local one, with nmr's own flags as they were given. The host keeps one checkout for each repository on each machine that delegates to it, so concurrent delegations from different worktrees do not share one, and `node_modules` and build output persist between runs. When the workspace packages' `bin` entries differ from those of the checkout's last successful install, nmr first removes pnpm's workspace state, so that the install links the new bins, which an unchanged lockfile would otherwise let pnpm skip; that install reruns the workspace's `prepare` scripts. nmr does not remove these checkouts.
|
|
102
102
|
|
|
103
103
|
- **Ignored files stay here.** A command that needs a gitignored file, such as a local `.env`, does not find it on the host.
|
|
104
104
|
- **A repository with submodules cannot be delegated**, nor can a tree containing a nested repository, because the snapshot does not include their content. A delegation flag refuses such a tree; under `NMR_DELEGATE` or the user config, the command runs here, and nmr prints one line saying why.
|
|
@@ -123,7 +123,7 @@ The host's nmr prints its output and verdicts, and nmr here adds one verdict for
|
|
|
123
123
|
A failure of the delegation itself, rather than of the command, exits 255, the status with which `ssh` reports its own failures, and names the step that failed, after what that step printed:
|
|
124
124
|
|
|
125
125
|
```console
|
|
126
|
-
❌ my-repo: ci: failed on imac2 in 10.0s (exit 255) — could not prepare imac2
|
|
126
|
+
❌ my-repo: ci: failed on 📡 imac2 in 10.0s (exit 255) — could not prepare imac2
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
The steps are `prepare`, which reaches the host and creates the checkout, `push`, `check out`, and `install`. A connection lost while the command runs is reported as `lost connection to <host>`.
|
package/docs/reporting.md
CHANGED
|
@@ -6,6 +6,8 @@ The verdict line and its four outcomes are shown in the [README](../README.md#wh
|
|
|
6
6
|
|
|
7
7
|
A recalled pass also includes an excerpt of the run that it recalls, if that run left one; see [Replaying a skipped run's output](check-cache.md#replaying-a-skipped-runs-output).
|
|
8
8
|
|
|
9
|
+
A pass also includes what the command stated about its own run, if the command is an nmr bin that states one, such as `nmr-compile` on a build-cache hit: `PASS nmr-core: compile: passed in 0.3s — build skipped as unchanged, saved ~2.1s`. The line names the saving under the same rule as a recalled pass. Because the bin states it through [`NMR_OUTCOME_FILE`](#reserved-environment-variables) rather than through its output, a quiet run reports it too.
|
|
10
|
+
|
|
9
11
|
The scope is the directory to which the command's registry belongs: a package's own directory, or the monorepo root. Durations and ages truncate and show at most two units, so a check that passed 119.9 seconds ago reports `1m 59s ago` and never `2m ago`.
|
|
10
12
|
|
|
11
13
|
Verdicts nest. A composite reports, and so does every command into which it expands; a `:pre` or `:post` hook reports nothing of its own, since the command that it wraps already reports for the chain, though a hook that delegates to a named command reports under that name. A `-R` or `-F` invocation reports nothing either; every scope to which it fans out reports instead. A cold repo-wide `nmr ci` in this package's own monorepo uses 26 verdict lines to report a run that prints roughly 250.
|
|
@@ -38,7 +40,7 @@ Every object contains `command`, `scope`, and `outcome`, and then the fields tha
|
|
|
38
40
|
|
|
39
41
|
| `outcome` | Also contains |
|
|
40
42
|
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
-
| `passed` | `durationMs`
|
|
43
|
+
| `passed` | `durationMs`, and `stated` when the command stated its own outcome, such as `{"kind":"build-unchanged","savedMs":2100}` |
|
|
42
44
|
| `failed` | `durationMs`, `exitCode` |
|
|
43
45
|
| `recalled` | `ageMs`, `savedMs`, and `replay` when the skip has excerpts to [replay](check-cache.md#replaying-a-skipped-runs-output) |
|
|
44
46
|
| `no-op` | `reason`, one of `empty-override`, `empty-workspace`, or `noop-override` |
|
|
@@ -72,6 +74,8 @@ Because the format is nmr's own reporting, and nmr does not tell the commands th
|
|
|
72
74
|
|
|
73
75
|
`NMR_ENTRY_CHAIN` is nmr's own as well. It lists the commands that the current run has entered and the directories in which it entered them, and nmr passes it to every process that it starts, so that a process re-entering a command already running in its directory fails instead of repeating without end; see [self-referential entries](scripts.md#three-tier-override-system). Do not set it: nmr reports a value that it cannot read and exits 1.
|
|
74
76
|
|
|
77
|
+
`NMR_OUTCOME_FILE` is nmr's own as well. nmr sets it for each command that it runs through the shell, naming a file of that step's own, in which an nmr bin states its outcome; nmr reads the file once the step exits 0 and removes it afterwards. nmr accepts the statement only from a command whose own script is a single shell command, and only from a process that names as its parent, in the record, nmr or the shell that nmr spawned for the step. A bin therefore cannot speak for a chain or for a hook around it. Whether a process that the step's command starts can state one depends on the shell: Where the shell runs the command as its child, as Debian's and Ubuntu's /bin/sh does, only the command itself can state an outcome; where the shell replaces itself with the command, as bash does, a process that the command starts can state one too, so a wrapper script that runs a bin as its child speaks for its step. `nmr-vitest` withholds the variable from the suite that it runs, so that a test exercising a bin cannot speak for the `test` step. nmr also removes the variable from every other process that it starts. Do not set it: nmr replaces an inherited value, and ignores a record that it cannot read.
|
|
78
|
+
|
|
75
79
|
### Output style
|
|
76
80
|
|
|
77
81
|
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code 1, whichever source named it. The flag outranks the variable, and both accept `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
|
package/docs/scripts.md
CHANGED
|
@@ -182,7 +182,7 @@ Neither is bound to a git hook. `prepush` is named for when a developer runs it,
|
|
|
182
182
|
|
|
183
183
|
### Check and quality
|
|
184
184
|
|
|
185
|
-
`check` and `check:strict` start their lint step first and [run it in the background](#running-a-step-in-the-background), alongside typecheck, the format check, and the tests.
|
|
185
|
+
`check` and `check:strict` start their lint step first and [run it in the background](#running-a-step-in-the-background), alongside typecheck, the format check, and the tests. At the root, both then run [`lint:workflows`](#workflow-lint) before typecheck, and `root:check` runs it after `root:lint:check`.
|
|
186
186
|
|
|
187
187
|
Neither gate computes coverage: Instrumentation multiplies the wall time of the run that they exist to keep short. Coverage is [`test:coverage`](vitest.md#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
|
|
188
188
|
|
|
@@ -194,6 +194,14 @@ Requires `eslint` >= 10 and, for `lint:strict`, `@williamthorsen/strict-lint` >=
|
|
|
194
194
|
|
|
195
195
|
The run never enters a package, so per-package `lint:pre` and `lint:post` hooks do not fire from the root. They still run from inside the package and under `nmr -F <package> lint`.
|
|
196
196
|
|
|
197
|
+
### Workflow lint
|
|
198
|
+
|
|
199
|
+
`lint:workflows`, a root command, lints the GitHub Actions workflows under `.github/workflows/` with [actionlint](https://github.com/rhysd/actionlint), including its shellcheck pass over every `run:` block, and fails on any finding. A repo without `.github/workflows/` passes. The command runs `nmr-actionlint`, which forwards trailing arguments to actionlint, so `nmr lint:workflows .github/workflows/ci.yaml` lints that file alone; inside `check`, `check:strict`, and `root:check`, the step declines them.
|
|
200
|
+
|
|
201
|
+
The result does not depend on what the machine has installed. nmr pins actionlint 1.7.12 and shellcheck 0.11.0, downloads each from its upstream GitHub release on first use, verifies it against the SHA-256 recorded in nmr, and caches it under `node_modules/.cache/nmr-tools/` at the monorepo root. The first run therefore needs access to `github.com`, and a failed download or a checksum mismatch fails the command. The pyflakes integration is off. The pins move only with a release of nmr.
|
|
202
|
+
|
|
203
|
+
The supported platforms are macOS and Linux on x64 and arm64; on any other, the command fails and names the platform. actionlint reads its own configuration from `.github/actionlint.yaml`, through which a repo can ignore a finding.
|
|
204
|
+
|
|
197
205
|
### Root-only
|
|
198
206
|
|
|
199
207
|
The `root:`-prefixed commands operate on root-level code only (not workspace packages).
|
package/docs/upgrades.md
CHANGED
|
@@ -40,11 +40,13 @@ export default defineConfig({
|
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`defineConfig` supplies nmr's shared upgrade policy, so a repo's config contains only what is specific to that repo. The policy is
|
|
43
|
+
`defineConfig` supplies nmr's shared upgrade policy, so a repo's config contains only what is specific to that repo. The policy is the pair of settings that report a dependency pinned to an exact version: Locked dependencies are included, and the range searched is `minor`. Any setting that a repo's config declares wins over nmr's default. Passing `undefined` clears a default rather than falling back to it. Clearing `mode` hands the range back to the upgrade tool's own default, which follows the range that each dependency declares.
|
|
44
|
+
|
|
45
|
+
The policy does not set `maturityPeriod`, so the quarantine on brand-new releases is pnpm's: The upgrade tool applies the `minimumReleaseAge` of `pnpm-workspace.yaml`, or pnpm 11's default when the file does not set one (under pnpm 10 or earlier, a workspace that does not set it is not quarantined), and the packages that its `minimumReleaseAgeExclude` lists skip the quarantine. A `maturityPeriod` in a repo's config replaces pnpm's period.
|
|
44
46
|
|
|
45
47
|
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that the upgrade tool does not read from any config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when a repo's config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
|
|
46
48
|
|
|
47
|
-
> **Note:** Only this file activates nmr's upgrade policy. A repo without one
|
|
49
|
+
> **Note:** Only this file activates nmr's upgrade policy. A repo without one does not get the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
|
|
48
50
|
|
|
49
51
|
Two consequences of the `minor` range mode declared by the policy:
|
|
50
52
|
|
|
@@ -53,13 +55,13 @@ Two consequences of the `minor` range mode declared by the policy:
|
|
|
53
55
|
|
|
54
56
|
### Per-package maturity periods
|
|
55
57
|
|
|
56
|
-
`packageMaturityPeriod` gives the packages that match a pattern a quarantine of their own, in days. Every other dependency keeps `maturityPeriod`. taze applies one maturity period to a whole run, so `nmr upgrade` runs it once for each entry and once more for every other dependency, each run headed on stderr by the patterns and the period that it covers. The command exits with the first non-zero exit code among the runs.
|
|
58
|
+
`packageMaturityPeriod` gives the packages that match a pattern a quarantine of their own, in days. Every other dependency keeps `maturityPeriod` or, while that is unset, pnpm's `minimumReleaseAge`. taze applies one maturity period to a whole run, so `nmr upgrade` runs it once for each entry and once more for every other dependency, each run headed on stderr by the patterns and the period that it covers. The command exits with the first non-zero exit code among the runs.
|
|
57
59
|
|
|
58
60
|
```ts
|
|
59
61
|
import { defineConfig } from '@williamthorsen/nmr/taze';
|
|
60
62
|
|
|
61
63
|
export default defineConfig({
|
|
62
|
-
// Propose first-party releases after one day, and third-party ones after
|
|
64
|
+
// Propose first-party releases after one day, and third-party ones after pnpm's `minimumReleaseAge`.
|
|
63
65
|
packageMaturityPeriod: {
|
|
64
66
|
'@williamthorsen/*': 1,
|
|
65
67
|
readyup: 1,
|
package/docs/utilities.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# Standalone utilities
|
|
2
2
|
|
|
3
|
-
The binaries behind nmr's default `clean`, `compile`, `fmt`, and package `test` commands, and a check for the `prepublishOnly` hooks of publishable packages.
|
|
3
|
+
The binaries behind nmr's default `clean`, `compile`, `fmt`, `lint:workflows`, and package `test` commands, and a check for the `prepublishOnly` hooks of publishable packages.
|
|
4
|
+
|
|
5
|
+
## `nmr-actionlint`
|
|
6
|
+
|
|
7
|
+
Lint the monorepo's GitHub Actions workflows with pinned actionlint and shellcheck binaries, forwarding any arguments to actionlint. This is the default root `lint:workflows` script; [Workflow lint](scripts.md#workflow-lint) covers the pins, the download, and the supported platforms.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
nmr-actionlint
|
|
11
|
+
```
|
|
4
12
|
|
|
5
13
|
## `nmr-clean`
|
|
6
14
|
|
|
7
|
-
Remove a package's build output (`dist`) and its `nmr-compile` cache
|
|
15
|
+
Remove a package's build output (`dist`) and its `nmr-compile` cache entries, the key and the recorded emit duration, so that the next build cannot skip on leftover state. Run from a package directory, it cleans that package; run from the monorepo root, it sweeps every workspace package in a single pass, running each package's resolved `clean`, so a package that overrides `clean`, in `.config/nmr.config.ts` or in its own `package.json`, gets its own command rather than the sweep. Removal is idempotent: Cleaning an unbuilt package is a silent no-op. This is the default `clean` script at both levels.
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
18
|
nmr-clean
|
|
@@ -24,6 +32,8 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
24
32
|
|
|
25
33
|
**What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package that nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that this build is about to write.
|
|
26
34
|
|
|
35
|
+
**What a skip reports.** Each build records how long its emit took, in a second entry beside the key (`.duration.json`), held to the key under which it was recorded. A skip names that time as the saving, `No changes detected. Skipping build, saved ~2.1s.`, when the recorded emit took at least a second and was recorded for the current inputs; otherwise the line ends at `Skipping build.` The time covers compiling and publishing the output, the work that a skip avoids, and not the hashing that a skip also does. The duration is a separate entry, so it never changes the key. Run through nmr, a skip also appears on the compile verdict line, `passed in 0.3s — build skipped as unchanged, saved ~2.1s`, which a quiet run still prints; see [what nmr reports](reporting.md#what-nmr-reports).
|
|
36
|
+
|
|
27
37
|
`typescript` is an optional peer dependency (`>=5.7.0 <7`), needed only by `nmr-compile`; the consuming repo provides it, and `nmr-compile` run without it fails with an error naming the peer and the version that it requires. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` or `.tsx` extensions for them to be rewritten.
|
|
28
38
|
|
|
29
39
|
```bash
|
|
@@ -41,13 +51,14 @@ A repo's own copy must be Prettier 3.0.0 or later. The design requires `--ignore
|
|
|
41
51
|
```bash
|
|
42
52
|
nmr-fmt --check
|
|
43
53
|
nmr-fmt --write packages/nmr
|
|
54
|
+
nmr-fmt --check -- -lead.js
|
|
44
55
|
```
|
|
45
56
|
|
|
46
57
|
### File selection
|
|
47
58
|
|
|
48
59
|
`fmt` and `fmt:check` format the files that git reports: tracked files, plus untracked files that git does not ignore. Prettier reads `.gitignore` and `.prettierignore` from the working directory alone, so a pattern in `packages/<pkg>/.gitignore` never applies to a run started at the monorepo root. Because git knows the whole hierarchy, including `.git/info/exclude` and `core.excludesFile`, and every `.prettierignore` in the tree is discovered from the repository root, package-level exclusions apply from any directory as well.
|
|
49
60
|
|
|
50
|
-
A file that git ignores is never formatted, even when named directly. Because trailing arguments are git pathspecs rather than Prettier flags, `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec for which git lists no file both fail it. Running outside a git repository fails rather than reporting a clean run.
|
|
61
|
+
A file that git ignores is never formatted, even when named directly. Because trailing arguments are git pathspecs rather than Prettier flags, `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec for which git lists no file both fail it. Every argument after `--` is a pathspec, even one that starts with `-`, so an unrecognized option fails the run only before `--`. Running outside a git repository fails rather than reporting a clean run.
|
|
51
62
|
|
|
52
63
|
`nmr-fmt` drops paths that the index names but the filesystem does not have (a file deleted but not yet staged), submodule gitlinks, and symlinks whatever they point at, before calling Prettier, which rejects a symlink named explicitly. It also skips a path that it is denied access to, such as a `.envrc` that a sandbox shields, and names each one on stderr, so that a check that passes does not hide a file that it never read. A pathspec whose every match is skipped still counts as matched: The run passes and names the skipped paths on stderr, so that a pre-commit hook passing only staged symlinks or a submodule bump does not fail. Any other failure to stat a listed path fails the run, with an error that names the path.
|
|
53
64
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"bin": {
|
|
59
59
|
"ensure-prepublish-hooks": "bin/ensure-prepublish-hooks.js",
|
|
60
60
|
"nmr": "bin/nmr.js",
|
|
61
|
+
"nmr-actionlint": "bin/nmr-actionlint.js",
|
|
61
62
|
"nmr-clean": "bin/nmr-clean.js",
|
|
62
63
|
"nmr-compile": "bin/nmr-compile.js",
|
|
63
64
|
"nmr-fmt": "bin/nmr-fmt.js",
|
|
@@ -78,9 +79,9 @@
|
|
|
78
79
|
"!docs/*.v*.md"
|
|
79
80
|
],
|
|
80
81
|
"dependencies": {
|
|
81
|
-
"@williamthorsen/nmr-core": "0.
|
|
82
|
-
"@williamthorsen/toolbelt.errors": "0.
|
|
83
|
-
"@williamthorsen/toolbelt.filesystem": "0.
|
|
82
|
+
"@williamthorsen/nmr-core": "0.14.0",
|
|
83
|
+
"@williamthorsen/toolbelt.errors": "0.9.0",
|
|
84
|
+
"@williamthorsen/toolbelt.filesystem": "0.10.0",
|
|
84
85
|
"glob": "13.0.6",
|
|
85
86
|
"prettier": "3.9.10",
|
|
86
87
|
"prettier-plugin-sh": "0.20.2",
|
|
@@ -88,10 +89,10 @@
|
|
|
88
89
|
"unconfig": "7.5.0"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
|
-
"@williamthorsen/toolbelt.testing": "0.
|
|
92
|
-
"@williamthorsen/toolbelt.vitest": "0.
|
|
92
|
+
"@williamthorsen/toolbelt.testing": "0.8.0",
|
|
93
|
+
"@williamthorsen/toolbelt.vitest": "0.13.0",
|
|
93
94
|
"esbuild": "0.28.2",
|
|
94
|
-
"readyup": "0.
|
|
95
|
+
"readyup": "0.44.0",
|
|
95
96
|
"tinyglobby": "0.2.17",
|
|
96
97
|
"typescript": "6.0.3",
|
|
97
98
|
"vite": "8.3.4",
|