@williamthorsen/nmr 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.meta/changelog.json +114 -0
  2. package/.readyup/kits/default.js +92 -87
  3. package/.readyup/manifest.json +8 -8
  4. package/CHANGELOG.md +89 -0
  5. package/README.md +37 -28
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache.d.ts +4 -3
  8. package/dist/esm/check-cache.js +28 -28
  9. package/dist/esm/cli-build.js +3 -0
  10. package/dist/esm/cli-clean.js +3 -1
  11. package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
  12. package/dist/esm/cli-report-catalog.js +2 -1
  13. package/dist/esm/cli-report-overrides.js +2 -1
  14. package/dist/esm/commands/build-output.d.ts +2 -2
  15. package/dist/esm/commands/build-output.js +9 -9
  16. package/dist/esm/commands/build.d.ts +6 -1
  17. package/dist/esm/commands/build.js +73 -72
  18. package/dist/esm/commands/clean.d.ts +3 -2
  19. package/dist/esm/commands/clean.js +25 -24
  20. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
  21. package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
  22. package/dist/esm/commands/fmt.js +33 -33
  23. package/dist/esm/commands/report-catalog.d.ts +2 -1
  24. package/dist/esm/commands/report-catalog.js +14 -11
  25. package/dist/esm/commands/report-overrides.d.ts +2 -1
  26. package/dist/esm/commands/report-overrides.js +11 -10
  27. package/dist/esm/config.js +39 -29
  28. package/dist/esm/context.js +8 -8
  29. package/dist/esm/default-scripts.d.ts +2 -2
  30. package/dist/esm/default-scripts.js +5 -5
  31. package/dist/esm/glyphs.d.ts +9 -0
  32. package/dist/esm/glyphs.js +9 -0
  33. package/dist/esm/help.d.ts +2 -2
  34. package/dist/esm/help.js +36 -30
  35. package/dist/esm/helpers/clampToBytes.js +3 -3
  36. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  37. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  38. package/dist/esm/helpers/duration.d.ts +2 -2
  39. package/dist/esm/helpers/duration.js +7 -7
  40. package/dist/esm/helpers/filter-selection.js +12 -12
  41. package/dist/esm/helpers/findClosestName.js +15 -15
  42. package/dist/esm/helpers/package-json.d.ts +2 -2
  43. package/dist/esm/helpers/package-json.js +26 -25
  44. package/dist/esm/helpers/transcript.d.ts +2 -2
  45. package/dist/esm/helpers/transcript.js +14 -14
  46. package/dist/esm/output-style.d.ts +41 -0
  47. package/dist/esm/output-style.js +17 -0
  48. package/dist/esm/prettier.js +5 -5
  49. package/dist/esm/recording.d.ts +8 -5
  50. package/dist/esm/recording.js +27 -17
  51. package/dist/esm/report-format.d.ts +1 -1
  52. package/dist/esm/report-format.js +7 -7
  53. package/dist/esm/resolveBinStyles.d.ts +12 -0
  54. package/dist/esm/resolveBinStyles.js +16 -0
  55. package/dist/esm/resolver.d.ts +2 -2
  56. package/dist/esm/resolver.js +16 -16
  57. package/dist/esm/runCli.js +309 -221
  58. package/dist/esm/runner.d.ts +1 -1
  59. package/dist/esm/runner.js +18 -18
  60. package/dist/esm/steps.d.ts +17 -5
  61. package/dist/esm/steps.js +37 -28
  62. package/dist/esm/tests.d.ts +7 -7
  63. package/dist/esm/tests.js +20 -8
  64. package/dist/esm/tiers.d.ts +1 -1
  65. package/dist/esm/tiers.js +8 -8
  66. package/dist/esm/verbosity.d.ts +1 -1
  67. package/dist/esm/verbosity.js +7 -7
  68. package/dist/esm/verdict.d.ts +15 -4
  69. package/dist/esm/verdict.js +78 -59
  70. package/dist/esm/vitest-source-resolution.js +48 -46
  71. package/dist/esm/vitest.d.ts +2 -2
  72. package/dist/esm/vitest.js +74 -25
  73. package/dist/esm/workspace.d.ts +2 -18
  74. package/dist/esm/workspace.js +9 -67
  75. package/docs/check-cache.md +5 -5
  76. package/docs/reporting.md +20 -2
  77. package/docs/scripts.md +2 -2
  78. package/docs/vitest.md +6 -6
  79. package/docs/workspace.md +24 -1
  80. package/package.json +9 -10
  81. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  82. package/dist/esm/helpers/workspace-patterns.js +0 -29
