@williamthorsen/nmr 0.41.0 → 0.43.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.
@@ -0,0 +1,2 @@
1
+ import { definePrettierConfig } from "./prettier.js";
2
+ export default definePrettierConfig();
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
2
  import { isBuiltin } from 'node:module';
3
3
  import path from 'node:path';
4
+ import { findDirectoryChainMatch } from '@williamthorsen/toolbelt.filesystem';
4
5
  import { parsePackageJson } from "./helpers/package-json.js";
5
6
  import { isObject } from "./helpers/type-guards.js";
6
7
  import { UserError } from "./UserError.js";
@@ -57,17 +58,10 @@ function findPackageDir(name, fromDir, cache) {
57
58
  if (cache?.has(key))
58
59
  return cache.get(key);
59
60
  let foundDir = findSelfReferenceDir(name, fromDir);
60
- let dir = fromDir;
61
- while (foundDir === undefined) {
62
- const candidate = path.join(dir, NODE_MODULES, name);
63
- if (existsSync(path.join(candidate, 'package.json'))) {
64
- foundDir = realpathSync(candidate);
65
- break;
66
- }
67
- const parent = path.dirname(dir);
68
- if (parent === dir)
69
- break;
70
- dir = parent;
61
+ if (foundDir === undefined) {
62
+ const match = findDirectoryChainMatch(fromDir, [path.join(NODE_MODULES, name, 'package.json')]);
63
+ if (match !== undefined)
64
+ foundDir = realpathSync(path.join(match.dir, NODE_MODULES, name));
71
65
  }
72
66
  cache?.set(key, foundDir);
73
67
  return foundDir;
package/docs/prettier.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Prettier config that `@williamthorsen/nmr/prettier` publishes: what it formats, how it scopes the shell plugin, and how to customize and adopt it.
4
4
 
5
- `prettier` is a peer dependency (`>=3.9.5 <4`), declared optional; the consuming repo provides it. The shell and Dockerfile plugin is a dependency of nmr rather than a peer, so its version is pinned centrally and two repos cannot format the same script differently.
5
+ `prettier` is a dependency of nmr. When the repo installs its own copy, `nmr-fmt` runs that copy instead, so that the editor and the formatter agree. When Prettier does not find a config searching from the working directory, `nmr-fmt` passes this config with `--config`, which also applies it to files below that directory; `.editorconfig` still applies alongside it. The shell and Dockerfile plugin is a dependency of nmr rather than a peer, so its version is pinned centrally and two repos cannot format the same script differently.
6
6
 
7
7
  ## What it formats
8
8
 
package/docs/scripts.md CHANGED
@@ -141,7 +141,7 @@ For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites
141
141
 
142
142
  ## Default commands
143
143
 
144
- `nmr --help` lists every default command and the shell command to which it resolves. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them. What the listing does not show is below.
144
+ `nmr --help` lists every default command and the shell command to which it resolves. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them. Under `Package scripts:`, the listing also shows each script in the current scope's `package.json` whose name the registry does not contain, a hook such as `build:pre` included; it omits npm and pnpm lifecycle scripts, such as `prepare` and `postinstall`, which the package manager runs. What the listing does not show is below.
145
145
 
146
146
  ### Build and CI
147
147
 
package/docs/utilities.md CHANGED
@@ -12,9 +12,11 @@ nmr-clean
12
12
 
13
13
  ## `nmr-compile`
14
14
 
15
- 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 keep 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 every input is unchanged 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.
15
+ 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 keep 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` or `.tsx` 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 every input is unchanged 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.
16
16
 
17
- **What it compiles.** Entry points are `src/**/*.ts` less the directories that contain test scaffolding rather than published code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#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 the compiler never emits a dangling specifier in its importer. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes): Helpers live in `test-utils/` precisely so that they stay covered.
17
+ **What it compiles.** Entry points are `src/**/*.{ts,tsx}` less the directories that contain test scaffolding rather than published code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#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 the compiler never emits a dangling specifier in its importer. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes): Helpers live in `test-utils/` precisely so that they stay covered.
18
+
19
+ **JSX.** A `.tsx` source compiles to `.js` like any other. The package's own tsconfig `jsx` option decides the transform, such as `react-jsx`; the build does not set one.
18
20
 
19
21
  **The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, so the directory contains exactly the current emit: A source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which composes correctly because it runs after the output is published. Because a bare `nmr compile` does not run any hook, assets that a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than replaced.
20
22
 
@@ -22,7 +24,7 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
22
24
 
23
25
  **What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package that nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that this build is about to write.
24
26
 
25
- `typescript` is a peer dependency (`>=5.7.0 <7`); the consuming repo provides it. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` extensions for them to be rewritten.
27
+ `typescript` is an optional peer dependency (`>=5.7.0 <7`), needed only by `nmr-compile`; the consuming repo provides it, and `nmr-compile` run without it fails with an error naming the peer and the version that it requires. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` or `.tsx` extensions for them to be rewritten.
26
28
 
27
29
  ```bash
28
30
  nmr-compile
@@ -32,9 +34,9 @@ nmr-compile
32
34
 
33
35
  Format, or check the formatting of, the files that git reports for the working directory. Exactly one of `--check` and `--write` is required; a bare invocation prints usage and exits non-zero rather than defaulting to a mutation. `--write` also lists the files that it rewrote. This is what `fmt` and `fmt:check` resolve to, in both registries.
34
36
 
35
- `prettier` is an optional peer dependency (`>=3.9.5 <4`); the consuming repo provides it, and it is resolved through the module graph rather than from `PATH`, so the copy that runs is the one that the repo declares and not whichever `prettier` happens to come first. A repository's formatter has to be the one that its editor and pre-commit hook also run, which is why nmr takes it from the consumer instead of bundling a copy. It is optional because a repo can use nmr purely as a script runner; one that formats without a resolvable Prettier gets a message naming the package and the range.
37
+ `prettier` is a dependency of nmr, but a repo's own copy takes precedence: `nmr-fmt` resolves Prettier from the working directory first and from nmr's installation second. A repository's formatter has to be the one that its editor and pre-commit hook also run, and resolution goes through the module graph rather than `PATH`, so the copy that runs is the one that the repo declares and not whichever `prettier` happens to come first. A repo that does not install Prettier, or nmr, formats with nmr's copy; when it does not have a Prettier config either, the [house config](prettier.md) applies.
36
38
 
37
- The floor is a currency policy, not a capability boundary. The design requires `--ignore-path` to honour every flag rather than only the last, so that a repository-root ignore file passed alongside a package-level one is not silently dropped. Prettier has done that since 3.0.0, while 2.x honoured only the final flag. The floor is at the current release because every consuming repo tracks it; lowering it to `>=3.0.0` would not affect correctness.
39
+ A repo's own copy must be Prettier 3.0.0 or later. The design requires `--ignore-path` to honour every flag rather than only the last, so that a repository-root ignore file passed alongside a package-level one is not silently dropped, and 2.x honoured only the final flag.
38
40
 
39
41
  ```bash
40
42
  nmr-fmt --check
@@ -47,7 +49,7 @@ nmr-fmt --write packages/nmr
47
49
 
48
50
  A file that git ignores is never formatted, even when named directly. Because trailing arguments are git pathspecs rather than Prettier flags, `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec that does not match any formattable file both fail it. Running outside a git repository fails rather than reporting a clean run.
49
51
 
50
- `nmr-fmt` drops paths that the index names but the filesystem does not have (a file deleted but not yet staged), and submodule gitlinks, before calling Prettier.
52
+ `nmr-fmt` drops paths that the index names but the filesystem does not have (a file deleted but not yet staged), submodule gitlinks, and symlinks whatever they point at, before calling Prettier, which rejects a symlink named explicitly. It also skips a path that it is denied access to, such as a `.envrc` that a sandbox shields, and names each one on stderr, so that a check that passes does not hide a file that it never read. Any other failure to stat a listed path fails the run, with an error that names the path.
51
53
 
52
54
  ## `ensure-prepublish-hooks`
53
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -77,32 +77,32 @@
77
77
  "!docs/*.v*.md"
78
78
  ],
79
79
  "dependencies": {
80
- "@williamthorsen/nmr-core": "0.13.1",
81
- "@williamthorsen/toolbelt.errors": "0.8.1",
80
+ "@williamthorsen/nmr-core": "0.13.2",
81
+ "@williamthorsen/toolbelt.errors": "0.8.2",
82
+ "@williamthorsen/toolbelt.filesystem": "0.9.2",
82
83
  "glob": "13.0.6",
84
+ "prettier": "3.9.9",
83
85
  "prettier-plugin-sh": "0.20.2",
84
- "taze": "21.1.0"
86
+ "taze": "21.3.0"
85
87
  },
86
88
  "devDependencies": {
87
- "@williamthorsen/toolbelt.filesystem": "0.9.1",
88
- "@williamthorsen/toolbelt.testing": "0.6.1",
89
- "@williamthorsen/toolbelt.vitest": "0.10.1",
89
+ "@williamthorsen/toolbelt.testing": "0.6.2",
90
+ "@williamthorsen/toolbelt.vitest": "0.10.2",
90
91
  "esbuild": "0.28.2",
91
- "prettier": "3.9.9",
92
- "readyup": "0.38.0",
92
+ "readyup": "0.40.0",
93
93
  "tinyglobby": "0.2.17",
94
94
  "typescript": "6.0.3",
95
95
  "vite": "8.3.1",
96
- "vitest": "4.1.11"
96
+ "vitest": "4.1.11",
97
+ "yaml": "2.9.1"
97
98
  },
98
99
  "peerDependencies": {
99
- "prettier": ">=3.9.5 <4",
100
100
  "typescript": ">=5.7.0 <7",
101
101
  "vite": ">=8.0.0 <9",
102
102
  "vitest": ">=4.0.0 <5"
103
103
  },
104
104
  "peerDependenciesMeta": {
105
- "prettier": {
105
+ "typescript": {
106
106
  "optional": true
107
107
  },
108
108
  "vite": {