@williamthorsen/nmr 0.34.0 → 0.35.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 (56) hide show
  1. package/.readyup/kits/default.js +92 -19
  2. package/.readyup/manifest.json +11 -8
  3. package/README.md +83 -17
  4. package/agents/guidance/rulebooks/nmr.md +1 -1
  5. package/dist/esm/UserError.d.ts +6 -0
  6. package/dist/esm/check-cache.d.ts +145 -0
  7. package/dist/esm/commands/build-output.d.ts +64 -0
  8. package/dist/esm/commands/build.d.ts +29 -1
  9. package/dist/esm/commands/build.js +25 -5
  10. package/dist/esm/commands/clean.d.ts +15 -0
  11. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +11 -0
  12. package/dist/esm/commands/fmt.d.ts +32 -0
  13. package/dist/esm/commands/report-catalog.d.ts +10 -0
  14. package/dist/esm/commands/report-overrides.d.ts +6 -0
  15. package/dist/esm/commands/taze.d.ts +18 -0
  16. package/dist/esm/config.d.ts +25 -0
  17. package/dist/esm/context.d.ts +11 -0
  18. package/dist/esm/default-scripts.d.ts +15 -0
  19. package/dist/esm/defineConfig.d.ts +9 -0
  20. package/dist/esm/help.d.ts +13 -0
  21. package/dist/esm/helpers/clampToBytes.d.ts +12 -0
  22. package/dist/esm/helpers/createBoundedBuffer.d.ts +7 -0
  23. package/dist/esm/helpers/deriveExcerpt.d.ts +10 -0
  24. package/dist/esm/helpers/duration.d.ts +15 -0
  25. package/dist/esm/helpers/hook-name.d.ts +8 -0
  26. package/dist/esm/helpers/package-json.d.ts +25 -0
  27. package/dist/esm/helpers/readStringValues.d.ts +7 -0
  28. package/dist/esm/helpers/reportClosing.d.ts +15 -0
  29. package/dist/esm/helpers/transcript.d.ts +25 -0
  30. package/dist/esm/helpers/type-guards.d.ts +1 -0
  31. package/dist/esm/helpers/workspace-patterns.d.ts +7 -0
  32. package/dist/esm/prettier.d.ts +8 -0
  33. package/dist/esm/recording.d.ts +39 -1
  34. package/dist/esm/replay-assembly.d.ts +18 -2
  35. package/dist/esm/report-format.d.ts +33 -0
  36. package/dist/esm/reportCliFailure.d.ts +8 -0
  37. package/dist/esm/resolve-scripts.d.ts +10 -1
  38. package/dist/esm/resolver.d.ts +67 -2
  39. package/dist/esm/resolver.js +1 -1
  40. package/dist/esm/runCli.d.ts +17 -0
  41. package/dist/esm/runCli.js +1 -1
  42. package/dist/esm/runner.d.ts +52 -0
  43. package/dist/esm/scripts.d.ts +1 -0
  44. package/dist/esm/steps.d.ts +71 -0
  45. package/dist/esm/taze.d.ts +16 -0
  46. package/dist/esm/tests.d.ts +40 -0
  47. package/dist/esm/tests.js +25 -0
  48. package/dist/esm/tiers.d.ts +56 -1
  49. package/dist/esm/tiers.js +22 -9
  50. package/dist/esm/types.d.ts +27 -0
  51. package/dist/esm/verbosity.d.ts +37 -0
  52. package/dist/esm/verdict.d.ts +45 -0
  53. package/dist/esm/vitest.d.ts +72 -2
  54. package/dist/esm/vitest.js +17 -9
  55. package/dist/esm/workspace.d.ts +18 -0
  56. package/package.json +18 -10
@@ -1,7 +1,9 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import type { OutputChannels } from './runner.js';
3
3
  import type { CheckCacheConfig, NmrConfig } from './types.js';
