@williamthorsen/nmr 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.meta/changelog.json +212 -4
  2. package/.readyup/kits/default.js +124 -104
  3. package/.readyup/manifest.json +12 -7
  4. package/CHANGELOG.md +108 -0
  5. package/README.md +41 -47
  6. package/agents/README.md +8 -8
  7. package/agents/guidance/rulebooks/nmr.md +8 -8
  8. package/bin/ensure-prepublish-hooks.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-taze.js +1 -1
  15. package/bin/nmr.js +1 -1
  16. package/dist/esm/UserError.d.ts +1 -2
  17. package/dist/esm/check-cache.d.ts +61 -63
  18. package/dist/esm/check-cache.js +24 -24
  19. package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
  20. package/dist/esm/commands/build-output.d.ts +37 -38
  21. package/dist/esm/commands/build-output.js +9 -9
  22. package/dist/esm/commands/build.d.ts +13 -14
  23. package/dist/esm/commands/build.js +67 -65
  24. package/dist/esm/commands/clean.d.ts +6 -6
  25. package/dist/esm/commands/clean.js +11 -11
  26. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
  27. package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
  28. package/dist/esm/commands/fmt.d.ts +8 -8
  29. package/dist/esm/commands/fmt.js +33 -33
  30. package/dist/esm/commands/report-catalog.d.ts +5 -7
  31. package/dist/esm/commands/report-catalog.js +10 -8
  32. package/dist/esm/commands/report-overrides.d.ts +0 -2
  33. package/dist/esm/commands/report-overrides.js +8 -8
  34. package/dist/esm/commands/taze.d.ts +9 -8
  35. package/dist/esm/config.d.ts +5 -5
  36. package/dist/esm/config.js +27 -27
  37. package/dist/esm/context.d.ts +1 -2
  38. package/dist/esm/context.js +8 -8
  39. package/dist/esm/default-scripts.d.ts +5 -6
  40. package/dist/esm/defineConfig.d.ts +1 -1
  41. package/dist/esm/git-ignored-paths.d.ts +9 -0
  42. package/dist/esm/git-ignored-paths.js +10 -0
  43. package/dist/esm/glyphs.d.ts +3 -3
  44. package/dist/esm/help.d.ts +4 -4
  45. package/dist/esm/help.js +21 -21
  46. package/dist/esm/helpers/clampToBytes.d.ts +4 -5
  47. package/dist/esm/helpers/clampToBytes.js +3 -3
  48. package/dist/esm/helpers/config-path.d.ts +0 -2
  49. package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
  50. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  51. package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
  52. package/dist/esm/helpers/duration.d.ts +10 -11
  53. package/dist/esm/helpers/duration.js +7 -7
  54. package/dist/esm/helpers/filter-selection.d.ts +19 -19
  55. package/dist/esm/helpers/filter-selection.js +12 -12
  56. package/dist/esm/helpers/findClosestName.d.ts +1 -1
  57. package/dist/esm/helpers/findClosestName.js +15 -15
  58. package/dist/esm/helpers/hook-name.d.ts +2 -5
  59. package/dist/esm/helpers/package-json.d.ts +11 -10
  60. package/dist/esm/helpers/package-json.js +26 -25
  61. package/dist/esm/helpers/readStringValues.d.ts +2 -3
  62. package/dist/esm/helpers/reportClosing.d.ts +8 -9
  63. package/dist/esm/helpers/transcript.d.ts +15 -15
  64. package/dist/esm/helpers/transcript.js +14 -14
  65. package/dist/esm/helpers/type-guards.d.ts +1 -0
  66. package/dist/esm/output-style.d.ts +12 -13
  67. package/dist/esm/prettier.d.ts +2 -2
  68. package/dist/esm/prettier.js +5 -5
  69. package/dist/esm/recording.d.ts +25 -20
  70. package/dist/esm/recording.js +28 -21
  71. package/dist/esm/replay-assembly.d.ts +10 -10
  72. package/dist/esm/report-format.d.ts +15 -20
  73. package/dist/esm/report-format.js +7 -7
  74. package/dist/esm/reportCliFailure.d.ts +2 -3
  75. package/dist/esm/resolve-scripts.d.ts +4 -5
  76. package/dist/esm/resolveBinStyles.d.ts +4 -5
  77. package/dist/esm/resolveBinStyles.js +3 -3
  78. package/dist/esm/resolver.d.ts +16 -20
  79. package/dist/esm/resolver.js +15 -15
  80. package/dist/esm/runCli.d.ts +6 -6
  81. package/dist/esm/runCli.js +261 -195
  82. package/dist/esm/runner.d.ts +20 -20
  83. package/dist/esm/runner.js +18 -18
  84. package/dist/esm/steps.d.ts +51 -38
  85. package/dist/esm/steps.js +32 -27
  86. package/dist/esm/taze.d.ts +1 -1
  87. package/dist/esm/tests.d.ts +23 -18
  88. package/dist/esm/tests.js +21 -9
  89. package/dist/esm/tiers.d.ts +28 -24
  90. package/dist/esm/tiers.js +13 -9
  91. package/dist/esm/types.d.ts +5 -5
  92. package/dist/esm/verbosity.d.ts +23 -21
  93. package/dist/esm/verbosity.js +7 -7
  94. package/dist/esm/verdict.d.ts +39 -32
  95. package/dist/esm/verdict.js +63 -50
  96. package/dist/esm/vitest-source-resolution.d.ts +11 -11
  97. package/dist/esm/vitest-source-resolution.js +49 -47
  98. package/dist/esm/vitest.d.ts +27 -26
  99. package/dist/esm/vitest.js +90 -33
  100. package/dist/esm/workspace.d.ts +6 -22
  101. package/dist/esm/workspace.js +9 -67
  102. package/docs/check-cache.md +57 -57
  103. package/docs/prettier.md +10 -10
  104. package/docs/reporting.md +48 -44
  105. package/docs/scripts.md +29 -29
  106. package/docs/upgrades.md +18 -18
  107. package/docs/utilities.md +13 -13
  108. package/docs/vitest.md +68 -66
  109. package/docs/workspace.md +27 -4
  110. package/package.json +6 -6
  111. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  112. package/dist/esm/helpers/workspace-patterns.js +0 -29
