@williamthorsen/nmr 2.0.0 → 2.0.2

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 (113) hide show
  1. package/.meta/changelog.json +164 -0
  2. package/.readyup/kits/default.js +51 -15
  3. package/.readyup/manifest.json +26 -21
  4. package/CHANGELOG.md +45 -0
  5. package/README.md +10 -4
  6. package/dist/esm/{check-cache-build-output.d.ts → check-cache/check-cache-build-output.d.ts} +1 -1
  7. package/dist/esm/{check-cache-build-output.js → check-cache/check-cache-build-output.js} +6 -6
  8. package/dist/esm/{check-cache-delegation.d.ts → check-cache/check-cache-delegation.d.ts} +3 -3
  9. package/dist/esm/{check-cache-delegation.js → check-cache/check-cache-delegation.js} +2 -2
  10. package/dist/esm/{check-cache-entries.js → check-cache/check-cache-entries.js} +1 -1
  11. package/dist/esm/{check-cache-gate.d.ts → check-cache/check-cache-gate.d.ts} +5 -4
  12. package/dist/esm/{check-cache-gate.js → check-cache/check-cache-gate.js} +10 -10
  13. package/dist/esm/{check-cache-key.d.ts → check-cache/check-cache-key.d.ts} +1 -1
  14. package/dist/esm/{check-cache-record.d.ts → check-cache/check-cache-record.d.ts} +6 -5
  15. package/dist/esm/{check-cache-record.js → check-cache/check-cache-record.js} +7 -6
  16. package/dist/esm/{check-cache.d.ts → check-cache/check-cache.d.ts} +10 -2
  17. package/dist/esm/{check-cache.js → check-cache/check-cache.js} +7 -4
  18. package/dist/esm/{config-check-cache.d.ts → check-cache/config-check-cache.d.ts} +1 -1
  19. package/dist/esm/{config-check-cache.js → check-cache/config-check-cache.js} +4 -4
  20. package/dist/esm/{pass-manifest.d.ts → check-cache/pass-manifest.d.ts} +1 -1
  21. package/dist/esm/{pass-manifest.js → check-cache/pass-manifest.js} +1 -1
  22. package/dist/esm/{recording.js → check-cache/recording.js} +2 -2
  23. package/dist/esm/{replay-assembly.d.ts → check-cache/replay-assembly.d.ts} +1 -1
  24. package/dist/esm/{replay-assembly.js → check-cache/replay-assembly.js} +2 -2
  25. package/dist/esm/child-env.d.ts +1 -1
  26. package/dist/esm/child-env.js +2 -2
  27. package/dist/esm/cli-build.js +3 -10
  28. package/dist/esm/cli-clean.js +1 -2
  29. package/dist/esm/cli-report-catalog.js +2 -3
  30. package/dist/esm/cli-report-overrides.js +3 -5
  31. package/dist/esm/commands/actionlint.d.ts +2 -7
  32. package/dist/esm/commands/actionlint.js +5 -11
  33. package/dist/esm/commands/build-output.d.ts +7 -32
  34. package/dist/esm/commands/build-output.js +8 -13
  35. package/dist/esm/commands/build-report.d.ts +6 -0
  36. package/dist/esm/commands/build-report.js +21 -0
  37. package/dist/esm/commands/build.d.ts +27 -4
  38. package/dist/esm/commands/build.js +24 -86
  39. package/dist/esm/commands/clean-report.d.ts +6 -0
  40. package/dist/esm/commands/clean-report.js +20 -0
  41. package/dist/esm/commands/clean.d.ts +39 -7
  42. package/dist/esm/commands/clean.js +55 -31
  43. package/dist/esm/commands/compile.d.ts +5 -0
  44. package/dist/esm/commands/compile.js +32 -0
  45. package/dist/esm/commands/fmt-cache.js +3 -2
  46. package/dist/esm/commands/fmt.d.ts +6 -2
  47. package/dist/esm/commands/fmt.js +48 -27
  48. package/dist/esm/commands/node-pin.d.ts +15 -5
  49. package/dist/esm/commands/node-pin.js +43 -10
  50. package/dist/esm/commands/package-manifest.d.ts +28 -0
  51. package/dist/esm/commands/package-manifest.js +32 -0
  52. package/dist/esm/commands/publish-output.d.ts +40 -0
  53. package/dist/esm/commands/publish-output.js +66 -0
  54. package/dist/esm/commands/report-catalog.d.ts +5 -0
  55. package/dist/esm/commands/report-catalog.js +19 -0
  56. package/dist/esm/commands/report-overrides.d.ts +5 -0
  57. package/dist/esm/commands/report-overrides.js +20 -1
  58. package/dist/esm/commands/run-vitest.js +21 -20
  59. package/dist/esm/commands/shellcheck.d.ts +2 -7
  60. package/dist/esm/commands/shellcheck.js +3 -11
  61. package/dist/esm/commands/spawn-inherited.d.ts +21 -0
  62. package/dist/esm/commands/spawn-inherited.js +11 -0
  63. package/dist/esm/commands/taze.d.ts +6 -6
  64. package/dist/esm/commands/taze.js +3 -7
  65. package/dist/esm/config-validation.js +1 -1
  66. package/dist/esm/config.js +3 -12
  67. package/dist/esm/{delegate-host.d.ts → delegation/delegate-host.d.ts} +3 -3
  68. package/dist/esm/{delegate-host.js → delegation/delegate-host.js} +9 -8
  69. package/dist/esm/{delegation-target.js → delegation/delegation-target.js} +1 -1
  70. package/dist/esm/{remote-probe.js → delegation/remote-probe.js} +2 -2
  71. package/dist/esm/{remote-run.js → delegation/remote-run.js} +5 -5
  72. package/dist/esm/{remote-session.js → delegation/remote-session.js} +1 -1
  73. package/dist/esm/{user-config.js → delegation/user-config.js} +4 -13
  74. package/dist/esm/env-switch.d.ts +25 -0
  75. package/dist/esm/env-switch.js +26 -0
  76. package/dist/esm/fan-out.d.ts +4 -1
  77. package/dist/esm/fan-out.js +1 -3
  78. package/dist/esm/help.js +1 -1
  79. package/dist/esm/helpers/filter-selection.d.ts +2 -0
  80. package/dist/esm/helpers/importConfigDefault.d.ts +7 -0
  81. package/dist/esm/helpers/importConfigDefault.js +45 -0
  82. package/dist/esm/invocation.d.ts +7 -3
  83. package/dist/esm/machine-lock.js +2 -1
  84. package/dist/esm/route.d.ts +6 -0
  85. package/dist/esm/route.js +15 -2
  86. package/dist/esm/run-chain.d.ts +1 -1
  87. package/dist/esm/run-chain.js +1 -1
  88. package/dist/esm/runCli.d.ts +9 -0
  89. package/dist/esm/runCli.js +24 -9
  90. package/dist/esm/runner.d.ts +2 -0
  91. package/dist/esm/runner.js +1 -1
  92. package/dist/esm/verdict.d.ts +1 -1
  93. package/docs/check-cache.md +7 -7
  94. package/docs/delegate.md +2 -2
  95. package/docs/machine-lock.md +4 -4
  96. package/docs/scripts.md +4 -0
  97. package/docs/upgrades.md +10 -2
  98. package/docs/utilities.md +6 -4
  99. package/package.json +7 -7
  100. /package/dist/esm/{check-cache-entries.d.ts → check-cache/check-cache-entries.d.ts} +0 -0
  101. /package/dist/esm/{check-cache-key.js → check-cache/check-cache-key.js} +0 -0
  102. /package/dist/esm/{recording.d.ts → check-cache/recording.d.ts} +0 -0
  103. /package/dist/esm/{delegation-target.d.ts → delegation/delegation-target.d.ts} +0 -0
  104. /package/dist/esm/{remote-host-session.d.ts → delegation/remote-host-session.d.ts} +0 -0
  105. /package/dist/esm/{remote-host-session.js → delegation/remote-host-session.js} +0 -0
  106. /package/dist/esm/{remote-probe-host.d.ts → delegation/remote-probe-host.d.ts} +0 -0
  107. /package/dist/esm/{remote-probe-host.js → delegation/remote-probe-host.js} +0 -0
  108. /package/dist/esm/{remote-probe.d.ts → delegation/remote-probe.d.ts} +0 -0
  109. /package/dist/esm/{remote-run.d.ts → delegation/remote-run.d.ts} +0 -0
  110. /package/dist/esm/{remote-session.d.ts → delegation/remote-session.d.ts} +0 -0
  111. /package/dist/esm/{remote-snapshot.d.ts → delegation/remote-snapshot.d.ts} +0 -0
  112. /package/dist/esm/{remote-snapshot.js → delegation/remote-snapshot.js} +0 -0
  113. /package/dist/esm/{user-config.d.ts → delegation/user-config.d.ts} +0 -0
