@williamthorsen/nmr 0.37.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 (82) hide show
  1. package/.meta/changelog.json +114 -0
  2. package/.readyup/kits/default.js +92 -87
  3. package/.readyup/manifest.json +8 -8
  4. package/CHANGELOG.md +89 -0
  5. package/README.md +37 -28
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache.d.ts +4 -3
  8. package/dist/esm/check-cache.js +28 -28
  9. package/dist/esm/cli-build.js +3 -0
  10. package/dist/esm/cli-clean.js +3 -1
  11. package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
  12. package/dist/esm/cli-report-catalog.js +2 -1
  13. package/dist/esm/cli-report-overrides.js +2 -1
  14. package/dist/esm/commands/build-output.d.ts +2 -2
  15. package/dist/esm/commands/build-output.js +9 -9
  16. package/dist/esm/commands/build.d.ts +6 -1
  17. package/dist/esm/commands/build.js +73 -72
  18. package/dist/esm/commands/clean.d.ts +3 -2
  19. package/dist/esm/commands/clean.js +25 -24
  20. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
  21. package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
  22. package/dist/esm/commands/fmt.js +33 -33
  23. package/dist/esm/commands/report-catalog.d.ts +2 -1
  24. package/dist/esm/commands/report-catalog.js +14 -11
  25. package/dist/esm/commands/report-overrides.d.ts +2 -1
  26. package/dist/esm/commands/report-overrides.js +11 -10
  27. package/dist/esm/config.js +39 -29
  28. package/dist/esm/context.js +8 -8
  29. package/dist/esm/default-scripts.d.ts +2 -2
  30. package/dist/esm/default-scripts.js +5 -5
  31. package/dist/esm/glyphs.d.ts +9 -0
  32. package/dist/esm/glyphs.js +9 -0
  33. package/dist/esm/help.d.ts +2 -2
  34. package/dist/esm/help.js +36 -30
  35. package/dist/esm/helpers/clampToBytes.js +3 -3
  36. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  37. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  38. package/dist/esm/helpers/duration.d.ts +2 -2
  39. package/dist/esm/helpers/duration.js +7 -7
  40. package/dist/esm/helpers/filter-selection.js +12 -12
  41. package/dist/esm/helpers/findClosestName.js +15 -15
  42. package/dist/esm/helpers/package-json.d.ts +2 -2
  43. package/dist/esm/helpers/package-json.js +26 -25
  44. package/dist/esm/helpers/transcript.d.ts +2 -2
  45. package/dist/esm/helpers/transcript.js +14 -14
  46. package/dist/esm/output-style.d.ts +41 -0
  47. package/dist/esm/output-style.js +17 -0
  48. package/dist/esm/prettier.js +5 -5
  49. package/dist/esm/recording.d.ts +8 -5
  50. package/dist/esm/recording.js +27 -17
  51. package/dist/esm/report-format.d.ts +1 -1
  52. package/dist/esm/report-format.js +7 -7
  53. package/dist/esm/resolveBinStyles.d.ts +12 -0
  54. package/dist/esm/resolveBinStyles.js +16 -0
  55. package/dist/esm/resolver.d.ts +2 -2
  56. package/dist/esm/resolver.js +16 -16
  57. package/dist/esm/runCli.js +309 -221
  58. package/dist/esm/runner.d.ts +1 -1
  59. package/dist/esm/runner.js +18 -18
  60. package/dist/esm/steps.d.ts +17 -5
  61. package/dist/esm/steps.js +37 -28
  62. package/dist/esm/tests.d.ts +7 -7
  63. package/dist/esm/tests.js +20 -8
  64. package/dist/esm/tiers.d.ts +1 -1
  65. package/dist/esm/tiers.js +8 -8
  66. package/dist/esm/verbosity.d.ts +1 -1
  67. package/dist/esm/verbosity.js +7 -7
  68. package/dist/esm/verdict.d.ts +15 -4
  69. package/dist/esm/verdict.js +78 -59
  70. package/dist/esm/vitest-source-resolution.js +48 -46
  71. package/dist/esm/vitest.d.ts +2 -2
  72. package/dist/esm/vitest.js +74 -25
  73. package/dist/esm/workspace.d.ts +2 -18
  74. package/dist/esm/workspace.js +9 -67
  75. package/docs/check-cache.md +5 -5
  76. package/docs/reporting.md +20 -2
  77. package/docs/scripts.md +2 -2
  78. package/docs/vitest.md +6 -6
  79. package/docs/workspace.md +24 -1
  80. package/package.json +9 -10
  81. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  82. package/dist/esm/helpers/workspace-patterns.js +0 -29
