@williamthorsen/nmr 0.34.0 → 0.36.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.
- package/.meta/changelog.json +1416 -0
- package/.readyup/kits/default.js +174 -19
- package/.readyup/manifest.json +11 -8
- package/CHANGELOG.md +894 -0
- package/README.md +91 -18
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/dist/esm/UserError.d.ts +6 -0
- package/dist/esm/check-cache.d.ts +145 -0
- package/dist/esm/commands/build-output.d.ts +64 -0
- package/dist/esm/commands/build.d.ts +29 -1
- package/dist/esm/commands/build.js +26 -6
- package/dist/esm/commands/clean.d.ts +15 -0
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +11 -0
- package/dist/esm/commands/fmt.d.ts +32 -0
- package/dist/esm/commands/report-catalog.d.ts +10 -0
- package/dist/esm/commands/report-overrides.d.ts +6 -0
- package/dist/esm/commands/taze.d.ts +18 -0
- package/dist/esm/config.d.ts +19 -2
- package/dist/esm/config.js +66 -9
- package/dist/esm/context.d.ts +11 -0
- package/dist/esm/default-scripts.d.ts +15 -0
- package/dist/esm/defineConfig.d.ts +9 -0
- package/dist/esm/help.d.ts +13 -0
- package/dist/esm/helpers/clampToBytes.d.ts +12 -0
- package/dist/esm/helpers/config-path.d.ts +8 -0
- package/dist/esm/helpers/config-path.js +7 -0
- package/dist/esm/helpers/createBoundedBuffer.d.ts +7 -0
- package/dist/esm/helpers/deriveExcerpt.d.ts +10 -0
- package/dist/esm/helpers/duration.d.ts +15 -0
- package/dist/esm/helpers/findClosestName.d.ts +7 -0
- package/dist/esm/helpers/findClosestName.js +38 -0
- package/dist/esm/helpers/hook-name.d.ts +8 -0
- package/dist/esm/helpers/package-json.d.ts +25 -0
- package/dist/esm/helpers/package-json.js +1 -1
- package/dist/esm/helpers/readStringValues.d.ts +7 -0
- package/dist/esm/helpers/reportClosing.d.ts +15 -0
- package/dist/esm/helpers/transcript.d.ts +25 -0
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/helpers/workspace-patterns.d.ts +7 -0
- package/dist/esm/prettier.d.ts +8 -0
- package/dist/esm/recording.d.ts +39 -1
- package/dist/esm/replay-assembly.d.ts +18 -2
- package/dist/esm/report-format.d.ts +33 -0
- package/dist/esm/reportCliFailure.d.ts +8 -0
- package/dist/esm/resolve-scripts.d.ts +10 -1
- package/dist/esm/resolver.d.ts +67 -2
- package/dist/esm/resolver.js +1 -1
- package/dist/esm/runCli.d.ts +17 -0
- package/dist/esm/runCli.js +2 -2
- package/dist/esm/runner.d.ts +52 -0
- package/dist/esm/scripts.d.ts +1 -0
- package/dist/esm/steps.d.ts +71 -0
- package/dist/esm/taze.d.ts +16 -0
- package/dist/esm/tests.d.ts +40 -0
- package/dist/esm/tests.js +25 -0
- package/dist/esm/tiers.d.ts +56 -1
- package/dist/esm/tiers.js +22 -9
- package/dist/esm/types.d.ts +28 -0
- package/dist/esm/verbosity.d.ts +37 -0
- package/dist/esm/verdict.d.ts +45 -0
- package/dist/esm/vitest-git-isolation.js +6 -0
- package/dist/esm/vitest.d.ts +72 -2
- package/dist/esm/vitest.js +17 -9
- package/dist/esm/workspace.d.ts +18 -0
- package/package.json +20 -10
package/README.md
CHANGED
|
@@ -3,25 +3,36 @@
|
|
|
3
3
|
Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
4
4
|
|
|
5
5
|
<!-- section:release-notes -->
|
|
6
|
-
## Release notes — v0.
|
|
6
|
+
## Release notes — v0.36.0 (2026-09-08)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- Report a package whose tarball would omit its changelog (#768)
|
|
11
11
|
|
|
12
|
-
Adds
|
|
12
|
+
Adds ReadyUp checks to verify that every published package bundles its changelog files: the human-readable `CHANGELOG.md` and the machine-readable `changelog.json`.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- Reject a checkCache command name that matches no command (#778)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Modifies `nmr` so that it rejects a `checkCache.extraCommands` or `checkCache.excludeCommands` name that matches no command, so a misspelt entry fails config load instead of silently having no effect.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Modifies `nmr` so that it resolves names listed in either field against nmr's default scripts, the repo config's script registries, and each package's `package.json` scripts, so a name that only a package declares is accepted.
|
|
19
|
+
|
|
20
|
+
Migration: Correct or remove any `checkCache.extraCommands` or `checkCache.excludeCommands` entry that names no command, and any entry that names a hook, which is rejected too. The error reports the field, the offending name, and the closest command where there is one.
|
|
21
|
+
|
|
22
|
+
- Report a bin target that is not a committed wrapper (#780)
|
|
19
23
|
|
|
20
|
-
-
|
|
24
|
+
- Adds `every bin target is a committed wrapper` to nmr's readyup kit, which reports a workspace `bin` target naming a path under `dist/`, or one that git does not track, because pnpm links a package's bins before anything is built and never retries a failed link.
|
|
25
|
+
- Adds `every bin wrapper's build-output target is covered by files`, which reports a wrapper loading build output that `files` omits, since that publishes a bin resolving to nothing.
|
|
26
|
+
|
|
27
|
+
Migration: Replace a `bin` target naming a path under `dist/` with a committed wrapper under `bin/` that imports the build entry, commit the wrapper, and extend `files` to cover the output that the wrapper loads.
|
|
28
|
+
|
|
29
|
+
### 🐛 Bug fixes
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
- Keep global git ignore rules and attributes out of test subprocesses (#773)
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
Fixes an issue where a test that spawns `git` read ignore rules and attributes from outside the repository, so a suite could pass or fail on whatever a developer or their machine happened to have configured. The setup file that `defineVitestConfig` loads by default now isolates both, alongside the git configuration already covered.
|
|
34
|
+
|
|
35
|
+
Migration: Where a test needs its own `core.excludesFile` or `core.attributesFile`, pass it on the invocation as `git -c core.excludesFile=<path>`. An environment-injected key now outranks repository-local config.
|
|
25
36
|
<!-- /section:release-notes -->
|
|
26
37
|
|
|
27
38
|
## Installation
|
|
@@ -49,7 +60,7 @@ From a package directory:
|
|
|
49
60
|
|
|
50
61
|
```bash
|
|
51
62
|
nmr test # Run tests for the current package
|
|
52
|
-
nmr build # Compile to .js and .d.ts
|
|
63
|
+
nmr build # Compile to .js and .d.ts
|
|
53
64
|
nmr check # Typecheck, format check, lint check, and tests
|
|
54
65
|
```
|
|
55
66
|
|
|
@@ -511,6 +522,10 @@ export default defineConfig({
|
|
|
511
522
|
|
|
512
523
|
`extraCommands` extends the default set rather than replacing it, so naming one command cannot silently drop the rest; `excludeCommands` is applied afterwards, so a name in both is excluded. `enabled: false` turns the gate off entirely. The key is `checkCache`, in the monorepo-root config.
|
|
513
524
|
|
|
525
|
+
**A name in either list must resolve to a command**, whether through nmr's defaults, the repo config's script records, or a package's own `package.json` scripts. Both lists are read by name alone, so a misspelt entry would otherwise be inert: the command it meant to name would go on running, indistinguishable from one that cannot be cached at all. A name that resolves nowhere fails config load, naming the closest command where there is one. A hook name is rejected too, since a hook is never gated on its own -- it runs as part of the chain of the command it wraps, and is skipped or run with it.
|
|
526
|
+
|
|
527
|
+
The test is resolvability rather than membership of the cacheable set, so an `excludeCommands` entry keeps standing after a release moves its name out of the defaults.
|
|
528
|
+
|
|
514
529
|
```ts
|
|
515
530
|
export default defineConfig({
|
|
516
531
|
output: {
|
|
@@ -801,7 +816,7 @@ Position determines ownership: flags before the command name are nmr's own, and
|
|
|
801
816
|
```bash
|
|
802
817
|
# From a package directory
|
|
803
818
|
nmr test # Run workspace test script
|
|
804
|
-
nmr build # Compile to .js and .d.ts
|
|
819
|
+
nmr build # Compile to .js and .d.ts
|
|
805
820
|
|
|
806
821
|
# From the monorepo root
|
|
807
822
|
nmr test # Root tests + recursive workspace tests
|
|
@@ -878,7 +893,7 @@ nmr-clean
|
|
|
878
893
|
|
|
879
894
|
### `nmr-compile`
|
|
880
895
|
|
|
881
|
-
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and `.d.ts`
|
|
896
|
+
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files carry their doc comments either way. Because the compiler parses each source file, every relative import form — static, re-export, dynamic `import()`, and bare side-effect — is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when no input has changed and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means — `nmr clean`, `rm -rf dist`, `git clean` — therefore forces a rebuild rather than a skip. This is the default `compile` script — run it from a package directory.
|
|
882
897
|
|
|
883
898
|
**What it compiles.** Entry points are `src/**/*.ts` less the directories that hold test scaffolding rather than shipped code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: the compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and its importer never emits a dangling specifier. This list is deliberately not the Vitest [coverage exclusions](#what-the-config-excludes) — helpers live in `test-utils/` precisely so they stay covered.
|
|
884
899
|
|
|
@@ -937,7 +952,7 @@ ensure-prepublish-hooks
|
|
|
937
952
|
|
|
938
953
|
## Conformance checks
|
|
939
954
|
|
|
940
|
-
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the [test-tier convention](#test-tiers), and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade.
|
|
955
|
+
nmr publishes a `readyup` kit that checks a consuming repo against the current release: the shared Vitest and Prettier configs, the workspace layout, the root script registry, the [test-tier convention](#test-tiers), that every `bin` target is a committed wrapper rather than build output that pnpm cannot link at install time, that `files` covers the output each wrapper loads, and that no `package.json` in the tree declares a `pnpm` field, which pnpm 11 reads no key from. Those settings belong in `pnpm-workspace.yaml`, and `pnpx codemod run pnpm-v10-to-v11` moves them. The kit ships inside the package, so it checks against the nmr version installed rather than whatever a repository ref happens to point at, and a tier added or renamed in nmr reaches the repo on upgrade. A repo that wants the tier convention in its own gate rather than in a command run by hand declares the [conventions check](#gating-the-test-file-conventions) instead, which also covers the half the kit does not.
|
|
941
956
|
|
|
942
957
|
Add `readyup` as a devDependency, then name nmr in its config:
|
|
943
958
|
|
|
@@ -1059,7 +1074,7 @@ import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
|
1059
1074
|
export default defineVitestConfig();
|
|
1060
1075
|
```
|
|
1061
1076
|
|
|
1062
|
-
`vitest` is a peer dependency (`>=4.0.0 <5`), declared optional — the consuming repo provides it, and repos that never import this subpath are unaffected.
|
|
1077
|
+
`vitest` is a peer dependency (`>=4.0.0 <5`), declared optional — the consuming repo provides it, and repos that never import this subpath are unaffected. `vite` is an optional peer as well (`>=8.0.0 <9`), required only by the [`tsconfigPaths` setting](#resolving-through-tsconfig-paths); a repo that declares a `vite` of its own below that range sees an unmet-peer warning whether or not it sets the flag.
|
|
1063
1078
|
|
|
1064
1079
|
### Test tiers
|
|
1065
1080
|
|
|
@@ -1086,7 +1101,7 @@ vitest # every project
|
|
|
1086
1101
|
|
|
1087
1102
|
**Every test file names its tier**, in the form `<subject>[.<aspect>].<tier>.test.ts`. Only the segment immediately before `.test.` selects a project, so an earlier one is free for documentation: `resolveConfig.packaged.unit.test.ts` reads as a companion to `resolveConfig.unit.test.ts` and still names `unit`.
|
|
1088
1103
|
|
|
1089
|
-
`unit` is defined by subtracting the tiers rather than by an allow-list of infixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped. That is a safety net, not a licence to omit the tier: a file whose tier segment is missing or misspelt runs under `unit` and reports success, so no test run distinguishes it from a conformant one. [nmr's readyup kit](#conformance-checks) reports those files
|
|
1104
|
+
`unit` is defined by subtracting the tiers rather than by an allow-list of infixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped. That is a safety net, not a licence to omit the tier: a file whose tier segment is missing or misspelt runs under `unit` and reports success, so no test run distinguishes it from a conformant one until the repo declares the [conventions check](#gating-the-test-file-conventions). In a repo that has not, [nmr's readyup kit](#conformance-checks) is what reports those files.
|
|
1090
1105
|
|
|
1091
1106
|
`localhost` and `remote` have no test script of their own. They are declared anyway, so that a `*.remote.test.ts` file cannot fall into `unit` and run in the default gate unnoticed.
|
|
1092
1107
|
|
|
@@ -1094,12 +1109,46 @@ Every tier above `unit` carries a 30-second `testTimeout` and `hookTimeout`, whe
|
|
|
1094
1109
|
|
|
1095
1110
|
To raise a budget for one tier and no other, use the `tiers` seam below. The `project` seam merges over both budgets as well, but like every option passed through it the value reaches all four projects at once -- raising a tier's budget raises `unit`'s with it. To lift the ceiling for a single file rather than a whole tier, pass a timeout to the individual test or hook, which stays the narrower tool.
|
|
1096
1111
|
|
|
1112
|
+
### Gating the test-file conventions
|
|
1113
|
+
|
|
1114
|
+
Both halves of the convention are silent on their own: an untiered file runs under the residual `unit` and passes, and a file outside `__tests__` is collected by nothing at all. `@williamthorsen/nmr/tests` exports a check that reports both, which a repo declares from a one-line test of its own:
|
|
1115
|
+
|
|
1116
|
+
```ts
|
|
1117
|
+
// __tests__/test-file-conventions.unit.test.ts
|
|
1118
|
+
import { checkTestFileConventions } from '@williamthorsen/nmr/tests';
|
|
1119
|
+
|
|
1120
|
+
checkTestFileConventions();
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
It declares one suite with an assertion per half, each carrying the remedy that fixes it: the four tier names and the rename form, or the `__tests__` directory the file belongs in. The guard names its own tier like any other test file, so a misnamed guard reports itself.
|
|
1124
|
+
|
|
1125
|
+
A repo linting with `vitest/require-hook` needs a disable directive on that call, which the rule reads as setup work where it declares the suite:
|
|
1126
|
+
|
|
1127
|
+
```ts
|
|
1128
|
+
// eslint-disable-next-line vitest/require-hook -- the call declares the suite, where the rule reads it as setup work.
|
|
1129
|
+
checkTestFileConventions();
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
The sweep starts at the monorepo root rather than at the directory Vitest supplies, so one call covers the whole repo from whichever package the run began in. `rootDir` overrides that, for a check pointed at a tree other than the repo's own.
|
|
1133
|
+
|
|
1134
|
+
An exported check rather than a step nmr runs for you, because nothing nmr runs reaches the whole tree: `nmr test` fans out per package, and a direct `vitest` invocation runs no nmr script at all. A test the repo owns also lets the repo scope what the sweep covers.
|
|
1135
|
+
|
|
1136
|
+
`exclude` names directory basenames the sweep prunes at any depth, additive to the ones nmr always prunes:
|
|
1137
|
+
|
|
1138
|
+
```ts
|
|
1139
|
+
checkTestFileConventions({ exclude: ['cypress', 'generated'] });
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
**Pass the same array to [`testCollectionExclude`](#what-the-config-excludes).** The two describe one scope, and naming a directory in only one is a defect in either direction. Pruned from the sweep alone, the directory's test files still run and still report nothing, which is the silence this check exists to end. Excluded from collection alone, the sweep reports files a consumer has no reason to act on.
|
|
1143
|
+
|
|
1144
|
+
Expect the first run to fail in a repo that has never gated this. Vitest 4 excludes only `node_modules` and `.git` by default, so a `__tests__` tree under a generated or vendored directory is collected and runs today; naming that directory in both lists is the fix, rather than widening what nmr prunes for everyone.
|
|
1145
|
+
|
|
1097
1146
|
### What the config supplies
|
|
1098
1147
|
|
|
1099
1148
|
Two settings a workspace test runner needs almost universally are part of what the factory produces, so a config file declares neither:
|
|
1100
1149
|
|
|
1101
1150
|
- **Workspace packages resolve from source.** `source` leads the emitted `resolve.conditions` and `ssr.resolve.conditions`, so a cross-package import reaches the dependency's `.ts` rather than its `dist/` and a suite runs without a prior build. A package declaring no such condition is unaffected.
|
|
1102
|
-
- **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device
|
|
1151
|
+
- **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device, sets `GIT_CONFIG_NOSYSTEM` and `GIT_ATTR_NOSYSTEM`, and injects `core.excludesFile` and `core.attributesFile` through `GIT_CONFIG_COUNT` and its key/value pairs, so a test that spawns `git` reads neither the developer's identity, nor a signing config that can block on a passphrase, nor their global ignore rules and attributes. Those last two need a step of their own: neither path is config-derived, so git resolves each from `$XDG_CONFIG_HOME/git/`, or from `~/.config/git/` where that variable is unset, whatever the config variables say. `GIT_CONFIG_GLOBAL` already replaces `$XDG_CONFIG_HOME/git/config`, so those two are the rest of what that directory supplies; the system attributes file at `$(prefix)/etc/gitattributes` is reached by no config variable at all, which is what `GIT_ATTR_NOSYSTEM` covers. Redirecting `XDG_CONFIG_HOME` would reach the per-user pair as well, and is not used, because other tools spawned by a test read that variable for configuration of their own: pnpm fails to start under `XDG_CONFIG_HOME=/dev/null`. An injected key outranks repository-local config, so a repository setting its own `core.excludesFile` is overridden under the isolation; a test needing its own value passes it on the invocation as `git -c core.excludesFile=...`, which outranks the injection. The setup file leads each project's `setupFiles`, ahead of every entry the layers add, because a shared setup establishes the environment the rest run in.
|
|
1103
1152
|
|
|
1104
1153
|
Both defaults exist because omitting either is silent rather than loud: the tiers still exist and the suite still runs green, testing `dist/` and the developer's git identity instead. Turn either off through its own option, which folds across layers like any other, the last layer to declare it winning:
|
|
1105
1154
|
|
|
@@ -1113,6 +1162,20 @@ export default defineVitestConfig({ isolateGit: false, resolveFromSource: false
|
|
|
1113
1162
|
|
|
1114
1163
|
**A package declaring a `source` condition must ship its sources.** Every nmr consumer now resolves that condition, so a published package whose `files` carries `dist` alone resolves to a path that is not in the tarball. A package that publishes only its build output takes a bespoke condition name instead, the way `@williamthorsen/nmr-core` uses `nmr-source` for the build's own bootstrap.
|
|
1115
1164
|
|
|
1165
|
+
### Resolving through tsconfig paths
|
|
1166
|
+
|
|
1167
|
+
A third resolution setting sits alongside those two, and this one is off. `tsconfigPaths: true` emits Vite's `resolve.tsconfigPaths`, so a test reaches an aliased specifier through the `paths` its `tsconfig.json` declares, the way `tsc` does:
|
|
1168
|
+
|
|
1169
|
+
```ts
|
|
1170
|
+
export default defineVitestConfig({ tsconfigPaths: true });
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
It folds across layers like the other two, the last layer to declare it winning. It needs no `ssr` counterpart the way a condition does: Vite holds this setting outside its per-environment resolve options, so the single key reaches the environment a test's own imports resolve through.
|
|
1174
|
+
|
|
1175
|
+
**It is an opt-in because its silent direction is the reverse of the two defaults'.** Leaving it off fails loudly, with an unresolved import naming the specifier. Turning it on is the silent direction, for a repo that declares `paths` for `tsc` alone: a specifier that resolved through a package's `exports` starts resolving through the alias instead, and nothing in the run reports the switch. A repo declaring no `paths` is unaffected either way.
|
|
1176
|
+
|
|
1177
|
+
**It requires Vite 8**, where `resolve.tsconfigPaths` arrived. nmr declares `vite` as an optional peer at `>=8.0.0 <9`, so a repo holding a stale `vite` of its own hears about it at install time. That check does not reach a repo whose only Vite comes in through Vitest's own dependency, and Vitest 4 accepts Vite 6 and 7: on those, the emitted key does nothing and the aliased import fails exactly as it would with the flag off.
|
|
1178
|
+
|
|
1116
1179
|
### Customizing by scope
|
|
1117
1180
|
|
|
1118
1181
|
Vitest applies some options at the root of a `projects` config and others per project, and placing one at the wrong level is silent rather than loud. The factory therefore takes separate override surfaces instead of one merged config:
|
|
@@ -1167,6 +1230,8 @@ Both factories fold any number of layers left to right: a later layer wins on a
|
|
|
1167
1230
|
|
|
1168
1231
|
Order is the point where `setupFiles` is concerned, since a shared setup file establishes the environment the package's own then runs in. A later layer's `project` block likewise wins over an earlier layer's `tiers` target: the nearer config is the more deliberate.
|
|
1169
1232
|
|
|
1233
|
+
**A package's own `vite.config.ts` fills that one slot too.** Vitest searches for a config by ascending from the run root and stopping at the first directory holding any of `vitest.config.*` or `vite.config.*`, so a package carrying only a Vite config ends the search there and never reaches the root config. Within a directory `vitest.config.*` is tried first, which makes the fix a Vitest config beside the Vite one, calling `defineVitestConfig()` as any other package config does. Without it the package declares no projects and every tier-selecting command fails with `No projects matched the filter`, `nmr ci` among them. [nmr's readyup kit](#conformance-checks) reports a package in that state.
|
|
1234
|
+
|
|
1170
1235
|
**A path in a shared layer must be absolute.** Vitest resolves `setupFiles` against each project's own root, not against the module that declared the path, so a bare `'./vitest.setup.ts'` in a shared module names a different file in every package that consumes it -- and a package that happens to own a file by that name loads the wrong one instead of failing. The co-located form stays correct in a config file that declares the path directly.
|
|
1171
1236
|
|
|
1172
1237
|
**Do not merge two returned configs.** `mergeConfig(defineVitestConfig(), defineVitestConfig(mine))` looks like the idiomatic recovery and fails at startup: both sides declare the same four project names, which Vitest rejects with `Project name "unit" ... is not unique`. Layers merge the factory's inputs instead, which is why they yield four projects however many fold.
|
|
@@ -1175,7 +1240,15 @@ A config file that omits the shared layer still loses those settings, silently -
|
|
|
1175
1240
|
|
|
1176
1241
|
### What the config excludes
|
|
1177
1242
|
|
|
1178
|
-
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
1243
|
+
Collection skips `**/node_modules/**`, `**/.git/**`, `**/coverage/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, and `**/*.d.ts`.
|
|
1244
|
+
|
|
1245
|
+
`testCollectionExclude` adds to the collection list. It takes directory basenames rather than globs, matched at any depth, so the same array serves the [conventions check](#gating-the-test-file-conventions):
|
|
1246
|
+
|
|
1247
|
+
```ts
|
|
1248
|
+
export default defineVitestConfig({ testCollectionExclude: ['cypress', 'generated'] });
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
Every layer's entries are concatenated, and a name the shared config already prunes is emitted once. A repo needing a glob rather than a directory name has the `project` seam's own `exclude`.
|
|
1179
1252
|
|
|
1180
1253
|
Because both seams concatenate, a consumer can add an exclusion but never remove one. A pattern therefore earns its place in these lists only by preventing a _silent_ failure, one a consumer cannot self-diagnose. A visible failure, such as a stray file sitting at 0% in the coverage report, is left to the consumer's own `project` seam.
|
|
1181
1254
|
|
|
@@ -1212,7 +1285,7 @@ The projects were once named `unit`, `integration`, and `app`, for what a test _
|
|
|
1212
1285
|
2. Rename every `*.app.test.ts` onto the convention. `app` was never a tier, so keep it as the aspect segment and add the tier after it: `scaffold.app.test.ts` becomes `scaffold.app.unit.test.ts`.
|
|
1213
1286
|
3. Replace `nmr test:integration` with `nmr test:tool`, and `nmr root:test:integration` with `nmr root:test:tool`, wherever a script or workflow names them.
|
|
1214
1287
|
|
|
1215
|
-
**A file left without a tier is a silent failure.** The residual `unit` claims it and it runs in the default gate -- green, with nothing reporting that the separation was lost. [nmr's readyup kit](#conformance-checks) reports every such file
|
|
1288
|
+
**A file left without a tier is a silent failure.** The residual `unit` claims it and it runs in the default gate -- green, with nothing reporting that the separation was lost. Declare the [conventions check](#gating-the-test-file-conventions), which turns that state into a failing test run, or run [nmr's readyup kit](#conformance-checks), which reports every such file. Do one of the two before treating the migration as done.
|
|
1216
1289
|
|
|
1217
1290
|
`nmr test` also runs the `tool` tier, which the old `test` excluded. Those tests previously ran in no default selection at all: `check`, `check:strict`, and `ci` all inherited the exclusion, so a green pipeline said nothing about them. Expect `nmr test` to take longer and to surface failures that were never gated.
|
|
1218
1291
|
|
|
@@ -13,5 +13,5 @@ delivery: ambient
|
|
|
13
13
|
- A check that already passed on the current working tree skips, printing `⏭️ <scope>: <command>: passed …` and, where the recalled run left one, `— replayed: <excerpt>` of what that run concluded. Any edit, addition, or deletion re-runs it, so read the replayed excerpt rather than paying for the answer again: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`. A composite replays its constituents' excerpts, each attributed as `<scope>: <command>: <excerpt>` and cut at the line's ceiling where there are too many to fit. A skip carrying no excerpt is one whose run wrote to a terminal, or one whose constituents recorded none.
|
|
14
14
|
- `nmr --log <command>` prints the whole run behind that excerpt -- up to 256 KiB, led by when it was recorded, how long it ran, and the command string that produced it -- so output you need in full is a recording to read rather than a run to pay for again. It runs nothing, and it is held to the tree rather than to the terminal: a recording a piped run made prints for a reader at one. A composite prints its constituents' excerpts, so the transcript of one of them is `nmr -F <pkg> --log <command>` or that constituent's own name. Nothing to show is said on one line and exits non-zero, naming which it is.
|
|
15
15
|
- Every command nmr runs reports one line -- `✅ <scope>: <command>: passed in 12.4s`, `❌ … failed in 1.2s (exit 1)`, `⏭️ … passed 4m ago on this tree`, or `⛔ … skipped, the override is empty` -- and a composite reports alongside every command it expands into. The lines print in every verbosity, so `-q` is the low-noise way to run rather than a silent one: it withholds the output of the commands nmr runs and keeps nmr's own verdicts, which is how a pass, a cached skip, and a `""`/`":"` override stay distinguishable. Expect a run to be quiet already without the flag: nmr recognizes a known agent harness in the environment and takes quiet from that, as it does from `NMR_COMMAND_VERBOSITY` or the repo's `output.commandVerbosity`. A command whose output you need in full is one to run with `NMR_COMMAND_VERBOSITY=full`, which outranks both.
|
|
16
|
-
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing the test reaches -- `tool` (a program the environment supplies), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else.
|
|
16
|
+
- Every test file lives under a `__tests__` directory and names its isolation tier last: `<subject>[.<aspect>].<tier>.test.ts`, where any earlier segment is free-form documentation. A tier is the furthest thing the test reaches -- `tool` (a program the environment supplies), `localhost` (a service on this machine), `remote` (a machine that isn't this one), or `unit`, the residual claiming everything else. A file outside `__tests__`, or one whose tier segment is missing or misspelt, fails the test run in a repo that declares nmr's exported conventions check, which reports both. In a repo that has not, neither fails anything -- the first is collected by nothing and the second runs under `unit` -- and `rdy run --packages` reports the misspelt tier alone.
|
|
17
17
|
- The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), the test tiers' timeout budgets and the `tiers` seam that raises one, which script reaches which tier, and the check-result cache's key, the retention key a replayed excerpt is additionally held to, the retention ceiling `--log` prints within, its `checkCache` config, and every condition that disables it.
|
package/dist/esm/UserError.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A failure in what the user declared -- a config file, a `package.json` -- rather than a fault in nmr.
|
|
3
|
+
*
|
|
4
|
+
* The CLI boundary prints one of these as its message alone. A stack trace records where nmr noticed the
|
|
5
|
+
* problem, which says nothing about the file the reader has to edit.
|
|
6
|
+
*/
|
|
1
7
|
export declare class UserError extends Error {
|
|
2
8
|
constructor(message: string);
|
|
3
9
|
}
|
|
@@ -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
|
|
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[];
|