@@ -1,30 +1,24 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
- import { readFile, rename, rm } from 'node:fs/promises';
3
+ import { readFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { formatGlyphLine, readCacheEntry, startTimer, STATUS_GLYPHS, UserError, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
- import { describeError } from '@williamthorsen/toolbelt.errors';
7
- import { glob } from 'glob';
8
- import { NMR_GLYPHS } from "../glyphs.js";
5
+ import { readCacheEntry, startTimer, UserError, writeCacheEntry } from '@williamthorsen/nmr-core';
9
6
  import { resolveConfigPath } from "../helpers/config-path.js";
10
- import { formatSaving } from "../helpers/duration.js";
11
7
  import { writeStatedOutcome } from "../stated-outcome.js";
12
- import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, readEmitDuration, resolveBuildCachePath, resolveScratchDirs, resolveToolchainFingerprint, writeEmitDuration, } from "./build-output.js";
8
+ import { DEFAULT_OUTDIR, hasExpectedBuildOutput, readEmitDuration, resolveBuildCachePath, resolveEntryPoints, resolveToolchainFingerprint, writeEmitDuration, } from "./build-output.js";
13
9
  import { assertSupportedTypeScript, formatDiagnostics, getCompiler, loadCompiler } from "./compiler.js";
14
- import { isWithin, rewriteSpecifiers } from "./rewrite-specifiers.js";
10
+ import { discardScratchDirs, publishOutput, removeScratchDirs } from "./publish-output.js";
11
+ import { rewriteSpecifiers } from "./rewrite-specifiers.js";
15
12
  import { resolveTsconfigChain } from "./tsconfig-chain.js";
16
13
  const SOURCE_ROOT = 'src';
17
- export async function buildPackage(packageDir, options) {
14
+ export async function buildPackage(packageDir, options = {}) {
18
15
  await loadCompiler(options.loadTypeScript);
19
16
  assertSupportedTypeScript();
20
17
  const cachePath = resolveBuildCachePath(packageDir);
21
18
  const outdir = options.outdir ?? DEFAULT_OUTDIR;
22
19
  const emitDir = resolveEmitDir(packageDir, outdir);
23
20
  const emitConfig = { outdir, declaration: true, rewriteRelativeImportExtensions: true };
24
- const entryPoints = await glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
25
- cwd: packageDir,
26
- ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
27
- });
21
+ const entryPoints = await resolveEntryPoints(packageDir, options);
28
22
  const configPath = resolveConfigPath(packageDir);
29
23
  const dependencies = [
30
24
  'package.json',
@@ -35,17 +29,19 @@ export async function buildPackage(packageDir, options) {
35
29
  compilerVersion: getCompiler().version,
36
30
  fingerprint: await resolveToolchainFingerprint(packageDir),
37
31
  };
38
- const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints), options.style);
39
- if (!changed) {
40
- await discardScratchDirs(resolveScratchDirs(emitDir));
41
- return;
32
+ const { reason, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
33
+ if (reason === 'unchanged') {
34
+ await discardScratchDirs(emitDir);
35
+ const savedMs = await recordUnchangedBuild(packageDir, currentHash, process.env);
36
+ return { kind: 'unchanged', ...(savedMs !== undefined && { savedMs }) };
42
37
  }
38
+ options.onProgress?.({ kind: reason });
43
39
  const readEmitMs = startTimer();
44
40
  const emittedFileCount = await emitPackage(packageDir, entryPoints, outdir);
45
41
  const emitMs = readEmitMs();
46
42
  await writeCacheEntry(cachePath, currentHash);
47
43
  await writeEmitDuration(packageDir, currentHash, emitMs);
48
- console.info(formatGlyphLine(NMR_GLYPHS, options.style, 'package', `${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`));
44
+ return { kind: 'emitted', fileCount: emittedFileCount, outdir };
49
45
  }
50
46
  export async function computeBuildHash(packageDir, files, emitConfig, toolchain) {
51
47
  const hash = createHash('sha256');
@@ -67,8 +63,7 @@ async function emitPackage(packageDir, entryPoints, outdir) {
67
63
  const rootNames = entryPoints.map((entry) => path.resolve(packageDir, entry));
68
64
  const sourceRoot = path.resolve(packageDir, SOURCE_ROOT);
69
65
  const emitDir = resolveEmitDir(packageDir, outdir);
70
- const scratchDirs = resolveScratchDirs(emitDir);
71
- await removeScratchDirs(scratchDirs);
66
+ await removeScratchDirs(emitDir);
72
67
  const emittedFiles = new Map();
73
68
  function collect(fileName, text, writeByteOrderMark) {
74
69
  emittedFiles.set(fileName, { text, writeByteOrderMark });
@@ -82,13 +77,7 @@ async function emitPackage(packageDir, entryPoints, outdir) {
82
77
  for (const [fileName, file] of emittedFiles) {
83
78
  stagedFiles.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
84
79
  }
85
- if (stagedFiles.size === 0) {
86
- await rm(emitDir, { force: true, recursive: true });
87
- return 0;
88
- }
89
- writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
90
- await swapIntoPlace(emitDir, scratchDirs);
91
- return stagedFiles.size;
80
+ return publishOutput(emitDir, stagedFiles);
92
81
  }
93
82
  function assertEmitSucceeded(result) {
94
83
  if (result.emitSkipped) {
@@ -110,17 +99,6 @@ function createCachingCompilerHost(compilerOptions) {
110
99
  };
111
100
  return host;
112
101
  }
113
- async function discardScratchDirs(scratchDirs) {
114
- try {
115
- await removeScratchDirs(scratchDirs);
116
- }
117
- catch {
118
- }
119
- }
120
- async function removeScratchDirs(scratchDirs) {
121
- await rm(scratchDirs.previousDir, { force: true, recursive: true });
122
- await rm(scratchDirs.stagingDir, { force: true, recursive: true });
123
- }
124
102
  function resolveEmitDir(packageDir, outdir) {
125
103
  const resolvedDir = path.resolve(packageDir, outdir);
126
104
  const relativePathFromPackage = path.relative(packageDir, resolvedDir);
@@ -132,24 +110,6 @@ function resolveEmitDir(packageDir, outdir) {
132
110
  }
133
111
  return resolvedDir;
134
112
  }
135
- async function swapIntoPlace(emitDir, scratchDirs) {
136
- const hadPreviousOutput = existsSync(emitDir);
137
- if (hadPreviousOutput) {
138
- await rename(emitDir, scratchDirs.previousDir);
139
- }
140
- try {
141
- await rename(scratchDirs.stagingDir, emitDir);
142
- }
143
- catch (error) {
144
- if (hadPreviousOutput && !existsSync(emitDir)) {
145
- await rename(scratchDirs.previousDir, emitDir);
146
- }
147
- throw new UserError(`failed to move the built output into ${emitDir}: ${describeError(error)}`, {
148
- cause: error,
149
- });
150
- }
151
- await discardScratchDirs(scratchDirs);
152
- }
153
113
  function synthesizeCompilerOptions(packageDir, outdir) {
154
114
  const ts = getCompiler();
155
115
  const configPath = path.join(packageDir, 'tsconfig.json');
@@ -176,38 +136,16 @@ function synthesizeCompilerOptions(packageDir, outdir) {
176
136
  noEmitOnError: false,
177
137
  };
178
138
  }
179
- function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
180
- const ts = getCompiler();
181
- for (const [fileName, file] of stagedFiles) {
182
- if (!isWithin(emitDir, fileName)) {
183
- throw new Error(`the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
184
- "Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
185
- }
186
- ts.sys.writeFile(path.join(stagingDir, path.relative(emitDir, fileName)), file.text, file.writeByteOrderMark);
187
- }
188
- }
189
- async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
190
- const packageName = path.basename(packageDir);
139
+ async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent) {
191
140
  const previousHash = await readCacheEntry(cachePath);
192
141
  const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
193
- if (previousHash === currentHash) {
194
- if (isOutputPresent) {
195
- const savedMs = await readEmitDuration(packageDir, currentHash);
196
- await writeStatedOutcome(process.env, { kind: 'build-unchanged', ...(savedMs !== undefined && { savedMs }) });
197
- const saving = formatSaving(savedMs ?? 0);
198
- const savingClause = saving === undefined ? '' : `, ${saving}`;
199
- console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build${savingClause}.`);
200
- return { changed: false, currentHash };
201
- }
202
- console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Build output is missing. Rebuilding.`));
203
- return { changed: true, currentHash };
142
+ if (previousHash !== currentHash) {
143
+ return { reason: 'inputs-changed', currentHash };
204
144
  }
205
- console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Changes detected.`));
206
- return { changed: true, currentHash };
145
+ return { reason: isOutputPresent ? 'unchanged' : 'output-missing', currentHash };
207
146
  }
208
- function describeEmit(fileCount, outdir) {
209
- if (fileCount === 0) {
210
- return 'Did not emit any output.';
211
- }
212
- return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
147
+ export async function recordUnchangedBuild(packageDir, digest, env) {
148
+ const savedMs = await readEmitDuration(packageDir, digest);
149
+ await writeStatedOutcome(env, { kind: 'build-unchanged', ...(savedMs !== undefined && { savedMs }) });
150
+ return savedMs;
213
151
  }
@@ -0,0 +1,6 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
+ import type { CleanOutcome, CleanProgress } from './clean.js';
3
+ /** Renders the line that closes a sweep, or `undefined` for a one-package clean, whose own line concludes it. */
4
+ export declare function renderCleanOutcome(outcome: CleanOutcome, style: OutputStyle): string | undefined;
5
+ /** Renders the line that reports a completed step of a clean. */
6
+ export declare function renderCleanProgress(progress: CleanProgress, style: OutputStyle): string;
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+ import { formatGlyphLine } from '@williamthorsen/nmr-core';
3
+ import { NMR_GLYPHS } from "../glyphs.js";
4
+ export function renderCleanOutcome(outcome, style) {
5
+ if (outcome.kind === 'package') {
6
+ return undefined;
7
+ }
8
+ return formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(outcome.cleanedDirs.length, outcome.skippedCount));
9
+ }
10
+ export function renderCleanProgress(progress, style) {
11
+ const message = progress.kind === 'check-results-removed'
12
+ ? 'Removed all recorded check results.'
13
+ : `${path.basename(progress.packageDir)}: Removed build output and cache.`;
14
+ return formatGlyphLine(NMR_GLYPHS, style, 'clean', message);
15
+ }
16
+ function describeSweep(cleanedCount, skippedCount) {
17
+ const packages = cleanedCount === 1 ? '1 package' : `${cleanedCount} packages`;
18
+ const skipClause = skippedCount === 0 ? '' : `, skipping ${skippedCount} with an empty clean override`;
19
+ return `Cleaned ${packages}${skipClause}.`;
20
+ }
@@ -1,18 +1,50 @@
1
- import { type OutputStyle } from '@williamthorsen/nmr-core';
1
+ import type { Writable } from 'node:stream';
2
+ /** What a clean did: one package's clean, or a sweep of every workspace package. */
3
+ export type CleanOutcome = {
4
+ kind: 'package';
5
+ checkResultsDir: string;
6
+ packageDir: string;
7
+ } | {
8
+ kind: 'sweep';
9
+ checkResultsDir: string;
10
+ cleanedDirs: string[];
11
+ skippedCount: number;
12
+ };
13
+ /** A step of a clean, reported as soon as it completes. */
14
+ export type CleanProgress = {
15
+ kind: 'check-results-removed';
16
+ } | {
17
+ kind: 'package-cleaned';
18
+ packageDir: string;
19
+ };
20
+ /** What a clean needs from its caller: a listener for its progress, and the streams for an override's output. */
21
+ export interface CleanOptions {
22
+ /** Called after each step completes, so that a caller can report it before a later override runs or fails. */
23
+ onProgress?: (progress: CleanProgress) => void;
24
+ /** The stream to which a package's own `clean` override writes its errors. */
25
+ stderr: Writable;
26
+ /** The stream to which a package's own `clean` override writes its output. */
27
+ stdout: Writable;
28
+ }
29
+ /**
30
+ * Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
31
+ *
32
+ * From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to `nmr-clean`
33
+ * before invoking it, so resolving again would apply the package's own override twice.
34
+ */
35
+ export declare function cleanFrom(cwd: string, options: CleanOptions): Promise<CleanOutcome>;
2
36
  /**
3
37
  * Removes a package's build output and the build-cache entries that describe it, so the package does not keep
4
38
  * any state that would make the next build skip. Removal is idempotent: An already-clean package is a silent
5
39
  * no-op.
6
40
  */
7
- export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
41
+ export declare function cleanPackage(packageDir: string): Promise<void>;
8
42
  /**
9
- * Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
10
- *
11
- * From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to this bin
12
- * before invoking it, so resolving again would apply the package's own override twice.
43
+ * Runs `nmr-clean` against `argv` from `cwd` and returns the exit code to report. The command takes neither an
44
+ * option nor an operand, so anything but `--help` is a usage error and removes nothing.
13
45
  *
14
46
  * This is the default `clean` script, and it is published as a bin because, under pnpm's isolated layout,
15
47
  * `pnpm exec` resolves only the bins of the consuming project's direct dependencies: pnpm links nmr's own bins
16
48
  * into the consumer's `node_modules/.bin`, but not the bins of nmr's dependencies.
17
49
  */
18
- export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
50
+ export declare function runClean(argv: string[], cwd: string): Promise<number>;
@@ -1,59 +1,81 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { formatGlyphLine, UserError } from '@williamthorsen/nmr-core';
3
+ import process from 'node:process';
4
+ import { UserError } from '@williamthorsen/nmr-core';
5
+ import { defineCommand, runCli } from '@williamthorsen/nmr-core/cli';
4
6
  import { NMR_CLEAN_BIN } from "../bins.js";
5
- import { removeCheckCache } from "../check-cache-entries.js";
7
+ import { removeCheckCache } from "../check-cache/check-cache-entries.js";
6
8
  import { loadRootConfig } from "../config.js";
7
9
  import { findContainingPackageDir } from "../context.js";
8
- import { NMR_GLYPHS } from "../glyphs.js";
9
10
  import { reportClosing } from "../helpers/reportClosing.js";
10
11
  import { resolveChannel } from "../output-channel.js";
12
+ import { resolveBinStyles } from "../resolveBinStyles.js";
11
13
  import { buildWorkspaceRegistry, resolveScript } from "../resolver.js";
12
14
  import { runCommand } from "../runner.js";
13
15
  import { renderChain } from "../steps.js";
14
16
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
15
17
  import { resolveBuildCachePath, resolveEmitDurationPath } from "./build-output.js";
18
+ import { renderCleanOutcome, renderCleanProgress } from "./clean-report.js";
16
19
  const CLEAN_COMMAND = 'clean';
17
20
  const BUILT_IN_CLEAN = 'nmr-clean';
18
21
  const OUTPUT_ROOT = 'dist';
19
- export async function cleanPackage(packageDir, style) {
20
- await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
21
- await rm(resolveBuildCachePath(packageDir), { force: true });
22
- await rm(resolveEmitDurationPath(packageDir), { force: true });
23
- console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', `${path.basename(packageDir)}: Removed build output and cache.`));
24
- }
25
- export async function runClean(cwd, style) {
22
+ export async function cleanFrom(cwd, options) {
26
23
  let monorepoRoot;
27
24
  try {
28
25
  monorepoRoot = findMonorepoRoot(cwd);
29
26
  }
30
27
  catch {
31
- await clearCheckCache(cwd, style);
32
- await cleanPackage(cwd, style);
33
- return;
28
+ await clearCheckCache(cwd, options);
29
+ await cleanReportedPackage(cwd, options);
30
+ return { kind: 'package', checkResultsDir: cwd, packageDir: cwd };
34
31
  }
35
- await clearCheckCache(monorepoRoot, style);
32
+ await clearCheckCache(monorepoRoot, options);
36
33
  const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
37
34
  const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
38
35
  if (packageDir !== undefined) {
39
- await cleanPackage(packageDir, style);
40
- return;
36
+ await cleanReportedPackage(packageDir, options);
37
+ return { kind: 'package', checkResultsDir: monorepoRoot, packageDir };
41
38
  }
42
- await sweepWorkspace(monorepoRoot, workspacePackageDirs, style);
39
+ return sweepWorkspace(monorepoRoot, workspacePackageDirs, options);
43
40
  }
44
- async function clearCheckCache(scopeDir, style) {
45
- await removeCheckCache(scopeDir);
46
- console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
41
+ export async function cleanPackage(packageDir) {
42
+ await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
43
+ await rm(resolveBuildCachePath(packageDir), { force: true });
44
+ await rm(resolveEmitDurationPath(packageDir), { force: true });
45
+ }
46
+ export async function runClean(argv, cwd) {
47
+ const command = defineCommand({
48
+ summary: 'Remove the build output and build cache of this package, or of every package from the monorepo root',
49
+ run: async () => {
50
+ const style = resolveBinStyles().stdout;
51
+ const outcome = await cleanFrom(cwd, {
52
+ onProgress: (progress) => {
53
+ console.info(renderCleanProgress(progress, style));
54
+ },
55
+ stderr: process.stderr,
56
+ stdout: process.stdout,
57
+ });
58
+ const closing = renderCleanOutcome(outcome, style);
59
+ if (closing !== undefined) {
60
+ reportClosing(closing);
61
+ }
62
+ return 0;
63
+ },
64
+ });
65
+ return runCli(argv, command, { name: NMR_CLEAN_BIN, stdout: process.stdout, stderr: process.stderr, baseDir: cwd });
47
66
  }
48
- function describeSweep(cleanedCount, skippedCount) {
49
- const packages = cleanedCount === 1 ? '1 package' : `${cleanedCount} packages`;
50
- const skipClause = skippedCount === 0 ? '' : `, skipping ${skippedCount} with an empty clean override`;
51
- return `Cleaned ${packages}${skipClause}.`;
67
+ async function cleanReportedPackage(packageDir, options) {
68
+ await cleanPackage(packageDir);
69
+ options.onProgress?.({ kind: 'package-cleaned', packageDir });
70
+ }
71
+ async function clearCheckCache(scopeDir, options) {
72
+ await removeCheckCache(scopeDir);
73
+ options.onProgress?.({ kind: 'check-results-removed' });
52
74
  }
53
- async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
75
+ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, options) {
54
76
  const config = await loadRootConfig(monorepoRoot);
55
77
  const registry = buildWorkspaceRegistry(config);
56
- let cleanedCount = 0;
78
+ const cleanedDirs = [];
57
79
  let skippedCount = 0;
58
80
  for (const packageDir of workspacePackageDirs) {
59
81
  const resolvedScript = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
@@ -63,18 +85,20 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
63
85
  continue;
64
86
  }
65
87
  if (resolvedCommand === BUILT_IN_CLEAN) {
66
- await cleanPackage(packageDir, style);
67
- cleanedCount++;
88
+ await cleanReportedPackage(packageDir, options);
89
+ cleanedDirs.push(packageDir);
68
90
  continue;
69
91
  }
70
92
  const { exitCode } = await runCommand(resolvedCommand, packageDir, {
71
93
  bin: NMR_CLEAN_BIN,
72
- channels: { stderr: resolveChannel(process.stderr, false), stdout: resolveChannel(process.stdout, false) },
94
+ channels: { stderr: resolveChannel(options.stderr, false), stdout: resolveChannel(options.stdout, false) },
95
+ stderr: options.stderr,
96
+ stdout: options.stdout,
73
97
  });
74
98
  if (exitCode !== 0) {
75
99
  throw new UserError(`\`${resolvedCommand}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
76
100
  }
77
- cleanedCount++;
101
+ cleanedDirs.push(packageDir);
78
102
  }
79
- reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleanedCount, skippedCount)));
103
+ return { kind: 'sweep', checkResultsDir: monorepoRoot, cleanedDirs, skippedCount };
80
104
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Runs `nmr-compile` against `argv` for the package at `packageDir` and returns the exit code to report. The command
3
+ * takes neither an option nor an operand, so anything but `--help` is a usage error and compiles nothing.
4
+ */
5
+ export declare function runCompile(argv: string[], packageDir: string): Promise<number>;
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { defineCommand, runCli } from '@williamthorsen/nmr-core/cli';
4
+ import { NMR_COMPILE_BIN } from "../bins.js";
5
+ import { loadWorkspaceConfig } from "../config.js";
6
+ import { resolveBinStyles } from "../resolveBinStyles.js";
7
+ import { buildPackage } from "./build.js";
8
+ import { renderBuildOutcome, renderBuildProgress } from "./build-report.js";
9
+ export async function runCompile(argv, packageDir) {
10
+ const command = defineCommand({
11
+ summary: "Compile the package's src tree to dist/esm",
12
+ run: async () => {
13
+ const { stdout } = resolveBinStyles();
14
+ const { build } = await loadWorkspaceConfig(packageDir);
15
+ const packageName = path.basename(packageDir);
16
+ const outcome = await buildPackage(packageDir, {
17
+ onProgress: (progress) => {
18
+ console.info(renderBuildProgress(progress, packageName, stdout));
19
+ },
20
+ ...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
21
+ });
22
+ console.info(renderBuildOutcome(outcome, packageName, stdout));
23
+ return 0;
24
+ },
25
+ });
26
+ return runCli(argv, command, {
27
+ name: NMR_COMPILE_BIN,
28
+ stdout: process.stdout,
29
+ stderr: process.stderr,
30
+ baseDir: packageDir,
31
+ });
32
+ }
@@ -2,7 +2,8 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
- import { NO_CACHE_ENV_VAR } from "../check-cache.js";
5
+ import { NO_CACHE_ENV_VAR } from "../check-cache/check-cache.js";
6
+ import { isEnvSwitchOn } from "../env-switch.js";
6
7
  export const FMT_CACHE_DIRECTORY = path.join('node_modules', '.cache', 'prettier', 'nmr-fmt');
7
8
  const LOCKFILE_NAMES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lock', 'bun.lockb'];
8
9
  const NO_LOCKFILE_MARKER = 'nolock';
@@ -10,7 +11,7 @@ const HASH_LENGTH = 16;
10
11
  const CACHE_FILE_PATTERN = /^([0-9a-f]{16})\.([0-9a-f]{16}|nolock)\.json$/;
11
12
  export function prepareFmtCache(options) {
12
13
  const { cwd, env, repositoryRoot, shouldBypassCache } = options;
13
- if (shouldBypassCache || env[NO_CACHE_ENV_VAR] === '1')
14
+ if (shouldBypassCache || isEnvSwitchOn(env, NO_CACHE_ENV_VAR))
14
15
  return { kind: 'uncached' };
15
16
  try {
16
17
  if (statSync(path.join(repositoryRoot, 'node_modules'), { throwIfNoEntry: false })?.isDirectory() !== true) {
@@ -1,15 +1,17 @@
1
+ import { type SpawnInherited } from './spawn-inherited.js';
1
2
  export type FormatMode = 'check' | 'write';
2
3
  /**
3
4
  * Runs `nmr-fmt` against `argv` and returns the exit code to report: It formats or checks the files that git
4
5
  * reports for `cwd`. A repository without a Prettier config of its own is formatted with the house config. The run
5
- * uses Prettier's content-keyed cache unless `--no-cache` or `NMR_NO_CACHE=1` bypasses it.
6
+ * uses Prettier's content-keyed cache unless `--no-cache` or `NMR_NO_CACHE=1` bypasses it, and an `NMR_NO_CACHE`
7
+ * that is neither on nor off is rejected as a usage error.
6
8
  *
7
9
  * Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
8
10
  * than passed along, since git would read it as a pathspec matching nothing and the run would report
9
11
  * success over an empty selection. Every argument after `--` is a pathspec, even one that starts
10
12
  * with `-`.
11
13
  */
12
- export declare function runFmt(argv: string[], cwd?: string): Promise<number>;
14
+ export declare function runFmt(argv: string[], cwd?: string, env?: NodeJS.ProcessEnv): Promise<number>;
13
15
  /** @internal */
14
16
  export interface RunPrettierOptions {
15
17
  cliPath: string;
@@ -23,6 +25,8 @@ export interface RunPrettierOptions {
23
25
  cwd: string;
24
26
  /** Overridable so that the multi-batch path is reachable without a fixture that hits the real ceiling. */
25
27
  budgetBytes?: number;
28
+ /** Runs the Prettier CLI on the caller's stdio. */
29
+ spawn?: SpawnInherited;
26
30
  }
27
31
  /**
28
32
  * Runs Prettier over the selection, returning the first non-zero exit code. Every batch runs even after
@@ -1,6 +1,4 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { spawnSync } from 'node:child_process';
3
- import { readFileSync } from 'node:fs';
4
2
  import { createRequire } from 'node:module';
5
3
  import path from 'node:path';
6
4
  import process from 'node:process';
@@ -9,8 +7,12 @@ import { isRecord, reportError } from '@williamthorsen/nmr-core';
9
7
  import { defineCommand, runCli, UsageError } from '@williamthorsen/nmr-core/cli';
10
8
  import { describeError } from '@williamthorsen/toolbelt.errors';
11
9
  import { NMR_FMT_BIN } from "../bins.js";
10
+ import { NO_CACHE_ENV_VAR } from "../check-cache/check-cache.js";
11
+ import { readEnvSwitch } from "../env-switch.js";
12
12
  import { prepareFmtCache } from "./fmt-cache.js";
13
13
  import { resolveFormatTargets } from "./format-targets.js";
14
+ import { BROKEN_MANIFEST_FIX, findPackageManifest, resolveDeclaredCli, } from "./package-manifest.js";
15
+ import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
14
16
  const PRETTIER_PACKAGE = 'prettier';
15
17
  const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default.config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
16
18
  const ARGUMENT_BUDGET_BYTES = 100_000;
@@ -18,7 +20,7 @@ const MODE_ARGS = {
18
20
  check: ['--check'],
19
21
  write: ['--list-different', '--write'],
20
22
  };
21
- export async function runFmt(argv, cwd = process.cwd()) {
23
+ export async function runFmt(argv, cwd = process.cwd(), env = process.env) {
22
24
  const command = defineCommand({
23
25
  summary: 'Format or check the files that git lists',
24
26
  flags: {
@@ -31,12 +33,20 @@ export async function runFmt(argv, cwd = process.cwd()) {
31
33
  if (flags.check === flags.write) {
32
34
  throw new UsageError(flags.check ? 'Pass --check or --write, not both.' : 'Pass --check or --write.');
33
35
  }
34
- return formatFiles({ mode: flags.check ? 'check' : 'write', pathspecs: operands.pathspec, shouldBypassCache: flags.noCache }, cwd);
36
+ const noCache = readEnvSwitch(env, NO_CACHE_ENV_VAR);
37
+ if (!noCache.ok) {
38
+ throw new UsageError(noCache.error);
39
+ }
40
+ return formatFiles({
41
+ mode: flags.check ? 'check' : 'write',
42
+ pathspecs: operands.pathspec,
43
+ shouldBypassCache: flags.noCache || noCache.isOn,
44
+ }, cwd, env);
35
45
  },
36
46
  });
37
47
  return runCli(argv, command, { name: NMR_FMT_BIN, stdout: process.stdout, stderr: process.stderr, baseDir: cwd });
38
48
  }
39
- async function formatFiles(request, cwd) {
49
+ async function formatFiles(request, cwd, env) {
40
50
  const resolvedTargets = resolveFormatTargets(cwd, request.pathspecs);
41
51
  if (!resolvedTargets.ok) {
42
52
  reportError(NMR_FMT_BIN, resolvedTargets.error);
@@ -70,7 +80,7 @@ async function formatFiles(request, cwd) {
70
80
  }
71
81
  const cache = prepareFmtCache({
72
82
  cwd,
73
- env: process.env,
83
+ env,
74
84
  repositoryRoot,
75
85
  shouldBypassCache: request.shouldBypassCache,
76
86
  });
@@ -92,17 +102,36 @@ function resolvePrettierCli(cwd) {
92
102
  ok: false,
93
103
  error: `\`${PRETTIER_PACKAGE}\` could not be resolved from this repository or from nmr's own installation, which declares it as a dependency. Reinstall \`@williamthorsen/nmr\`.`,
94
104
  };
95
- const manifestPath = resolveFrom(path.join(cwd, 'package.json'), `${PRETTIER_PACKAGE}/package.json`) ??
96
- resolveFrom(import.meta.url, `${PRETTIER_PACKAGE}/package.json`);
97
- if (manifestPath === undefined)
98
- return missingResult;
99
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
100
- if (!isRecord(manifest))
101
- return missingResult;
102
- const bin = isRecord(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
103
- if (typeof bin !== 'string')
105
+ const repositoryLookup = findPackageManifest(path.join(cwd, 'package.json'), PRETTIER_PACKAGE);
106
+ const copy = repositoryLookup.kind === 'absent'
107
+ ? {
108
+ lookup: findPackageManifest(import.meta.url, PRETTIER_PACKAGE),
109
+ origin: "nmr's installation",
110
+ reinstallStep: 'Reinstall `@williamthorsen/nmr`.',
111
+ }
112
+ : {
113
+ lookup: repositoryLookup,
114
+ origin: 'this repository',
115
+ reinstallStep: `Reinstall this repository's \`${PRETTIER_PACKAGE}\` dependency.`,
116
+ };
117
+ const { lookup } = copy;
118
+ if (lookup.kind === 'absent')
104
119
  return missingResult;
105
- return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin), manifestPath };
120
+ if (lookup.kind === 'broken') {
121
+ return {
122
+ ok: false,
123
+ error: `\`${PRETTIER_PACKAGE}\` could not be loaded from ${copy.origin}: ${lookup.cause}. ${BROKEN_MANIFEST_FIX}`,
124
+ };
125
+ }
126
+ const { manifest, manifestPath } = lookup;
127
+ const cliPath = resolveDeclaredCli(manifest, manifestPath, PRETTIER_PACKAGE);
128
+ if (cliPath === undefined) {
129
+ return {
130
+ ok: false,
131
+ error: `\`${PRETTIER_PACKAGE}\` at ${path.dirname(manifestPath)} does not declare a CLI. ${copy.reinstallStep}`,
132
+ };
133
+ }
134
+ return { ok: true, cliPath, manifestPath };
106
135
  }
107
136
  async function findRepositoryConfig(manifestPath, cwd) {
108
137
  try {
@@ -122,7 +151,7 @@ async function findRepositoryConfig(manifestPath, cwd) {
122
151
  }
123
152
  }
124
153
  export function runPrettier(options) {
125
- const { cacheLocation, cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES, } = options;
154
+ const { cacheLocation, cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES, spawn = spawnInherited, } = options;
126
155
  const prettierArgs = [
127
156
  cliPath,
128
157
  ...(configPath === undefined ? [] : ['--config', configPath]),
@@ -136,12 +165,12 @@ export function runPrettier(options) {
136
165
  ];
137
166
  let firstFailureStatus = 0;
138
167
  for (const batch of batchWithinBudget(files, budgetBytes)) {
139
- const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
168
+ const result = spawn(process.execPath, [...prettierArgs, ...batch], { cwd });
140
169
  if (result.error) {
141
170
  reportError(NMR_FMT_BIN, `could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
142
171
  return 1;
143
172
  }
144
- const status = result.status ?? 1;
173
+ const status = resolveInheritedExitCode(result);
145
174
  if (status !== 0 && firstFailureStatus === 0) {
146
175
  firstFailureStatus = status;
147
176
  }
@@ -166,11 +195,3 @@ function batchWithinBudget(files, budgetBytes) {
166
195
  batches.push(batch);
167
196
  return batches;
168
197
  }
169
- function resolveFrom(parent, specifier) {
170
- try {
171
- return createRequire(parent).resolve(specifier);
172
- }
173
- catch {
174
- return undefined;
175
- }
176
- }