@@ -2,19 +2,18 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { readFile, rename, rm } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { readCacheEntry, writeCacheEntry } from '@williamthorsen/nmr-core';
5
+ import { formatGlyphLine, readCacheEntry, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
6
  import { glob } from 'glob';
7
7
  import * as ts from 'typescript';
8
+ import { NMR_GLYPHS } from "../glyphs.js";
8
9
  import { resolveConfigPath } from "../helpers/config-path.js";
9
10
  import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, resolveScratchDirs, resolveToolchainFingerprint, } from "./build-output.js";
10
- const PACKAGE_ICON = '📦';
11
- const SKIPPED_ICON = '⏭️';
12
11
  const SOURCE_ROOT = 'src';
13
12
  const MINIMUM_TYPESCRIPT_MAJOR = 5;
14
13
  const MINIMUM_TYPESCRIPT_MINOR = 7;
15
14
  const TS_EXTENSION = '.ts';
16
15
  const JS_EXTENSION = '.js';
17
- export async function buildPackage(packageDir, options = {}) {
16
+ export async function buildPackage(packageDir, options) {
18
17
  assertSupportedTypeScript();
19
18
  const cachePath = resolveBuildCachePath(packageDir);
20
19
  const outdir = options.outdir ?? DEFAULT_OUTDIR;
@@ -34,14 +33,14 @@ export async function buildPackage(packageDir, options = {}) {
34
33
  compilerVersion: ts.version,
35
34
  fingerprint: await resolveToolchainFingerprint(packageDir),
36
35
  };
37
- const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
36
+ const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints), options.style);
38
37
  if (!changed) {
39
38
  await discardScratchDirs(resolveScratchDirs(emitDir));
40
39
  return;
41
40
  }
42
41
  const emittedFileCount = await emitPackage(packageDir, entryPoints, outdir);
43
42
  await writeCacheEntry(cachePath, currentHash);
44
- console.info(`${PACKAGE_ICON} ${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`);
43
+ console.info(formatGlyphLine(NMR_GLYPHS, options.style, 'package', `${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`));
45
44
  }
