@williamthorsen/nmr 0.38.0 → 0.39.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 +84 -0
- package/.readyup/kits/default.js +84 -79
- package/.readyup/manifest.json +5 -5
- package/CHANGELOG.md +70 -0
- package/README.md +37 -11
- package/dist/esm/check-cache.d.ts +2 -2
- 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 +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.js +65 -63
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +2 -2
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.js +9 -7
- package/dist/esm/commands/report-overrides.js +7 -7
- package/dist/esm/config.js +25 -25
- package/dist/esm/context.js +8 -8
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/duration.d.ts +2 -2
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/package-json.d.ts +2 -2
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/transcript.d.ts +2 -2
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +5 -5
- package/dist/esm/recording.js +21 -14
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.js +252 -186
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +13 -1
- package/dist/esm/steps.js +32 -27
- package/dist/esm/tests.d.ts +7 -7
- package/dist/esm/tests.js +20 -8
- package/dist/esm/tiers.d.ts +1 -1
- package/dist/esm/tiers.js +8 -8
- package/dist/esm/verbosity.d.ts +1 -1
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +9 -2
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.js +48 -46
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +74 -25
- package/dist/esm/workspace.d.ts +2 -18
- package/dist/esm/workspace.js +9 -67
- package/docs/reporting.md +6 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +3 -4
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
|
@@ -11,33 +11,33 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
11
11
|
const packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
12
12
|
const packages = [];
|
|
13
13
|
for (const packageDir of packageDirs) {
|
|
14
|
-
const
|
|
15
|
-
const packageName =
|
|
16
|
-
const isPrivate =
|
|
14
|
+
const packageJson = readPackageJson(packageDir);
|
|
15
|
+
const packageName = packageJson.name ?? path.basename(packageDir);
|
|
16
|
+
const isPrivate = packageJson.private === true;
|
|
17
17
|
if (isPrivate) {
|
|
18
18
|
packages.push({
|
|
19
19
|
packageName,
|
|
20
20
|
packageDir,
|
|
21
21
|
isPrivate: true,
|
|
22
|
-
prepublishOnly:
|
|
22
|
+
prepublishOnly: packageJson.scripts?.['prepublishOnly'],
|
|
23
23
|
action: 'ok',
|
|
24
24
|
});
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
27
|
+
const existingHook = packageJson.scripts?.['prepublishOnly'];
|
|
28
|
+
if (existingHook) {
|
|
29
29
|
packages.push({
|
|
30
30
|
packageName,
|
|
31
31
|
packageDir,
|
|
32
32
|
isPrivate: false,
|
|
33
|
-
prepublishOnly:
|
|
33
|
+
prepublishOnly: existingHook,
|
|
34
34
|
action: 'ok',
|
|
35
35
|
});
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
if (options.
|
|
39
|
-
const action = options.
|
|
40
|
-
if (!options.
|
|
38
|
+
if (options.shouldFix) {
|
|
39
|
+
const action = options.isDryRun ? 'would-fix' : 'fixed';
|
|
40
|
+
if (!options.isDryRun) {
|
|
41
41
|
addPrepublishOnly(packageDir, hookCommand);
|
|
42
42
|
}
|
|
43
43
|
packages.push({
|
|
@@ -62,57 +62,57 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
62
62
|
return { packages, hasFailures };
|
|
63
63
|
}
|
|
64
64
|
export function reportPrepublishHooks(result, hookCommand, style) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
65
|
+
const publishablePackages = result.packages.filter((packageStatus) => !packageStatus.isPrivate);
|
|
66
|
+
if (publishablePackages.length === 0) {
|
|
67
67
|
console.info('No publishable packages found.');
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
for (const
|
|
71
|
-
console.info(renderHookStatus(
|
|
70
|
+
for (const packageStatus of publishablePackages) {
|
|
71
|
+
console.info(renderHookStatus(packageStatus, hookCommand, style));
|
|
72
72
|
}
|
|
73
|
-
reportClosing(describeHookRun(
|
|
73
|
+
reportClosing(describeHookRun(publishablePackages));
|
|
74
74
|
}
|
|
75
75
|
function addPrepublishOnly(packageDir, command) {
|
|
76
76
|
const filePath = path.join(packageDir, 'package.json');
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
if (!isObject(
|
|
77
|
+
const rawText = readFileSync(filePath, 'utf8');
|
|
78
|
+
const parsedManifest = JSON.parse(rawText);
|
|
79
|
+
if (!isObject(parsedManifest)) {
|
|
80
80
|
throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
|
|
81
81
|
}
|
|
82
|
-
const scripts = isObject(
|
|
82
|
+
const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
|
|
83
83
|
scripts['prepublishOnly'] = command;
|
|
84
|
-
|
|
85
|
-
writeFileSync(filePath, JSON.stringify(
|
|
84
|
+
parsedManifest['scripts'] = scripts;
|
|
85
|
+
writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
|
|
86
86
|
}
|
|
87
|
-
function countAction(
|
|
88
|
-
return
|
|
87
|
+
function countAction(publishablePackages, action) {
|
|
88
|
+
return publishablePackages.filter((packageStatus) => packageStatus.action === action).length;
|
|
89
89
|
}
|
|
90
|
-
function describeHookRun(
|
|
91
|
-
const packages =
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
return `${
|
|
90
|
+
function describeHookRun(publishablePackages) {
|
|
91
|
+
const packages = publishablePackages.length === 1 ? '1 publishable package' : `${publishablePackages.length} publishable packages`;
|
|
92
|
+
const missingCount = countAction(publishablePackages, 'missing');
|
|
93
|
+
if (missingCount > 0) {
|
|
94
|
+
return `${missingCount} of ${packages} ${missingCount === 1 ? 'is' : 'are'} missing prepublishOnly. Run with --fix to add it.`;
|
|
95
95
|
}
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return `Added prepublishOnly to ${
|
|
96
|
+
const fixedCount = countAction(publishablePackages, 'fixed');
|
|
97
|
+
if (fixedCount > 0) {
|
|
98
|
+
return `Added prepublishOnly to ${fixedCount} of ${packages}.`;
|
|
99
99
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
return `Would add prepublishOnly to ${
|
|
100
|
+
const wouldFixCount = countAction(publishablePackages, 'would-fix');
|
|
101
|
+
if (wouldFixCount > 0) {
|
|
102
|
+
return `Would add prepublishOnly to ${wouldFixCount} of ${packages}.`;
|
|
103
103
|
}
|
|
104
|
-
return `${packages} ${
|
|
104
|
+
return `${packages} ${publishablePackages.length === 1 ? 'has' : 'have'} prepublishOnly.`;
|
|
105
105
|
}
|
|
106
|
-
function renderHookStatus(
|
|
106
|
+
function renderHookStatus(packageStatus, hookCommand, style) {
|
|
107
107
|
const statuses = STATUS_GLYPHS[style];
|
|
108
|
-
switch (
|
|
108
|
+
switch (packageStatus.action) {
|
|
109
109
|
case 'ok':
|
|
110
|
-
return `${statuses.passed.text} ${
|
|
110
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: prepublishOnly = "${packageStatus.prepublishOnly}"`;
|
|
111
111
|
case 'missing':
|
|
112
|
-
return `${statuses.failed.text} ${
|
|
112
|
+
return `${statuses.failed.text} ${packageStatus.packageName}: missing prepublishOnly`;
|
|
113
113
|
case 'fixed':
|
|
114
|
-
return `${statuses.passed.text} ${
|
|
114
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: added prepublishOnly = "${hookCommand}"`;
|
|
115
115
|
case 'would-fix':
|
|
116
|
-
return `~ ${
|
|
116
|
+
return `~ ${packageStatus.packageName}: would add prepublishOnly = "${hookCommand}"`;
|
|
117
117
|
}
|
|
118
118
|
}
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -18,21 +18,21 @@ const MODE_ARGS = {
|
|
|
18
18
|
write: ['--list-different', '--write'],
|
|
19
19
|
};
|
|
20
20
|
export function runFmt(argv, cwd = process.cwd()) {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
reportError(`nmr-fmt: ${
|
|
21
|
+
const parsedArgs = parseFmtArgs(argv);
|
|
22
|
+
if (!parsedArgs.ok) {
|
|
23
|
+
reportError(`nmr-fmt: ${parsedArgs.error}`);
|
|
24
24
|
process.stderr.write(`${USAGE}\n`);
|
|
25
25
|
return 1;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
reportError(`nmr-fmt: ${
|
|
27
|
+
const resolvedTargets = resolveFormatTargets(cwd, parsedArgs.pathspecs);
|
|
28
|
+
if (!resolvedTargets.ok) {
|
|
29
|
+
reportError(`nmr-fmt: ${resolvedTargets.error}`);
|
|
30
30
|
return 1;
|
|
31
31
|
}
|
|
32
|
-
const { files, ignorePaths } =
|
|
32
|
+
const { files, ignorePaths } = resolvedTargets.targets;
|
|
33
33
|
if (files.length === 0) {
|
|
34
|
-
if (
|
|
35
|
-
reportError(`nmr-fmt: no formattable files matched: ${
|
|
34
|
+
if (parsedArgs.pathspecs.length > 0) {
|
|
35
|
+
reportError(`nmr-fmt: no formattable files matched: ${parsedArgs.pathspecs.join(', ')}`);
|
|
36
36
|
return 1;
|
|
37
37
|
}
|
|
38
38
|
return 0;
|
|
@@ -42,29 +42,29 @@ export function runFmt(argv, cwd = process.cwd()) {
|
|
|
42
42
|
reportError(`nmr-fmt: ${cli.error}`);
|
|
43
43
|
return 1;
|
|
44
44
|
}
|
|
45
|
-
return runPrettier({ cliPath: cli.cliPath, mode:
|
|
45
|
+
return runPrettier({ cliPath: cli.cliPath, mode: parsedArgs.mode, files, ignorePaths, cwd });
|
|
46
46
|
}
|
|
47
47
|
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
48
48
|
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
49
49
|
if (!toplevel.ok)
|
|
50
50
|
return toplevel;
|
|
51
51
|
const repositoryRoot = toplevel.stdout.trim();
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
54
|
-
return
|
|
52
|
+
const listedFiles = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
53
|
+
if (!listedFiles.ok)
|
|
54
|
+
return listedFiles;
|
|
55
55
|
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
56
56
|
if (!ignoreFiles.ok)
|
|
57
57
|
return ignoreFiles;
|
|
58
|
-
const
|
|
58
|
+
const discoveredIgnorePaths = splitNulSeparated(ignoreFiles.stdout)
|
|
59
59
|
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
60
60
|
.map((file) => path.resolve(repositoryRoot, file));
|
|
61
61
|
return {
|
|
62
62
|
ok: true,
|
|
63
63
|
targets: {
|
|
64
|
-
files: dedupe(splitNulSeparated(
|
|
64
|
+
files: dedupe(splitNulSeparated(listedFiles.stdout))
|
|
65
65
|
.filter((file) => isFormattableFile(cwd, file))
|
|
66
66
|
.toSorted(),
|
|
67
|
-
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...
|
|
67
|
+
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discoveredIgnorePaths]),
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
}
|
|
@@ -76,11 +76,11 @@ function parseFmtArgs(argv) {
|
|
|
76
76
|
const pathspecs = [];
|
|
77
77
|
for (const arg of argv) {
|
|
78
78
|
if (arg === '--check' || arg === '--write') {
|
|
79
|
-
const
|
|
80
|
-
if (mode !== undefined && mode !==
|
|
79
|
+
const requestedMode = arg === '--check' ? 'check' : 'write';
|
|
80
|
+
if (mode !== undefined && mode !== requestedMode) {
|
|
81
81
|
return { ok: false, error: 'Pass --check or --write, not both.' };
|
|
82
82
|
}
|
|
83
|
-
mode =
|
|
83
|
+
mode = requestedMode;
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
if (arg.startsWith('-')) {
|
|
@@ -94,7 +94,7 @@ function parseFmtArgs(argv) {
|
|
|
94
94
|
return { ok: true, mode, pathspecs };
|
|
95
95
|
}
|
|
96
96
|
function resolvePrettierCli() {
|
|
97
|
-
const
|
|
97
|
+
const missingResult = {
|
|
98
98
|
ok: false,
|
|
99
99
|
error: `\`${PRETTIER_PACKAGE}\` (${PRETTIER_RANGE}) could not be resolved. Install it in this repository.`,
|
|
100
100
|
};
|
|
@@ -103,14 +103,14 @@ function resolvePrettierCli() {
|
|
|
103
103
|
manifestPath = createRequire(import.meta.url).resolve(`${PRETTIER_PACKAGE}/package.json`);
|
|
104
104
|
}
|
|
105
105
|
catch {
|
|
106
|
-
return
|
|
106
|
+
return missingResult;
|
|
107
107
|
}
|
|
108
108
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
109
109
|
if (!isObject(manifest))
|
|
110
|
-
return
|
|
110
|
+
return missingResult;
|
|
111
111
|
const bin = isObject(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
|
|
112
112
|
if (typeof bin !== 'string')
|
|
113
|
-
return
|
|
113
|
+
return missingResult;
|
|
114
114
|
return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
|
|
115
115
|
}
|
|
116
116
|
export function runPrettier(options) {
|
|
@@ -121,7 +121,7 @@ export function runPrettier(options) {
|
|
|
121
121
|
...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
|
|
122
122
|
...MODE_ARGS[mode],
|
|
123
123
|
];
|
|
124
|
-
let
|
|
124
|
+
let firstFailureStatus = 0;
|
|
125
125
|
for (const batch of batchWithinBudget(files, budgetBytes)) {
|
|
126
126
|
const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
|
|
127
127
|
if (result.error) {
|
|
@@ -129,25 +129,25 @@ export function runPrettier(options) {
|
|
|
129
129
|
return 1;
|
|
130
130
|
}
|
|
131
131
|
const status = result.status ?? 1;
|
|
132
|
-
if (status !== 0 &&
|
|
133
|
-
|
|
132
|
+
if (status !== 0 && firstFailureStatus === 0) {
|
|
133
|
+
firstFailureStatus = status;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
return
|
|
136
|
+
return firstFailureStatus;
|
|
137
137
|
}
|
|
138
|
-
function batchWithinBudget(files,
|
|
138
|
+
function batchWithinBudget(files, budgetBytes) {
|
|
139
139
|
const batches = [];
|
|
140
140
|
let batch = [];
|
|
141
|
-
let
|
|
141
|
+
let sizeBytes = 0;
|
|
142
142
|
for (const file of files) {
|
|
143
|
-
const
|
|
144
|
-
if (batch.length > 0 &&
|
|
143
|
+
const costBytes = Buffer.byteLength(file) + 1;
|
|
144
|
+
if (batch.length > 0 && sizeBytes + costBytes > budgetBytes) {
|
|
145
145
|
batches.push(batch);
|
|
146
146
|
batch = [];
|
|
147
|
-
|
|
147
|
+
sizeBytes = 0;
|
|
148
148
|
}
|
|
149
149
|
batch.push(file);
|
|
150
|
-
|
|
150
|
+
sizeBytes += costBytes;
|
|
151
151
|
}
|
|
152
152
|
if (batch.length > 0)
|
|
153
153
|
batches.push(batch);
|
|
@@ -27,15 +27,17 @@ function describeCatalog(count, monorepoRoot) {
|
|
|
27
27
|
return `${subject} unread. Run \`nmr upgrade\` from ${monorepoRoot} to include ${object}.`;
|
|
28
28
|
}
|
|
29
29
|
function findCataloguedDependencies(packageDir) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const packageJson = readPackageJson(packageDir);
|
|
31
|
+
const foundDependencies = new Map();
|
|
32
32
|
for (const field of DEPENDENCY_FIELDS) {
|
|
33
|
-
const
|
|
34
|
-
for (const [name, specifier] of
|
|
35
|
-
if (specifier.startsWith(CATALOG_PROTOCOL) && !
|
|
36
|
-
|
|
33
|
+
const declaredEntries = Object.entries(packageJson[field] ?? {});
|
|
34
|
+
for (const [name, specifier] of declaredEntries) {
|
|
35
|
+
if (specifier.startsWith(CATALOG_PROTOCOL) && !foundDependencies.has(name)) {
|
|
36
|
+
foundDependencies.set(name, specifier);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
return [...
|
|
40
|
+
return [...foundDependencies]
|
|
41
|
+
.map(([name, specifier]) => ({ name, specifier }))
|
|
42
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
41
43
|
}
|
|
@@ -5,13 +5,13 @@ import { reportClosing } from "../helpers/reportClosing.js";
|
|
|
5
5
|
import { UserError } from "../UserError.js";
|
|
6
6
|
import { readWorkspaceOverrides } from "../workspace.js";
|
|
7
7
|
export function reportOverrides(monorepoRoot, style) {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
8
|
+
const declaredOverrides = listEntries(readWorkspaceOverrides(monorepoRoot));
|
|
9
|
+
if (declaredOverrides.length > 0) {
|
|
10
10
|
console.warn(formatGlyphLine(NMR_GLYPHS, style, 'overrides', 'WARN: pnpm overrides are active:'));
|
|
11
|
-
for (const [name, version] of
|
|
11
|
+
for (const [name, version] of declaredOverrides) {
|
|
12
12
|
console.warn(`- ${name} → ${version}`);
|
|
13
13
|
}
|
|
14
|
-
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(
|
|
14
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'overrides', describeOverrides(declaredOverrides.length)), console.warn);
|
|
15
15
|
}
|
|
16
16
|
rejectLegacyOverrides(monorepoRoot);
|
|
17
17
|
}
|
|
@@ -24,13 +24,13 @@ function listEntries(overrides) {
|
|
|
24
24
|
return Object.entries(overrides ?? {}).toSorted(([a], [b]) => a.localeCompare(b));
|
|
25
25
|
}
|
|
26
26
|
function rejectLegacyOverrides(monorepoRoot) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
27
|
+
const legacyOverrides = listEntries(getPnpmOverrides(readPackageJson(monorepoRoot)));
|
|
28
|
+
if (legacyOverrides.length === 0) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
throw new UserError([
|
|
32
32
|
'pnpm 11 reads no `pnpm.overrides` from package.json, so these pin nothing while an upgrade run with `--write` goes on rewriting them:',
|
|
33
|
-
...
|
|
33
|
+
...legacyOverrides.map(([name, version]) => `- ${name} → ${String(version)}`),
|
|
34
34
|
'Move them to the `overrides` block in pnpm-workspace.yaml, quoting each version, or run `pnpx codemod run pnpm-v10-to-v11`.',
|
|
35
35
|
].join('\n'));
|
|
36
36
|
}
|
package/dist/esm/config.js
CHANGED
|
@@ -113,12 +113,12 @@ function validateCheckCacheField(value, configPath) {
|
|
|
113
113
|
}
|
|
114
114
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
115
115
|
const config = {};
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
if (typeof
|
|
116
|
+
const enabledValue = checkCache['enabled'];
|
|
117
|
+
if (enabledValue !== undefined) {
|
|
118
|
+
if (typeof enabledValue !== 'boolean') {
|
|
119
119
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
120
120
|
}
|
|
121
|
-
config.enabled =
|
|
121
|
+
config.enabled = enabledValue;
|
|
122
122
|
}
|
|
123
123
|
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
124
124
|
const commands = checkCache[field];
|
|
@@ -144,22 +144,22 @@ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
|
144
144
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
|
|
145
145
|
'which is never gated on its own: it runs as part of the chain of the command it wraps.');
|
|
146
146
|
}
|
|
147
|
-
const
|
|
147
|
+
const registeredNames = new Set([
|
|
148
148
|
...Object.keys(buildRootRegistry(config)),
|
|
149
149
|
...Object.keys(buildWorkspaceRegistry(config)),
|
|
150
150
|
]);
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
151
|
+
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
152
|
+
if (missingEntries.length === 0) {
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
155
|
+
const declaredNames = readDeclaredScriptNames(baseDir);
|
|
156
|
+
const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
|
|
157
|
+
if (unresolvableEntry === undefined) {
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
|
-
const
|
|
161
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${
|
|
162
|
-
`\`${
|
|
160
|
+
const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
|
|
161
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` names no command: ` +
|
|
162
|
+
`\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
|
|
163
163
|
}
|
|
164
164
|
function readDeclaredScriptNames(monorepoRoot) {
|
|
165
165
|
let packageDirs;
|
|
@@ -203,8 +203,8 @@ function validateOutputField(value, configPath) {
|
|
|
203
203
|
const commandVerbosity = output['commandVerbosity'];
|
|
204
204
|
if (commandVerbosity !== undefined) {
|
|
205
205
|
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
206
|
-
const
|
|
207
|
-
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`',
|
|
206
|
+
const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
207
|
+
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
|
|
208
208
|
}
|
|
209
209
|
config.commandVerbosity = commandVerbosity;
|
|
210
210
|
}
|
|
@@ -258,9 +258,9 @@ export async function loadConfig(baseDir) {
|
|
|
258
258
|
if (!existsSync(configPath)) {
|
|
259
259
|
return {};
|
|
260
260
|
}
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
return validateConfig(
|
|
261
|
+
const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
262
|
+
const loadedConfig = isObject(importedModule) ? importedModule['default'] : undefined;
|
|
263
|
+
return validateConfig(loadedConfig, configPath, baseDir);
|
|
264
264
|
}
|
|
265
265
|
export async function loadRootConfig(monorepoRoot) {
|
|
266
266
|
const config = await loadConfig(monorepoRoot);
|
|
@@ -281,20 +281,20 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
284
|
+
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
285
|
+
if (unrecognizedKeys.length === 0) {
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
|
-
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${
|
|
289
|
-
`${formatKeyList(
|
|
288
|
+
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
|
|
289
|
+
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
290
290
|
}
|
|
291
291
|
function assertTierKeys(config, tier, configPath) {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
292
|
+
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
293
|
+
if (unsupportedKeys.length === 0) {
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
297
|
-
`not ${
|
|
297
|
+
`not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
298
298
|
}
|
|
299
299
|
function formatKeyList(keys, prefix) {
|
|
300
300
|
return keys
|
package/dist/esm/context.js
CHANGED
|
@@ -2,17 +2,17 @@ import path from 'node:path';
|
|
|
2
2
|
import { loadRootConfig } from "./config.js";
|
|
3
3
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
|
|
4
4
|
export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
5
|
-
const
|
|
6
|
-
let
|
|
7
|
-
for (const
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
5
|
+
const resolvedDir = path.resolve(dir);
|
|
6
|
+
let nearestDir;
|
|
7
|
+
for (const packageDir of workspacePackageDirs) {
|
|
8
|
+
const resolvedPackageDir = path.resolve(packageDir);
|
|
9
|
+
if (resolvedDir !== resolvedPackageDir && !resolvedDir.startsWith(resolvedPackageDir + path.sep))
|
|
10
10
|
continue;
|
|
11
|
-
if (
|
|
12
|
-
|
|
11
|
+
if (nearestDir === undefined || resolvedPackageDir.length > nearestDir.length) {
|
|
12
|
+
nearestDir = resolvedPackageDir;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
return
|
|
15
|
+
return nearestDir;
|
|
16
16
|
}
|
|
17
17
|
export async function resolveContext(cwd) {
|
|
18
18
|
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { NmrConfig } from './types.js';
|
|
|
8
8
|
* `package.json:scripts` that match a registry name in the active section
|
|
9
9
|
* are inlined as overrides: the registry value is replaced with the
|
|
10
10
|
* override value and the row's command name is suffixed with `*`. The
|
|
11
|
-
* active section is the root section when `
|
|
11
|
+
* active section is the root section when `shouldUseRoot` is true (root cwd
|
|
12
12
|
* or `-w`), otherwise the workspace section. A footnote is appended once
|
|
13
13
|
* if any override marker was rendered.
|
|
14
14
|
*/
|
|
15
|
-
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined,
|
|
15
|
+
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, shouldUseRoot: boolean): string;
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -6,7 +6,7 @@ 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
12
|
* Creates an accumulator that retains the first and last bytes it is fed and drops the middle.
|
|
@@ -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
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `1m 30s`, where rounding would claim `2m`. Below a minute the tenth is kept, since that is the scale at which
|
|
6
6
|
* one check's runtime differs from another's; a whole number sheds the `.0` rather than reading `12.0s`.
|
|
7
7
|
*/
|
|
8
|
-
export declare function formatDuration(
|
|
8
|
+
export declare function formatDuration(durationMs: number): string;
|
|
9
9
|
/**
|
|
10
10
|
* Renders the clause a skip spends on the time it saved, or `undefined` when there is no saving worth naming.
|
|
11
11
|
* A skip that can say what it saved earns the clause; one that cannot says nothing, and the caller drops it
|
|
@@ -14,4 +14,4 @@ export declare function formatDuration(milliseconds: number): string;
|
|
|
14
14
|
* Guards its own input rather than trusting the caller's, because the clause is shared: a non-finite duration
|
|
15
15
|
* compares false against the threshold, and would reach a reader as `saved ~NaNs`.
|
|
16
16
|
*/
|
|
17
|
-
export declare function formatSaving(
|
|
17
|
+
export declare function formatSaving(durationMs: number): string | undefined;
|