@williamthorsen/nmr 0.48.0 → 0.49.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 +158 -0
- package/.readyup/kits/default.js +1527 -1460
- package/.readyup/manifest.json +96 -11
- package/CHANGELOG.md +41 -0
- package/README.md +36 -8
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +178 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +9 -3
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +31 -0
- package/dist/esm/remote-probe.js +78 -0
- package/dist/esm/remote-run.d.ts +28 -73
- package/dist/esm/remote-run.js +103 -165
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +39 -1002
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +1 -1
- package/dist/esm/verdict.js +1 -1
- package/docs/check-cache.md +28 -12
- package/docs/delegate.md +96 -16
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +3 -3
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { assertResolvableCheckCacheCommands } from "./config-check-cache.js";
|
|
2
|
+
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
3
|
+
import { findUnexpressibleToken } from "./steps.js";
|
|
4
|
+
import { UserError } from "./UserError.js";
|
|
5
|
+
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
6
|
+
export const CONFIG_TIERS = {
|
|
7
|
+
root: {
|
|
8
|
+
label: 'a monorepo-root config',
|
|
9
|
+
honoredKeys: ['checkCache', 'delegate', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
|
|
10
|
+
elsewhere: "the package's own config",
|
|
11
|
+
},
|
|
12
|
+
workspace: {
|
|
13
|
+
label: 'a package config',
|
|
14
|
+
honoredKeys: ['build'],
|
|
15
|
+
elsewhere: 'the monorepo-root config',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
19
|
+
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
20
|
+
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
21
|
+
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
22
|
+
const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
|
|
23
|
+
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
|
|
24
|
+
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
25
|
+
export function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
26
|
+
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
27
|
+
if (unrecognizedKeys.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
|
|
31
|
+
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
32
|
+
}
|
|
33
|
+
export function validateConfig(value, configPath, baseDir) {
|
|
34
|
+
if (!isObject(value)) {
|
|
35
|
+
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
36
|
+
}
|
|
37
|
+
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
38
|
+
const config = {};
|
|
39
|
+
const build = validateBuildField(value, configPath);
|
|
40
|
+
if (build)
|
|
41
|
+
config.build = build;
|
|
42
|
+
const checkCache = validateCheckCacheField(value, configPath);
|
|
43
|
+
if (checkCache)
|
|
44
|
+
config.checkCache = checkCache;
|
|
45
|
+
if (value['delegate'] !== undefined)
|
|
46
|
+
config.delegate = validateDelegateField(value['delegate'], configPath);
|
|
47
|
+
const devBin = validateStringRecordField(value, 'devBin', configPath);
|
|
48
|
+
if (devBin)
|
|
49
|
+
config.devBin = devBin;
|
|
50
|
+
const output = validateOutputField(value, configPath);
|
|
51
|
+
if (output)
|
|
52
|
+
config.output = output;
|
|
53
|
+
const workspaceScripts = validateScriptField(value, 'workspaceScripts', configPath);
|
|
54
|
+
if (workspaceScripts)
|
|
55
|
+
config.workspaceScripts = workspaceScripts;
|
|
56
|
+
const rootScripts = validateScriptField(value, 'rootScripts', configPath);
|
|
57
|
+
if (rootScripts)
|
|
58
|
+
config.rootScripts = rootScripts;
|
|
59
|
+
const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
|
|
60
|
+
if (selfHostedRunner)
|
|
61
|
+
config.selfHostedRunner = selfHostedRunner;
|
|
62
|
+
assertResolvableCheckCacheCommands(config, configPath, baseDir);
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
66
|
+
for (const key of Object.keys(value)) {
|
|
67
|
+
const replacement = retiredKeys.get(key);
|
|
68
|
+
if (replacement === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function assertValidElements(scripts, fieldName, configPath) {
|
|
74
|
+
for (const [command, script] of Object.entries(scripts)) {
|
|
75
|
+
if (typeof script === 'string')
|
|
76
|
+
continue;
|
|
77
|
+
for (const spec of script) {
|
|
78
|
+
if (typeof spec !== 'string') {
|
|
79
|
+
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
80
|
+
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
81
|
+
}
|
|
82
|
+
const element = typeof spec === 'string' ? spec : spec.run;
|
|
83
|
+
const token = findUnexpressibleToken(element);
|
|
84
|
+
if (token === undefined)
|
|
85
|
+
continue;
|
|
86
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
|
|
87
|
+
`\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
|
|
88
|
+
`reference that name here.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function formatKeyList(keys, prefix) {
|
|
93
|
+
return keys
|
|
94
|
+
.toSorted()
|
|
95
|
+
.map((key) => `\`${prefix}${key}\``)
|
|
96
|
+
.join(', ');
|
|
97
|
+
}
|
|
98
|
+
function isOptionalBoolean(value) {
|
|
99
|
+
return value === undefined || typeof value === 'boolean';
|
|
100
|
+
}
|
|
101
|
+
function isScriptRecord(value) {
|
|
102
|
+
if (!isObject(value))
|
|
103
|
+
return false;
|
|
104
|
+
for (const v of Object.values(value)) {
|
|
105
|
+
if (typeof v === 'string')
|
|
106
|
+
continue;
|
|
107
|
+
if (!Array.isArray(v) || !v.every(isScriptElement))
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function isScriptElement(value) {
|
|
113
|
+
if (typeof value === 'string')
|
|
114
|
+
return true;
|
|
115
|
+
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
116
|
+
return false;
|
|
117
|
+
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
118
|
+
}
|
|
119
|
+
function isStringArray(value) {
|
|
120
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
121
|
+
}
|
|
122
|
+
function validateBuildField(value, configPath) {
|
|
123
|
+
const build = value['build'];
|
|
124
|
+
if (build === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (!isObject(build)) {
|
|
128
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
129
|
+
}
|
|
130
|
+
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
131
|
+
const extraIgnorePatterns = build['extraIgnorePatterns'];
|
|
132
|
+
if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
|
|
133
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
134
|
+
}
|
|
135
|
+
return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
|
|
136
|
+
}
|
|
137
|
+
function validateCheckCacheField(value, configPath) {
|
|
138
|
+
const checkCache = value['checkCache'];
|
|
139
|
+
if (checkCache === undefined) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (!isObject(checkCache)) {
|
|
143
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
144
|
+
}
|
|
145
|
+
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
146
|
+
const config = {};
|
|
147
|
+
const enabledValue = checkCache['enabled'];
|
|
148
|
+
if (enabledValue !== undefined) {
|
|
149
|
+
if (typeof enabledValue !== 'boolean') {
|
|
150
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
151
|
+
}
|
|
152
|
+
config.enabled = enabledValue;
|
|
153
|
+
}
|
|
154
|
+
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
155
|
+
const commands = checkCache[field];
|
|
156
|
+
if (commands === undefined) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!isStringArray(commands)) {
|
|
160
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
|
|
161
|
+
}
|
|
162
|
+
config[field] = commands;
|
|
163
|
+
}
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
function validateDelegateField(delegate, configPath) {
|
|
167
|
+
if (delegate === false) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` accepts only \`false\`, which keeps the repo's checks on the ` +
|
|
171
|
+
'machine that has the tree. Hosts and `when` belong in the user config, `~/.config/nmr/config.ts`.');
|
|
172
|
+
}
|
|
173
|
+
function validateOutputField(value, configPath) {
|
|
174
|
+
const output = value['output'];
|
|
175
|
+
if (output === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!isObject(output)) {
|
|
179
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
180
|
+
}
|
|
181
|
+
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
|
|
182
|
+
const config = {};
|
|
183
|
+
const commandVerbosity = output['commandVerbosity'];
|
|
184
|
+
if (commandVerbosity !== undefined) {
|
|
185
|
+
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
186
|
+
const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
187
|
+
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
|
|
188
|
+
}
|
|
189
|
+
config.commandVerbosity = commandVerbosity;
|
|
190
|
+
}
|
|
191
|
+
const extraAgentEnvVars = output['extraAgentEnvVars'];
|
|
192
|
+
if (extraAgentEnvVars !== undefined) {
|
|
193
|
+
if (!isStringArray(extraAgentEnvVars)) {
|
|
194
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
195
|
+
}
|
|
196
|
+
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
197
|
+
}
|
|
198
|
+
return config;
|
|
199
|
+
}
|
|
200
|
+
function validateSelfHostedRunnerField(value, configPath) {
|
|
201
|
+
const selfHostedRunner = value['selfHostedRunner'];
|
|
202
|
+
if (selfHostedRunner === undefined) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!isObject(selfHostedRunner)) {
|
|
206
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
|
|
207
|
+
}
|
|
208
|
+
assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
|
|
209
|
+
const exempt = selfHostedRunner['exempt'];
|
|
210
|
+
if (typeof exempt !== 'string' || exempt.trim() === '') {
|
|
211
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`);
|
|
212
|
+
}
|
|
213
|
+
return { exempt };
|
|
214
|
+
}
|
|
215
|
+
function validateScriptField(value, fieldName, configPath) {
|
|
216
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const scripts = value[fieldName];
|
|
220
|
+
if (!isScriptRecord(scripts)) {
|
|
221
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
222
|
+
'where an element is a command string or ' +
|
|
223
|
+
'`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
|
|
224
|
+
}
|
|
225
|
+
assertValidElements(scripts, fieldName, configPath);
|
|
226
|
+
return scripts;
|
|
227
|
+
}
|
|
228
|
+
function validateStringRecordField(value, fieldName, configPath) {
|
|
229
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
if (!isStringRecord(value[fieldName])) {
|
|
233
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
|
|
234
|
+
}
|
|
235
|
+
return value[fieldName];
|
|
236
|
+
}
|
package/dist/esm/config.js
CHANGED
|
@@ -8,279 +8,19 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import { CONFIG_TIERS, validateConfig } from "./config-validation.js";
|
|
11
12
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
12
13
|
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
13
|
-
import { findClosestName } from "./helpers/findClosestName.js";
|
|
14
14
|
import { isHookName } from "./helpers/hook-name.js";
|
|
15
|
-
import { isObject
|
|
15
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
16
16
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { describeScript } from "./resolver.js";
|
|
18
|
+
import { readSelfReference, readStepListSelfReference } from "./steps.js";
|
|
19
19
|
import { UserError } from "./UserError.js";
|
|
20
|
-
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
21
|
-
import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
|
|
22
|
-
const CONFIG_TIERS = {
|
|
23
|
-
root: {
|
|
24
|
-
label: 'a monorepo-root config',
|
|
25
|
-
honoredKeys: ['checkCache', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
|
|
26
|
-
elsewhere: "the package's own config",
|
|
27
|
-
},
|
|
28
|
-
workspace: {
|
|
29
|
-
label: 'a package config',
|
|
30
|
-
honoredKeys: ['build'],
|
|
31
|
-
elsewhere: 'the monorepo-root config',
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
35
|
-
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
36
|
-
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
37
|
-
const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
|
|
38
|
-
const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
|
|
39
|
-
const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
|
|
40
20
|
const SCRIPT_FIELDS = [
|
|
41
21
|
{ field: 'rootScripts', scope: 'root' },
|
|
42
22
|
{ field: 'workspaceScripts', scope: 'workspaceRegistry' },
|
|
43
23
|
];
|
|
44
|
-
const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
|
|
45
|
-
function isScriptRecord(value) {
|
|
46
|
-
if (!isObject(value))
|
|
47
|
-
return false;
|
|
48
|
-
for (const v of Object.values(value)) {
|
|
49
|
-
if (typeof v === 'string')
|
|
50
|
-
continue;
|
|
51
|
-
if (!Array.isArray(v) || !v.every(isScriptElement))
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
function isScriptElement(value) {
|
|
57
|
-
if (typeof value === 'string')
|
|
58
|
-
return true;
|
|
59
|
-
if (!isObject(value) || typeof value['run'] !== 'string')
|
|
60
|
-
return false;
|
|
61
|
-
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
62
|
-
}
|
|
63
|
-
function validateScriptField(value, fieldName, configPath) {
|
|
64
|
-
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
const scripts = value[fieldName];
|
|
68
|
-
if (!isScriptRecord(scripts)) {
|
|
69
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
|
|
70
|
-
'where an element is a command string or ' +
|
|
71
|
-
'`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
|
|
72
|
-
}
|
|
73
|
-
assertValidElements(scripts, fieldName, configPath);
|
|
74
|
-
return scripts;
|
|
75
|
-
}
|
|
76
|
-
function assertValidElements(scripts, fieldName, configPath) {
|
|
77
|
-
for (const [command, script] of Object.entries(scripts)) {
|
|
78
|
-
if (typeof script === 'string')
|
|
79
|
-
continue;
|
|
80
|
-
for (const spec of script) {
|
|
81
|
-
if (typeof spec !== 'string') {
|
|
82
|
-
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
83
|
-
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
84
|
-
}
|
|
85
|
-
const element = typeof spec === 'string' ? spec : spec.run;
|
|
86
|
-
const token = findUnexpressibleToken(element);
|
|
87
|
-
if (token === undefined)
|
|
88
|
-
continue;
|
|
89
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
|
|
90
|
-
`\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
|
|
91
|
-
`reference that name here.`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function isOptionalBoolean(value) {
|
|
96
|
-
return value === undefined || typeof value === 'boolean';
|
|
97
|
-
}
|
|
98
|
-
function isStringArray(value) {
|
|
99
|
-
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
100
|
-
}
|
|
101
|
-
function validateBuildField(value, configPath) {
|
|
102
|
-
const build = value['build'];
|
|
103
|
-
if (build === undefined) {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
if (!isObject(build)) {
|
|
107
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
108
|
-
}
|
|
109
|
-
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
110
|
-
const extraIgnorePatterns = build['extraIgnorePatterns'];
|
|
111
|
-
if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
|
|
112
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
113
|
-
}
|
|
114
|
-
return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
|
|
115
|
-
}
|
|
116
|
-
function validateCheckCacheField(value, configPath) {
|
|
117
|
-
const checkCache = value['checkCache'];
|
|
118
|
-
if (checkCache === undefined) {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
if (!isObject(checkCache)) {
|
|
122
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
123
|
-
}
|
|
124
|
-
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
125
|
-
const config = {};
|
|
126
|
-
const enabledValue = checkCache['enabled'];
|
|
127
|
-
if (enabledValue !== undefined) {
|
|
128
|
-
if (typeof enabledValue !== 'boolean') {
|
|
129
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
130
|
-
}
|
|
131
|
-
config.enabled = enabledValue;
|
|
132
|
-
}
|
|
133
|
-
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
134
|
-
const commands = checkCache[field];
|
|
135
|
-
if (commands === undefined) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (!isStringArray(commands)) {
|
|
139
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
|
|
140
|
-
}
|
|
141
|
-
config[field] = commands;
|
|
142
|
-
}
|
|
143
|
-
return config;
|
|
144
|
-
}
|
|
145
|
-
function validateSelfHostedRunnerField(value, configPath) {
|
|
146
|
-
const selfHostedRunner = value['selfHostedRunner'];
|
|
147
|
-
if (selfHostedRunner === undefined) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
if (!isObject(selfHostedRunner)) {
|
|
151
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
|
|
152
|
-
}
|
|
153
|
-
assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
|
|
154
|
-
const exempt = selfHostedRunner['exempt'];
|
|
155
|
-
if (typeof exempt !== 'string' || exempt.trim() === '') {
|
|
156
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`);
|
|
157
|
-
}
|
|
158
|
-
return { exempt };
|
|
159
|
-
}
|
|
160
|
-
function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
161
|
-
const { checkCache } = config;
|
|
162
|
-
if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const fields = ['excludeCommands', 'extraCommands'];
|
|
166
|
-
const entries = fields.flatMap((field) => (checkCache[field] ?? []).map((command) => ({ command, field })));
|
|
167
|
-
const hook = entries.find(({ command }) => isHookName(command));
|
|
168
|
-
if (hook !== undefined) {
|
|
169
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
|
|
170
|
-
'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
|
|
171
|
-
}
|
|
172
|
-
const registeredNames = new Set([
|
|
173
|
-
...Object.keys(buildRootRegistry(config, baseDir)),
|
|
174
|
-
...Object.keys(buildWorkspaceRegistry(config)),
|
|
175
|
-
]);
|
|
176
|
-
const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
|
|
177
|
-
if (missingEntries.length === 0) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const declaredNames = readDeclaredScriptNames(baseDir);
|
|
181
|
-
const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
|
|
182
|
-
if (unresolvableEntry === undefined) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
|
|
186
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
|
|
187
|
-
`\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
|
|
188
|
-
}
|
|
189
|
-
function readDeclaredScriptNames(monorepoRoot) {
|
|
190
|
-
let packageDirs;
|
|
191
|
-
try {
|
|
192
|
-
packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
packageDirs = [];
|
|
196
|
-
}
|
|
197
|
-
const names = new Set();
|
|
198
|
-
const dirs = [monorepoRoot, ...packageDirs];
|
|
199
|
-
for (const dir of dirs) {
|
|
200
|
-
const scripts = readPackageScripts(dir);
|
|
201
|
-
for (const name of Object.keys(scripts)) {
|
|
202
|
-
names.add(name);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return names;
|
|
206
|
-
}
|
|
207
|
-
function readPackageScripts(packageDir) {
|
|
208
|
-
try {
|
|
209
|
-
return readPackageJsonScripts(packageDir) ?? {};
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error instanceof UserError) {
|
|
213
|
-
return {};
|
|
214
|
-
}
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function validateOutputField(value, configPath) {
|
|
219
|
-
const output = value['output'];
|
|
220
|
-
if (output === undefined) {
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
if (!isObject(output)) {
|
|
224
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
225
|
-
}
|
|
226
|
-
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
|
|
227
|
-
const config = {};
|
|
228
|
-
const commandVerbosity = output['commandVerbosity'];
|
|
229
|
-
if (commandVerbosity !== undefined) {
|
|
230
|
-
if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
|
|
231
|
-
const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
232
|
-
throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
|
|
233
|
-
}
|
|
234
|
-
config.commandVerbosity = commandVerbosity;
|
|
235
|
-
}
|
|
236
|
-
const extraAgentEnvVars = output['extraAgentEnvVars'];
|
|
237
|
-
if (extraAgentEnvVars !== undefined) {
|
|
238
|
-
if (!isStringArray(extraAgentEnvVars)) {
|
|
239
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
240
|
-
}
|
|
241
|
-
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
242
|
-
}
|
|
243
|
-
return config;
|
|
244
|
-
}
|
|
245
|
-
function validateStringRecordField(value, fieldName, configPath) {
|
|
246
|
-
if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
|
|
247
|
-
return undefined;
|
|
248
|
-
}
|
|
249
|
-
if (!isStringRecord(value[fieldName])) {
|
|
250
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
|
|
251
|
-
}
|
|
252
|
-
return value[fieldName];
|
|
253
|
-
}
|
|
254
|
-
function validateConfig(value, configPath, baseDir) {
|
|
255
|
-
if (!isObject(value)) {
|
|
256
|
-
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
257
|
-
}
|
|
258
|
-
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
259
|
-
const config = {};
|
|
260
|
-
const build = validateBuildField(value, configPath);
|
|
261
|
-
if (build)
|
|
262
|
-
config.build = build;
|
|
263
|
-
const checkCache = validateCheckCacheField(value, configPath);
|
|
264
|
-
if (checkCache)
|
|
265
|
-
config.checkCache = checkCache;
|
|
266
|
-
const devBin = validateStringRecordField(value, 'devBin', configPath);
|
|
267
|
-
if (devBin)
|
|
268
|
-
config.devBin = devBin;
|
|
269
|
-
const output = validateOutputField(value, configPath);
|
|
270
|
-
if (output)
|
|
271
|
-
config.output = output;
|
|
272
|
-
const workspaceScripts = validateScriptField(value, 'workspaceScripts', configPath);
|
|
273
|
-
if (workspaceScripts)
|
|
274
|
-
config.workspaceScripts = workspaceScripts;
|
|
275
|
-
const rootScripts = validateScriptField(value, 'rootScripts', configPath);
|
|
276
|
-
if (rootScripts)
|
|
277
|
-
config.rootScripts = rootScripts;
|
|
278
|
-
const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
|
|
279
|
-
if (selfHostedRunner)
|
|
280
|
-
config.selfHostedRunner = selfHostedRunner;
|
|
281
|
-
assertResolvableCheckCacheCommands(config, configPath, baseDir);
|
|
282
|
-
return config;
|
|
283
|
-
}
|
|
284
24
|
export async function loadConfig(baseDir) {
|
|
285
25
|
const configPath = resolveConfigPath(baseDir);
|
|
286
26
|
if (!existsSync(configPath)) {
|
|
@@ -302,14 +42,6 @@ export async function loadWorkspaceConfig(packageDir) {
|
|
|
302
42
|
assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
|
|
303
43
|
return config;
|
|
304
44
|
}
|
|
305
|
-
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
306
|
-
for (const key of Object.keys(value)) {
|
|
307
|
-
const replacement = retiredKeys.get(key);
|
|
308
|
-
if (replacement === undefined)
|
|
309
|
-
continue;
|
|
310
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
45
|
function assertNoSelfReferences(config, configPath, monorepoRoot) {
|
|
314
46
|
for (const { field, scope } of SCRIPT_FIELDS) {
|
|
315
47
|
const scripts = config[field] ?? {};
|
|
@@ -334,14 +66,6 @@ function assertNoSelfReferences(config, configPath, monorepoRoot) {
|
|
|
334
66
|
}
|
|
335
67
|
}
|
|
336
68
|
}
|
|
337
|
-
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
338
|
-
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
339
|
-
if (unrecognizedKeys.length === 0) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
|
|
343
|
-
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
344
|
-
}
|
|
345
69
|
function assertTierKeys(config, tier, configPath) {
|
|
346
70
|
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
347
71
|
if (unsupportedKeys.length === 0) {
|
|
@@ -350,12 +74,6 @@ function assertTierKeys(config, tier, configPath) {
|
|
|
350
74
|
throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
351
75
|
`not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
352
76
|
}
|
|
353
|
-
function formatKeyList(keys, prefix) {
|
|
354
|
-
return keys
|
|
355
|
-
.toSorted()
|
|
356
|
-
.map((key) => `\`${prefix}${key}\``)
|
|
357
|
-
.join(', ');
|
|
358
|
-
}
|
|
359
77
|
function formatSelfReferenceRemedy(options) {
|
|
360
78
|
const { isDefault, key, selfReference } = options;
|
|
361
79
|
if (isDefault) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
3
|
+
import type { ScriptRegistry } from './resolve-scripts.js';
|
|
4
|
+
import { type ResolvedScript } from './resolver.js';
|
|
5
|
+
import type { NmrConfig } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Throws a `UserError` for a `package.json` entry that chains steps onto a re-invocation of its own command,
|
|
8
|
+
* which resolution discards, leaving those steps to run nowhere. Accepts every other entry in silence.
|
|
9
|
+
*
|
|
10
|
+
* Runs ahead of resolution, so that a command that the registry does not define reports the declaration that
|
|
11
|
+
* names it rather than the name that resolution could not find. A `--log` rejects none, running nothing whose
|
|
12
|
+
* steps could go missing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertNoSelfReference(options: {
|
|
15
|
+
anchorDir: string;
|
|
16
|
+
command: string;
|
|
17
|
+
isReading: boolean;
|
|
18
|
+
isWorkspaceRoot: boolean;
|
|
19
|
+
monorepoRoot: string;
|
|
20
|
+
registry: ScriptRegistry;
|
|
21
|
+
}): void;
|
|
22
|
+
/**
|
|
23
|
+
* Reports a step that reaches nmr through a shell, when the resolved script contains one.
|
|
24
|
+
*
|
|
25
|
+
* Ahead of the gate, so that a command that usually skips still reports its crossing. A `--log` reports
|
|
26
|
+
* none, running nothing that could cross. Reads the resolved steps rather than the full chain: the line names
|
|
27
|
+
* a declaration to edit, and a hook, a passthrough, and a `devBin` substitution are none.
|
|
28
|
+
*/
|
|
29
|
+
export declare function reportNmrCrossing(options: {
|
|
30
|
+
config: NmrConfig;
|
|
31
|
+
isReading: boolean;
|
|
32
|
+
isWorkspaceRoot: boolean;
|
|
33
|
+
monorepoRoot: string;
|
|
34
|
+
registry: ScriptRegistry;
|
|
35
|
+
resolvedScript: ResolvedScript;
|
|
36
|
+
shouldUseRoot: boolean;
|
|
37
|
+
stderr: Writable;
|
|
38
|
+
style: OutputStyle;
|
|
39
|
+
}): void;
|