4
+ /** A recorded pass: what ran, on which tree, and what it cost. */
4
5
  export interface CheckCacheEntry {
6
+ /** The full cache key the pass was recorded under; a hit is this matching the key computed now. */
5
7
  key: string;
6
8
  treeHash: string;
7
9
  headSha: string;
@@ -9,44 +11,94 @@ export interface CheckCacheEntry {
9
11
  nmrVersion: string;
10
12
  nodeVersion: string;
11
13
  durationMs: number;
14
+ /** ISO-8601 instant the pass completed. */
12
15
  recordedAt: string;
16
+ /**
17
+ * The build digest each covered package's output carried when the pass was recorded. Build output is
18
+ * git-ignored, so the tree hash cannot describe it; comparing these is what separates output built from this
19
+ * tree from output another tree left behind.
20
+ */
13
21
  buildDigests: Record<string, string>;
22
+ /** What a skip replays in place of the run it recalls, absent on a pass that retained nothing. */
14
23
  retention?: Retention;
15
24
  }
25
+ /** What locates one command's recorded pass at one scope, and the transcript beside it. */
16
26
  export interface EntryRef {
17
27
  anchorDir: string;
18
28
  command: string;
19
29
  monorepoRoot: string;
20
30
  }
31
+ /** One command's excerpt, attributed to the scope and the command that produced it. */
21
32
  export interface ReplayLine {
22
33
  command: string;
23
34
  excerpt: string;
24
35
  scope: string;
25
36
  }
37
+ /**
38
+ * What a recalled pass replays, the key certifying the excerpts describe this environment's output, and the
39
+ * run that last vouched for them.
40
+ *
41
+ * A list rather than one excerpt: a composite's entry carries its constituents' lines, and a nested one's are
42
+ * spliced into its parent's, where the attribution cannot be re-derived from the entry that holds them.
43
+ *
44
+ * The witness is what admits a constituent's line into the assembly its parent records: a run writes it when
45
+ * it records an excerpt, and restamps it when it recalls one and replays it.
46
+ */
26
47
  export interface Retention {
27
48
  key: string;
28
49
  replay: ReplayLine[];
29
50
  runId: string;
30
51
  }
52
+ /** What nmr's own build has left on disk across the workspace. */
31
53
  export interface BuildOutputState {
54
+ /** Covered packages whose output is absent. */
32
55
  missing: string[];
56
+ /** The digest of the inputs each covered package's output was built from, keyed by package name. */
33
57
  digests: Record<string, string>;
34
58
  }
59
+ /** The parts of the running interpreter that can change what a check concludes. */
35
60
  export interface RuntimeIdentity {
36
61
  arch: string;
37
62
  nodeVersion: string;
38
63
  platform: string;
39
64
  }
65
+ /** One working tree, observed once per top-level invocation and shared with every process below it. */
40
66
  export interface TreeSnapshot {
41
67
  hash: string;
42
68
  headSha: string;
43
69
  }
70
+ /** The interpreter this process is running on, folded into every key and recorded alongside every pass. */
44
71
  export declare const CURRENT_RUNTIME: RuntimeIdentity;
72
+ /** Set to `1` to hear why the gate did not skip, or why it is disabled. */
45
73
  export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
74
+ /**
75
+ * The commands cacheable without configuration: those whose whole contribution is an exit status, and that
76
+ * reach nothing beyond a checkout and an install.
77
+ *
78
+ * Excluded on purpose. `audit` and `prepush` consult a vulnerability database that changes without the tree
79
+ * (`prepush`'s `ci` constituent still skips while its `audit` always runs). `build` and `compile` carry a cache
80
+ * of their own. `fix`, `fmt`, `lint`, and `upgrade` mutate the tree they are asked about. `test:all` reaches
81
+ * whatever the environment supplies. Anything a repo adds here promises exit-status-only semantics through its
82
+ * whole chain, hooks included.
83
+ */
46
84
  export declare const DEFAULT_CACHEABLE_COMMANDS: string[];
85
+ /** Set to `1` for the standing equivalent of `--no-cache`: skip the lookup, still record on success. */
47
86
  export declare const NO_CACHE_ENV_VAR = "NMR_NO_CACHE";
87
+ /** Carries one run's identity down the spawned chain, so an entry can name the run that vouched for it. */
48
88
  export declare const RUN_ID_ENV_VAR = "NMR_RUN_ID";
89
+ /** Carries the top-level tree snapshot down the spawned chain, so one invocation hashes the tree once. */
49
90
  export declare const TREE_SNAPSHOT_ENV_VAR = "NMR_TREE_SNAPSHOT";
91
+ /**
92
+ * Restamps a recalled entry's retention with the run that is replaying it, so an excerpt this run certified
93
+ * can join the assembly a composite above it records.
94
+ *
95
+ * A recall certifies as surely as a recording does: the pass key matched, so the excerpt describes this tree,
96
+ * and the caller reaches this only where the retention key matched too, so it describes this presentation
97
+ * environment. Everything else the entry records stands -- the instant and the duration belong to the run that
98
+ * earned the pass, and a recall must not make it read as later or longer than it was.
99
+ *
100
+ * A cache that cannot be written is not worth failing a green run over, so that failure goes to the debug note.
101
+ */
50
102
  export declare function certifyRetention(options: {
51
103
  anchorDir: string;
52
104
  command: string;
@@ -56,6 +108,14 @@ export declare function certifyRetention(options: {
56
108
  runId: string;
57
109
  stderr: Writable;
58
110
  }): Promise<void>;
111
+ /**
112
+ * Folds everything that can change what a command concludes into one key: the tree's content, the command
113
+ * string that would run, the scope it would run in, nmr's own version, the interpreter, what is installed, and
114
+ * the environment variables a check can read. A hit is this key matching a recorded one, so an ingredient left
115
+ * out here is an ingredient that could change while the cache still claims a pass.
116
+ *
117
+ * Reports a reason instead of a key when the install fingerprint cannot be read, which disables the gate.
118
+ */
59
119
  export declare function computeCacheKey(options: {
60
120
  anchorDir: string;
61
121
  command: string;
@@ -72,26 +132,105 @@ export declare function computeCacheKey(options: {
72
132
  ok: false;
73
133
  reason: string;
74
134
  };
135
+ /**
136
+ * Folds what changes a transcript without changing a conclusion onto the pass key: the channel each of the
137
+ * command's output streams ran on, and the environment variables a tool presents itself through.
138
+ *
139
+ * Taking the pass key as an ingredient rather than recomputing its parts is what keeps the two from drifting
140
+ * apart. The channel kind is what keeps a run at a terminal from replaying a piped recording; folding in raw
141
+ * TTY-ness instead would be wrong under quiet mode, where the child sees pipes at a terminal and the
142
+ * transcript is reproducible.
143
+ */
75
144
  export declare function computeRetentionKey(options: {
76
145
  channels: OutputChannels;
77
146
  env: NodeJS.ProcessEnv;
78
147
  passKey: string;
79
148
  }): string;
149
+ /** Renders a snapshot for the environment of every process below this one. */
80
150
  export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
151
+ /**
152
+ * Names a covered package whose output differs between two observations of it, or `undefined` when every
153
+ * package agrees. A package that has appeared or disappeared between them counts as a disagreement, because
154
+ * the output the earlier observation describes is not the output the later one found.
155
+ */
81
156
  export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
157
+ /**
158
+ * Renders the warning for a `--no-cache` that landed after the command name, where it is an argument to the
159
+ * command rather than a flag to nmr. Passing it on unchanged is the honest thing to do with an argument, so
160
+ * the warning is all that separates this from a silently un-bypassed run.
161
+ */
82
162
  export declare function formatMisplacedNoCacheWarning(command: string): string;
163
+ /**
164
+ * Reports whether a command's passes are recorded at all, which is what separates a command with no recording
165
+ * from one that could never have had one.
166
+ *
167
+ * A hook is excluded here rather than at one caller: it is not a command anyone asks for, so nothing records
168
+ * it, and a repo naming one in `extraCommands` must not make the gate and a reader of its entries disagree.
169
+ */
83
170
  export declare function isCacheableCommand(checkCache: CheckCacheConfig | undefined, command: string): boolean;
171
+ /**
172
+ * Reads the state of the build output nmr's own build covers. Build output is git-ignored, so the tree hash
173
+ * says nothing about it: a `ci` whose `build` constituent is cached would otherwise skip on a tree whose `dist`
174
+ * had been deleted, or whose `dist` was compiled from a different tree, and hand back a green exit over a
175
+ * repository that cannot run.
176
+ *
177
+ * A package whose `build` or `compile` is overridden emits somewhere this does not know about, so it is left
178
+ * out rather than made a permanent miss.
179
+ */
84
180
  export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
181
+ /**
182
+ * Reads the entry recorded for one command at one scope, or `undefined` when there is none to trust.
183
+ *
184
+ * Retention is vouched for separately from the pass it rides on: an excerpt of a shape this cannot read is
185
+ * dropped, leaving a pass that skips cleanly and reports its verdict alone. Voiding the pass instead would
186
+ * cost a full run to avoid a line nobody would have printed.
187
+ */
85
188
  export declare function readCheckCacheEntry(options: {
86
189
  anchorDir: string;
87
190
  command: string;
88
191
  monorepoRoot: string;
89
192
  }): Promise<CheckCacheEntry | undefined>;
193
+ /**
194
+ * Reads the whole output one recorded pass retained, or `undefined` where it retained none.
195
+ *
196
+ * Held to nothing on its own: the entry beside it is what says which tree the bytes describe, and a caller
197
+ * that has not matched the entry's key is reading a transcript of some other tree.
198
+ */
90
199
  export declare function readTranscript(ref: EntryRef): Promise<string | undefined>;
200
+ /**
201
+ * Makes the transcript beside one entry be exactly what this pass retained, removing what an earlier pass
202
+ * left when this one retained nothing.
203
+ *
204
+ * A composite retains nothing of its own, so without the removal a leaf's transcript would stand beside an
205
+ * entry that never produced it, and `--log` would date another run's bytes by this one's instant.
206
+ */
91
207
  export declare function recordTranscript(ref: EntryRef, transcript: string | undefined): Promise<void>;
208
+ /** Removes every recorded pass for a monorepo, or for a standalone package outside one. */
92
209
  export declare function removeCheckCache(scopeDir: string): Promise<void>;
210
+ /**
211
+ * Merges a repo's `checkCache` configuration into the default set. Extending rather than replacing means
212
+ * declaring one command cannot silently drop the defaults; excluding is how a repo retires a name whose chain
213
+ * turned out to do more than report an exit status.
214
+ */
93
215
  export declare function resolveCacheableCommands(checkCache: CheckCacheConfig | undefined): Set<string>;
216
+ /**
217
+ * Resolves the identity of the run this invocation belongs to: the one an ancestor nmr process passed down,
218
+ * and otherwise a fresh one, this invocation being where the run starts.
219
+ *
220
+ * Unbounded where the tree snapshot is bounded by a HEAD comparison: a process that outlives its run hands a
221
+ * stale identity to the invocations it later makes, and what keeps that harmless is the tree hash every
222
+ * constituent entry is held to before its excerpt joins an assembly.
223
+ */
94
224
  export declare function resolveRunId(env: NodeJS.ProcessEnv): string;
225
+ /**
226
+ * Resolves the tree snapshot this invocation gates on: the one a parent nmr process already took, when there
227
+ * is one, and otherwise a fresh hash of the working tree. Reports a reason instead when no snapshot can be
228
+ * had, which disables the gate.
229
+ *
230
+ * The monorepo root must be the git toplevel. A repository holding the monorepo inside a subdirectory has
231
+ * content outside it that the checks may still read, and a hash covering more than the monorepo would move
232
+ * for edits that cannot affect it.
233
+ */
95
234
  export declare function resolveTreeSnapshot(options: {
96
235
  monorepoRoot: string;
97
236
  env: NodeJS.ProcessEnv;
@@ -102,10 +241,16 @@ export declare function resolveTreeSnapshot(options: {
102
241
  ok: false;
103
242
  reason: string;
104
243
  };
244
+ /** Records a pass, replacing whatever this command last recorded at this scope. */
105
245
  export declare function writeCheckCacheEntry(options: {
106
246
  anchorDir: string;
107
247
  command: string;
108
248
  entry: CheckCacheEntry;
109
249
  monorepoRoot: string;
110
250
  }): Promise<void>;
251
+ /**
252
+ * Writes a note explaining a gate decision, but only when `NMR_DEBUG=1`. The gate is silent by default: a
253
+ * reason to run is not news, and a line per invocation explaining why nothing was skipped would bury the
254
+ * output of the command that did run.
255
+ */
111
256
  export declare function writeDebugNote(message: string, env: NodeJS.ProcessEnv, stderr: Writable): void;
@@ -1,19 +1,83 @@
1
1
  export interface BuildOptions {
2
2
  entryGlobs?: string[];
3
+ /** Adds to the effective ignore set, whether that set is the default or an `ignorePatterns` override. */
3
4
  extraIgnorePatterns?: string[];
5
+ /** Replaces the default ignore set. */
4
6
  ignorePatterns?: string[];
5
7
  outdir?: string;
6
8
  }
9
+ /** The pair of directories a build publishes through, both siblings of the emit directory. */
7
10
  export interface ScratchDirs {
8
11
  previous: string;
9
12
  staging: string;
10
13
  }
11
14
  export declare const DEFAULT_ENTRY_GLOBS: string[];
15
+ /**
16
+ * Directories holding test scaffolding rather than shipped code, excluded from entry-point selection so a
17
+ * package does not publish its own helpers. Deliberately not the vitest factory's `COVERAGE_EXCLUDE`: helpers
18
+ * live in `test-utils/` precisely so they stay inside the coverage include set, so the two lists overlap
19
+ * without converging and neither can be derived from the other.
20
+ *
21
+ * Ignoring a file removes it as an entry point, not from the emit. The compiler still emits whatever the
22
+ * surviving entry points import, which is what keeps a production module that uses a helper from emitting a
23
+ * dangling specifier. Widening this list can therefore only drop files nothing in production reaches.
24
+ */
12
25
  export declare const DEFAULT_IGNORE_PATTERNS: string[];
13
26
  export declare const DEFAULT_OUTDIR = "dist/esm/";
27
+ /**
28
+ * Reports whether the output a build of `packageDir` would produce is currently on disk. Applies the rule the
29
+ * build applies, on the same options the build was given, so a caller probing for output and the build deciding
30
+ * whether to rebuild cannot disagree: a package whose entry points emit nothing expects no output, and reports
31
+ * present. Passing options the build was not given is what would make the two disagree.
32
+ */
14
33
  export declare function hasBuildOutput(packageDir: string, options?: BuildOptions): Promise<boolean>;
34
+ /**
35
+ * Reports whether the output a previous build would have produced is still on disk. Entry points that
36
+ * emit nothing expect no output, so their absent outdir is not deleted output: a `src` tree holding only
37
+ * declaration files, or none at all, would otherwise be reported as missing output and recompiled forever.
38
+ * The emit is what makes an outdir, so what counts is whether any entry point emits, not how many there are.
39
+ */
15
40
  export declare function hasExpectedBuildOutput(packageDir: string, outdir: string, entryPoints: string[]): boolean;
41
+ /**
42
+ * Returns the digest of the inputs the output currently on disk was built from, or `undefined` when the package
43
+ * has never been built. Two packages holding the same sources report the same digest, and a package whose output
44
+ * came from a different tree reports a different one, which is what tells a stale `dist` from a current one.
45
+ */
16
46
  export declare function readBuildDigest(packageDir: string): Promise<string | undefined>;
47
+ /**
48
+ * Resolves the absolute path of a package's build-cache file, one entry in the shared store. The package
49
+ * directory is the entry's scope, so the store keys the file to it and packages sharing a hoisted
50
+ * `node_modules` never collide.
51
+ *
52
+ * Three things hold the path where every previously written entry already sits: the `.hash` extension, the
53
+ * package's own directory name as the slug, and the absence of `discriminators`, which would otherwise fold
54
+ * into the digest. Changing any of them strands every entry on disk.
55
+ */
17
56
  export declare function resolveBuildCachePath(packageDir: string): string;
57
+ /**
58
+ * Resolves the two scratch directories a build publishes through: `staging`, which the emit is written to, and
59
+ * `previous`, which the outgoing output is renamed aside to. Both are siblings of the emit directory, so a
60
+ * rename between them never crosses a filesystem, and both are dot-prefixed so a leftover stays out of the
61
+ * globs that select sources.
62
+ *
63
+ * The names are fixed rather than unique because the build removes both before use. That removal is what a
64
+ * unique name would need a sweep to accomplish, and a sweep cannot tell a directory orphaned by a killed run
65
+ * from one a concurrent build is still writing. A directory left behind by a failed build is cleared the same
66
+ * way, so the failure path needs no cleanup of its own.
67
+ */
18
68
  export declare function resolveScratchDirs(emitDir: string): ScratchDirs;
69
+ /**
70
+ * Resolves the fingerprint of the nmr compiling `packageDir`, which joins the build digest beside the compiler
71
+ * version: the same sources emit differently across nmr versions as they do across TypeScript versions. It is
72
+ * nmr's own build digest where one is on disk, which is what moves on a dev-loop edit the version does not
73
+ * follow, and nmr's package version otherwise -- the case in a consuming repo, whose installed copy was built
74
+ * elsewhere.
75
+ *
76
+ * A build of nmr itself takes the version too. Its digest is the entry that build is about to overwrite, so
77
+ * folding it would make the key a function of its own previous value, which never settles: nmr would rebuild on
78
+ * every invocation and move the fingerprint every other package reads.
79
+ *
80
+ * What the fingerprint names is the nmr that produced the output, not the sources sitting in `src`, so a package
81
+ * the pre-rebuild binary compiled keeps the old fingerprint and rebuilds on the run after.
82
+ */
19
83
  export declare function resolveToolchainFingerprint(packageDir: string, selfDir?: string): Promise<string>;
@@ -1,8 +1,36 @@
1
- import type { BuildOptions } from './build-output.js';
1
+ import { type BuildOptions } from './build-output.js';
2
+ /**
3
+ * The toolchain a build's emit comes from: the resolved compiler and the nmr running it. Both join the build
4
+ * hash, because the same sources can emit differently across either.
5
+ */
2
6
  export interface BuildToolchain {
3
7
  compilerVersion: string;
4
8
  fingerprint: string;
5
9
  }
10
+ /**
11
+ * Compiles a package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and
12
+ * `.d.ts` from two programs and rewriting relative `.ts` specifiers and tsconfig `paths` aliases to
13
+ * runnable relative `.js` specifiers in both outputs. Skips the build only when no input has changed
14
+ * and the previous output is still on disk.
15
+ *
16
+ * The build owns its output directory: every emit replaces it wholesale, so `dist` is a function of the
17
+ * current inputs rather than an accumulation of every build that ever ran. Assets belong outside it, or in a
18
+ * `build:post` hook, which runs after the output is published.
19
+ */
6
20
  export declare function buildPackage(packageDir: string, options?: BuildOptions): Promise<void>;
21
+ /**
22
+ * Produces a digest of the given files (paths and contents), the emit config, and the toolchain.
23
+ * The file list is sorted so the digest is order-invariant, and each path is folded in so renames are detected.
24
+ * The toolchain joins it because the same sources emit differently across TypeScript versions, and across the
25
+ * nmr versions whose emit logic drives them.
26
+ */
7
27
  export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, toolchain: BuildToolchain): Promise<string>;
28
+ /**
29
+ * Resolves a package's full tsconfig `extends` chain, returning every config file in it (the leaf `tsconfig.json` and
30
+ * each base it transitively extends, up to the repo root) as paths relative to `packageDir`.
31
+ * Emit is driven by the fully-resolved compiler options, so the base configs (where `target`, `module`, `paths`,
32
+ * `lib`, and `strict` are actually defined) must be in the cache's hashed input set; otherwise a change to a base
33
+ * config would not bust the cache and stale output could ship. Paths are returned relative to `packageDir`, so
34
+ * `computeBuildHash` reads them and folds a stable, location-independent path string into the digest.
35
+ */
8
36
  export declare function resolveTsconfigChain(packageDir: string, configFileName?: string): string[];
@@ -85,14 +85,15 @@ async function emitPackage(packageDir, entryPoints, outdir) {
85
85
  const emitDir = resolveEmitDir(packageDir, outdir);
86
86
  const scratchDirs = resolveScratchDirs(emitDir);
87
87
  await removeScratchDirs(scratchDirs);
88
- const program = ts.createProgram(rootNames, compilerOptions);
89
88
  const emitted = new Map();
90
- const emitResult = program.emit(undefined, (fileName, text, writeByteOrderMark) => {
89
+ function collect(fileName, text, writeByteOrderMark) {
91
90
  emitted.set(fileName, { text, writeByteOrderMark });
92
- });
93
- if (emitResult.emitSkipped) {
94
- throw new Error(`nmr-compile: emit failed.\n${formatDiagnostics(emitResult.diagnostics)}`);
95
91
  }
92
+ const host = createCachingCompilerHost(compilerOptions);
93
+ const scriptProgram = ts.createProgram(rootNames, { ...compilerOptions, declaration: false }, host);
94
+ assertEmitSucceeded(scriptProgram.emit(undefined, collect));
95
+ const declarationProgram = ts.createProgram(rootNames, { ...compilerOptions, emitDeclarationOnly: true, removeComments: false }, host, scriptProgram);
96
+ assertEmitSucceeded(declarationProgram.emit(undefined, collect));
96
97
  const staged = new Map();
97
98
  for (const [fileName, file] of emitted) {
98
99
  staged.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
@@ -105,6 +106,25 @@ async function emitPackage(packageDir, entryPoints, outdir) {
105
106
  await swapIntoPlace(emitDir, scratchDirs);
106
107
  return staged.size;
107
108
  }
109
+ function assertEmitSucceeded(result) {
110
+ if (result.emitSkipped) {
111
+ throw new Error(`nmr-compile: emit failed.\n${formatDiagnostics(result.diagnostics)}`);
112
+ }
113
+ }
114
+ function createCachingCompilerHost(compilerOptions) {
115
+ const host = ts.createCompilerHost(compilerOptions);
116
+ const parsed = new Map();
117
+ const readSourceFile = host.getSourceFile.bind(host);
118
+ host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
119
+ if (parsed.has(fileName)) {
120
+ return parsed.get(fileName);
121
+ }
122
+ const sourceFile = readSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
123
+ parsed.set(fileName, sourceFile);
124
+ return sourceFile;
125
+ };
126
+ return host;
127
+ }
108
128
  async function discardScratchDirs(scratchDirs) {
109
129
  try {
110
130
  await removeScratchDirs(scratchDirs);
@@ -1,2 +1,17 @@
1
+ /**
2
+ * Removes a package's build output and the build-cache entry that describes it, so no state survives to
3
+ * make the next build skip. Removal is idempotent: an already-clean package is a silent no-op.
4
+ */
1
5
  export declare function cleanPackage(packageDir: string): Promise<void>;
6
+ /**
7
+ * Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
8
+ *
9
+ * From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to this bin
10
+ * before invoking it, so resolving again would apply the package's own override twice.
11
+ *
12
+ * This is the default `clean` script, shipped as a bin rather than delegating to `rimraf`. Under pnpm's
13
+ * isolated layout, `pnpm exec` resolves only bins of the consuming project's own direct dependencies, so
14
+ * a `rimraf` in nmr's dependency tree would be unreachable — while nmr's own bins are linked into the
15
+ * consumer's `node_modules/.bin`, nmr being a direct dependency.
16
+ */
2
17
  export declare function runClean(cwd?: string): Promise<void>;
@@ -10,9 +10,20 @@ export interface EnsurePrepublishHooksResult {
10
10
  hasFailures: boolean;
11
11
  }
12
12
  export declare const DEFAULT_HOOK = "npm run build";
13
+ /**
14
+ * Check (and optionally fix) whether all publishable workspace packages
15
+ * have a `prepublishOnly` script.
16
+ */
13
17
  export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
14
18
  fix: boolean;
15
19
  dryRun: boolean;
16
20
  command?: string;
17
21
  }): EnsurePrepublishHooksResult;
22
+ /**
23
+ * Reports one line per publishable package and closes with what the run came to. Private packages appear in
24
+ * neither: they publish nothing, so a `prepublishOnly` is not theirs to carry.
25
+ *
26
+ * Every line goes to stdout, the `✗` included, and the exit code is what carries a failure. A report split
27
+ * across two streams leaves no reader holding the whole list.
28
+ */
18
29
  export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string): void;
@@ -1,6 +1,8 @@
1
1
  export type FormatMode = 'check' | 'write';
2
2
  export interface FormatTargets {
3
+ /** Paths relative to the working directory, as `git ls-files` emits them. */
3
4
  files: string[];
5
+ /** Absolute paths to every `.prettierignore` governing the repository, root-most first. */
4
6
  ignorePaths: string[];
5
7
  }
6
8
  export type ResolveTargetsResult = {
@@ -10,14 +12,44 @@ export type ResolveTargetsResult = {
10
12
  ok: false;
11
13
  error: string;
12
14
  };
15
+ /**
16
+ * Formats or checks the files git reports for `cwd`, and returns the exit code to report.
17
+ *
18
+ * Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
19
+ * than passed along, since git would read it as a pathspec matching nothing and the run would report
20
+ * success over an empty selection.
21
+ */
13
22
  export declare function runFmt(argv: string[], cwd?: string): number;
23
+ /**
24
+ * Resolves what Prettier should format and which ignore files govern it, using git rather than Prettier's
25
+ * own discovery. Prettier reads `.gitignore` and `.prettierignore` from the working directory only, so a
26
+ * pattern in `packages/<pkg>/.gitignore` is invisible to a root-level run; git knows the whole hierarchy,
27
+ * along with `.git/info/exclude` and `core.excludesFile`, which Prettier cannot read under any flag.
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.
31
+ *
32
+ * `pathspecs` narrow the selection and are passed to git verbatim, so they carry git pathspec semantics
33
+ * rather than shell glob semantics.
34
+ */
14
35
  export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
36
+ /** @internal */
15
37
  export interface RunPrettierOptions {
16
38
  cliPath: string;
17
39
  mode: FormatMode;
18
40
  files: string[];
19
41
  ignorePaths: string[];
20
42
  cwd: string;
43
+ /** Overridable so the multi-batch path is reachable without a fixture that hits the real ceiling. */
21
44
  budgetBytes?: number;
22
45
  }
46
+ /**
47
+ * Runs Prettier over the selection, returning the first non-zero exit code. Every batch runs even after
48
+ * one fails, so a check reports every offending file rather than only those in the first batch.
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`.
52
+ *
53
+ * @internal
54
+ */
23
55
  export declare function runPrettier(options: RunPrettierOptions): number;
@@ -1 +1,11 @@
1
+ /**
2
+ * Reports the catalogued dependencies a package-scoped upgrade pass cannot see.
3
+ *
4
+ * The upgrade tool reads `pnpm-workspace.yaml` only when it sits at the working directory, so a pass run
5
+ * inside a package drops every dependency a catalog declares -- and a package whose dependencies are all
6
+ * catalogued reports itself up to date. Naming them, and the root a covering pass runs from, is what keeps
7
+ * that from reading as a clean bill of health.
8
+ *
9
+ * Runs ahead of the report produced by the workspace `upgrade` script.
10
+ */
1
11
  export declare function reportCatalog(cwd: string): void;
@@ -1 +1,7 @@
1
+ /**
2
+ * Reports the pnpm dependency overrides declared in the monorepo root's `pnpm-workspace.yaml`, and rejects a
3
+ * `pnpm.overrides` block left behind in the root `package.json`.
4
+ *
5
+ * Runs ahead of the report produced by the root `upgrade` script.
6
+ */
1
7
  export declare function reportOverrides(monorepoRoot: string): void;
@@ -4,9 +4,27 @@ export interface SpawnOutcome {
4
4
  error?: Error | undefined;
5
5
  }
6
6
  export interface RunTazeOptions {
7
+ /** Resolves the absolute path of taze's CLI entry. Injected by tests. */
7
8
  resolveCliPath?: () => string;
9
+ /** Runs the CLI and reports how it exited. Injected by tests. */
8
10
  spawn?: (nodePath: string, argv: string[]) => SpawnOutcome;
11
+ /** Stream that error output is written to. Defaults to `process.stderr`. */
9
12
  stderr?: Writable;
10
13
  }
14
+ /**
15
+ * Runs taze from nmr's own dependency tree, forwarding `args`, and returns its exit code.
16
+ *
17
+ * Consumers depend on nmr, not on taze, so taze is transitive and its bin is absent from the consumer's
18
+ * root `node_modules/.bin`. This launcher is what bridges that gap: pnpm links nmr's own bins into the
19
+ * consumer root, and nmr resolves taze from the tree it does control.
20
+ *
21
+ * A request timeout is the one argument added, and only where `args` carries none. Neither other site can hold
22
+ * it: an invocation's trailing arguments are appended to the resolved command string, so the same flag in the
23
+ * `upgrade` script would be doubled by `nmr upgrade --request-timeout ...`, and taze's parser collects the pair
24
+ * into an array it reads as a near-zero deadline; a `taze.config.ts` setting lands in the object taze's own CLI
25
+ * defaults overwrite. Every other argument is forwarded untouched, so invocation policy (`--recursive`) stays in
26
+ * the script registry, visible in `nmr` help output and overridable per repo, and upgrade policy in `taze.ts`.
27
+ */
11
28
  export declare function runTaze(args: string[], options?: RunTazeOptions): number;
29
+ /** Resolves the absolute path of taze's CLI entry from nmr's own dependency tree. */
12
30
  export declare function resolveTazeCliPath(): string;
@@ -1,6 +1,31 @@
1
1
  import type { NmrConfig } from './types.js';
2
+ /** The config's path relative to the directory holding it, for a message composed without one in hand. */
2
3
  export declare const CONFIG_RELATIVE_PATH: string;
4
+ /**
5
+ * Resolves the config-file path for a directory, whether that is the monorepo root or a package.
6
+ * Callers that only need to know whether a config exists -- the build, which folds it into its cache digest -- go
7
+ * through this rather than spelling the path again, so that the two cannot drift into hashing a file nothing reads.
8
+ */
3
9
  export declare function resolveConfigPath(baseDir: string): string;
10
+ /**
11
+ * Loads the nmr configuration from `.config/nmr.config.ts` under `baseDir`, which is the monorepo root for
12
+ * the repo-wide config and a package directory for the workspace tier. Returns an empty config if the file
13
+ * doesn't exist.
14
+ */
4
15
  export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
16
+ /**
17
+ * Loads the monorepo-root `.config/nmr.config.ts`, the tier that feeds script resolution and `devBin`.
18
+ * Returns an empty config if the file doesn't exist.
19
+ *
20
+ * Throws on a recognized key this tier does not honor. `build` governs one package's compile, which reads the
21
+ * package's own config alone, so a `build` accepted here would apply its patterns nowhere.
22
+ */
5
23
  export declare function loadRootConfig(monorepoRoot: string): Promise<NmrConfig>;
24
+ /**
25
+ * Loads a single package's `.config/nmr.config.ts`, the tier `nmr-compile` reads from its working directory.
26
+ * Returns an empty config if the file doesn't exist.
27
+ *
28
+ * Throws on a recognized key this tier does not honor. Dropping one would leave a package building on
29
+ * settings its own config file appears to change, which nothing in the build's output would reveal.
30
+ */
6
31
  export declare function loadWorkspaceConfig(packageDir: string): Promise<NmrConfig>;
@@ -5,5 +5,16 @@ export interface ResolvedContext {
5
5
  packageDir?: string;
6
6
  config: NmrConfig;
7
7
  }
8
+ /**
9
+ * Determines whether a directory is inside a workspace package.
10
+ * Returns the package directory if so, or `undefined` if in root context.
11
+ *
12
+ * A workspace may nest one package inside another, so the deepest containing directory wins — matching
13
+ * pnpm, which resolves a cwd to the package that encloses it most nearly.
14
+ */
8
15
  export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
16
+ /**
17
+ * Resolves the full execution context: monorepo root, whether we're in a
18
+ * workspace package or root context, and the loaded configuration.
19
+ */
9
20
  export declare function resolveContext(cwd?: string): Promise<ResolvedContext>;
@@ -1,8 +1,23 @@
1
+ /**
2
+ * One element of a composite, paired with what it does with the invocation's trailing arguments. The bare
3
+ * string form of an element is this spec with `declinesArgs` left at its default.
4
+ */
1
5
  export interface StepSpec {
6
+ /** A command name, optionally preceded by nmr's own flags, as it would be typed after `nmr`. */
2
7
  run: string;
8
+ /**
9
+ * Set where the trailing arguments cannot narrow this step's work: it is a prerequisite the narrowed steps
10
+ * run against, or the tool it reaches would be misled by them. A declining step runs unnarrowed.
11
+ */
3
12
  declinesArgs?: boolean;
4
13
  }
5
14
  export type ScriptValue = string | ReadonlyArray<string | StepSpec>;
6
15
  export type ScriptRegistry = Record<string, ScriptValue>;
16
+ /**
17
+ * Workspace scripts, identical for every package.
18
+ * Four Vitest projects are recognized: `tool`, `localhost`, `remote`, and `unit`. The latter is also a catch-all.
19
+ * Name a test file with the matching infix to associate it with a project.
20
+ * Example: `nmr test:tool` runs every file carrying the `tool` infix, such as `my-file.tool.test.ts`.
21
+ */
7
22
  export declare const workspaceScripts: ScriptRegistry;
8
23
  export declare const rootScripts: ScriptRegistry;
@@ -1,3 +1,12 @@
1
1
  import type { NmrConfig } from './types.js';
2
2
  export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, StepSpec, } from './types.js';
3
+ /**
4
+ * Type-safe identity function for configuration files.
5
+ *
6
+ * Usage in `.config/nmr.config.ts`:
7
+ * ```ts
8
+ * import { defineConfig } from '@williamthorsen/nmr/config';
9
+ * export default defineConfig({ ... });
10
+ * ```
11
+ */
3
12
  export declare function defineConfig(config: NmrConfig): NmrConfig;