@williamthorsen/nmr 0.38.0 → 0.40.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 +212 -4
- package/.readyup/kits/default.js +124 -104
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +108 -0
- package/README.md +41 -47
- package/agents/README.md +8 -8
- package/agents/guidance/rulebooks/nmr.md +8 -8
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/UserError.d.ts +1 -2
- package/dist/esm/check-cache.d.ts +61 -63
- package/dist/esm/check-cache.js +24 -24
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
- package/dist/esm/commands/build-output.d.ts +37 -38
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +13 -14
- package/dist/esm/commands/build.js +67 -65
- package/dist/esm/commands/clean.d.ts +6 -6
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.d.ts +8 -8
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +5 -7
- package/dist/esm/commands/report-catalog.js +10 -8
- package/dist/esm/commands/report-overrides.d.ts +0 -2
- package/dist/esm/commands/report-overrides.js +8 -8
- package/dist/esm/commands/taze.d.ts +9 -8
- package/dist/esm/config.d.ts +5 -5
- package/dist/esm/config.js +27 -27
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +5 -6
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/git-ignored-paths.d.ts +9 -0
- package/dist/esm/git-ignored-paths.js +10 -0
- package/dist/esm/glyphs.d.ts +3 -3
- package/dist/esm/help.d.ts +4 -4
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.d.ts +4 -5
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/config-path.d.ts +0 -2
- package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
- package/dist/esm/helpers/duration.d.ts +10 -11
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.d.ts +19 -19
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.d.ts +1 -1
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/hook-name.d.ts +2 -5
- package/dist/esm/helpers/package-json.d.ts +11 -10
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/readStringValues.d.ts +2 -3
- package/dist/esm/helpers/reportClosing.d.ts +8 -9
- package/dist/esm/helpers/transcript.d.ts +15 -15
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/output-style.d.ts +12 -13
- package/dist/esm/prettier.d.ts +2 -2
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +25 -20
- package/dist/esm/recording.js +28 -21
- package/dist/esm/replay-assembly.d.ts +10 -10
- package/dist/esm/report-format.d.ts +15 -20
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/reportCliFailure.d.ts +2 -3
- package/dist/esm/resolve-scripts.d.ts +4 -5
- package/dist/esm/resolveBinStyles.d.ts +4 -5
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +16 -20
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.d.ts +6 -6
- package/dist/esm/runCli.js +261 -195
- package/dist/esm/runner.d.ts +20 -20
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +51 -38
- package/dist/esm/steps.js +32 -27
- package/dist/esm/taze.d.ts +1 -1
- package/dist/esm/tests.d.ts +23 -18
- package/dist/esm/tests.js +21 -9
- package/dist/esm/tiers.d.ts +28 -24
- package/dist/esm/tiers.js +13 -9
- package/dist/esm/types.d.ts +5 -5
- package/dist/esm/verbosity.d.ts +23 -21
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +39 -32
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.d.ts +11 -11
- package/dist/esm/vitest-source-resolution.js +49 -47
- package/dist/esm/vitest.d.ts +27 -26
- package/dist/esm/vitest.js +90 -33
- package/dist/esm/workspace.d.ts +6 -22
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +57 -57
- package/docs/prettier.md +10 -10
- package/docs/reporting.md +48 -44
- package/docs/scripts.md +29 -29
- package/docs/upgrades.md +18 -18
- package/docs/utilities.md +13 -13
- package/docs/vitest.md +68 -66
- package/docs/workspace.md +27 -4
- package/package.json +6 -6
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/help.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
2
|
import { OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG } from "./output-style.js";
|
|
3
3
|
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
4
|
-
export function generateHelp(config, packageDir,
|
|
4
|
+
export function generateHelp(config, packageDir, shouldUseRoot) {
|
|
5
5
|
const lines = [
|
|
6
6
|
'Usage: nmr [flags] <command> [args...]',
|
|
7
7
|
'',
|
|
@@ -26,16 +26,16 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
26
26
|
const overrides = packageDir === undefined ? {} : collectOverrides(packageDir);
|
|
27
27
|
let hadOverride = false;
|
|
28
28
|
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config));
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
29
|
+
const workspaceMarkedNames = !shouldUseRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
|
|
30
|
+
if (workspaceMarkedNames.size > 0)
|
|
31
31
|
hadOverride = true;
|
|
32
|
-
formatRegistry(workspaceRegistry,
|
|
32
|
+
formatRegistry(workspaceRegistry, workspaceMarkedNames, lines);
|
|
33
33
|
lines.push('', 'Root commands:');
|
|
34
34
|
const rootRegistry = filterHooks(buildRootRegistry(config));
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
35
|
+
const rootMarkedNames = shouldUseRoot ? applyOverrides(rootRegistry, overrides) : new Set();
|
|
36
|
+
if (rootMarkedNames.size > 0)
|
|
37
37
|
hadOverride = true;
|
|
38
|
-
formatRegistry(rootRegistry,
|
|
38
|
+
formatRegistry(rootRegistry, rootMarkedNames, lines);
|
|
39
39
|
if (hadOverride) {
|
|
40
40
|
lines.push('', '* Overridden by package.json');
|
|
41
41
|
}
|
|
@@ -45,43 +45,43 @@ function collectOverrides(packageDir) {
|
|
|
45
45
|
const scripts = readPackageJsonScripts(packageDir);
|
|
46
46
|
if (!scripts)
|
|
47
47
|
return {};
|
|
48
|
-
const
|
|
48
|
+
const candidateOverrides = {};
|
|
49
49
|
for (const [name, value] of Object.entries(scripts)) {
|
|
50
50
|
if (isHookName(name))
|
|
51
51
|
continue;
|
|
52
52
|
if (isSelfReferential(value, name, packageDir))
|
|
53
53
|
continue;
|
|
54
|
-
|
|
54
|
+
candidateOverrides[name] = value;
|
|
55
55
|
}
|
|
56
|
-
return
|
|
56
|
+
return candidateOverrides;
|
|
57
57
|
}
|
|
58
58
|
function applyOverrides(registry, overrides) {
|
|
59
|
-
const
|
|
59
|
+
const markedNames = new Set();
|
|
60
60
|
for (const [name, value] of Object.entries(overrides)) {
|
|
61
61
|
if (!Object.hasOwn(registry, name)) {
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
registry[name] = value;
|
|
65
|
-
|
|
65
|
+
markedNames.add(name);
|
|
66
66
|
}
|
|
67
|
-
return
|
|
67
|
+
return markedNames;
|
|
68
68
|
}
|
|
69
69
|
function filterHooks(registry) {
|
|
70
|
-
const
|
|
70
|
+
const hooklessRegistry = {};
|
|
71
71
|
for (const [key, value] of Object.entries(registry)) {
|
|
72
72
|
if (!isHookName(key))
|
|
73
|
-
|
|
73
|
+
hooklessRegistry[key] = value;
|
|
74
74
|
}
|
|
75
|
-
return
|
|
75
|
+
return hooklessRegistry;
|
|
76
76
|
}
|
|
77
|
-
function formatRegistry(registry,
|
|
77
|
+
function formatRegistry(registry, markedNames, lines) {
|
|
78
78
|
const keys = Object.keys(registry);
|
|
79
79
|
if (keys.length === 0)
|
|
80
80
|
return;
|
|
81
|
-
const
|
|
82
|
-
const
|
|
81
|
+
const maxKeyLength = Math.max(...keys.map((k) => k.length));
|
|
82
|
+
const padWidth = Math.max(maxKeyLength + 1 + 2, 20);
|
|
83
83
|
for (const [key, value] of Object.entries(registry)) {
|
|
84
|
-
const marker =
|
|
85
|
-
lines.push(` ${(key + marker).padEnd(
|
|
84
|
+
const marker = markedNames.has(key) ? '*' : ' ';
|
|
85
|
+
lines.push(` ${(key + marker).padEnd(padWidth)} ${describeScript(value)}`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
/** Marks a value that was cut, so a reader can tell a truncated one from a complete one. */
|
|
1
|
+
/** Marks a value that was cut, so that a reader can tell a truncated one from a complete one. */
|
|
2
2
|
export declare const TRUNCATION_MARK = "\u2026";
|
|
3
3
|
/**
|
|
4
4
|
* Cuts a string that would overrun a byte budget, marking the cut. What comes back is at most `budgetBytes`
|
|
5
5
|
* long, the mark included.
|
|
6
6
|
*
|
|
7
7
|
* Cuts between code points rather than between bytes, so a multi-byte character is never left in halves.
|
|
8
|
-
* Graphemes are left unconsidered, which can separate an emoji from a modifier following it -- a
|
|
9
|
-
*
|
|
8
|
+
* Graphemes are left unconsidered, which can separate an emoji from a modifier following it -- a risk only
|
|
9
|
+
* at a budget that the callers here never come near.
|
|
10
10
|
*
|
|
11
|
-
* A budget too small to hold the mark yields
|
|
12
|
-
* caller asked for.
|
|
11
|
+
* The return never exceeds the budget: A budget too small to hold the mark yields an empty string.
|
|
13
12
|
*/
|
|
14
13
|
export declare function clampToBytes(value: string, budgetBytes: number): string;
|
|
@@ -7,14 +7,14 @@ export function clampToBytes(value, budgetBytes) {
|
|
|
7
7
|
if (budget < 0) {
|
|
8
8
|
return '';
|
|
9
9
|
}
|
|
10
|
-
let
|
|
10
|
+
let keptText = '';
|
|
11
11
|
let bytes = 0;
|
|
12
12
|
for (const character of value) {
|
|
13
13
|
const size = Buffer.byteLength(character);
|
|
14
14
|
if (bytes + size > budget)
|
|
15
15
|
break;
|
|
16
|
-
|
|
16
|
+
keptText += character;
|
|
17
17
|
bytes += size;
|
|
18
18
|
}
|
|
19
|
-
return `${
|
|
19
|
+
return `${keptText}${TRUNCATION_MARK}`;
|
|
20
20
|
}
|
|
@@ -2,7 +2,5 @@
|
|
|
2
2
|
export declare const CONFIG_RELATIVE_PATH: string;
|
|
3
3
|
/**
|
|
4
4
|
* Resolves the config-file path for a directory, whether that is the monorepo root or a package.
|
|
5
|
-
* Callers that only need to know whether a config exists -- the build, which folds it into its cache digest -- go
|
|
6
|
-
* through this rather than spelling the path again, so that the two cannot drift into hashing a file nothing reads.
|
|
7
5
|
*/
|
|
8
6
|
export declare function resolveConfigPath(baseDir: string): string;
|
|
@@ -6,11 +6,11 @@ export interface BoundedBuffer {
|
|
|
6
6
|
/** Retains a chunk, dropping bytes from the middle once both ends are full. */
|
|
7
7
|
append: (chunk: Buffer) => void;
|
|
8
8
|
/** Renders the retained bytes, with a marker naming the dropped count standing in for what was dropped. */
|
|
9
|
-
|
|
9
|
+
readBuffer: () => Buffer;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Creates an accumulator that retains the first and last bytes it is fed and drops the middle.
|
|
13
|
-
* Both ends are kept because a failing command's first error and its trailing summary each
|
|
12
|
+
* Creates an accumulator that retains the first and last bytes that it is fed and drops the middle.
|
|
13
|
+
* Both ends are kept because a failing command's first error and its trailing summary each contain information
|
|
14
14
|
* that the other end does not.
|
|
15
15
|
*/
|
|
16
16
|
export declare function createBoundedBuffer(options?: BoundedBufferOptions): BoundedBuffer;
|
|
@@ -40,7 +40,7 @@ export function createBoundedBuffer(options = {}) {
|
|
|
40
40
|
tailOffset = (tailOffset + chunk.length) % tailLimitBytes;
|
|
41
41
|
tailBytes = Math.min(tailLimitBytes, tailBytes + chunk.length);
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function readBuffer() {
|
|
44
44
|
const headBuffer = Buffer.concat(headChunks);
|
|
45
45
|
if (tailRing === undefined) {
|
|
46
46
|
return headBuffer;
|
|
@@ -53,5 +53,5 @@ export function createBoundedBuffer(options = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
return Buffer.concat([headBuffer, Buffer.from(formatElisionMarker(elidedBytes)), tailBuffer]);
|
|
55
55
|
}
|
|
56
|
-
return { append,
|
|
56
|
+
return { append, readBuffer };
|
|
57
57
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reduces a transcript to the one line a verdict's detail slot
|
|
2
|
+
* Reduces a transcript to the one line shown in a verdict's detail slot, or `undefined` when it contained nothing.
|
|
3
3
|
*
|
|
4
4
|
* The excerpt is the last blank-line-delimited block: a tool that prints progress separates its closing
|
|
5
|
-
* statement with a blank line,
|
|
6
|
-
* trailing lines fits vitest's four-line summary and v8's six-line coverage summary only by coincidence.
|
|
5
|
+
* statement with a blank line, and that blank line is the tool's own mark of where its summary starts.
|
|
7
6
|
*
|
|
8
|
-
* The line
|
|
9
|
-
*
|
|
7
|
+
* The line is bounded by `MAX_EXCERPT_BYTES` alone; the ceiling on a verdict line is applied where that line is
|
|
8
|
+
* composed.
|
|
10
9
|
*/
|
|
11
10
|
export declare function deriveExcerpt(transcript: string): string | undefined;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Renders a duration at the two coarsest units that say something, for a line a reader skims.
|
|
2
|
+
* Renders a duration at the two coarsest units that say something, for a line that a reader skims.
|
|
3
3
|
*
|
|
4
|
-
* Truncates at every unit rather than rounding, so a value never reads as more time than elapsed:
|
|
5
|
-
* `1m
|
|
6
|
-
* one check's runtime differs from another's; a whole number
|
|
4
|
+
* Truncates at every unit rather than rounding, so that a value never reads as more time than elapsed: 119.9
|
|
5
|
+
* seconds is `1m 59s`, whereas rounding would claim `2m`. Below a minute the tenth is kept, since that is the
|
|
6
|
+
* scale at which one check's runtime differs from another's; a whole number is rendered without the `.0`, as
|
|
7
|
+
* `12s` rather than `12.0s`.
|
|
7
8
|
*/
|
|
8
|
-
export declare function formatDuration(
|
|
9
|
+
export declare function formatDuration(durationMs: number): string;
|
|
9
10
|
/**
|
|
10
|
-
* Renders the clause
|
|
11
|
-
* A skip that can say what it saved earns the clause; one that cannot says nothing, and the caller drops it
|
|
12
|
-
* rather than printing a saving of `~0s`.
|
|
11
|
+
* Renders the clause stating the time that a skip saved, or `undefined` for a saving under a second.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* A non-finite duration also returns `undefined`: It compares false against the threshold and would otherwise
|
|
14
|
+
* render as `saved ~NaNs`.
|
|
16
15
|
*/
|
|
17
|
-
export declare function formatSaving(
|
|
16
|
+
export declare function formatSaving(durationMs: number): string | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function formatDuration(
|
|
2
|
-
const total = Math.max(0,
|
|
1
|
+
export function formatDuration(durationMs) {
|
|
2
|
+
const total = Math.max(0, durationMs);
|
|
3
3
|
if (total < MILLISECONDS_PER_MINUTE) {
|
|
4
4
|
return `${truncateToTenth(total)}s`;
|
|
5
5
|
}
|
|
@@ -10,11 +10,11 @@ export function formatDuration(milliseconds) {
|
|
|
10
10
|
}
|
|
11
11
|
return joinUnits(`${Math.floor(totalMinutes / MINUTES_PER_HOUR)}h`, totalMinutes % MINUTES_PER_HOUR, 'm');
|
|
12
12
|
}
|
|
13
|
-
export function formatSaving(
|
|
14
|
-
if (!Number.isFinite(
|
|
13
|
+
export function formatSaving(durationMs) {
|
|
14
|
+
if (!Number.isFinite(durationMs) || durationMs < MILLISECONDS_PER_SECOND) {
|
|
15
15
|
return undefined;
|
|
16
16
|
}
|
|
17
|
-
return `saved ~${formatDuration(
|
|
17
|
+
return `saved ~${formatDuration(durationMs)}`;
|
|
18
18
|
}
|
|
19
19
|
const MILLISECONDS_PER_MINUTE = 60_000;
|
|
20
20
|
const MILLISECONDS_PER_SECOND = 1_000;
|
|
@@ -23,6 +23,6 @@ const SECONDS_PER_MINUTE = 60;
|
|
|
23
23
|
function joinUnits(major, minor, minorUnit) {
|
|
24
24
|
return minor === 0 ? major : `${major} ${minor}${minorUnit}`;
|
|
25
25
|
}
|
|
26
|
-
function truncateToTenth(
|
|
27
|
-
return Math.floor(
|
|
26
|
+
function truncateToTenth(durationMs) {
|
|
27
|
+
return Math.floor(durationMs / 100) / 10;
|
|
28
28
|
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Asks pnpm what a `-F` pattern selects, so an invocation that would select nothing is caught before it
|
|
3
|
-
* delegates, and one selecting several packages is
|
|
2
|
+
* Asks pnpm what a `-F` pattern selects, so that an invocation that would select nothing is caught before it
|
|
3
|
+
* delegates, and one selecting several packages is distinguished from one selecting a single package.
|
|
4
4
|
*
|
|
5
|
-
* pnpm
|
|
6
|
-
*
|
|
7
|
-
* every form but the simplest silently unchecked.
|
|
5
|
+
* pnpm answers because `-F` accepts pnpm's whole selector language: names and globs alongside `./path`, `{dir}`,
|
|
6
|
+
* `pkg...`, `!negation`, and `[since]`.
|
|
8
7
|
*
|
|
9
|
-
* Runs from the monorepo root, where the delegate runs, so a path pattern resolves against the same
|
|
10
|
-
* in the probe as in the run it stands in for.
|
|
8
|
+
* Runs from the monorepo root, where the delegate runs, so that a path pattern resolves against the same
|
|
9
|
+
* directory in the probe as in the run that it stands in for.
|
|
11
10
|
*
|
|
12
|
-
* The listing counts the root project wherever the filter
|
|
13
|
-
* composes runs there only
|
|
14
|
-
* and executes nowhere. A listing of the root alone is therefore the one reading the listing
|
|
15
|
-
* and
|
|
16
|
-
* root: two of them run in two scopes, while one beside the root may run in that
|
|
11
|
+
* The listing counts the root project wherever the filter does not exclude it, while the `exec` that a
|
|
12
|
+
* delegation composes runs there only when the pattern selects the root positively: `-F '!./packages/*'` lists
|
|
13
|
+
* the root and executes nowhere. A listing of the root alone is therefore the one reading that the listing
|
|
14
|
+
* cannot settle, and that reading is checked against `exec` itself. Every other listing settles on its own, and
|
|
15
|
+
* counts the packages beside the root: two of them run in two scopes, while one beside the root may run in that
|
|
16
|
+
* package alone.
|
|
17
17
|
*/
|
|
18
18
|
export declare function readFilterSelection(pattern: string, monorepoRoot: string): FilterSelection;
|
|
19
19
|
/**
|
|
20
20
|
* Reduces a listing to what it proves about the selection.
|
|
21
21
|
*
|
|
22
|
-
* Every outcome the probe cannot read resolves to `unresolved`, which claims nothing about the selection,
|
|
23
|
-
* invocation delegates as it would have. A pattern pnpm rejects rather than resolves, such as a bad
|
|
24
|
-
* a changed-since selector, is pnpm's to report, and reporting it here as a match failure would name a
|
|
25
|
-
* different fault than the one the user has.
|
|
22
|
+
* Every outcome that the probe cannot read resolves to `unresolved`, which claims nothing about the selection,
|
|
23
|
+
* and the invocation delegates as it would have. A pattern that pnpm rejects rather than resolves, such as a bad
|
|
24
|
+
* git ref in a changed-since selector, is pnpm's to report, and reporting it here as a match failure would name a
|
|
25
|
+
* different fault than the one that the user has.
|
|
26
26
|
*/
|
|
27
27
|
export declare function interpretSelectionProbe(probe: SelectionProbe, monorepoRoot: string): ProbeReading;
|
|
28
28
|
/**
|
|
29
|
-
* Reduces a marked delegate run to what it proves: a scope that wrote the marker is a scope the run
|
|
30
|
-
* run is made only for a listing of the root alone,
|
|
29
|
+
* Reduces a marked delegate run to what it proves: a scope that wrote the marker is a scope that the run
|
|
30
|
+
* reached. Because the run is made only for a listing of the root alone, that scope is the only one.
|
|
31
31
|
*/
|
|
32
32
|
export declare function interpretDelegateProbe(probe: SelectionProbe): FilterSelection;
|
|
33
33
|
/** What a `-F` pattern selected, as far as pnpm could be asked. `multiple` is two or more packages beside the root. */
|
|
34
34
|
export type FilterSelection = 'empty' | 'multiple' | 'single' | 'unresolved';
|
|
35
35
|
/** What a listing proves, `root-only` being the reading that only the delegate can settle. */
|
|
36
36
|
export type ProbeReading = FilterSelection | 'root-only';
|
|
37
|
-
/** The part of a probe's completion a selection is read
|
|
37
|
+
/** The part of a probe's completion from which a selection is read. */
|
|
38
38
|
export interface SelectionProbe {
|
|
39
39
|
error: Error | undefined;
|
|
40
40
|
status: number | null;
|
|
@@ -5,31 +5,31 @@ import process from 'node:process';
|
|
|
5
5
|
import { isObject } from "./type-guards.js";
|
|
6
6
|
const SELECTION_MARKER = 'nmr-selected-scope';
|
|
7
7
|
export function readFilterSelection(pattern, monorepoRoot) {
|
|
8
|
-
const
|
|
8
|
+
const listingRun = spawnSync('pnpm', ['ls', '--filter', pattern, '--depth', '-1', '--json'], {
|
|
9
9
|
cwd: monorepoRoot,
|
|
10
10
|
encoding: 'utf8',
|
|
11
11
|
});
|
|
12
|
-
const
|
|
13
|
-
return
|
|
12
|
+
const probeReading = interpretSelectionProbe({ error: listingRun.error, status: listingRun.status, stdout: listingRun.stdout }, monorepoRoot);
|
|
13
|
+
return probeReading === 'root-only' ? readDelegateSelection(pattern, monorepoRoot) : probeReading;
|
|
14
14
|
}
|
|
15
15
|
export function interpretSelectionProbe(probe, monorepoRoot) {
|
|
16
16
|
if (probe.error !== undefined || probe.status !== 0) {
|
|
17
17
|
return 'unresolved';
|
|
18
18
|
}
|
|
19
|
-
let
|
|
19
|
+
let parsedListing;
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
parsedListing = JSON.parse(probe.stdout);
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
24
|
return 'unresolved';
|
|
25
25
|
}
|
|
26
|
-
if (!Array.isArray(
|
|
26
|
+
if (!Array.isArray(parsedListing)) {
|
|
27
27
|
return 'unresolved';
|
|
28
28
|
}
|
|
29
|
-
if (
|
|
29
|
+
if (parsedListing.length === 0) {
|
|
30
30
|
return 'empty';
|
|
31
31
|
}
|
|
32
|
-
const packageCount =
|
|
32
|
+
const packageCount = parsedListing.filter((entry) => !isRootEntry(entry, monorepoRoot)).length;
|
|
33
33
|
if (packageCount === 0) {
|
|
34
34
|
return 'root-only';
|
|
35
35
|
}
|
|
@@ -45,9 +45,9 @@ function isRootEntry(entry, monorepoRoot) {
|
|
|
45
45
|
if (!isObject(entry) || typeof entry['path'] !== 'string') {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
|
-
return
|
|
48
|
+
return resolveRealPath(entry['path']) === resolveRealPath(monorepoRoot);
|
|
49
49
|
}
|
|
50
|
-
function
|
|
50
|
+
function resolveRealPath(target) {
|
|
51
51
|
try {
|
|
52
52
|
return realpathSync(target);
|
|
53
53
|
}
|
|
@@ -56,6 +56,6 @@ function readRealPath(target) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function readDelegateSelection(pattern, monorepoRoot) {
|
|
59
|
-
const
|
|
60
|
-
return interpretDelegateProbe({ error:
|
|
59
|
+
const probeRun = spawnSync('pnpm', ['--filter', pattern, 'exec', process.execPath, '--eval', `process.stdout.write('${SELECTION_MARKER}')`], { cwd: monorepoRoot, encoding: 'utf8' });
|
|
60
|
+
return interpretDelegateProbe({ error: probeRun.error, status: probeRun.status, stdout: probeRun.stdout });
|
|
61
61
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Finds the candidate closest to a name, or `undefined`
|
|
2
|
+
* Finds the candidate closest to a name, or `undefined` when none is near enough to be worth naming.
|
|
3
3
|
*
|
|
4
4
|
* The ceiling scales with the name's length, so a short name admits one slip and a long one a few. A fixed
|
|
5
5
|
* ceiling would either miss a typo in `root:test:coverage` or offer `fix` for `ci`.
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
export function findClosestName(name, candidates) {
|
|
2
2
|
const ceiling = Math.max(1, Math.floor(name.length / 4));
|
|
3
|
-
let
|
|
4
|
-
let
|
|
3
|
+
let closestName;
|
|
4
|
+
let shortestDistance = Infinity;
|
|
5
5
|
for (const candidate of candidates) {
|
|
6
6
|
const distance = measureDistance(name, candidate);
|
|
7
|
-
if (distance <
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
if (distance < shortestDistance) {
|
|
8
|
+
shortestDistance = distance;
|
|
9
|
+
closestName = candidate;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
return
|
|
12
|
+
return shortestDistance <= ceiling ? closestName : undefined;
|
|
13
13
|
}
|
|
14
14
|
function measureDistance(first, second) {
|
|
15
15
|
if (first.length === 0 || second.length === 0) {
|
|
16
16
|
return Math.max(first.length, second.length);
|
|
17
17
|
}
|
|
18
|
-
let
|
|
19
|
-
let
|
|
18
|
+
let previousRow = Array.from({ length: second.length + 1 }, (_unused, column) => column);
|
|
19
|
+
let currentRow = Array.from({ length: second.length + 1 }, () => 0);
|
|
20
20
|
for (let row = 1; row <= first.length; row += 1) {
|
|
21
|
-
|
|
21
|
+
currentRow[0] = row;
|
|
22
22
|
for (let column = 1; column <= second.length; column += 1) {
|
|
23
|
-
const substitution = readCell(
|
|
24
|
-
|
|
23
|
+
const substitution = readCell(previousRow, column - 1) + (first[row - 1] === second[column - 1] ? 0 : 1);
|
|
24
|
+
currentRow[column] = Math.min(readCell(currentRow, column - 1) + 1, readCell(previousRow, column) + 1, substitution);
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const completedRow = currentRow;
|
|
27
|
+
currentRow = previousRow;
|
|
28
|
+
previousRow = completedRow;
|
|
29
29
|
}
|
|
30
|
-
return readCell(
|
|
30
|
+
return readCell(previousRow, second.length);
|
|
31
31
|
}
|
|
32
32
|
function readCell(row, index) {
|
|
33
33
|
const value = row[index];
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `:pre` or `:post` suffix preceded by at least one character.
|
|
2
|
+
* Reports whether `key` is a hook script name: a `:pre` or `:post` suffix preceded by at least one character.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* purely about suffix shape; it does not consult the registry or care
|
|
7
|
-
* whether the parent command exists.
|
|
4
|
+
* Reads the suffix alone, without consulting the registry for the parent command.
|
|
8
5
|
*/
|
|
9
6
|
export declare function isHookName(key: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `package.json` fields that
|
|
2
|
+
* The `package.json` fields that contain a dependency's version specifier.
|
|
3
3
|
*
|
|
4
4
|
* pnpm accepts the `catalog:` protocol in all four, so a reader looking for catalogued dependencies has to
|
|
5
|
-
* sweep the whole set rather than the two that
|
|
5
|
+
* sweep the whole set rather than the two that contain most of them.
|
|
6
6
|
*/
|
|
7
7
|
export declare const DEPENDENCY_FIELDS: readonly ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
8
8
|
export type DependencyField = (typeof DEPENDENCY_FIELDS)[number];
|
|
@@ -19,26 +19,27 @@ export type PackageJson = {
|
|
|
19
19
|
[K in DependencyField]?: Record<string, string>;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Reads and parses the package.json at the given directory, keeping only the fields nmr reads.
|
|
22
|
+
* Reads and parses the package.json at the given directory, keeping only the fields that nmr reads.
|
|
23
23
|
*/
|
|
24
24
|
export declare function readPackageJson(dir: string): PackageJson;
|
|
25
25
|
/**
|
|
26
|
-
* Returns the `pnpm.overrides` block a package.json
|
|
26
|
+
* Returns the `pnpm.overrides` block declared by a package.json, every key that it contains and each value as
|
|
27
|
+
* written.
|
|
27
28
|
*/
|
|
28
|
-
export declare function getPnpmOverrides(
|
|
29
|
+
export declare function getPnpmOverrides(packageJson: PackageJson): Record<string, unknown> | undefined;
|
|
29
30
|
/**
|
|
30
31
|
* Parses a `package.json`'s text, rejecting what does not parse.
|
|
31
32
|
*
|
|
32
|
-
* A file the user wrote by hand is theirs to fix, so the failure names it rather than reporting the parser's
|
|
33
|
+
* A file that the user wrote by hand is theirs to fix, so the failure names it rather than reporting the parser's
|
|
33
34
|
* own stack.
|
|
34
35
|
*/
|
|
35
|
-
export declare function parsePackageJson(
|
|
36
|
+
export declare function parsePackageJson(rawText: string, file: string): unknown;
|
|
36
37
|
/**
|
|
37
38
|
* Narrows a `scripts` record to strings, rejecting any value that is not one.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
* Dropping one silently would let a caller run something else in its place and report nothing.
|
|
40
|
+
* A value of any other type is malformed however it got there, because npm and pnpm read a script as a string
|
|
41
|
+
* too. Dropping one silently would let a caller run something else in its place and report nothing.
|
|
41
42
|
*/
|
|
42
43
|
export declare function readScriptRecord(dir: string, scripts: Record<string, unknown>): Record<string, string>;
|
|
43
|
-
/** Resolves the `package.json` path for a directory: the file tier-3 scripts are read
|
|
44
|
+
/** Resolves the `package.json` path for a directory: the file from which tier-3 scripts are read. */
|
|
44
45
|
export declare function resolvePackageJsonPath(dir: string): string;
|
|
@@ -14,45 +14,46 @@ export const DEPENDENCY_FIELDS = [
|
|
|
14
14
|
];
|
|
15
15
|
export function readPackageJson(dir) {
|
|
16
16
|
const file = resolvePackageJsonPath(dir);
|
|
17
|
-
const
|
|
18
|
-
if (!isObject(
|
|
17
|
+
const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
|
|
18
|
+
if (!isObject(parsedManifest)) {
|
|
19
19
|
throw new UserError(`Invalid package.json in ${dir}: expected an object`);
|
|
20
20
|
}
|
|
21
|
-
const
|
|
22
|
-
if (typeof
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
if (typeof
|
|
27
|
-
|
|
28
|
-
if (typeof
|
|
29
|
-
|
|
30
|
-
if (isObject(parsed['scripts'])) {
|
|
31
|
-
pkg.scripts = readScriptRecord(dir, parsed['scripts']);
|
|
21
|
+
const packageJson = {};
|
|
22
|
+
if (typeof parsedManifest['name'] === 'string')
|
|
23
|
+
packageJson.name = parsedManifest['name'];
|
|
24
|
+
if (parsedManifest['private'] === true)
|
|
25
|
+
packageJson.private = true;
|
|
26
|
+
if (typeof parsedManifest['version'] === 'string')
|
|
27
|
+
packageJson.version = parsedManifest['version'];
|
|
28
|
+
if (typeof parsedManifest['packageManager'] === 'string') {
|
|
29
|
+
packageJson.packageManager = parsedManifest['packageManager'];
|
|
32
30
|
}
|
|
33
|
-
if (isObject(
|
|
34
|
-
|
|
31
|
+
if (isObject(parsedManifest['scripts'])) {
|
|
32
|
+
packageJson.scripts = readScriptRecord(dir, parsedManifest['scripts']);
|
|
33
|
+
}
|
|
34
|
+
if (isObject(parsedManifest['pnpm'])) {
|
|
35
|
+
const pnpm = parsedManifest['pnpm'];
|
|
35
36
|
if (isObject(pnpm['overrides'])) {
|
|
36
|
-
|
|
37
|
+
packageJson.pnpm = { overrides: pnpm['overrides'] };
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
for (const field of DEPENDENCY_FIELDS) {
|
|
40
|
-
const
|
|
41
|
-
if (isObject(
|
|
42
|
-
|
|
41
|
+
const declaredDependencies = parsedManifest[field];
|
|
42
|
+
if (isObject(declaredDependencies)) {
|
|
43
|
+
packageJson[field] = readStringValues(declaredDependencies);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
return
|
|
46
|
+
return packageJson;
|
|
46
47
|
}
|
|
47
|
-
export function getPnpmOverrides(
|
|
48
|
-
if (!isObject(
|
|
48
|
+
export function getPnpmOverrides(packageJson) {
|
|
49
|
+
if (!isObject(packageJson.pnpm))
|
|
49
50
|
return undefined;
|
|
50
|
-
const overrides =
|
|
51
|
+
const overrides = packageJson.pnpm.overrides;
|
|
51
52
|
return isObject(overrides) ? overrides : undefined;
|
|
52
53
|
}
|
|
53
|
-
export function parsePackageJson(
|
|
54
|
+
export function parsePackageJson(rawText, file) {
|
|
54
55
|
try {
|
|
55
|
-
return JSON.parse(
|
|
56
|
+
return JSON.parse(rawText);
|
|
56
57
|
}
|
|
57
58
|
catch (error) {
|
|
58
59
|
throw new UserError(`Invalid package.json at ${file}: ${describeError(error)}`);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keeps the entries whose value is a string, dropping the rest.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* that easy to reach without malformed intent: an unquoted `18` parses as a number.
|
|
4
|
+
* Drops rather than rejects, so that one malformed value does not hide the entries beside it. YAML's implicit
|
|
5
|
+
* typing makes such a value easy to write: An unquoted `18` parses as a number.
|
|
7
6
|
*/
|
|
8
7
|
export declare function readStringValues(record: Record<string, unknown>): Record<string, string>;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reports the line that closes a bin's output, separated from the items above it by a blank line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the run
|
|
7
|
-
*
|
|
4
|
+
* When a bin prints one line per item, the last line that a reader sees is whichever item happened to be last.
|
|
5
|
+
* The convention that this implements: The header says what the list is, and the closing statement says what
|
|
6
|
+
* the run concluded, so the count and the remedy are stated here rather than in the header. The blank line makes
|
|
7
|
+
* the statement findable once the lines are flattened.
|
|
8
8
|
*
|
|
9
|
-
* The blank line
|
|
10
|
-
*
|
|
11
|
-
* passes `console.warn`.
|
|
9
|
+
* The closer is one write and cannot be interleaved: The blank line goes in the same call as the statement.
|
|
10
|
+
* `log` writes the closer to the stream that its items went to: A bin reporting to stderr passes `console.warn`.
|
|
12
11
|
*
|
|
13
|
-
* A bin whose whole output is one or two statements about one subject
|
|
14
|
-
* closes by printing plainly rather than through here.
|
|
12
|
+
* A bin whose whole output is one or two statements about one subject does not have any items to be separated
|
|
13
|
+
* from, and closes by printing plainly rather than through here.
|
|
15
14
|
*/
|
|
16
15
|
export declare function reportClosing(message: string, log?: (line: string) => void): void;
|