package/CHANGELOG.md CHANGED
@@ -2,6 +2,95 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.39.0 — 2026-09-22
6
+
7
+ ### 🎉 Features
8
+
9
+ - Report why a workspace selects no package, and no-op nmr's own -R steps (#841)
10
+
11
+ - Drops a `-R` step that nmr composed into a script where the workspace holds no package, so `typecheck` and the five `test` scripts run there instead of failing the run.
12
+ - Adds a no-op verdict for a command that the drop leaves with no step at all.
13
+ - Replaces the single refusal for a workspace that selects no package with one message per cause.
14
+
15
+ Migration: In a repo that declares a workspace package pattern and deliberately holds no package, delete the `.config/nmr.config.ts` entries added to work around the `-R` refusal, the `build: ':'` override included.
16
+
17
+ - 🚨 **Breaking:** Prefix the booleans and reject unrecognized Vitest option keys (#849)
18
+
19
+ - Renames two boolean options of `defineVitestConfig` and `defineRootVitestConfig`, the factories that build a repo's Vitest config: `isolateGit` becomes `shouldIsolateGit`, and `resolveFromSource` becomes `shouldResolveFromSource`.
20
+ - Makes both factories reject an option key outside the recognized set, naming the offending keys and the recognized ones, rather than ignoring a misspelled or retired key and letting its option fall back to the default with nothing in the run reporting it.
21
+ - Adds a retired-key check that runs ahead of the unrecognized-key check, reporting the new name for a config that still sets `isolateGit` or `resolveFromSource`.
22
+
23
+ Migration: In each options object passed to `defineVitestConfig` or `defineRootVitestConfig`, rename `isolateGit` to `shouldIsolateGit` and `resolveFromSource` to `shouldResolveFromSource`, and correct any other key that the factory now rejects. `defineRootVitestConfig` recognizes `monorepoRoot` on the last layer alone, so a config that sets it on an earlier layer now throws where the old code ignored it.
24
+
25
+ - 🚨 **Breaking:** End nmr's source value names with the kind of the value (#850)
26
+
27
+ - Renames two fields of `TestFileConventionsReport`, `misplaced` and `untiered`, to `misplacedFiles` and `untieredFiles`, and renames `TestFileConventionsOptions`'s `exclude` to `excludedBasenames`, in the `@williamthorsen/nmr/tests` entry point.
28
+ - Adds a guard to `checkTestFileConventions` and `reportTestFileConventions` that throws a `TypeError` naming `excludedBasenames` when a call still sets `exclude`.
29
+
30
+ Migration: Read `report.misplacedFiles` and `report.untieredFiles` in place of `report.misplaced` and `report.untiered`, and pass `excludedBasenames` in place of `exclude`. A call that still sets `exclude` throws rather than running with no exclusions, so the failure surfaces where Vitest loads the call site.
31
+
32
+ - 🚨 **Breaking:** Hoist the pnpm-workspace resolver into nmr-core as a total API (#864)
33
+
34
+ - Adds a `./workspace` subpath export to `@williamthorsen/nmr-core`, whose `resolveWorkspace` returns a pnpm workspace's package directories, or the cause of an empty result, as a value rather than as a thrown error.
35
+ - Reports a `pnpm-workspace.yaml` that the YAML parser rejects under a cause of its own, `unreadable-manifest`, so `nmr -R build` names the syntax error and its remedy in place of failing with the parser's own error.
36
+ - Removes `diagnoseEmptyWorkspace` and `EmptyWorkspaceDiagnosis` from `@williamthorsen/nmr/workspace`, since `resolveWorkspace` reports the same cause in one call.
37
+
38
+ Migration: Replace each `diagnoseEmptyWorkspace` call with `resolveWorkspace`, which reports the cause on the empty result it already returns, and rename the `no-manifest` cause to `no-package`. `EmptyWorkspaceCause` also gains `unreadable-manifest`, so an exhaustive switch over it stops compiling until it handles the new member; every other export of `@williamthorsen/nmr/workspace` keeps its signature and its `UserError` throws.
39
+
40
+ - 🚨 **Breaking:** Adopt the shared workspace resolver in release-kit, honoring exclusions (#865)
41
+
42
+ - 🚨 **Breaking:** Narrows `no-pattern` in `@williamthorsen/nmr-core`'s `EmptyWorkspaceCause`, moving an absent or empty `packages` list onto `no-packages-list` and a value that is not a list of strings onto `unreadable-packages`.
43
+ - Stops `release-kit` from tagging, publishing, and label-syncing a package that a `!` entry in `pnpm-workspace.yaml` excludes, which its previous `glob`-based resolution could not remove from the match.
44
+ - Replaces the release of the root package alone with a failure naming the condition that emptied the workspace, when `pnpm-workspace.yaml` does not hold valid YAML or declares a workspace that does not resolve to a package.
45
+ - Replaces the `no-pattern` remedy that `nmr` printed for a `pnpm-workspace.yaml` declaring no `packages` list, and for one declaring a `packages` value that is not a list of strings.
46
+ - Raises `engines.node` to `>=24.16.0` in `@williamthorsen/nmr-core` and `@williamthorsen/release-kit`, because Node 24.0 through 24.15 ignores the `followSymlinks` option that the workspace matcher passes to `globSync` and drops a symlinked package directory from the resolution without reporting it.
47
+
48
+ Migration: Add a `no-packages-list` arm and an `unreadable-packages` arm to any `switch` that covers `EmptyWorkspaceCause` exhaustively. `no-pattern` now names only a list that the resolver read in full and whose entries all failed to become a positive pattern, so move the handling of an absent `packages` key, an empty list, and a value that is not a list of strings out of the `no-pattern` arm into the two new ones. Route `no-packages-list` to single-package behavior rather than to a failure path: pnpm resolves such a manifest to the root package alone.
49
+
50
+ ### ♻️ Refactoring
51
+
52
+ - Align nmr's function names with naming conventions (#852)
53
+
54
+ - Renames the functions in `packages/nmr` to align with naming conventions.
55
+
56
+ - Suffix nmr's unit-measuring numbers and expand its abbreviations (#854)
57
+
58
+ - Renames every unit-measuring number and abbreviated identifier under `packages/nmr/src`, adding `Ms`, `Bytes`, and `Offset` suffixes to the quantities and expanding `pkg`, `Len`, and `Idx` to `package`, `Length`, and `Index`. The renames are not part of any public API.
59
+
60
+ ### 🧪 Tests
61
+
62
+ - End nmr's test value names with the kind of the value (#851)
63
+
64
+ - Aligns the name of every non-boolean value in `packages/nmr/src/**/__tests__` with naming conventions.
65
+ - Renames `inline` to `inlineBody` in the repo-root `__tests__/reusable-workflow-runtime.app.unit.test.ts`.
66
+
67
+ ### 👷 CI
68
+
69
+ - Make the release workflow Corepack-agnostic (#842)
70
+
71
+ - Removes the `Disable Corepack` step from `release.reusable.yaml`, which Node 25's removal of Corepack from the distribution would make fail with `command not found` once `actions/setup-node` moves past Node 24, and keeps `COREPACK_ENABLE_STRICT: '0'`, which guards the source-build path where the pnpm `packageManager` pin survives.
72
+ - Adds a case to `reusable-workflow-runtime.app.unit.test.ts` that fails the test run on any reusable workflow whose `run:` commands invoke `corepack`, enumerated from `.github/workflows` so that a workflow added later is covered without editing a list.
73
+ - Drops the Corepack recommendation from the README's pnpm prerequisite, which now points at pnpm's installation page, and from the fix text of the `.tool-versions does not list pnpm` check in nmr's readyup kit, which now names the `packageManager` field.
74
+
75
+ ## 0.38.0 — 2026-09-18
76
+
77
+ ### 🎉 Features
78
+
79
+ - Give nmr plain output in pipes and CI logs (#838)
80
+
81
+ - Stops `nmr` from printing emoji to a pipe or a CI log: When the output stream is not a terminal or `CI` is set, a verdict reads `PASS`, `FAIL`, `SKIP`, or `NOOP` and a command icon is omitted, so that a log can be searched for a verdict.
82
+ - Adds `--output-style <auto|plain|rich>` to `nmr` and the `NMR_OUTPUT_STYLE` variable, each forcing either style; the flag outranks the variable, a value that names no style exits 1 with a usage error, and neither enters a check-cache key: A rich run and a plain run share a recorded pass.
83
+ - Replaces several rich glyphs, taking the status markers from the set that release-kit and v11y-check print so that one status has one glyph across the three tools: a recalled pass shows ⏩ in place of ⏭️, a skipped empty override shows ⚪ in place of ⛔, a warning and the `--log` refusal show 🟠 in place of ⚠️ and 📭, the `--log` header shows 💾 in place of 📼, and `nmr-ensure-prepublish-hooks` reports ✅ and ❌ in place of ✓ and ✗.
84
+
85
+ ### 🪦 Removed
86
+
87
+ - 🚨 **Breaking:** Rename the declinesArgs step declaration to shouldDeclineArguments (#822)
88
+
89
+ - Makes a `.config/nmr.config.ts` that still declares `declinesArgs` on a composite script's `{ run, declinesArgs }` step fail to load, with an error that names `shouldDeclineArguments` as the replacement.
90
+ - Renames the matching field of the `StepSpec` type exported by `@williamthorsen/nmr/config` and `@williamthorsen/nmr/scripts`.
91
+
92
+ Migration: Rename `declinesArgs` to `shouldDeclineArguments` in each composite script step of `.config/nmr.config.ts`, and in any code that sets or reads the field on a `StepSpec`.
93
+
5
94
  ## 0.37.0 — 2026-09-15
6
95
 
7
96
  ### 🎉 Features
package/README.md CHANGED
@@ -5,43 +5,50 @@
5
5
  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.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v0.37.0 (2026-09-15)
8
+ ## Release notes — v0.39.0 (2026-09-22)
9
9
 
10
10
  ### 🎉 Features
11
11
 
12
- - Guard lefthook install in root prepare and report unguarded installs (#798)
12
+ - Report why a workspace selects no package, and no-op nmr's own -R steps (#841)
13
13
 
14
- - Adds a `warn` check to the readyup kit of `@williamthorsen/nmr` that reports a root script run by `pnpm install`, such as `prepare` or `postinstall`, whose command calls `lefthook install` without `lefthook check-install`.
15
- - Includes in the check's suggested fix the guarded form `lefthook check-install || lefthook install`, which skips rewriting `.git/hooks` while `lefthook.yml` is unchanged since the last install, so that a repo using that form can run `pnpm install` without write access to `.git/hooks`, as inside the Claude Code sandbox, and under pnpm's default `verifyDepsBeforeRun` avoids a reinstall before every `pnpm exec`.
14
+ - Drops a `-R` step that nmr composed into a script where the workspace holds no package, so `typecheck` and the five `test` scripts run there instead of failing the run.
15
+ - Adds a no-op verdict for a command that the drop leaves with no step at all.
16
+ - Replaces the single refusal for a workspace that selects no package with one message per cause.
16
17
 
17
- - Report misplaced test files and a missing guard from the readyup kit (#804)
18
+ Migration: In a repo that declares a workspace package pattern and deliberately holds no package, delete the `.config/nmr.config.ts` entries added to work around the `-R` refusal, the `build: ':'` override included.
18
19
 
19
- - Adds an `error` check to the `@williamthorsen/nmr` readyup kit that reports each `*.test.ts` or `*.test.tsx` file outside a `__tests__` directory, which no Vitest project collects and which therefore runs nowhere.
20
- - Adds a `warn` check that passes once a test file under `__tests__` imports `checkTestFileConventions` from `@williamthorsen/nmr/tests`, and whose fix shows the one-line guard test to add.
21
- - Makes the kit skip both test-file checks once it finds that guard, because the guard reports the same files and applies the repo's `exclude` list, which the kit cannot read.
20
+ - 🚨 **Breaking:** Prefix the booleans and reject unrecognized Vitest option keys (#849)
22
21
 
23
- ### 🐛 Bug fixes
22
+ - Renames two boolean options of `defineVitestConfig` and `defineRootVitestConfig`, the factories that build a repo's Vitest config: `isolateGit` becomes `shouldIsolateGit`, and `resolveFromSource` becomes `shouldResolveFromSource`.
23
+ - Makes both factories reject an option key outside the recognized set, naming the offending keys and the recognized ones, rather than ignoring a misspelled or retired key and letting its option fall back to the default with nothing in the run reporting it.
24
+ - Adds a retired-key check that runs ahead of the unrecognized-key check, reporting the new name for a config that still sets `isolateGit` or `resolveFromSource`.
24
25
 
25
- - Fail when a -F pattern selects no workspace (#785)
26
+ Migration: In each options object passed to `defineVitestConfig` or `defineRootVitestConfig`, rename `isolateGit` to `shouldIsolateGit` and `resolveFromSource` to `shouldResolveFromSource`, and correct any other key that the factory now rejects. `defineRootVitestConfig` recognizes `monorepoRoot` on the last layer alone, so a config that sets it on an earlier layer now throws where the old code ignored it.
26
27
 
27
- - Fixes a run that reported success without doing anything. `nmr -F <pattern> <command>` printed nothing and exited 0 when the pattern matched no package. It now exits 1, names the rejected pattern, and explains the likely cause.
28
- - Covers three further selections that ran nothing and exited 0: an exclusion that leaves only the workspace root, such as `nmr -F '!./packages/*'`; `-R` in a workspace that declares no package; and an empty `-F ''`.
28
+ - 🚨 **Breaking:** End nmr's source value names with the kind of the value (#850)
29
29
 
30
- Migration: Replace any `-F` pattern that names a directory with the package's manifest `name`. A filter that selects nothing now fails the run instead of passing in silence.
30
+ - Renames two fields of `TestFileConventionsReport`, `misplaced` and `untiered`, to `misplacedFiles` and `untieredFiles`, and renames `TestFileConventionsOptions`'s `exclude` to `excludedBasenames`, in the `@williamthorsen/nmr/tests` entry point.
31
+ - Adds a guard to `checkTestFileConventions` and `reportTestFileConventions` that throws a `TypeError` naming `excludedBasenames` when a call still sets `exclude`.
31
32
 
32
- - Fix test runs failing on a dependency with a TypeScript source export (#793)
33
+ Migration: Read `report.misplacedFiles` and `report.untieredFiles` in place of `report.misplaced` and `report.untiered`, and pass `excludedBasenames` in place of `exclude`. A call that still sets `exclude` throws rather than running with no exclusions, so the failure surfaces where Vitest loads the call site.
33
34
 
34
- - Fixes the issue that, with `resolveFromSource` at its default of `true`, every test in a file failed with `Stripping types is currently unsupported for files under node_modules` when the file's imports reached a dependency whose `exports` map declares a `source` condition pointing at TypeScript.
35
- - Limits the source resolution enabled by `resolveFromSource` to packages whose real path lies outside every `node_modules`, which covers workspace packages and packages linked into the repo from elsewhere.
36
- - Makes an import of a workspace package fail when that package's `source` condition names a missing file, instead of resolving to the build output without warning.
37
- - Makes conditions that a config adds to `resolve.conditions` or `ssr.resolve.conditions` extend Vite's defaults when `resolveFromSource` is `false`, instead of replacing them.
35
+ - 🚨 **Breaking:** Hoist the pnpm-workspace resolver into nmr-core as a total API (#864)
38
36
 
39
- Migration: Point each workspace package's `source` condition at a file that exists, or remove the condition. Build a workspace package before running a test that reaches it through an externalized dependency (one that Vitest loads through Node instead of transforming): Node resolves that import without the `source` condition, so it loads the package's build output.
37
+ - Adds a `./workspace` subpath export to `@williamthorsen/nmr-core`, whose `resolveWorkspace` returns a pnpm workspace's package directories, or the cause of an empty result, as a value rather than as a thrown error.
38
+ - Reports a `pnpm-workspace.yaml` that the YAML parser rejects under a cause of its own, `unreadable-manifest`, so `nmr -R build` names the syntax error and its remedy in place of failing with the parser's own error.
39
+ - Removes `diagnoseEmptyWorkspace` and `EmptyWorkspaceDiagnosis` from `@williamthorsen/nmr/workspace`, since `resolveWorkspace` reports the same cause in one call.
40
40
 
41
- - Prevent a recursive or multi-package run from hanging under pnpm 12 (#814)
41
+ Migration: Replace each `diagnoseEmptyWorkspace` call with `resolveWorkspace`, which reports the cause on the empty result it already returns, and rename the `no-manifest` cause to `no-package`. `EmptyWorkspaceCause` also gains `unreadable-manifest`, so an exhaustive switch over it stops compiling until it handles the new member; every other export of `@williamthorsen/nmr/workspace` keeps its signature and its `UserError` throws.
42
42
 
43
- - Fixes a hang under pnpm 12: In a terminal, `nmr -R <command>`, a root command that runs in every package such as `nmr build`, and a `-F` pattern that selects several packages printed nothing and waited until Ctrl+C when a package command read keyboard input or changed the terminal's mode, as the spinner in `next build` does.
44
- - Gives no terminal input to each package of a `-R` run or a multi-package `-F` run, including a dependency-chain filter such as `-F 'app...'`, and keeps it for a `-F` filter that selects one package, which an interactive command such as `nmr -F <pkg> test:watch` needs.
43
+ - 🚨 **Breaking:** Adopt the shared workspace resolver in release-kit, honoring exclusions (#865)
44
+
45
+ - 🚨 **Breaking:** Narrows `no-pattern` in `@williamthorsen/nmr-core`'s `EmptyWorkspaceCause`, moving an absent or empty `packages` list onto `no-packages-list` and a value that is not a list of strings onto `unreadable-packages`.
46
+ - Stops `release-kit` from tagging, publishing, and label-syncing a package that a `!` entry in `pnpm-workspace.yaml` excludes, which its previous `glob`-based resolution could not remove from the match.
47
+ - Replaces the release of the root package alone with a failure naming the condition that emptied the workspace, when `pnpm-workspace.yaml` does not hold valid YAML or declares a workspace that does not resolve to a package.
48
+ - Replaces the `no-pattern` remedy that `nmr` printed for a `pnpm-workspace.yaml` declaring no `packages` list, and for one declaring a `packages` value that is not a list of strings.
49
+ - Raises `engines.node` to `>=24.16.0` in `@williamthorsen/nmr-core` and `@williamthorsen/release-kit`, because Node 24.0 through 24.15 ignores the `followSymlinks` option that the workspace matcher passes to `globSync` and drops a symlinked package directory from the resolution without reporting it.
50
+
51
+ Migration: Add a `no-packages-list` arm and an `unreadable-packages` arm to any `switch` that covers `EmptyWorkspaceCause` exhaustively. `no-pattern` now names only a list that the resolver read in full and whose entries all failed to become a positive pattern, so move the handling of an absent `packages` key, an empty list, and a value that is not a list of strings out of the `no-pattern` arm into the two new ones. Route `no-packages-list` to single-package behavior rather than to a failure path: pnpm resolves such a manifest to the root package alone.
45
52
  <!-- /section:release-notes -->
46
53
 
47
54
  ## Installation
@@ -176,13 +183,15 @@ Every command nmr runs reports one line naming the scope it ran at, the command,
176
183
  ```console
177
184
  ✅ nmr-core: test: passed in 12.4s
178
185
  ❌ nmr: build: failed in 1.2s (exit 1)
179
- ⏭️ nmr-core: test: passed 4m ago on this tree, saved ~12s
180
- ⛔ nmr-core: lint: skipped, the override is empty
186
+ ⏩ nmr-core: test: passed 4m ago on this tree, saved ~12s
187
+ ⚪ nmr-core: lint: skipped, the override is empty
181
188
  ```
182
189
 
183
- **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command whose override resolved to `""` or `":"` reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
190
+ **A pipe and a CI log get words rather than emoji.** The same four lines read `PASS`, `FAIL`, `SKIP`, and `NOOP` there, which `grep FAIL` finds. `--output-style` and `NMR_OUTPUT_STYLE` each force either style.
191
+
192
+ **Verdicts print in every verbosity.** `-q` withholds the output of the commands nmr runs, never nmr's own words: a passing quiet run reports its verdicts and nothing else. A command that ran nothing reports the skip rather than exiting 0 in silence, which is what separates it from a command that passed: an override resolving to `""` or `":"`, and a fan-out step that a package-free workspace left nothing to do. An `NMR_RUN_IF_PRESENT` miss reports nothing, having no command to report on.
184
193
 
185
- [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, and where the verbosity of a run comes from.
194
+ [What nmr reports](docs/reporting.md) covers how verdicts nest, the `--json` format, the output style, and where the verbosity of a run comes from.
186
195
 
187
196
  ## Check-result cache
188
197
 
@@ -193,7 +202,7 @@ $ nmr ci
193
202
  # ... four minutes of build, typecheck, lint, and tests ...
194
203
 
195
204
  $ nmr ci
196
- ⏭️ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
205
+ ⏩ node-monorepo-tools: ci: passed 2m ago on this tree, saved ~4m
197
206
  ```
198
207
 
199
208
  [Check-result cache](docs/check-cache.md) covers what is cached, what a hit requires, replaying and reading a recorded run, and bypassing or configuring the cache.
@@ -297,7 +306,7 @@ rdy list --from npm:@williamthorsen/nmr # what nmr publishes
297
306
  ## Documentation
298
307
 
299
308
  - [Scripts and configuration](docs/scripts.md): override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
300
- - [What nmr reports](docs/reporting.md): verdicts, `--json`, and verbosity
309
+ - [What nmr reports](docs/reporting.md): verdicts, `--json`, output style, and verbosity
301
310
  - [Check-result cache](docs/check-cache.md): what is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
302
311
  - [Dependency upgrades](docs/upgrades.md): `nmr upgrade`, the upgrade policy, `report-catalog`, `report-overrides`, and `nmr-taze`
303
312
  - [Standalone utilities](docs/utilities.md): `nmr-clean`, `nmr-compile`, `nmr-fmt`, and `ensure-prepublish-hooks`
@@ -10,8 +10,8 @@ delivery: ambient
10
10
  - Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
11
11
  - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
12
12
  - `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs the audit first, then `ci`, so a vulnerability stops the run before the long gate. Neither installs or binds a git hook.
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.
13
+ - A check that already passed on the current working tree skips, printing `SKIP <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
- - 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.
15
+ - Every command nmr runs reports one line -- `PASS <scope>: <command>: passed in 12.4s`, `FAIL … failed in 1.2s (exit 1)`, `SKIP … passed 4m ago on this tree`, or `NOOP … skipped, the override is empty` -- and a composite reports alongside every command it expands into. Those are the words a pipe and a CI log get; a terminal gets ✅, ❌, ⏩, and ⚪ in their place, and `--output-style rich` or `NMR_OUTPUT_STYLE=rich` forces the emoji on any stream. 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
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 both and warns that the check is missing.
17
17
  - The rest is in `node_modules/@williamthorsen/nmr/docs/`: `scripts.md` for pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`) and script overrides and their `""`/`":"` skip values; `upgrades.md` for `nmr upgrade` ceilings (report-only until `nmr upgrade --write`); `utilities.md` for the `nmr-compile` build (there is no repo-local build script); `vitest.md` for the test tiers' timeout budgets, the `tiers` seam that raises one, and which script reaches which tier; and `check-cache.md` for 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.
@@ -1,4 +1,5 @@
1
1
  import type { Writable } from 'node:stream';
2
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
3
  import type { OutputChannels } from './runner.js';
3
4
  import type { CheckCacheConfig, NmrConfig } from './types.js';
4
5
  /** A recorded pass: what ran, on which tree, and what it cost. */
@@ -52,7 +53,7 @@ export interface Retention {
52
53
  /** What nmr's own build has left on disk across the workspace. */
53
54
  export interface BuildOutputState {
54
55
  /** Covered packages whose output is absent. */
55
- missing: string[];
56
+ missingPackages: string[];
56
57
  /** The digest of the inputs each covered package's output was built from, keyed by package name. */
57
58
  digests: Record<string, string>;
58
59
  }
@@ -153,13 +154,13 @@ export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
153
154
  * package agrees. A package that has appeared or disappeared between them counts as a disagreement, because
154
155
  * the output the earlier observation describes is not the output the later one found.
155
156
  */
156
- export declare function findStaleBuildOutput(earlier: Record<string, string>, later: Record<string, string>): string | undefined;
157
+ export declare function findStaleBuildOutput(earlierDigests: Record<string, string>, laterDigests: Record<string, string>): string | undefined;
157
158
  /**
158
159
  * Renders the warning for a `--no-cache` that landed after the command name, where it is an argument to the
159
160
  * command rather than a flag to nmr. Passing it on unchanged is the honest thing to do with an argument, so
160
161
  * the warning is all that separates this from a silently un-bypassed run.
161
162
  */
162
- export declare function formatMisplacedNoCacheWarning(command: string): string;
163
+ export declare function formatMisplacedNoCacheWarning(command: string, style: OutputStyle): string;
163
164
  /**
164
165
  * Reports whether a command's passes are recorded at all, which is what separates a command with no recording
165
166
  * from one that could never have had one.
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { readFileSync, realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
- import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
5
+ import { hashWorkingTree, readCacheEntry, readHeadSha, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
6
  import { describeError } from '@williamthorsen/toolbelt.errors';
7
7
  import { hasBuildOutput, readBuildDigest } from "./commands/build-output.js";
8
8
  import { loadWorkspaceConfig } from "./config.js";
@@ -103,19 +103,19 @@ export function computeRetentionKey(options) {
103
103
  export function encodeTreeSnapshot(snapshot) {
104
104
  return `${snapshot.hash} ${snapshot.headSha}`;
105
105
  }
106
- export function findStaleBuildOutput(earlier, later) {
107
- const names = [...new Set([...Object.keys(earlier), ...Object.keys(later)])].toSorted();
108
- return names.find((name) => earlier[name] !== later[name]);
106
+ export function findStaleBuildOutput(earlierDigests, laterDigests) {
107
+ const names = [...new Set([...Object.keys(earlierDigests), ...Object.keys(laterDigests)])].toSorted();
108
+ return names.find((name) => earlierDigests[name] !== laterDigests[name]);
109
109
  }
110
- export function formatMisplacedNoCacheWarning(command) {
111
- return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
112
- `Did you mean \`nmr --no-cache ${command}\`?`);
110
+ export function formatMisplacedNoCacheWarning(command, style) {
111
+ return (`${STATUS_GLYPHS[style].warning.text} --no-cache after the command name is passed to \`${command}\`, ` +
112
+ `not read by nmr. Did you mean \`nmr --no-cache ${command}\`?`);
113
113
  }
114
114
  export function isCacheableCommand(checkCache, command) {
115
115
  return !isHookName(command) && checkCache?.enabled !== false && resolveCacheableCommands(checkCache).has(command);
116
116
  }
117
117
  export async function readBuildOutputState(monorepoRoot, config) {
118
- const state = { missing: [], digests: {} };
118
+ const state = { missingPackages: [], digests: {} };
119
119
  let packageDirs;
120
120
  try {
121
121
  packageDirs = getWorkspacePackageDirs(monorepoRoot);
@@ -138,17 +138,17 @@ export async function readBuildOutputState(monorepoRoot, config) {
138
138
  state.digests[name] = (await readBuildDigest(packageDir)) ?? '';
139
139
  }
140
140
  else {
141
- state.missing.push(name);
141
+ state.missingPackages.push(name);
142
142
  }
143
143
  }
144
144
  return state;
145
145
  }
146
146
  export async function readCheckCacheEntry(options) {
147
- const parsed = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
148
- if (parsed === undefined) {
147
+ const parsedEntry = await readJsonCacheEntry(resolveEntryPath(options), isParsedCheckCacheEntry);
148
+ if (parsedEntry === undefined) {
149
149
  return undefined;
150
150
  }
151
- const { retention, ...pass } = parsed;
151
+ const { retention, ...pass } = parsedEntry;
152
152
  return isRetention(retention) ? { ...pass, retention } : pass;
153
153
  }
154
154
  export async function readTranscript(ref) {
@@ -168,23 +168,23 @@ export async function removeCheckCache(scopeDir) {
168
168
  export function resolveCacheableCommands(checkCache) {
169
169
  const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
170
170
  const excludedCommands = checkCache?.excludeCommands ?? [];
171
- for (const excluded of excludedCommands) {
172
- commands.delete(excluded);
171
+ for (const excludedCommand of excludedCommands) {
172
+ commands.delete(excludedCommand);
173
173
  }
174
174
  return commands;
175
175
  }
176
176
  export function resolveRunId(env) {
177
- const inherited = env[RUN_ID_ENV_VAR];
178
- return inherited === undefined || inherited === '' ? randomUUID() : inherited;
177
+ const inheritedRunId = env[RUN_ID_ENV_VAR];
178
+ return inheritedRunId === undefined || inheritedRunId === '' ? randomUUID() : inheritedRunId;
179
179
  }
180
180
  export function resolveTreeSnapshot(options) {
181
- const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
182
- if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
183
- return { ok: true, snapshot: inherited };
181
+ const inheritedSnapshot = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
182
+ if (inheritedSnapshot !== undefined && readHeadSha(options.monorepoRoot) === inheritedSnapshot.headSha) {
183
+ return { ok: true, snapshot: inheritedSnapshot };
184
184
  }
185
- const hashed = hashWorkingTree(options.monorepoRoot);
186
- if (!hashed.ok) {
187
- return hashed;
185
+ const hashedTree = hashWorkingTree(options.monorepoRoot);
186
+ if (!hashedTree.ok) {
187
+ return hashedTree;
188
188
  }
189
189
  let physicalRoot;
190
190
  try {
@@ -193,10 +193,10 @@ export function resolveTreeSnapshot(options) {
193
193
  catch {
194
194
  return { ok: false, reason: `could not resolve ${options.monorepoRoot}` };
195
195
  }
196
- if (hashed.toplevel !== physicalRoot) {
197
- return { ok: false, reason: `the monorepo root is not the git toplevel (${hashed.toplevel})` };
196
+ if (hashedTree.toplevel !== physicalRoot) {
197
+ return { ok: false, reason: `the monorepo root is not the git toplevel (${hashedTree.toplevel})` };
198
198
  }
199
- return { ok: true, snapshot: { hash: hashed.hash, headSha: hashed.headSha } };
199
+ return { ok: true, snapshot: { hash: hashedTree.hash, headSha: hashedTree.headSha } };
200
200
  }
201
201
  export async function writeCheckCacheEntry(options) {
202
202
  await writeCacheEntry(resolveEntryPath(options), JSON.stringify(options.entry, undefined, 2));
@@ -212,11 +212,11 @@ function composeEnvParts(names, env) {
212
212
  return [name, value === undefined ? 'unset' : 'set', value ?? ''];
213
213
  });
214
214
  }
215
- function decodeTreeSnapshot(encoded) {
216
- if (encoded === undefined) {
215
+ function decodeTreeSnapshot(encodedSnapshot) {
216
+ if (encodedSnapshot === undefined) {
217
217
  return undefined;
218
218
  }
219
- const [hash, headSha] = encoded.split(' ', 2);
219
+ const [hash, headSha] = encodedSnapshot.split(' ', 2);
220
220
  if (hash === undefined || headSha === undefined || hash === '' || headSha === '') {
221
221
  return undefined;
222
222
  }
@@ -2,10 +2,13 @@ import { reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { buildPackage } from "./commands/build.js";
4
4
  import { loadWorkspaceConfig } from "./config.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
7
+ const { stdout } = resolveBinStyles();
6
8
  const packageDir = process.cwd();
7
9
  const { build } = await loadWorkspaceConfig(packageDir);
8
10
  await buildPackage(packageDir, {
11
+ style: stdout,
9
12
  ...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
10
13
  });
11
14
  }
@@ -1,8 +1,10 @@
1
+ import process from 'node:process';
1
2
  import { reportError } from '@williamthorsen/nmr-core';
2
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { runClean } from "./commands/clean.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
6
  try {
5
- await runClean(process.cwd());
7
+ await runClean(process.cwd(), resolveBinStyles().stdout);
6
8
  }
7
9
  catch (error) {
8
10
  reportError(describeError(error));
@@ -1,18 +1,20 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { DEFAULT_HOOK, ensurePrepublishHooks, reportPrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
4
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
5
6
  const flagSchema = {
6
- fix: { long: '--fix', type: 'boolean' },
7
- dryRun: { long: '--dry-run', type: 'boolean' },
7
+ shouldFix: { long: '--fix', type: 'boolean' },
8
+ isDryRun: { long: '--dry-run', type: 'boolean' },
8
9
  command: { long: '--command', type: 'string' },
9
10
  };
10
- const { fix, dryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
11
+ const { shouldFix, isDryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
11
12
  try {
13
+ const { stdout } = resolveBinStyles();
12
14
  const monorepoRoot = findMonorepoRoot();
13
- const options = command ? { fix, dryRun, command } : { fix, dryRun };
15
+ const options = command ? { shouldFix, isDryRun, command } : { shouldFix, isDryRun };
14
16
  const result = ensurePrepublishHooks(monorepoRoot, options);
15
- reportPrepublishHooks(result, command ?? DEFAULT_HOOK);
17
+ reportPrepublishHooks(result, command ?? DEFAULT_HOOK, stdout);
16
18
  if (result.hasFailures) {
17
19
  process.exitCode = 1;
18
20
  }
@@ -2,8 +2,9 @@ import process from 'node:process';
2
2
  import { reportError } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { reportCatalog } from "./commands/report-catalog.js";
5
+ import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
6
- reportCatalog(process.cwd());
7
+ reportCatalog(process.cwd(), resolveBinStyles().stderr);
7
8
  }
8
9
  catch (error) {
9
10
  reportError(describeError(error));
@@ -1,10 +1,11 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { reportOverrides } from "./commands/report-overrides.js";
4
+ import { resolveBinStyles } from "./resolveBinStyles.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
5
6
  try {
6
7
  const monorepoRoot = findMonorepoRoot();
7
- reportOverrides(monorepoRoot);
8
+ reportOverrides(monorepoRoot, resolveBinStyles().stderr);
8
9
  }
9
10
  catch (error) {
10
11
  reportError(describeError(error));
@@ -8,8 +8,8 @@ export interface BuildOptions {
8
8
  }
9
9
  /** The pair of directories a build publishes through, both siblings of the emit directory. */
10
10
  export interface ScratchDirs {
11
- previous: string;
12
- staging: string;
11
+ previousDir: string;
12
+ stagingDir: string;
13
13
  }
14
14
  export declare const DEFAULT_ENTRY_GLOBS: string[];
15
15
  /**
@@ -17,8 +17,8 @@ export async function hasBuildOutput(packageDir, options = {}) {
17
17
  return hasExpectedBuildOutput(packageDir, outdir, entryPoints);
18
18
  }
19
19
  export function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
20
- const emitsOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
21
- if (!emitsOutput) {
20
+ const doesEmitOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
21
+ if (!doesEmitOutput) {
22
22
  return true;
23
23
  }
24
24
  const outputDir = path.resolve(packageDir, outdir);
@@ -40,8 +40,8 @@ export function resolveScratchDirs(emitDir) {
40
40
  const parent = path.dirname(emitDir);
41
41
  const name = path.basename(emitDir);
42
42
  return {
43
- previous: path.join(parent, `.${name}.previous`),
44
- staging: path.join(parent, `.${name}.staging`),
43
+ previousDir: path.join(parent, `.${name}.previous`),
44
+ stagingDir: path.join(parent, `.${name}.staging`),
45
45
  };
46
46
  }
47
47
  export async function resolveToolchainFingerprint(packageDir, selfDir = SELF_DIR) {
@@ -54,14 +54,14 @@ export async function resolveToolchainFingerprint(packageDir, selfDir = SELF_DIR
54
54
  return readPackageJson(selfDir).version ?? '';
55
55
  }
56
56
  function isSameDir(left, right) {
57
- return toRealPath(left) === toRealPath(right);
57
+ return resolveRealPath(left) === resolveRealPath(right);
58
58
  }
59
- function toRealPath(dir) {
60
- const absolute = path.resolve(dir);
59
+ function resolveRealPath(dir) {
60
+ const absoluteDir = path.resolve(dir);
61
61
  try {
62
- return realpathSync(absolute);
62
+ return realpathSync(absoluteDir);
63
63
  }
64
64
  catch {
65
- return absolute;
65
+ return absoluteDir;
66
66
  }
67
67
  }
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import { type BuildOptions } from './build-output.js';
2
3
  /**
3
4
  * The toolchain a build's emit comes from: the resolved compiler and the nmr running it. Both join the build
@@ -7,6 +8,10 @@ export interface BuildToolchain {
7
8
  compilerVersion: string;
8
9
  fingerprint: string;
9
10
  }
11
+ /** What a build needs beyond its sources: the options shaping the emit, and the style it reports in. */
12
+ export interface BuildPackageOptions extends BuildOptions {
13
+ style: OutputStyle;
14
+ }
10
15
  /**
11
16
  * Compiles a package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` and
12
17
  * `.d.ts` from two programs and rewriting relative `.ts` specifiers and tsconfig `paths` aliases to
@@ -17,7 +22,7 @@ export interface BuildToolchain {
17
22
  * current inputs rather than an accumulation of every build that ever ran. Assets belong outside it, or in a
18
23
  * `build:post` hook, which runs after the output is published.
19
24
  */
20
- export declare function buildPackage(packageDir: string, options?: BuildOptions): Promise<void>;
25
+ export declare function buildPackage(packageDir: string, options: BuildPackageOptions): Promise<void>;
21
26
  /**
22
27
  * Produces a digest of the given files (paths and contents), the emit config, and the toolchain.
23
28
  * The file list is sorted so the digest is order-invariant, and each path is folded in so renames are detected.