46
45
  export async function computeBuildHash(packageDir, files, emitConfig, toolchain) {
47
46
  const hash = createHash('sha256');
@@ -61,18 +60,18 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
61
60
  const resolvedChain = [];
62
61
  const seen = new Set();
63
62
  function walk(configPath) {
64
- const normalized = path.resolve(configPath);
65
- if (seen.has(normalized)) {
63
+ const normalizedPath = path.resolve(configPath);
64
+ if (seen.has(normalizedPath)) {
66
65
  return;
67
66
  }
68
- seen.add(normalized);
69
- resolvedChain.push(normalized);
70
- 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));
71
70
  if (configFile.error) {
72
- 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])}`);
73
72
  }
74
73
  for (const entry of normalizeExtendsField(configFile.config)) {
75
- walk(resolveExtendsTarget(entry, normalized));
74
+ walk(resolveExtendsTarget(entry, normalizedPath));
76
75
  }
77
76
  }
78
77
  walk(path.resolve(packageDir, configFileName));
@@ -85,26 +84,26 @@ async function emitPackage(packageDir, entryPoints, outdir) {
85
84
  const emitDir = resolveEmitDir(packageDir, outdir);
86
85
  const scratchDirs = resolveScratchDirs(emitDir);
87
86
  await removeScratchDirs(scratchDirs);
88
- const emitted = new Map();
87
+ const emittedFiles = new Map();
89
88
  function collect(fileName, text, writeByteOrderMark) {
90
- emitted.set(fileName, { text, writeByteOrderMark });
89
+ emittedFiles.set(fileName, { text, writeByteOrderMark });
91
90
  }
92
91
  const host = createCachingCompilerHost(compilerOptions);
93
92
  const scriptProgram = ts.createProgram(rootNames, { ...compilerOptions, declaration: false }, host);
94
93
  assertEmitSucceeded(scriptProgram.emit(undefined, collect));
95
94
  const declarationProgram = ts.createProgram(rootNames, { ...compilerOptions, emitDeclarationOnly: true, removeComments: false }, host, scriptProgram);
96
95
  assertEmitSucceeded(declarationProgram.emit(undefined, collect));
97
- const staged = new Map();
98
- for (const [fileName, file] of emitted) {
99
- 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) });
100
99
  }
101
- if (staged.size === 0) {
100
+ if (stagedFiles.size === 0) {
102
101
  await rm(emitDir, { force: true, recursive: true });
103
102
  return 0;
104
103
  }
105
- writeStagedOutput(staged, emitDir, scratchDirs.staging);
104
+ writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
106
105
  await swapIntoPlace(emitDir, scratchDirs);
107
- return staged.size;
106
+ return stagedFiles.size;
108
107
  }
109
108
  function assertEmitSucceeded(result) {
110
109
  if (result.emitSkipped) {
@@ -113,14 +112,14 @@ function assertEmitSucceeded(result) {
113
112
  }
114
113
  function createCachingCompilerHost(compilerOptions) {
115
114
  const host = ts.createCompilerHost(compilerOptions);
116
- const parsed = new Map();
115
+ const parsedFiles = new Map();
117
116
  const readSourceFile = host.getSourceFile.bind(host);
118
117
  host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
119
- if (parsed.has(fileName)) {
120
- return parsed.get(fileName);
118
+ if (parsedFiles.has(fileName)) {
119
+ return parsedFiles.get(fileName);
121
120
  }
122
121
  const sourceFile = readSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
123
- parsed.set(fileName, sourceFile);
122
+ parsedFiles.set(fileName, sourceFile);
124
123
  return sourceFile;
125
124
  };
126
125
  return host;
@@ -133,29 +132,31 @@ async function discardScratchDirs(scratchDirs) {
133
132
  }
134
133
  }
135
134
  async function removeScratchDirs(scratchDirs) {
136
- await rm(scratchDirs.previous, { force: true, recursive: true });
137
- 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 });
138
137
  }
139
138
  function resolveEmitDir(packageDir, outdir) {
140
- const resolved = path.resolve(packageDir, outdir);
141
- const relative = path.relative(packageDir, resolved);
142
- 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)) {
143
144
  throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
144
145
  'The build replaces its output directory on each emit, so the directory must sit below the package root.');
145
146
  }
146
- return resolved;
147
+ return resolvedDir;
147
148
  }
148
149
  async function swapIntoPlace(emitDir, scratchDirs) {
149
150
  const hadPreviousOutput = existsSync(emitDir);
150
151
  if (hadPreviousOutput) {
151
- await rename(emitDir, scratchDirs.previous);
152
+ await rename(emitDir, scratchDirs.previousDir);
152
153
  }
153
154
  try {
154
- await rename(scratchDirs.staging, emitDir);
155
+ await rename(scratchDirs.stagingDir, emitDir);
155
156
  }
156
157
  catch (error) {
157
158
  if (hadPreviousOutput && !existsSync(emitDir)) {
158
- await rename(scratchDirs.previous, emitDir);
159
+ await rename(scratchDirs.previousDir, emitDir);
159
160
  }
160
161
  throw error;
161
162
  }
@@ -167,13 +168,13 @@ function synthesizeCompilerOptions(packageDir, outdir) {
167
168
  if (configFile.error) {
168
169
  throw new Error(`nmr-compile: failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
169
170
  }
170
- const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
171
- if (parsed.errors.length > 0) {
172
- 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)}`);
173
174
  }
174
175
  const resolvedOutDir = path.resolve(packageDir, outdir);
175
176
  return {
176
- ...parsed.options,
177
+ ...parsedConfig.options,
177
178
  noEmit: false,
178
179
  emitDeclarationOnly: false,
179
180
  declaration: true,
@@ -186,8 +187,8 @@ function synthesizeCompilerOptions(packageDir, outdir) {
186
187
  noEmitOnError: false,
187
188
  };
188
189
  }
189
- function writeStagedOutput(staged, emitDir, stagingDir) {
190
- for (const [fileName, file] of staged) {
190
+ function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
191
+ for (const [fileName, file] of stagedFiles) {
191
192
  if (!isWithin(emitDir, fileName)) {
192
193
  throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
193
194
  "Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
@@ -199,57 +200,57 @@ function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
199
200
  if (!isRewritableOutput(outputFile)) {
200
201
  return text;
201
202
  }
202
- 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));
203
204
  const aliasPrefixes = collectAliasPrefixes(compilerOptions);
204
205
  const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
205
206
  const edits = [];
206
- forEachModuleSpecifier(sourceFile, (literal) => {
207
+ visitModuleSpecifiers(sourceFile, (literal) => {
207
208
  const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
208
209
  if (replacement === undefined) {
209
210
  return;
210
211
  }
211
- const start = literal.getStart(sourceFile);
212
- const quote = text[start] ?? '"';
213
- 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}` });
214
215
  });