@@ -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
- 'The build replaces its output directory on each emit, so the directory must sit below the package root.');
145
+ 'The build replaces its output directory on each emit, so the directory must be 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.toSorted((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
  }
@@ -307,7 +309,7 @@ function collectAliasPrefixes(compilerOptions) {
307
309
  }
308
310
  function describeEmit(fileCount, outdir) {
309
311
  if (fileCount === 0) {
310
- return 'Emitted no output.';
312
+ return 'Did not emit any output.';
311
313
  }
312
314
  return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
313
315
  }
@@ -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) {
@@ -1,7 +1,8 @@
1
1
  import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  /**
3
- * Removes a package's build output and the build-cache entry that describes it, so no state survives to
4
- * make the next build skip. Removal is idempotent: an already-clean package is a silent no-op.
3
+ * Removes a package's build output and the build-cache entry that describes it, so the package does not keep
4
+ * any state that would make the next build skip. Removal is idempotent: An already-clean package is a silent
5
+ * no-op.
5
6
  */
6
7
  export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
7
8
  /**
@@ -10,9 +11,8 @@ export declare function cleanPackage(packageDir: string, style: OutputStyle): Pr
10
11
  * From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to this bin
11
12
  * before invoking it, so resolving again would apply the package's own override twice.
12
13
  *
13
- * This is the default `clean` script, shipped as a bin rather than delegating to `rimraf`. Under pnpm's
14
- * isolated layout, `pnpm exec` resolves only bins of the consuming project's own direct dependencies, so
15
- * a `rimraf` in nmr's dependency tree would be unreachable — while nmr's own bins are linked into the
16
- * consumer's `node_modules/.bin`, nmr being a direct dependency.
14
+ * This is the default `clean` script, and it is published as a bin because, under pnpm's isolated layout,
15
+ * `pnpm exec` resolves only the bins of the consuming project's direct dependencies: pnpm links nmr's own bins
16
+ * into the consumer's `node_modules/.bin`, but not the bins of nmr's dependencies.
17
17
  */
18
18
  export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
@@ -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
  }
