@williamthorsen/nmr 0.30.0 → 0.32.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/.readyup/kits/default.js +56 -16
- package/.readyup/manifest.json +34 -3
- package/README.md +265 -64
- package/agents/guidance/rulebooks/nmr.md +5 -2
- package/bin/nmr-report-catalog.js +11 -0
- package/dist/esm/UserError.d.ts +3 -0
- package/dist/esm/UserError.js +6 -0
- package/dist/esm/check-cache.d.ts +36 -1
- package/dist/esm/check-cache.js +91 -23
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -24
- package/dist/esm/cli-report-catalog.d.ts +1 -0
- package/dist/esm/cli-report-catalog.js +11 -0
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/build-output.d.ts +1 -0
- package/dist/esm/commands/build-output.js +25 -2
- package/dist/esm/commands/build.d.ts +5 -1
- package/dist/esm/commands/build.js +22 -8
- package/dist/esm/commands/clean.js +12 -0
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -0
- package/dist/esm/commands/ensure-prepublish-hooks.js +43 -0
- package/dist/esm/commands/report-catalog.d.ts +1 -0
- package/dist/esm/commands/report-catalog.js +40 -0
- package/dist/esm/commands/report-overrides.js +30 -7
- package/dist/esm/commands/taze.js +2 -1
- package/dist/esm/config.d.ts +1 -0
- package/dist/esm/config.js +66 -18
- package/dist/esm/default-scripts.d.ts +5 -1
- package/dist/esm/default-scripts.js +13 -10
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.js +3 -1
- package/dist/esm/helpers/clampToBytes.d.ts +2 -0
- package/dist/esm/helpers/clampToBytes.js +20 -0
- package/dist/esm/helpers/createBoundedBuffer.js +1 -4
- package/dist/esm/helpers/deriveExcerpt.d.ts +1 -0
- package/dist/esm/helpers/deriveExcerpt.js +30 -0
- package/dist/esm/helpers/duration.js +16 -9
- package/dist/esm/helpers/package-json.d.ts +11 -4
- package/dist/esm/helpers/package-json.js +51 -22
- package/dist/esm/helpers/readStringValues.d.ts +1 -0
- package/dist/esm/helpers/readStringValues.js +8 -0
- package/dist/esm/helpers/reportClosing.d.ts +1 -0
- package/dist/esm/helpers/reportClosing.js +3 -0
- package/dist/esm/helpers/transcript.d.ts +5 -0
- package/dist/esm/helpers/transcript.js +59 -0
- package/dist/esm/recording.d.ts +65 -0
- package/dist/esm/recording.js +96 -0
- package/dist/esm/replay-assembly.d.ts +9 -0
- package/dist/esm/replay-assembly.js +26 -0
- package/dist/esm/report-format.d.ts +18 -0
- package/dist/esm/report-format.js +25 -0
- package/dist/esm/reportCliFailure.d.ts +2 -0
- package/dist/esm/reportCliFailure.js +11 -0
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolver.d.ts +9 -1
- package/dist/esm/resolver.js +28 -18
- package/dist/esm/runCli.js +339 -74
- package/dist/esm/runner.d.ts +9 -3
- package/dist/esm/runner.js +25 -4
- package/dist/esm/scripts.d.ts +1 -1
- package/dist/esm/steps.d.ts +9 -2
- package/dist/esm/steps.js +117 -4
- package/dist/esm/types.d.ts +11 -2
- package/dist/esm/verbosity.d.ts +15 -4
- package/dist/esm/verbosity.js +32 -12
- package/dist/esm/verdict.d.ts +29 -0
- package/dist/esm/verdict.js +150 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +20 -3
- package/package.json +14 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reportClosing(message: string, log?: (line: string) => void): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RetainedOutput } from '../runner.js';
|
|
2
|
+
export declare const TRANSCRIPT_LIMIT_BYTES = 262144;
|
|
3
|
+
export declare function cleanTranscript(raw: string): string;
|
|
4
|
+
export declare function composeTranscript(retained: RetainedOutput): string | undefined;
|
|
5
|
+
export declare function formatElisionMarker(byteCount: number): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const TRANSCRIPT_LIMIT_BYTES = 262_144;
|
|
2
|
+
const ANSI_PATTERN = /\u{1B}(?:\[[\u{30}-\u{3F}]*[\u{20}-\u{2F}]*[\u{40}-\u{7E}]|\][^\u{7}\u{1B}]*(?:\u{7}|\u{1B}\\))/gu;
|
|
3
|
+
const END_LIMIT_BYTES = TRANSCRIPT_LIMIT_BYTES / 2;
|
|
4
|
+
const STREAM_MARKER = '\n… nmr: stderr …\n';
|
|
5
|
+
export function cleanTranscript(raw) {
|
|
6
|
+
return raw
|
|
7
|
+
.replaceAll(ANSI_PATTERN, '')
|
|
8
|
+
.split('\n')
|
|
9
|
+
.map((line) => renderCarriageReturns(line))
|
|
10
|
+
.join('\n');
|
|
11
|
+
}
|
|
12
|
+
export function composeTranscript(retained) {
|
|
13
|
+
const streams = [retained.stdout, retained.stderr]
|
|
14
|
+
.map((stream) => cleanTranscript(stream.toString('utf8')))
|
|
15
|
+
.filter((stream) => stream.trim() !== '');
|
|
16
|
+
if (streams.length === 0) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return clampTranscript(streams.join(STREAM_MARKER));
|
|
20
|
+
}
|
|
21
|
+
export function formatElisionMarker(byteCount) {
|
|
22
|
+
const unit = byteCount === 1 ? 'byte' : 'bytes';
|
|
23
|
+
return `\n… nmr elided ${byteCount.toLocaleString('en-US')} ${unit} …\n`;
|
|
24
|
+
}
|
|
25
|
+
function alignEnd(buffer, end) {
|
|
26
|
+
let aligned = Math.min(buffer.length, end);
|
|
27
|
+
while (aligned > 0 && isContinuationByte(buffer[aligned])) {
|
|
28
|
+
aligned--;
|
|
29
|
+
}
|
|
30
|
+
return aligned;
|
|
31
|
+
}
|
|
32
|
+
function alignStart(buffer, start) {
|
|
33
|
+
let aligned = Math.max(0, start);
|
|
34
|
+
while (aligned < buffer.length && isContinuationByte(buffer[aligned])) {
|
|
35
|
+
aligned++;
|
|
36
|
+
}
|
|
37
|
+
return aligned;
|
|
38
|
+
}
|
|
39
|
+
function clampTranscript(transcript) {
|
|
40
|
+
const buffer = Buffer.from(transcript, 'utf8');
|
|
41
|
+
if (buffer.length <= TRANSCRIPT_LIMIT_BYTES) {
|
|
42
|
+
return transcript;
|
|
43
|
+
}
|
|
44
|
+
const headEnd = alignEnd(buffer, END_LIMIT_BYTES);
|
|
45
|
+
const tailStart = alignStart(buffer, buffer.length - END_LIMIT_BYTES);
|
|
46
|
+
const head = buffer.subarray(0, headEnd).toString('utf8');
|
|
47
|
+
const tail = buffer.subarray(tailStart).toString('utf8');
|
|
48
|
+
return `${head}${formatElisionMarker(tailStart - headEnd)}${tail}`;
|
|
49
|
+
}
|
|
50
|
+
function isContinuationByte(byte) {
|
|
51
|
+
return byte !== undefined && (byte & 0b1100_0000) === 0b1000_0000;
|
|
52
|
+
}
|
|
53
|
+
function renderCarriageReturns(line) {
|
|
54
|
+
if (!line.includes('\r')) {
|
|
55
|
+
return line;
|
|
56
|
+
}
|
|
57
|
+
const segments = line.split('\r').filter((segment) => segment.trim() !== '');
|
|
58
|
+
return segments.at(-1) ?? '';
|
|
59
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CheckCacheEntry } from './check-cache.js';
|
|
2
|
+
export interface RunIdentity {
|
|
3
|
+
commandString: string;
|
|
4
|
+
nmrVersion: string;
|
|
5
|
+
nodeVersion: string;
|
|
6
|
+
treeHash: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface Recording {
|
|
9
|
+
entry: CheckCacheEntry;
|
|
10
|
+
transcript?: string;
|
|
11
|
+
}
|
|
12
|
+
export type KeyDifference = {
|
|
13
|
+
ingredient: 'tree';
|
|
14
|
+
} | {
|
|
15
|
+
ingredient: 'command-string';
|
|
16
|
+
} | {
|
|
17
|
+
ingredient: 'nmr-version';
|
|
18
|
+
current: string;
|
|
19
|
+
recorded: string;
|
|
20
|
+
} | {
|
|
21
|
+
ingredient: 'node-version';
|
|
22
|
+
current: string;
|
|
23
|
+
recorded: string;
|
|
24
|
+
} | {
|
|
25
|
+
ingredient: 'other';
|
|
26
|
+
};
|
|
27
|
+
export type RecordingLookup = {
|
|
28
|
+
ok: true;
|
|
29
|
+
recording: Recording;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
refusal: RecordingRefusal;
|
|
33
|
+
};
|
|
34
|
+
export type RecordingRefusal = {
|
|
35
|
+
kind: 'uncacheable';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'gate-aside';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'unrecorded';
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'mismatched';
|
|
42
|
+
ageMs: number;
|
|
43
|
+
difference: KeyDifference;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'no-output';
|
|
46
|
+
ageMs: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function renderRecording(options: {
|
|
49
|
+
command: string;
|
|
50
|
+
recording: Recording;
|
|
51
|
+
scope: string;
|
|
52
|
+
}): string;
|
|
53
|
+
export declare function renderRefusal(options: {
|
|
54
|
+
command: string;
|
|
55
|
+
refusal: RecordingRefusal;
|
|
56
|
+
scope: string;
|
|
57
|
+
}): string;
|
|
58
|
+
export declare function resolveRecording(options: {
|
|
59
|
+
anchorDir: string;
|
|
60
|
+
command: string;
|
|
61
|
+
current: RunIdentity;
|
|
62
|
+
isCacheable: boolean;
|
|
63
|
+
key: string | undefined;
|
|
64
|
+
monorepoRoot: string;
|
|
65
|
+
}): Promise<RecordingLookup>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readCheckCacheEntry, readTranscript } from "./check-cache.js";
|
|
2
|
+
import { formatDuration } from "./helpers/duration.js";
|
|
3
|
+
export function renderRecording(options) {
|
|
4
|
+
const { entry } = options.recording;
|
|
5
|
+
const age = formatDuration(Math.max(0, Date.now() - Date.parse(entry.recordedAt)));
|
|
6
|
+
const header = `📼 ${options.scope}: ${options.command} — recorded ${entry.recordedAt} (${age} ago), ` +
|
|
7
|
+
`ran in ${formatDuration(entry.durationMs)}\n$ ${entry.commandString}\n\n`;
|
|
8
|
+
return `${header}${appendNewline(renderBody(options.recording))}`;
|
|
9
|
+
}
|
|
10
|
+
export function renderRefusal(options) {
|
|
11
|
+
return `📭 ${options.scope}: ${options.command}: no recording; ${describeRefusal(options.command, options.refusal)}`;
|
|
12
|
+
}
|
|
13
|
+
export async function resolveRecording(options) {
|
|
14
|
+
const { anchorDir, command, monorepoRoot } = options;
|
|
15
|
+
if (!options.isCacheable) {
|
|
16
|
+
return { ok: false, refusal: { kind: 'uncacheable' } };
|
|
17
|
+
}
|
|
18
|
+
if (options.key === undefined) {
|
|
19
|
+
return { ok: false, refusal: { kind: 'gate-aside' } };
|
|
20
|
+
}
|
|
21
|
+
const entry = await readCheckCacheEntry({ anchorDir, command, monorepoRoot });
|
|
22
|
+
if (entry === undefined) {
|
|
23
|
+
return { ok: false, refusal: { kind: 'unrecorded' } };
|
|
24
|
+
}
|
|
25
|
+
const ageMs = Math.max(0, Date.now() - Date.parse(entry.recordedAt));
|
|
26
|
+
if (entry.key !== options.key) {
|
|
27
|
+
return { ok: false, refusal: { kind: 'mismatched', ageMs, difference: findKeyDifference(entry, options.current) } };
|
|
28
|
+
}
|
|
29
|
+
const transcript = await readTranscript({ anchorDir, command, monorepoRoot });
|
|
30
|
+
if (transcript === undefined && (entry.retention?.replay.length ?? 0) === 0) {
|
|
31
|
+
return { ok: false, refusal: { kind: 'no-output', ageMs } };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, recording: { entry, ...(transcript !== undefined && { transcript }) } };
|
|
34
|
+
}
|
|
35
|
+
function appendNewline(body) {
|
|
36
|
+
return body.endsWith('\n') ? body : `${body}\n`;
|
|
37
|
+
}
|
|
38
|
+
function describeDifference(difference) {
|
|
39
|
+
switch (difference.ingredient) {
|
|
40
|
+
case 'tree':
|
|
41
|
+
return 'on a tree this is not';
|
|
42
|
+
case 'command-string':
|
|
43
|
+
return 'over a command chain this is not';
|
|
44
|
+
case 'nmr-version':
|
|
45
|
+
return `under nmr ${difference.recorded}, not ${difference.current}`;
|
|
46
|
+
case 'node-version':
|
|
47
|
+
return `under Node ${difference.recorded}, not ${difference.current}`;
|
|
48
|
+
case 'other':
|
|
49
|
+
return 'under an install or environment this run does not share';
|
|
50
|
+
default: {
|
|
51
|
+
const unhandled = difference;
|
|
52
|
+
throw new Error(`Unhandled key difference: ${JSON.stringify(unhandled)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function describeRefusal(command, refusal) {
|
|
57
|
+
switch (refusal.kind) {
|
|
58
|
+
case 'uncacheable':
|
|
59
|
+
return `\`${command}\` is outside the check-result cache, so no run of it is recorded`;
|
|
60
|
+
case 'gate-aside':
|
|
61
|
+
return 'the check-result cache is standing aside here (NMR_DEBUG=1 reports why)';
|
|
62
|
+
case 'unrecorded':
|
|
63
|
+
return 'nothing has recorded a pass for this scope';
|
|
64
|
+
case 'mismatched':
|
|
65
|
+
return `the last pass was ${formatDuration(refusal.ageMs)} ago, ${describeDifference(refusal.difference)}`;
|
|
66
|
+
case 'no-output':
|
|
67
|
+
return `the pass ${formatDuration(refusal.ageMs)} ago retained none, as a run printing nothing or writing to a terminal does`;
|
|
68
|
+
default: {
|
|
69
|
+
const unhandled = refusal;
|
|
70
|
+
throw new Error(`Unhandled refusal: ${JSON.stringify(unhandled)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function findKeyDifference(entry, current) {
|
|
75
|
+
if (current.treeHash !== undefined && entry.treeHash !== current.treeHash) {
|
|
76
|
+
return { ingredient: 'tree' };
|
|
77
|
+
}
|
|
78
|
+
if (entry.commandString !== current.commandString) {
|
|
79
|
+
return { ingredient: 'command-string' };
|
|
80
|
+
}
|
|
81
|
+
if (entry.nmrVersion !== current.nmrVersion) {
|
|
82
|
+
return { ingredient: 'nmr-version', current: current.nmrVersion, recorded: entry.nmrVersion };
|
|
83
|
+
}
|
|
84
|
+
if (entry.nodeVersion !== current.nodeVersion) {
|
|
85
|
+
return { ingredient: 'node-version', current: current.nodeVersion, recorded: entry.nodeVersion };
|
|
86
|
+
}
|
|
87
|
+
return { ingredient: 'other' };
|
|
88
|
+
}
|
|
89
|
+
function renderBody(recording) {
|
|
90
|
+
if (recording.transcript !== undefined) {
|
|
91
|
+
return recording.transcript;
|
|
92
|
+
}
|
|
93
|
+
return (recording.entry.retention?.replay ?? [])
|
|
94
|
+
.map((line) => `${line.scope}: ${line.command}: ${line.excerpt}`)
|
|
95
|
+
.join('\n');
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReplayLine } from './check-cache.js';
|
|
2
|
+
import type { Step } from './steps.js';
|
|
3
|
+
export declare function assembleReplay(options: {
|
|
4
|
+
anchorDir: string;
|
|
5
|
+
monorepoRoot: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
steps: readonly Step[];
|
|
8
|
+
treeHash: string;
|
|
9
|
+
}): Promise<ReplayLine[]>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readCheckCacheEntry } from "./check-cache.js";
|
|
2
|
+
import { readNmrStep } from "./steps.js";
|
|
3
|
+
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
4
|
+
export async function assembleReplay(options) {
|
|
5
|
+
const { anchorDir, monorepoRoot, runId, steps, treeHash } = options;
|
|
6
|
+
const candidates = steps.flatMap((step) => {
|
|
7
|
+
const target = readNmrStep(step);
|
|
8
|
+
if (target === undefined) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const scopeDirs = target.isDelegate
|
|
12
|
+
? resolveDelegateScopes(monorepoRoot)
|
|
13
|
+
: [target.isWorkspaceRoot ? monorepoRoot : anchorDir];
|
|
14
|
+
return scopeDirs.map((scopeDir) => ({ anchorDir: scopeDir, command: target.command, monorepoRoot }));
|
|
15
|
+
});
|
|
16
|
+
const entries = await Promise.all(candidates.map((candidate) => readCheckCacheEntry(candidate)));
|
|
17
|
+
return entries.flatMap((entry) => entry?.treeHash === treeHash && entry.retention?.runId === runId ? entry.retention.replay : []);
|
|
18
|
+
}
|
|
19
|
+
function resolveDelegateScopes(monorepoRoot) {
|
|
20
|
+
try {
|
|
21
|
+
return getWorkspacePackageDirs(monorepoRoot);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function formatReportFormatRejection(source: string, value: string): string;
|
|
2
|
+
export declare function isReportFormat(value: string): value is ReportFormat;
|
|
3
|
+
export declare function readReportFormatEnv(env: NodeJS.ProcessEnv): ReportFormatRead;
|
|
4
|
+
export declare const REPORT_FORMAT_ENV_VAR = "NMR_REPORT_FORMAT";
|
|
5
|
+
export declare const REPORT_FORMATS: readonly ["text", "json"];
|
|
6
|
+
export type ReportFormat = (typeof REPORT_FORMATS)[number];
|
|
7
|
+
export type ReportFormatRead = {
|
|
8
|
+
ok: true;
|
|
9
|
+
format?: ReportFormat;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function resolveReportFormat(options: ResolveReportFormatOptions): ReportFormat;
|
|
15
|
+
export interface ResolveReportFormatOptions {
|
|
16
|
+
envFormat: ReportFormat | undefined;
|
|
17
|
+
jsonFlag: boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatReportFormatRejection(source, value) {
|
|
2
|
+
return `${source} is \`${value}\`, which is not one of: ${REPORT_FORMATS.join(', ')}`;
|
|
3
|
+
}
|
|
4
|
+
export function isReportFormat(value) {
|
|
5
|
+
const names = REPORT_FORMATS;
|
|
6
|
+
return names.includes(value);
|
|
7
|
+
}
|
|
8
|
+
export function readReportFormatEnv(env) {
|
|
9
|
+
const raw = env[REPORT_FORMAT_ENV_VAR];
|
|
10
|
+
if (raw === undefined || raw === '') {
|
|
11
|
+
return { ok: true };
|
|
12
|
+
}
|
|
13
|
+
if (!isReportFormat(raw)) {
|
|
14
|
+
return { ok: false, error: formatReportFormatRejection(REPORT_FORMAT_ENV_VAR, raw) };
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, format: raw };
|
|
17
|
+
}
|
|
18
|
+
export const REPORT_FORMAT_ENV_VAR = 'NMR_REPORT_FORMAT';
|
|
19
|
+
export const REPORT_FORMATS = ['text', 'json'];
|
|
20
|
+
export function resolveReportFormat(options) {
|
|
21
|
+
const { envFormat, jsonFlag } = options;
|
|
22
|
+
if (jsonFlag)
|
|
23
|
+
return 'json';
|
|
24
|
+
return envFormat ?? 'text';
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError, isError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { UserError } from "./UserError.js";
|
|
4
|
+
export function reportCliFailure(error, stderr) {
|
|
5
|
+
if (error instanceof UserError) {
|
|
6
|
+
reportError(error.message, stderr);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const detail = isError(error) && error.stack !== undefined ? error.stack : describeError(error);
|
|
10
|
+
stderr.write(`${detail}\n`);
|
|
11
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ScriptRegistry } from './default-scripts.js';
|
|
2
|
-
export type { ScriptRegistry, ScriptValue } from './default-scripts.js';
|
|
2
|
+
export type { ScriptRegistry, ScriptValue, StepSpec } from './default-scripts.js';
|
|
3
3
|
export declare function getDefaultWorkspaceScripts(): ScriptRegistry;
|
|
4
4
|
export declare function getDefaultRootScripts(): ScriptRegistry;
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -3,8 +3,16 @@ import type { Step } from './steps.js';
|
|
|
3
3
|
import type { NmrConfig } from './types.js';
|
|
4
4
|
export declare function applyDevBin(command: string, devBin: Record<string, string> | undefined, monorepoRoot: string): string;
|
|
5
5
|
export declare function applyDevBinToSteps(steps: readonly Step[], devBin: Record<string, string> | undefined, monorepoRoot: string): readonly Step[];
|
|
6
|
+
export type ScriptOrigin = {
|
|
7
|
+
tier: 'registry';
|
|
8
|
+
key: string;
|
|
9
|
+
} | {
|
|
10
|
+
tier: 'package';
|
|
11
|
+
file: string;
|
|
12
|
+
key: string;
|
|
13
|
+
};
|
|
6
14
|
export interface ResolvedScript {
|
|
7
|
-
|
|
15
|
+
origin: ScriptOrigin;
|
|
8
16
|
steps: readonly Step[];
|
|
9
17
|
}
|
|
10
18
|
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): readonly Step[];
|
package/dist/esm/resolver.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
4
|
+
import { parsePackageJson, readScriptRecord, resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
3
5
|
import { isObject } from "./helpers/type-guards.js";
|
|
4
6
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
5
7
|
import { composeNmrStep } from "./steps.js";
|
|
@@ -41,33 +43,38 @@ export function expandScript(script, workspaceRoot) {
|
|
|
41
43
|
if (typeof script === 'string') {
|
|
42
44
|
return [{ kind: 'opaque', command: script }];
|
|
43
45
|
}
|
|
44
|
-
return script.map((element) =>
|
|
46
|
+
return script.map((element) => typeof element === 'string'
|
|
47
|
+
? composeNmrStep(element, workspaceRoot)
|
|
48
|
+
: composeNmrStep(element.run, workspaceRoot, element.declinesArgs));
|
|
45
49
|
}
|
|
46
50
|
export function describeScript(script) {
|
|
47
|
-
return typeof script === 'string' ? script : `[${script.join(', ')}]`;
|
|
51
|
+
return typeof script === 'string' ? script : `[${script.map(describeElement).join(', ')}]`;
|
|
52
|
+
}
|
|
53
|
+
function describeElement(element) {
|
|
54
|
+
if (typeof element === 'string') {
|
|
55
|
+
return element;
|
|
56
|
+
}
|
|
57
|
+
return element.declinesArgs === true ? `${element.run} (no args)` : element.run;
|
|
48
58
|
}
|
|
49
59
|
export function readPackageJsonScripts(packageDir) {
|
|
60
|
+
const file = resolvePackageJsonPath(packageDir);
|
|
61
|
+
let raw;
|
|
50
62
|
try {
|
|
51
|
-
|
|
52
|
-
const parsed = JSON.parse(raw);
|
|
53
|
-
if (!isObject(parsed))
|
|
54
|
-
return undefined;
|
|
55
|
-
const scripts = parsed['scripts'];
|
|
56
|
-
if (!isObject(scripts))
|
|
57
|
-
return undefined;
|
|
58
|
-
const result = {};
|
|
59
|
-
for (const [key, val] of Object.entries(scripts)) {
|
|
60
|
-
if (typeof val === 'string')
|
|
61
|
-
result[key] = val;
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
63
|
+
raw = readFileSync(file, 'utf8');
|
|
64
64
|
}
|
|
65
65
|
catch (error) {
|
|
66
|
-
if (error
|
|
66
|
+
if (hasErrnoCode(error, 'ENOENT')) {
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
69
|
throw error;
|
|
70
70
|
}
|
|
71
|
+
const parsed = parsePackageJson(raw, file);
|
|
72
|
+
if (!isObject(parsed))
|
|
73
|
+
return undefined;
|
|
74
|
+
const scripts = parsed['scripts'];
|
|
75
|
+
if (!isObject(scripts))
|
|
76
|
+
return undefined;
|
|
77
|
+
return readScriptRecord(packageDir, scripts);
|
|
71
78
|
}
|
|
72
79
|
export function buildWorkspaceRegistry(config) {
|
|
73
80
|
return {
|
|
@@ -91,7 +98,10 @@ export function resolveScript(commandName, registry, packageDir, workspaceRoot)
|
|
|
91
98
|
if (pkgScripts && Object.hasOwn(pkgScripts, commandName)) {
|
|
92
99
|
const override = pkgScripts[commandName];
|
|
93
100
|
if (override !== undefined && !isSelfReferential(override, commandName)) {
|
|
94
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
origin: { tier: 'package', file: resolvePackageJsonPath(packageDir), key: commandName },
|
|
103
|
+
steps: [{ kind: 'opaque', command: override }],
|
|
104
|
+
};
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
}
|
|
@@ -102,5 +112,5 @@ export function resolveScript(commandName, registry, packageDir, workspaceRoot)
|
|
|
102
112
|
if (registryEntry === undefined) {
|
|
103
113
|
return undefined;
|
|
104
114
|
}
|
|
105
|
-
return {
|
|
115
|
+
return { origin: { tier: 'registry', key: commandName }, steps: expandScript(registryEntry, workspaceRoot) };
|
|
106
116
|
}
|