215
216
  if (edits.length === 0) {
216
217
  return text;
217
218
  }
218
- const orderedEdits = [...edits].sort((a, b) => b.start - a.start);
219
+ const orderedEdits = [...edits].sort((a, b) => b.startOffset - a.startOffset);
219
220
  let updatedText = text;
220
221
  for (const edit of orderedEdits) {
221
- 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);
222
223
  }
223
224
  return updatedText;
224
225
  }
225
226
  function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
226
227
  if (isRelativeSpecifier(specifier)) {
227
- const rewritten = swapTypeScriptExtension(specifier);
228
- return rewritten === specifier ? undefined : rewritten;
228
+ const rewrittenSpecifier = swapTypeScriptExtension(specifier);
229
+ return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
229
230
  }
230
231
  if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
231
232
  return undefined;
232
233
  }
233
- const resolved = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
234
- if (!resolved) {
234
+ const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
235
+ if (!resolvedModule) {
235
236
  throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
236
237
  `Verify the tsconfig 'paths' mapping and that the target file exists.`);
237
238
  }
238
- if (!isWithin(sourceRoot, resolved.resolvedFileName)) {
239
+ if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
239
240
  const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
240
- const bareResolved = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
241
- if (bareResolved) {
241
+ const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
242
+ if (bareResolvedModule) {
242
243
  return undefined;
243
244
  }
244
245
  throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
245
- `${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 ` +
246
247
  `the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
247
248
  `emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
248
249
  }
249
- const relative = toRelativeSpecifier(path.dirname(sourceContainingFile), resolved.resolvedFileName);
250
- return swapTypeScriptExtension(relative);
250
+ const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
251
+ return swapTypeScriptExtension(relativeSpecifier);
251
252
  }