@@ -12,19 +12,19 @@ export interface EnsurePrepublishHooksResult {
12
12
  }
13
13
  export declare const DEFAULT_HOOK = "npm run build";
14
14
  /**
15
- * Check (and optionally fix) whether all publishable workspace packages
15
+ * Checks (and optionally fixes) whether all publishable workspace packages
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
  /**
24
- * Reports one line per publishable package and closes with what the run came to. Private packages appear in
25
- * neither: they publish nothing, so a `prepublishOnly` is not theirs to carry.
24
+ * Reports one line per publishable package and closes with a summary of the run. Private packages appear in
25
+ * neither: They publish nothing, so they do not need a `prepublishOnly`.
26
26
  *
27
- * Every line goes to stdout, a failure's included, and the exit code is what carries a failure. A report split
28
- * across two streams leaves no reader holding the whole list.
27
+ * Every line goes to stdout, a failure's included, and the exit code signals a failure. When a report is split
28
+ * across two streams, a reader of either stream does not see the whole list.
29
29
  */
30
30
  export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string, style: OutputStyle): void;
@@ -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 pkg = readPackageJson(packageDir);
15
- const packageName = pkg.name ?? path.basename(packageDir);
16
- const isPrivate = pkg.private === true;
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: pkg.scripts?.['prepublishOnly'],
22
+ prepublishOnly: packageJson.scripts?.['prepublishOnly'],
23
23
  action: 'ok',
24
24
  });
25
25
  continue;
26
26
  }
27
- const existing = pkg.scripts?.['prepublishOnly'];
28
- if (existing) {
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: existing,
33
+ prepublishOnly: existingHook,
34
34
  action: 'ok',
35
35
  });
36
36
  continue;
37
37
  }
