@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.
Files changed (64) hide show
  1. package/.meta/changelog.json +84 -0
  2. package/.readyup/kits/default.js +84 -79
  3. package/.readyup/manifest.json +5 -5
  4. package/CHANGELOG.md +70 -0
  5. package/README.md +37 -11
  6. package/dist/esm/check-cache.d.ts +2 -2
  7. package/dist/esm/check-cache.js +24 -24
  8. package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
  9. package/dist/esm/commands/build-output.d.ts +2 -2
  10. package/dist/esm/commands/build-output.js +9 -9
  11. package/dist/esm/commands/build.js +65 -63
  12. package/dist/esm/commands/clean.js +11 -11
  13. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +2 -2
  14. package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
  15. package/dist/esm/commands/fmt.js +33 -33
  16. package/dist/esm/commands/report-catalog.js +9 -7
  17. package/dist/esm/commands/report-overrides.js +7 -7
  18. package/dist/esm/config.js +25 -25
  19. package/dist/esm/context.js +8 -8
  20. package/dist/esm/help.d.ts +2 -2
  21. package/dist/esm/help.js +21 -21
  22. package/dist/esm/helpers/clampToBytes.js +3 -3
  23. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  24. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  25. package/dist/esm/helpers/duration.d.ts +2 -2
  26. package/dist/esm/helpers/duration.js +7 -7
  27. package/dist/esm/helpers/filter-selection.js +12 -12
  28. package/dist/esm/helpers/findClosestName.js +15 -15
  29. package/dist/esm/helpers/package-json.d.ts +2 -2
  30. package/dist/esm/helpers/package-json.js +26 -25
  31. package/dist/esm/helpers/transcript.d.ts +2 -2
  32. package/dist/esm/helpers/transcript.js +14 -14
  33. package/dist/esm/prettier.js +5 -5
  34. package/dist/esm/recording.d.ts +5 -5
  35. package/dist/esm/recording.js +21 -14
  36. package/dist/esm/report-format.d.ts +1 -1
  37. package/dist/esm/report-format.js +7 -7
  38. package/dist/esm/resolveBinStyles.js +3 -3
  39. package/dist/esm/resolver.d.ts +2 -2
  40. package/dist/esm/resolver.js +15 -15
  41. package/dist/esm/runCli.js +252 -186
  42. package/dist/esm/runner.d.ts +1 -1
  43. package/dist/esm/runner.js +18 -18
  44. package/dist/esm/steps.d.ts +13 -1
  45. package/dist/esm/steps.js +32 -27
  46. package/dist/esm/tests.d.ts +7 -7
  47. package/dist/esm/tests.js +20 -8
  48. package/dist/esm/tiers.d.ts +1 -1
  49. package/dist/esm/tiers.js +8 -8
  50. package/dist/esm/verbosity.d.ts +1 -1
  51. package/dist/esm/verbosity.js +7 -7
  52. package/dist/esm/verdict.d.ts +9 -2
  53. package/dist/esm/verdict.js +63 -50
  54. package/dist/esm/vitest-source-resolution.js +48 -46
  55. package/dist/esm/vitest.d.ts +2 -2
  56. package/dist/esm/vitest.js +74 -25
  57. package/dist/esm/workspace.d.ts +2 -18
  58. package/dist/esm/workspace.js +9 -67
  59. package/docs/reporting.md +6 -2
  60. package/docs/vitest.md +6 -6
  61. package/docs/workspace.md +24 -1
  62. package/package.json +3 -4
  63. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  64. package/dist/esm/helpers/workspace-patterns.js +0 -29
package/README.md CHANGED
@@ -5,24 +5,50 @@
5
5
  Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v0.38.0 (2026-09-18)
8
+ ## Release notes — v0.39.0 (2026-09-22)
9
9
 
10
10
  ### 🎉 Features
11
11
 