252
- function forEachModuleSpecifier(sourceFile, visit) {
253
+ function visitModuleSpecifiers(sourceFile, visit) {
253
254
  function walk(node) {
254
255
  const specifier = getModuleSpecifier(node);
255
256
  if (specifier !== undefined) {
@@ -275,19 +276,19 @@ function getModuleSpecifier(node) {
275
276
  }
276
277
  return undefined;
277
278
  }
278
- async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, outputPresent) {
279
+ async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
279
280
  const packageName = path.basename(packageDir);
280
281
  const previousHash = await readCacheEntry(cachePath);
281
282
  const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
282
283
  if (previousHash === currentHash) {
283
- if (outputPresent) {
284
- console.info(`${SKIPPED_ICON} ${packageName}: No changes detected. Skipping build.`);
284
+ if (isOutputPresent) {
285
+ console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build.`);
285
286
  return { changed: false, currentHash };
286
287
  }
287
- console.info(`${PACKAGE_ICON} ${packageName}: Build output is missing. Rebuilding.`);
288
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Build output is missing. Rebuilding.`));
288
289
  return { changed: true, currentHash };
289
290
  }
290
- console.info(`${PACKAGE_ICON} ${packageName}: Changes detected.`);
291
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Changes detected.`));
291
292
  return { changed: true, currentHash };
292
293
  }
293
294
  function assertSupportedTypeScript() {
@@ -347,12 +348,12 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
347
348
  }
348
349
  throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
349
350
  }
350
- const resolved = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
351
+ const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
351
352
  resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
352
- if (resolved === undefined) {
353
+ if (resolvedPath === undefined) {
353
354
  throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
354
355
  }
355
- return resolved;
356
+ return resolvedPath;
356
357
  }
357
358
  function resolvePackageSpecifier(specifier, fromConfigPath) {
358
359
  const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
@@ -362,8 +363,8 @@ function isRelativeSpecifier(specifier) {
362
363
  return specifier.startsWith('./') || specifier.startsWith('../');
363
364
  }
364
365
  function isWithin(parent, child) {
365
- const relative = path.relative(parent, child);
366
- return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
366
+ const relativePath = path.relative(parent, child);
367
+ return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
367
368
  }
368
369
  function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
369
370
  const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
@@ -371,11 +372,11 @@ function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
371
372
  const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
372
373
  return path.join(sourceRoot, `${withoutExtension}.ts`);
373
374
  }
374
- function toRelativeSpecifier(fromDir, targetFile) {
375
- const relative = path.relative(fromDir, targetFile).split(path.sep).join('/');
376
- 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}`;
377
378
  }
378
- function scriptKindFor(file) {
379
+ function resolveScriptKind(file) {
379
380
  return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
380
381
  }
381
382
  function swapTypeScriptExtension(specifier) {
@@ -1,8 +1,9 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Removes a package's build output and the build-cache entry that describes it, so no state survives to
3
4
  * make the next build skip. Removal is idempotent: an already-clean package is a silent no-op.
4
5
  */
5
- export declare function cleanPackage(packageDir: string): Promise<void>;
6
+ export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
6
7
  /**
7
8
  * Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
8
9
  *
@@ -14,4 +15,4 @@ export declare function cleanPackage(packageDir: string): Promise<void>;
14
15
  * a `rimraf` in nmr's dependency tree would be unreachable — while nmr's own bins are linked into the
15
16
  * consumer's `node_modules/.bin`, nmr being a direct dependency.
16
17
  */
17
- export declare function runClean(cwd?: string): Promise<void>;
18
+ export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
@@ -1,66 +1,67 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { formatGlyphLine } from '@williamthorsen/nmr-core';
3
4
  import { removeCheckCache } from "../check-cache.js";
4
5
  import { loadRootConfig } from "../config.js";
5
6
  import { findContainingPackageDir } from "../context.js";
7
+ import { NMR_GLYPHS } from "../glyphs.js";
6
8
  import { reportClosing } from "../helpers/reportClosing.js";
7
9
  import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
8
10
  import { resolveChannel, runCommand } from "../runner.js";
9
11
  import { renderChain } from "../steps.js";
10
12
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
11
13
  import { resolveBuildCachePath } from "./build-output.js";
12
- const CLEAN_ICON = '🧹';
13
14
  const CLEAN_COMMAND = 'clean';
14
15
  const BUILT_IN_CLEAN = 'nmr-clean';
15
16
  const OUTPUT_ROOT = 'dist';
16
- export async function cleanPackage(packageDir) {
17
+ export async function cleanPackage(packageDir, style) {
17
18
  await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
18
19
  await rm(resolveBuildCachePath(packageDir), { force: true });
19
- console.info(`${CLEAN_ICON} ${path.basename(packageDir)}: Removed build output and cache.`);
20
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', `${path.basename(packageDir)}: Removed build output and cache.`));
20
21
  }
21
- export async function runClean(cwd = process.cwd()) {
22
+ export async function runClean(cwd, style) {
22
23
  let monorepoRoot;
23
24
  try {
24
25
  monorepoRoot = findMonorepoRoot(cwd);
25
26
  }
26
27
  catch {
27
- await clearCheckCache(cwd);
28
- await cleanPackage(cwd);
28
+ await clearCheckCache(cwd, style);
29
+ await cleanPackage(cwd, style);
29
30
  return;
30
31
  }
31
- await clearCheckCache(monorepoRoot);
32
+ await clearCheckCache(monorepoRoot, style);
32
33
  const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
33
34
  const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
34
35
  if (packageDir !== undefined) {
35
- await cleanPackage(packageDir);
36
+ await cleanPackage(packageDir, style);
36
37
  return;
37
38
  }
38
- await sweepWorkspace(monorepoRoot, workspacePackageDirs);
39
+ await sweepWorkspace(monorepoRoot, workspacePackageDirs, style);
39
40
  }
40
- async function clearCheckCache(scopeDir) {
41
+ async function clearCheckCache(scopeDir, style) {
41
42
  await removeCheckCache(scopeDir);
42
- console.info(`${CLEAN_ICON} Removed all recorded check results.`);
43
+ console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
43
44
  }
44
- function describeSweep(cleaned, skipped) {
45
- const packages = cleaned === 1 ? '1 package' : `${cleaned} packages`;
46
- 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`;
47
48
  return `Cleaned ${packages}${skipClause}.`;
48
49
  }
49
- async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
50
+ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
50
51
  const config = await loadRootConfig(monorepoRoot);
51
52
  const registry = buildWorkspaceRegistry(config);
52
- let cleaned = 0;
53
- let skipped = 0;
53
+ let cleanedCount = 0;
54
+ let skippedCount = 0;
54
55
  for (const packageDir of workspacePackageDirs) {
55
- const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
56
- 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);
57
58
  if (resolvedCommand === undefined || resolvedCommand === '') {
58
- skipped++;
59
+ skippedCount++;
59
60
  continue;
60
61
  }
61
62
  if (resolvedCommand === BUILT_IN_CLEAN) {
62
- await cleanPackage(packageDir);
63
- cleaned++;
63
+ await cleanPackage(packageDir, style);
64
+ cleanedCount++;
64
65
  continue;
65
66
  }
66
67
  const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
@@ -70,7 +71,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
70
71
  if (exitCode !== 0) {
71
72
  throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
72
73
  }
73
- cleaned++;
74
+ cleanedCount++;
74
75
  }
75
- reportClosing(`${CLEAN_ICON} ${describeSweep(cleaned, skipped)}`);
76
+ reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleanedCount, skippedCount)));
76
77
  }
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  export interface PackageHookStatus {
2
3
  packageName: string;
3
4
  packageDir: string;
@@ -15,15 +16,15 @@ export declare const DEFAULT_HOOK = "npm run build";
15
16
  * have a `prepublishOnly` script.
16
17
  */
17
18
  export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
18
- fix: boolean;
19
- dryRun: boolean;
19
+ shouldFix: boolean;
20
+ isDryRun: boolean;
20
21
  command?: string;
21
22
  }): EnsurePrepublishHooksResult;
22
23
  /**
23
24
  * Reports one line per publishable package and closes with what the run came to. Private packages appear in
24
25
  * neither: they publish nothing, so a `prepublishOnly` is not theirs to carry.
25
26
  *
26
- * Every line goes to stdout, the `✗` included, and the exit code is what carries a failure. A report split
27
+ * Every line goes to stdout, a failure's included, and the exit code is what carries a failure. A report split
27
28
  * across two streams leaves no reader holding the whole list.
28
29
  */
29
- export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string): void;
30
+ export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string, style: OutputStyle): void;
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
3
4
  import { readPackageJson } from "../helpers/package-json.js";
4
5
  import { reportClosing } from "../helpers/reportClosing.js";
5
6
  import { isObject } from "../helpers/type-guards.js";
@@ -10,33 +11,33 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
10
11
  const packageDirs = getWorkspacePackageDirs(monorepoRoot);
11
12
  const packages = [];
12
13
  for (const packageDir of packageDirs) {
13
- const pkg = readPackageJson(packageDir);
14
- const packageName = pkg.name ?? path.basename(packageDir);
15
- const isPrivate = pkg.private === true;
14
+ const packageJson = readPackageJson(packageDir);
15
+ const packageName = packageJson.name ?? path.basename(packageDir);
16
+ const isPrivate = packageJson.private === true;
16
17
  if (isPrivate) {
17
18
  packages.push({
18
19
  packageName,
19
20
  packageDir,
20
21
  isPrivate: true,
21
- prepublishOnly: pkg.scripts?.['prepublishOnly'],
22
+ prepublishOnly: packageJson.scripts?.['prepublishOnly'],
22
23
  action: 'ok',
23
24
  });
24
25
  continue;
25
26
  }
26
- const existing = pkg.scripts?.['prepublishOnly'];
27
- if (existing) {
27
+ const existingHook = packageJson.scripts?.['prepublishOnly'];
28
+ if (existingHook) {
28
29
  packages.push({
29
30
  packageName,
30
31
  packageDir,
31
32
  isPrivate: false,
32
- prepublishOnly: existing,
33
+ prepublishOnly: existingHook,
33
34
  action: 'ok',
34
35
  });
35
36
  continue;
36
37
  }
37
- if (options.fix) {
38
- const action = options.dryRun ? 'would-fix' : 'fixed';
39
- if (!options.dryRun) {
38
+ if (options.shouldFix) {
39
+ const action = options.isDryRun ? 'would-fix' : 'fixed';
40
+ if (!options.isDryRun) {
40
41
  addPrepublishOnly(packageDir, hookCommand);
41
42
  }
42
43
  packages.push({
@@ -60,57 +61,58 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
60
61
  const hasFailures = packages.some((p) => p.action === 'missing');
61
62
  return { packages, hasFailures };
62
63
  }
63
- export function reportPrepublishHooks(result, hookCommand) {
64
- const publishable = result.packages.filter((pkg) => !pkg.isPrivate);
65
- if (publishable.length === 0) {
64
+ export function reportPrepublishHooks(result, hookCommand, style) {
65
+ const publishablePackages = result.packages.filter((packageStatus) => !packageStatus.isPrivate);
66
+ if (publishablePackages.length === 0) {
66
67
  console.info('No publishable packages found.');
67
68
  return;
68
69
  }
69
- for (const pkg of publishable) {
70
- console.info(renderHookStatus(pkg, hookCommand));
70
+ for (const packageStatus of publishablePackages) {
71
+ console.info(renderHookStatus(packageStatus, hookCommand, style));
71
72
  }
72
- reportClosing(describeHookRun(publishable));
73
+ reportClosing(describeHookRun(publishablePackages));
73
74
  }
74
75
  function addPrepublishOnly(packageDir, command) {
75
76
  const filePath = path.join(packageDir, 'package.json');
76
- const raw = readFileSync(filePath, 'utf8');
77
- const parsed = JSON.parse(raw);
78
- if (!isObject(parsed)) {
77
+ const rawText = readFileSync(filePath, 'utf8');
78
+ const parsedManifest = JSON.parse(rawText);
79
+ if (!isObject(parsedManifest)) {
79
80
  throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
80
81
  }
81
- const scripts = isObject(parsed['scripts']) ? parsed['scripts'] : {};
82
+ const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
82
83
  scripts['prepublishOnly'] = command;
83
- parsed['scripts'] = scripts;
84
- writeFileSync(filePath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
84
+ parsedManifest['scripts'] = scripts;
85
+ writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
85
86
  }
86
- function countAction(publishable, action) {
87
- return publishable.filter((pkg) => pkg.action === action).length;
87
+ function countAction(publishablePackages, action) {
88
+ return publishablePackages.filter((packageStatus) => packageStatus.action === action).length;
88
89
  }
89
- function describeHookRun(publishable) {
90
- const packages = publishable.length === 1 ? '1 publishable package' : `${publishable.length} publishable packages`;
91
- const missing = countAction(publishable, 'missing');
92
- if (missing > 0) {
93
- 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.`;
94
95
  }
95
- const fixed = countAction(publishable, 'fixed');
96
- if (fixed > 0) {
97
- 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}.`;
98
99
  }
99
- const wouldFix = countAction(publishable, 'would-fix');
100
- if (wouldFix > 0) {
101
- 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}.`;
102
103
  }
103
- return `${packages} ${publishable.length === 1 ? 'has' : 'have'} prepublishOnly.`;
104
+ return `${packages} ${publishablePackages.length === 1 ? 'has' : 'have'} prepublishOnly.`;
104
105
  }
105
- function renderHookStatus(pkg, hookCommand) {
106
- switch (pkg.action) {
106
+ function renderHookStatus(packageStatus, hookCommand, style) {
107
+ const statuses = STATUS_GLYPHS[style];
108
+ switch (packageStatus.action) {
107
109
  case 'ok':
108
- return `✓ ${pkg.packageName}: prepublishOnly = "${pkg.prepublishOnly}"`;
110
+ return `${statuses.passed.text} ${packageStatus.packageName}: prepublishOnly = "${packageStatus.prepublishOnly}"`;
109
111
  case 'missing':
110
- return `✗ ${pkg.packageName}: missing prepublishOnly`;
112
+ return `${statuses.failed.text} ${packageStatus.packageName}: missing prepublishOnly`;
111
113
  case 'fixed':
112
- return `✓ ${pkg.packageName}: added prepublishOnly = "${hookCommand}"`;
114
+ return `${statuses.passed.text} ${packageStatus.packageName}: added prepublishOnly = "${hookCommand}"`;
113
115
  case 'would-fix':
114
- return `~ ${pkg.packageName}: would add prepublishOnly = "${hookCommand}"`;
116
+ return `~ ${packageStatus.packageName}: would add prepublishOnly = "${hookCommand}"`;
115
117
  }
116
118
  }