38
- if (options.fix) {
39
- const action = options.dryRun ? 'would-fix' : 'fixed';
40
- if (!options.dryRun) {
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 publishable = result.packages.filter((pkg) => !pkg.isPrivate);
66
- if (publishable.length === 0) {
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 pkg of publishable) {
71
- console.info(renderHookStatus(pkg, hookCommand, style));
70
+ for (const packageStatus of publishablePackages) {
71
+ console.info(renderHookStatus(packageStatus, hookCommand, style));
72
72
  }
73
- reportClosing(describeHookRun(publishable));
73
+ reportClosing(describeHookRun(publishablePackages));
74
74
  }
75
75
  function addPrepublishOnly(packageDir, command) {
76
76
  const filePath = path.join(packageDir, 'package.json');
77
- const raw = readFileSync(filePath, 'utf8');
78
- const parsed = JSON.parse(raw);
79
- if (!isObject(parsed)) {
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(parsed['scripts']) ? parsed['scripts'] : {};
82
+ const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
83
83
  scripts['prepublishOnly'] = command;
84
- parsed['scripts'] = scripts;
85
- writeFileSync(filePath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
84
+ parsedManifest['scripts'] = scripts;
85
+ writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
86
86
  }
87
- function countAction(publishable, action) {
88
- return publishable.filter((pkg) => pkg.action === action).length;
87
+ function countAction(publishablePackages, action) {
88
+ return publishablePackages.filter((packageStatus) => packageStatus.action === action).length;
89
89
  }
90
- function describeHookRun(publishable) {
91
- const packages = publishable.length === 1 ? '1 publishable package' : `${publishable.length} publishable packages`;
92
- const missing = countAction(publishable, 'missing');
93
- if (missing > 0) {
94
- return `${missing} of ${packages} ${missing === 1 ? 'is' : 'are'} missing prepublishOnly. Run with --fix to add it.`;
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 fixed = countAction(publishable, 'fixed');
97
- if (fixed > 0) {
98
- return `Added prepublishOnly to ${fixed} of ${packages}.`;
96
+ const fixedCount = countAction(publishablePackages, 'fixed');
97
+ if (fixedCount > 0) {
98
+ return `Added prepublishOnly to ${fixedCount} of ${packages}.`;
99
99
  }
100
- const wouldFix = countAction(publishable, 'would-fix');
101
- if (wouldFix > 0) {
102
- return `Would add prepublishOnly to ${wouldFix} of ${packages}.`;
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} ${publishable.length === 1 ? 'has' : 'have'} prepublishOnly.`;
104
+ return `${packages} ${publishablePackages.length === 1 ? 'has' : 'have'} prepublishOnly.`;
105
105
  }
106
- function renderHookStatus(pkg, hookCommand, style) {
106
+ function renderHookStatus(packageStatus, hookCommand, style) {
107
107
  const statuses = STATUS_GLYPHS[style];
108
- switch (pkg.action) {
108
+ switch (packageStatus.action) {
109
109
  case 'ok':
110
- return `${statuses.passed.text} ${pkg.packageName}: prepublishOnly = "${pkg.prepublishOnly}"`;
110
+ return `${statuses.passed.text} ${packageStatus.packageName}: prepublishOnly = "${packageStatus.prepublishOnly}"`;
111
111
  case 'missing':
112
- return `${statuses.failed.text} ${pkg.packageName}: missing prepublishOnly`;
112
+ return `${statuses.failed.text} ${packageStatus.packageName}: missing prepublishOnly`;
113
113
  case 'fixed':
114
- return `${statuses.passed.text} ${pkg.packageName}: added prepublishOnly = "${hookCommand}"`;
114
+ return `${statuses.passed.text} ${packageStatus.packageName}: added prepublishOnly = "${hookCommand}"`;
115
115
  case 'would-fix':
116
- return `~ ${pkg.packageName}: would add prepublishOnly = "${hookCommand}"`;
116
+ return `~ ${packageStatus.packageName}: would add prepublishOnly = "${hookCommand}"`;
117
117
  }
118
118
  }
@@ -13,7 +13,7 @@ export type ResolveTargetsResult = {
13
13
  error: string;
14
14
  };
15
15
  /**
16
- * Formats or checks the files git reports for `cwd`, and returns the exit code to report.
16
+ * Formats or checks the files that git reports for `cwd`, and returns the exit code to report.
17
17
  *
18
18
  * Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
19
19
  * than passed along, since git would read it as a pathspec matching nothing and the run would report
@@ -26,11 +26,11 @@ export declare function runFmt(argv: string[], cwd?: string): number;
26
26
  * pattern in `packages/<pkg>/.gitignore` is invisible to a root-level run; git knows the whole hierarchy,
27
27
  * along with `.git/info/exclude` and `core.excludesFile`, which Prettier cannot read under any flag.
28
28
  *
29
- * File selection is anchored at `cwd` and ignore discovery at the repository root. That split is the point:
30
- * scoping still follows where the caller stands, while the rules applied to a given file no longer do.
29
+ * Because file selection is anchored at `cwd` and ignore discovery at the repository root, scoping follows
30
+ * `cwd` while the ignore rules applied to a given file are the same from every directory.
31
31
  *
32
- * `pathspecs` narrow the selection and are passed to git verbatim, so they carry git pathspec semantics
33
- * rather than shell glob semantics.
32
+ * `pathspecs` narrow the selection and are passed to git verbatim: They have git pathspec semantics rather
33
+ * than shell glob semantics.
34
34
  */
35
35
  export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
36
36
  /** @internal */
@@ -40,15 +40,15 @@ export interface RunPrettierOptions {
40
40
  files: string[];
41
41
  ignorePaths: string[];
42
42
  cwd: string;
43
- /** Overridable so the multi-batch path is reachable without a fixture that hits the real ceiling. */
43
+ /** Overridable so that the multi-batch path is reachable without a fixture that hits the real ceiling. */
44
44
  budgetBytes?: number;
45
45
  }
46
46
  /**
47
47
  * Runs Prettier over the selection, returning the first non-zero exit code. Every batch runs even after
48
48
  * one fails, so a check reports every offending file rather than only those in the first batch.
49
49
  *
50
- * The CLI runs under this process's own Node rather than through its shebang, so it does not depend on
51
- * the file's execute bit or on PATH holding a `node`.
50
+ * Because the CLI runs under this process's own Node rather than through its shebang, it does not depend
51
+ * on the file's execute bit or on PATH containing a `node`.
52
52
  *
53
53
  * @internal
54
54
  */