12
- - Give nmr plain output in pipes and CI logs (#838)
12
+ - Report why a workspace selects no package, and no-op nmr's own -R steps (#841)
13
13
 
14
- - Stops `nmr` from printing emoji to a pipe or a CI log: When the output stream is not a terminal or `CI` is set, a verdict reads `PASS`, `FAIL`, `SKIP`, or `NOOP` and a command icon is omitted, so that a log can be searched for a verdict.
15
- - Adds `--output-style <auto|plain|rich>` to `nmr` and the `NMR_OUTPUT_STYLE` variable, each forcing either style; the flag outranks the variable, a value that names no style exits 1 with a usage error, and neither enters a check-cache key: A rich run and a plain run share a recorded pass.
16
- - Replaces several rich glyphs, taking the status markers from the set that release-kit and v11y-check print so that one status has one glyph across the three tools: a recalled pass shows ⏩ in place of ⏭️, a skipped empty override shows ⚪ in place of ⛔, a warning and the `--log` refusal show 🟠 in place of ⚠️ and 📭, the `--log` header shows 💾 in place of 📼, and `nmr-ensure-prepublish-hooks` reports ✅ and ❌ in place of ✓ and ✗.
14
+ - Drops a `-R` step that nmr composed into a script where the workspace holds no package, so `typecheck` and the five `test` scripts run there instead of failing the run.
15
+ - Adds a no-op verdict for a command that the drop leaves with no step at all.
16
+ - Replaces the single refusal for a workspace that selects no package with one message per cause.
17
17
 
18
- ### 🪦 Removed
18
+ Migration: In a repo that declares a workspace package pattern and deliberately holds no package, delete the `.config/nmr.config.ts` entries added to work around the `-R` refusal, the `build: ':'` override included.
19
19
 
20
- - 🚨 **Breaking:** Rename the declinesArgs step declaration to shouldDeclineArguments (#822)
20
+ - 🚨 **Breaking:** Prefix the booleans and reject unrecognized Vitest option keys (#849)
21
21
 
22
- - Makes a `.config/nmr.config.ts` that still declares `declinesArgs` on a composite script's `{ run, declinesArgs }` step fail to load, with an error that names `shouldDeclineArguments` as the replacement.
23
- - Renames the matching field of the `StepSpec` type exported by `@williamthorsen/nmr/config` and `@williamthorsen/nmr/scripts`.
22
+ - Renames two boolean options of `defineVitestConfig` and `defineRootVitestConfig`, the factories that build a repo's Vitest config: `isolateGit` becomes `shouldIsolateGit`, and `resolveFromSource` becomes `shouldResolveFromSource`.
23
+ - Makes both factories reject an option key outside the recognized set, naming the offending keys and the recognized ones, rather than ignoring a misspelled or retired key and letting its option fall back to the default with nothing in the run reporting it.
24
+ - Adds a retired-key check that runs ahead of the unrecognized-key check, reporting the new name for a config that still sets `isolateGit` or `resolveFromSource`.
24
25
 
25
- Migration: Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.
26
+ Migration: In each options object passed to `defineVitestConfig` or `defineRootVitestConfig`, rename `isolateGit` to `shouldIsolateGit` and `resolveFromSource` to `shouldResolveFromSource`, and correct any other key that the factory now rejects. `defineRootVitestConfig` recognizes `monorepoRoot` on the last layer alone, so a config that sets it on an earlier layer now throws where the old code ignored it.
27
+
28
+ - 🚨 **Breaking:** End nmr's source value names with the kind of the value (#850)
29
+
30
+ - Renames two fields of `TestFileConventionsReport`, `misplaced` and `untiered`, to `misplacedFiles` and `untieredFiles`, and renames `TestFileConventionsOptions`'s `exclude` to `excludedBasenames`, in the `@williamthorsen/nmr/tests` entry point.
31
+ - Adds a guard to `checkTestFileConventions` and `reportTestFileConventions` that throws a `TypeError` naming `excludedBasenames` when a call still sets `exclude`.
32
+
33
+ Migration: Read `report.misplacedFiles` and `report.untieredFiles` in place of `report.misplaced` and `report.untiered`, and pass `excludedBasenames` in place of `exclude`. A call that still sets `exclude` throws rather than running with no exclusions, so the failure surfaces where Vitest loads the call site.
34
+
35
+ - 🚨 **Breaking:** Hoist the pnpm-workspace resolver into nmr-core as a total API (#864)
36
+
37
+ - Adds a `./workspace` subpath export to `@williamthorsen/nmr-core`, whose `resolveWorkspace` returns a pnpm workspace's package directories, or the cause of an empty result, as a value rather than as a thrown error.
38
+ - Reports a `pnpm-workspace.yaml` that the YAML parser rejects under a cause of its own, `unreadable-manifest`, so `nmr -R build` names the syntax error and its remedy in place of failing with the parser's own error.
39
+ - Removes `diagnoseEmptyWorkspace` and `EmptyWorkspaceDiagnosis` from `@williamthorsen/nmr/workspace`, since `resolveWorkspace` reports the same cause in one call.
40
+
41
+ Migration: Replace each `diagnoseEmptyWorkspace` call with `resolveWorkspace`, which reports the cause on the empty result it already returns, and rename the `no-manifest` cause to `no-package`. `EmptyWorkspaceCause` also gains `unreadable-manifest`, so an exhaustive switch over it stops compiling until it handles the new member; every other export of `@williamthorsen/nmr/workspace` keeps its signature and its `UserError` throws.
42
+
43
+ - 🚨 **Breaking:** Adopt the shared workspace resolver in release-kit, honoring exclusions (#865)
44
+
45
+ - 🚨 **Breaking:** Narrows `no-pattern` in `@williamthorsen/nmr-core`'s `EmptyWorkspaceCause`, moving an absent or empty `packages` list onto `no-packages-list` and a value that is not a list of strings onto `unreadable-packages`.
46
+ - Stops `release-kit` from tagging, publishing, and label-syncing a package that a `!` entry in `pnpm-workspace.yaml` excludes, which its previous `glob`-based resolution could not remove from the match.
47
+ - Replaces the release of the root package alone with a failure naming the condition that emptied the workspace, when `pnpm-workspace.yaml` does not hold valid YAML or declares a workspace that does not resolve to a package.
48
+ - Replaces the `no-pattern` remedy that `nmr` printed for a `pnpm-workspace.yaml` declaring no `packages` list, and for one declaring a `packages` value that is not a list of strings.
49
+ - Raises `engines.node` to `>=24.16.0` in `@williamthorsen/nmr-core` and `@williamthorsen/release-kit`, because Node 24.0 through 24.15 ignores the `followSymlinks` option that the workspace matcher passes to `globSync` and drops a symlinked package directory from the resolution without reporting it.
50
+
51
+ Migration: Add a `no-packages-list` arm and an `unreadable-packages` arm to any `switch` that covers `EmptyWorkspaceCause` exhaustively. `no-pattern` now names only a list that the resolver read in full and whose entries all failed to become a positive pattern, so move the handling of an absent `packages` key, an empty list, and a value that is not a list of strings out of the `no-pattern` arm into the two new ones. Route `no-packages-list` to single-package behavior rather than to a failure path: pnpm resolves such a manifest to the root package alone.
26
52
  <!-- /section:release-notes -->
27
53
 
28
54
  ## Installation
@@ -163,7 +189,7 @@ Every command nmr runs reports one line naming the scope it ran at, the command,
163
189
 
164
190
  **A pipe and a CI log get words rather than emoji.** The same four lines read `PASS`, `FAIL`, `SKIP`, and `NOOP` there, which `grep FAIL` finds. `--output-style` and `NMR_OUTPUT_STYLE` each force either style.
165
191
 
166
- **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
192
+ **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command that ran nothing reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed: an override resolving to `""` or `":"`, and a fan-out step that a package-free workspace left nothing to do. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
167
193
 
168
194
  [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, the output style, and where the verbosity of a run comes from.
169
195
 
@@ -53,7 +53,7 @@ export interface Retention {
53
53
  /** What nmr's own build has left on disk across the workspace. */
54
54
  export interface BuildOutputState {
55
55
  /** Covered packages whose output is absent. */
56
- missing: string[];
56
+ missingPackages: string[];
57
57
  /** The digest of the inputs each covered package's output was built from, keyed by package name. */
58
58
  digests: Record<string, string>;
59
59
  }
@@ -154,7 +154,7 @@ export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
154
154
  * package agrees. A package that has appeared or disappeared between them counts as a disagreement, because
155
155
  * the output the earlier observation describes is not the output the later one found.
156
156
  */
157
- export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
157
+ export declare function findStaleBuildOutput(earlierDigests: Record<string, string>, laterDigests: Record<string, string>): string | undefined;
158
158
  /**
159
159
  * Renders the warning for a `--no-cache` that landed after the command name, where it is an argument to the
160
160
  * command rather than a flag to nmr. Passing it on unchanged is the honest thing to do with an argument, so
@@ -103,9 +103,9 @@ export function computeRetentionKey(options) {
103
103
  export function encodeTreeSnapshot(snapshot) {
104
104
  return `${snapshot.hash} ${snapshot.headSha}`;
105
105
  }
106
- export function findStaleBuildOutput(earlier, later) {
107
- const names = [...new Set([...Object.keys(earlier), ...Object.keys(later)])].toSorted();
108
- return names.find((name) => earlier[name] !== later[name]);
106
+ export function findStaleBuildOutput(earlierDigests, laterDigests) {
107
+ const names = [...new Set([...Object.keys(earlierDigests), ...Object.keys(laterDigests)])].toSorted();
108
+ return names.find((name) => earlierDigests[name] !== laterDigests[name]);
109
109
  }
110
110
  export function formatMisplacedNoCacheWarning(command, style) {
111
111
  return (`${STATUS_GLYPHS[style].warning.text} --no-cache after the command name is passed to \`${command}\`, ` +
@@ -115,7 +115,7 @@ export function isCacheableCommand(checkCache, command) {
115
115
  return !isHookName(command) && checkCache?.enabled !== false && resolveCacheableCommands(checkCache).has(command);
116
116
  }
117
117
  export async function readBuildOutputState(monorepoRoot, config) {
118
- const state = { missing: [], digests: {} };
118
+ const state = { missingPackages: [], digests: {} };
119
119
  let packageDirs;
120
120
  try {
121
121
  packageDirs = getWorkspacePackageDirs(monorepoRoot);
@@ -138,17 +138,17 @@ export async function readBuildOutputState(monorepoRoot, config) {
138
138
  state.digests[name] = (await readBuildDigest(packageDir)) ?? '';
139
139
  }
140
140
  else {
141
- state.missing.push(name);
141
+ state.missingPackages.push(name);
142
142
  }
143
143
  }
144
144
  return state;
145
145
  }
146
146
  export async function readCheckCacheEntry(options) {
147
- const parsed = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
148
- if (parsed === undefined) {
147
+ const parsedEntry = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
148
+ if (parsedEntry === undefined) {
149
149
  return undefined;
150
150
  }
151
- const { retention, ...pass } = parsed;
151
+ const { retention, ...pass } = parsedEntry;
152
152
  return isRetention(retention) ? { ...pass, retention } : pass;
153
153
  }
154
154
  export async function readTranscript(ref) {
@@ -168,23 +168,23 @@ export async function removeCheckCache(scopeDir) {
168
168
  export function resolveCacheableCommands(checkCache) {
169
169
  const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
170
170
  const excludedCommands = checkCache?.excludeCommands ?? [];
171
- for (const excluded of excludedCommands) {
172
- commands.delete(excluded);
171
+ for (const excludedCommand of excludedCommands) {
172
+ commands.delete(excludedCommand);
173
173
  }
174
174
  return commands;
175
175
  }
176
176
  export function resolveRunId(env) {
177
- const inherited = env[RUN_ID_ENV_VAR];
178
- return inherited === undefined || inherited === '' ? randomUUID() : inherited;
177
+ const inheritedRunId = env[RUN_ID_ENV_VAR];
178
+ return inheritedRunId === undefined || inheritedRunId === '' ? randomUUID() : inheritedRunId;
179
179
  }
180
180
  export function resolveTreeSnapshot(options) {
181
- const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
182
- if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
183
- return { ok: true, snapshot: inherited };
181
+ const inheritedSnapshot = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
182
+ if (inheritedSnapshot !== undefined && readHeadSha(options.monorepoRoot) === inheritedSnapshot.headSha) {
183
+ return { ok: true, snapshot: inheritedSnapshot };
184
184
  }
185
- const hashed = hashWorkingTree(options.monorepoRoot);
186
- if (!hashed.ok) {
187
- return hashed;
185
+ const hashedTree = hashWorkingTree(options.monorepoRoot);
186
+ if (!hashedTree.ok) {
187
+ return hashedTree;
188
188
  }
189
189
  let physicalRoot;
190
190
  try {
@@ -193,10 +193,10 @@ export function resolveTreeSnapshot(options) {
193
193
  catch {
194
194
  return { ok: false, reason: `could not resolve ${options.monorepoRoot}` };
195
195
  }
196
- if (hashed.toplevel !== physicalRoot) {
197
- return { ok: false, reason: `the monorepo root is not the git toplevel (${hashed.toplevel})` };
196
+ if (hashedTree.toplevel !== physicalRoot) {
197
+ return { ok: false, reason: `the monorepo root is not the git toplevel (${hashedTree.toplevel})` };
198
198
  }
199
- return { ok: true, snapshot: { hash: hashed.hash, headSha: hashed.headSha } };
199
+ return { ok: true, snapshot: { hash: hashedTree.hash, headSha: hashedTree.headSha } };
200
200
  }
201
201
  export async function writeCheckCacheEntry(options) {
202
202
  await writeCacheEntry(resolveEntryPath(options), JSON.stringify(options.entry, undefined, 2));
@@ -212,11 +212,11 @@ function composeEnvParts(names, env) {
212
212
  return [name, value === undefined ? 'unset' : 'set', value ?? ''];
213
213
  });
214
214
  }
215
- function decodeTreeSnapshot(encoded) {
216
- if (encoded === undefined) {
215
+ function decodeTreeSnapshot(encodedSnapshot) {
216
+ if (encodedSnapshot === undefined) {
217
217
  return undefined;
218
218
  }
219
- const [hash, headSha] = encoded.split(' ', 2);
219
+ const [hash, headSha] = encodedSnapshot.split(' ', 2);
220
220
  if (hash === undefined || headSha === undefined || hash === '' || headSha === '') {
221
221
  return undefined;
222
222
  }
@@ -4,15 +4,15 @@ import { DEFAULT_HOOK, ensurePrepublishHooks, reportPrepublishHooks } from "./co
4
4
  import { resolveBinStyles } from "./resolveBinStyles.js";
5
5
  import { findMonorepoRoot } from "./workspace.js";
6
6
  const flagSchema = {
7
- fix: { long: '--fix', type: 'boolean' },
8
- dryRun: { long: '--dry-run', type: 'boolean' },
7
+ shouldFix: { long: '--fix', type: 'boolean' },
8
+ isDryRun: { long: '--dry-run', type: 'boolean' },
9
9
  command: { long: '--command', type: 'string' },
10
10
  };
11
- const { fix, dryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
11
+ const { shouldFix, isDryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
12
12
  try {
13
13
  const { stdout } = resolveBinStyles();
14
14
  const monorepoRoot = findMonorepoRoot();
15
- const options = command ? { fix, dryRun, command } : { fix, dryRun };
15
+ const options = command ? { shouldFix, isDryRun, command } : { shouldFix, isDryRun };
16
16
  const result = ensurePrepublishHooks(monorepoRoot, options);
17
17
  reportPrepublishHooks(result, command ?? DEFAULT_HOOK, stdout);
18
18
  if (result.hasFailures) {
@@ -8,8 +8,8 @@ export interface BuildOptions {
8
8
  }
9
9
  /** The pair of directories a build publishes through, both siblings of the emit directory. */
10
10
  export interface ScratchDirs {
11
- previous: string;
12
- staging: string;
11
+ previousDir: string;
12
+ stagingDir: string;
13
13
  }
14
14
  export declare const DEFAULT_ENTRY_GLOBS: string[];
15
15
  /**
@@ -17,8 +17,8 @@ export async function hasBuildOutput(packageDir, options = {}) {
17
17
  return hasExpectedBuildOutput(packageDir, outdir, entryPoints);
18
18
  }
19
19
  export function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
20
- const emitsOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
21
- if (!emitsOutput) {
20
+ const doesEmitOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
21
+ if (!doesEmitOutput) {
22
22
  return true;
23
23
  }
24
24
  const outputDir = path.resolve(packageDir, outdir);
@@ -40,8 +40,8 @@ export function resolveScratchDirs(emitDir) {
40
40
  const parent = path.dirname(emitDir);
41
41
  const name = path.basename(emitDir);
42
42
  return {
43
- previous: path.join(parent, `.${name}.previous`),
44
- staging: path.join(parent, `.${name}.staging`),
43
+ previousDir: path.join(parent, `.${name}.previous`),
44
+ stagingDir: path.join(parent, `.${name}.staging`),
45
45
  };
46
46
  }
47
47
  export async function resolveToolchainFingerprint(packageDir, selfDir = SELF_DIR) {
@@ -54,14 +54,14 @@ export async function resolveToolchainFingerprint(packageDir, selfDir = SELF_DIR
54
54
  return readPackageJson(selfDir).version ?? '';
55
55
  }
56
56
  function isSameDir(left, right) {
57
- return toRealPath(left) === toRealPath(right);
57
+ return resolveRealPath(left) === resolveRealPath(right);
58
58
  }
59
- function toRealPath(dir) {
60
- const absolute = path.resolve(dir);
59
+ function resolveRealPath(dir) {
60
+ const absoluteDir = path.resolve(dir);
61
61
  try {
62
- return realpathSync(absolute);
62
+ return realpathSync(absoluteDir);
63
63
  }
64
64
  catch {
65
- return absolute;
65
+ return absoluteDir;
66
66
  }
67
67
  }
@@ -60,18 +60,18 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
60
60
  const resolvedChain = [];
61
61
  const seen = new Set();
62
62
  function walk(configPath) {
63
- const normalized = path.resolve(configPath);
64
- if (seen.has(normalized)) {
63
+ const normalizedPath = path.resolve(configPath);
64
+ if (seen.has(normalizedPath)) {
65
65
  return;
66
66
  }
67
- seen.add(normalized);
68
- resolvedChain.push(normalized);
69
- const configFile = ts.readConfigFile(normalized, (fileName) => ts.sys.readFile(fileName));
67
+ seen.add(normalizedPath);
68
+ resolvedChain.push(normalizedPath);
69
+ const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
70
70
  if (configFile.error) {
71
- throw new Error(`nmr-compile: failed to read ${normalized}.\n${formatDiagnostics([configFile.error])}`);
71
+ throw new Error(`nmr-compile: failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
72
72
  }
73
73
  for (const entry of normalizeExtendsField(configFile.config)) {
74
- walk(resolveExtendsTarget(entry, normalized));
74
+ walk(resolveExtendsTarget(entry, normalizedPath));
75
75
  }
76
76
  }
77
77
  walk(path.resolve(packageDir, configFileName));
@@ -84,26 +84,26 @@ async function emitPackage(packageDir, entryPoints, outdir) {
84
84
  const emitDir = resolveEmitDir(packageDir, outdir);
85
85
  const scratchDirs = resolveScratchDirs(emitDir);
86
86
  await removeScratchDirs(scratchDirs);
87
- const emitted = new Map();
87
+ const emittedFiles = new Map();
88
88
  function collect(fileName, text, writeByteOrderMark) {
89
- emitted.set(fileName, { text, writeByteOrderMark });
89
+ emittedFiles.set(fileName, { text, writeByteOrderMark });
90
90
  }
91
91
  const host = createCachingCompilerHost(compilerOptions);
92
92
  const scriptProgram = ts.createProgram(rootNames, { ...compilerOptions, declaration: false }, host);
93
93
  assertEmitSucceeded(scriptProgram.emit(undefined, collect));
94
94
  const declarationProgram = ts.createProgram(rootNames, { ...compilerOptions, emitDeclarationOnly: true, removeComments: false }, host, scriptProgram);
95
95
  assertEmitSucceeded(declarationProgram.emit(undefined, collect));
96
- const staged = new Map();
97
- for (const [fileName, file] of emitted) {
98
- staged.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
96
+ const stagedFiles = new Map();
97
+ for (const [fileName, file] of emittedFiles) {
98
+ stagedFiles.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
99
99
  }
100
- if (staged.size === 0) {
100
+ if (stagedFiles.size === 0) {
101
101
  await rm(emitDir, { force: true, recursive: true });
102
102
  return 0;
103
103
  }
104
- writeStagedOutput(staged, emitDir, scratchDirs.staging);
104
+ writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
105
105
  await swapIntoPlace(emitDir, scratchDirs);
106
- return staged.size;
106
+ return stagedFiles.size;
107
107
  }
108
108
  function assertEmitSucceeded(result) {
109
109
  if (result.emitSkipped) {
@@ -112,14 +112,14 @@ function assertEmitSucceeded(result) {
112
112
  }
113
113
  function createCachingCompilerHost(compilerOptions) {
114
114
  const host = ts.createCompilerHost(compilerOptions);
115
- const parsed = new Map();
115
+ const parsedFiles = new Map();
116
116
  const readSourceFile = host.getSourceFile.bind(host);
117
117
  host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
118
- if (parsed.has(fileName)) {
119
- return parsed.get(fileName);
118
+ if (parsedFiles.has(fileName)) {
119
+ return parsedFiles.get(fileName);
120
120
  }
121
121
  const sourceFile = readSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
122
- parsed.set(fileName, sourceFile);
122
+ parsedFiles.set(fileName, sourceFile);
123
123
  return sourceFile;
124
124
  };
125
125
  return host;
@@ -132,29 +132,31 @@ async function discardScratchDirs(scratchDirs) {
132
132
  }
133
133
  }
134
134
  async function removeScratchDirs(scratchDirs) {
135
- await rm(scratchDirs.previous, { force: true, recursive: true });
136
- await rm(scratchDirs.staging, { force: true, recursive: true });
135
+ await rm(scratchDirs.previousDir, { force: true, recursive: true });
136
+ await rm(scratchDirs.stagingDir, { force: true, recursive: true });
137
137
  }
138
138
  function resolveEmitDir(packageDir, outdir) {
139
- const resolved = path.resolve(packageDir, outdir);
140
- const relative = path.relative(packageDir, resolved);
141
- if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
139
+ const resolvedDir = path.resolve(packageDir, outdir);
140
+ const relativePathFromPackage = path.relative(packageDir, resolvedDir);
141
+ if (relativePathFromPackage === '' ||
142
+ relativePathFromPackage.startsWith('..') ||
143
+ path.isAbsolute(relativePathFromPackage)) {
142
144
  throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
143
145
  'The build replaces its output directory on each emit, so the directory must sit below the package root.');
144
146
  }
145
- return resolved;
147
+ return resolvedDir;
146
148
  }
147
149
  async function swapIntoPlace(emitDir, scratchDirs) {
148
150
  const hadPreviousOutput = existsSync(emitDir);
149
151
  if (hadPreviousOutput) {
150
- await rename(emitDir, scratchDirs.previous);
152
+ await rename(emitDir, scratchDirs.previousDir);
151
153
  }
152
154
  try {
153
- await rename(scratchDirs.staging, emitDir);
155
+ await rename(scratchDirs.stagingDir, emitDir);
154
156
  }
155
157
  catch (error) {
156
158
  if (hadPreviousOutput && !existsSync(emitDir)) {
157
- await rename(scratchDirs.previous, emitDir);
159
+ await rename(scratchDirs.previousDir, emitDir);
158
160
  }
159
161
  throw error;
160
162
  }
@@ -166,13 +168,13 @@ function synthesizeCompilerOptions(packageDir, outdir) {
166
168
  if (configFile.error) {
167
169
  throw new Error(`nmr-compile: failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
168
170
  }
169
- const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
170
- if (parsed.errors.length > 0) {
171
- throw new Error(`nmr-compile: failed to parse ${configPath}.\n${formatDiagnostics(parsed.errors)}`);
171
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
172
+ if (parsedConfig.errors.length > 0) {
173
+ throw new Error(`nmr-compile: failed to parse ${configPath}.\n${formatDiagnostics(parsedConfig.errors)}`);
172
174
  }
173
175
  const resolvedOutDir = path.resolve(packageDir, outdir);
174
176
  return {
175
- ...parsed.options,
177
+ ...parsedConfig.options,
176
178
  noEmit: false,
177
179
  emitDeclarationOnly: false,
178
180
  declaration: true,
@@ -185,8 +187,8 @@ function synthesizeCompilerOptions(packageDir, outdir) {
185
187
  noEmitOnError: false,
186
188
  };
187
189
  }
188
- function writeStagedOutput(staged, emitDir, stagingDir) {
189
- for (const [fileName, file] of staged) {
190
+ function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
191
+ for (const [fileName, file] of stagedFiles) {
190
192
  if (!isWithin(emitDir, fileName)) {
191
193
  throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
192
194
  "Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
@@ -198,57 +200,57 @@ function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
198
200
  if (!isRewritableOutput(outputFile)) {
199
201
  return text;
200
202
  }
201
- const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, scriptKindFor(outputFile));
203
+ const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, resolveScriptKind(outputFile));
202
204
  const aliasPrefixes = collectAliasPrefixes(compilerOptions);
203
205
  const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
204
206
  const edits = [];
205
- forEachModuleSpecifier(sourceFile, (literal) => {
207
+ visitModuleSpecifiers(sourceFile, (literal) => {
206
208
  const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
207
209
  if (replacement === undefined) {
208
210
  return;
209
211
  }
210
- const start = literal.getStart(sourceFile);
211
- const quote = text[start] ?? '"';
212
- edits.push({ start, end: literal.getEnd(), text: `${quote}${replacement}${quote}` });
212
+ const startOffset = literal.getStart(sourceFile);
213
+ const quote = text[startOffset] ?? '"';
214
+ edits.push({ startOffset, endOffset: literal.getEnd(), text: `${quote}${replacement}${quote}` });
213
215
  });
214
216
  if (edits.length === 0) {
215
217
  return text;
216
218
  }
217
- const orderedEdits = [...edits].sort((a, b) => b.start - a.start);
219
+ const orderedEdits = [...edits].sort((a, b) => b.startOffset - a.startOffset);
218
220
  let updatedText = text;
219
221
  for (const edit of orderedEdits) {
220
- updatedText = updatedText.slice(0, edit.start) + edit.text + updatedText.slice(edit.end);
222
+ updatedText = updatedText.slice(0, edit.startOffset) + edit.text + updatedText.slice(edit.endOffset);
221
223
  }
222
224
  return updatedText;
223
225
  }
224
226
  function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
225
227
  if (isRelativeSpecifier(specifier)) {
226
- const rewritten = swapTypeScriptExtension(specifier);
227
- return rewritten === specifier ? undefined : rewritten;
228
+ const rewrittenSpecifier = swapTypeScriptExtension(specifier);
229
+ return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
228
230
  }
229
231
  if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
230
232
  return undefined;
231
233
  }
232
- const resolved = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
233
- if (!resolved) {
234
+ const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
235
+ if (!resolvedModule) {
234
236
  throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
235
237
  `Verify the tsconfig 'paths' mapping and that the target file exists.`);
236
238
  }
237
- if (!isWithin(sourceRoot, resolved.resolvedFileName)) {
239
+ if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
238
240
  const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
239
- const bareResolved = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
240
- if (bareResolved) {
241
+ const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
242
+ if (bareResolvedModule) {
241
243
  return undefined;
242
244
  }
243
245
  throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
244
- `${resolved.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
246
+ `${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
245
247
  `the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
246
248
  `emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
247
249
  }
248
- const relative = toRelativeSpecifier(path.dirname(sourceContainingFile), resolved.resolvedFileName);
249
- return swapTypeScriptExtension(relative);
250
+ const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
251
+ return swapTypeScriptExtension(relativeSpecifier);
250
252
  }
251
- function forEachModuleSpecifier(sourceFile, visit) {
253
+ function visitModuleSpecifiers(sourceFile, visit) {
252
254
  function walk(node) {
253
255
  const specifier = getModuleSpecifier(node);
254
256
  if (specifier !== undefined) {
@@ -274,12 +276,12 @@ function getModuleSpecifier(node) {
274
276
  }
275
277
  return undefined;
276
278
  }
277
- async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, outputPresent, style) {
279
+ async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
278
280
  const packageName = path.basename(packageDir);
279
281
  const previousHash = await readCacheEntry(cachePath);
280
282
  const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
281
283
  if (previousHash === currentHash) {
282
- if (outputPresent) {
284
+ if (isOutputPresent) {
283
285
  console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build.`);
284
286
  return { changed: false, currentHash };
285
287
  }
@@ -346,12 +348,12 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
346
348
  }
347
349
  throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
348
350
  }
349
- const resolved = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
351
+ const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
350
352
  resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
351
- if (resolved === undefined) {
353
+ if (resolvedPath === undefined) {
352
354
  throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
353
355
  }
354
- return resolved;
356
+ return resolvedPath;
355
357
  }
356
358
  function resolvePackageSpecifier(specifier, fromConfigPath) {
357
359
  const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
@@ -361,8 +363,8 @@ function isRelativeSpecifier(specifier) {
361
363
  return specifier.startsWith('./') || specifier.startsWith('../');
362
364
  }
363
365
  function isWithin(parent, child) {
364
- const relative = path.relative(parent, child);
365
- return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
366
+ const relativePath = path.relative(parent, child);
367
+ return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
366
368
  }
367
369
  function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
368
370
  const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
@@ -370,11 +372,11 @@ function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
370
372
  const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
371
373
  return path.join(sourceRoot, `${withoutExtension}.ts`);
372
374
  }
373
- function toRelativeSpecifier(fromDir, targetFile) {
374
- const relative = path.relative(fromDir, targetFile).split(path.sep).join('/');
375
- return relative.startsWith('.') ? relative : `./${relative}`;
375
+ function buildRelativeSpecifier(fromDir, targetFile) {
376
+ const relativePath = path.relative(fromDir, targetFile).split(path.sep).join('/');
377
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
376
378
  }
377
- function scriptKindFor(file) {
379
+ function resolveScriptKind(file) {
378
380
  return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
379
381
  }
380
382
  function swapTypeScriptExtension(specifier) {
@@ -42,26 +42,26 @@ async function clearCheckCache(scopeDir, style) {
42
42
  await removeCheckCache(scopeDir);
43
43
  console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
44
44
  }
45
- function describeSweep(cleaned, skipped) {
46
- const packages = cleaned === 1 ? '1 package' : `${cleaned} packages`;
47
- const skipClause = skipped === 0 ? '' : `, skipping ${skipped} with an empty clean override`;
45
+ function describeSweep(cleanedCount, skippedCount) {
46
+ const packages = cleanedCount === 1 ? '1 package' : `${cleanedCount} packages`;
47
+ const skipClause = skippedCount === 0 ? '' : `, skipping ${skippedCount} with an empty clean override`;
48
48
  return `Cleaned ${packages}${skipClause}.`;
49
49
  }
50
50
  async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
51
51
  const config = await loadRootConfig(monorepoRoot);
52
52
  const registry = buildWorkspaceRegistry(config);
53
- let cleaned = 0;
54
- let skipped = 0;
53
+ let cleanedCount = 0;
54
+ let skippedCount = 0;
55
55
  for (const packageDir of workspacePackageDirs) {
56
- const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
57
- const resolvedCommand = resolved === undefined ? undefined : renderChain(resolved.steps);
56
+ const resolvedScript = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
57
+ const resolvedCommand = resolvedScript === undefined ? undefined : renderChain(resolvedScript.steps);
58
58
  if (resolvedCommand === undefined || resolvedCommand === '') {
59
- skipped++;
59
+ skippedCount++;
60
60
  continue;
61
61
  }
62
62
  if (resolvedCommand === BUILT_IN_CLEAN) {
63
63
  await cleanPackage(packageDir, style);
64
- cleaned++;
64
+ cleanedCount++;
65
65
  continue;
66
66
  }
67
67
  const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
@@ -71,7 +71,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
71
71
  if (exitCode !== 0) {
72
72
  throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
73
73
  }
74
- cleaned++;
74
+ cleanedCount++;
75
75
  }
76
- reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleaned, skipped)));
76
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleanedCount, skippedCount)));
77
77
  }
@@ -16,8 +16,8 @@ export declare const DEFAULT_HOOK = "npm run build";
16
16
  * have a `prepublishOnly` script.
17
17
  */
18
18
  export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
19
- fix: boolean;
20
- dryRun: boolean;
19
+ shouldFix: boolean;
20
+ isDryRun: boolean;
21
21
  command?: string;
22
22
  }): EnsurePrepublishHooksResult;
23
23
  /**