@williamthorsen/nmr 0.20.0 → 0.22.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/README.md +258 -97
- package/bin/nmr-fmt.js +11 -0
- package/dist/esm/cli-fmt.d.ts +1 -0
- package/dist/esm/cli-fmt.js +3 -0
- package/dist/esm/commands/clean.js +2 -2
- package/dist/esm/commands/fmt.d.ts +23 -0
- package/dist/esm/commands/fmt.js +172 -0
- package/dist/esm/default-scripts.d.ts +1 -3
- package/dist/esm/default-scripts.js +16 -21
- package/dist/esm/help.js +2 -3
- package/dist/esm/prettier.d.ts +8 -0
- package/dist/esm/prettier.js +60 -0
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolve-scripts.js +3 -6
- package/dist/esm/resolver.d.ts +1 -2
- package/dist/esm/resolver.js +3 -6
- package/dist/esm/runCli.js +2 -4
- package/dist/esm/vitest.d.ts +15 -0
- package/dist/esm/vitest.js +69 -0
- package/dist/esm/workspace.js +7 -3
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -3,33 +3,17 @@
|
|
|
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.22.0 (2026-07-31)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
- 🚨 **Breaking:**
|
|
10
|
+
- 🚨 **Breaking:** Ship shell and Dockerfile formatting from a shared nmr Prettier config (#540)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Adds the ability to format shell scripts and Dockerfiles to `nmr fmt`. `@williamthorsen/nmr` now provides a common config for consistency across projects. Shell fences inside Markdown are now formatted as well. These features are absorbed into `nmr fmt` and `nmr fmt:check`; the `fmt:sh` and `fmt:all` commands are removed.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- 🚨 **Breaking:** Require the monorepo root in the shared root Vitest config (#541)
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
nmr now excludes packages matched by a `!`-prefixed pattern in `pnpm-workspace.yaml` from the packages it operates on. Deep globs such as `packages/**` now match nested packages, and running a command from inside a nested package targets that package rather than an enclosing ancestor. Exclusion patterns must be quoted to take effect. nmr now requires Node.js 24.16 or later.
|
|
19
|
-
|
|
20
|
-
- Report pnpm overrides when reviewing dependencies (#514)
|
|
21
|
-
|
|
22
|
-
From the monorepo root, `nmr upgrade` now reports the repo's active pnpm dependency overrides alongside the upgrade report, making clear when a pin is the reason an expected upgrade is absent. Repos that wired up this override report themselves with an install-time hook can now remove that hook.
|
|
23
|
-
|
|
24
|
-
- Trim the bundled agent guidance to a cheatsheet (#516)
|
|
25
|
-
|
|
26
|
-
Trims the agent guidance nmr installs into consuming repos from 100 lines to 10. Only the bare essentials are now injected into context. Agents are instructed to consult the package's README for details.
|
|
27
|
-
|
|
28
|
-
### 🐛 Bug fixes
|
|
29
|
-
|
|
30
|
-
- Anchor script execution to the resolved registry's directory (#517)
|
|
31
|
-
|
|
32
|
-
Fixes an issue where `nmr --workspace-root`, run from inside a workspace package, either failed outright or silently acted on the wrong directory. Commands now behave the same regardless of the directory they are invoked from: root commands act on the monorepo root, and package commands on their own package. Two changes for existing users: `nmr --workspace-root clean` now sweeps every workspace package instead of cleaning only the current one, and relative paths passed to a script now resolve against the directory the script runs in.
|
|
16
|
+
Modifies the testing configuration so that root-level test runs always cover the repository containing the configuration. The shared root Vitest config now requires a `monorepoRoot` option specifying an absolute path to the repository's own root, in place of the optional search origin. A repository nested inside a parent monorepo fails instead of resolving silently to its parent.
|
|
33
17
|
<!-- /section:release-notes -->
|
|
34
18
|
|
|
35
19
|
## Installation
|
|
@@ -56,17 +40,17 @@ nmr works out of the box with no configuration. It ships with built-in scripts f
|
|
|
56
40
|
From a package directory:
|
|
57
41
|
|
|
58
42
|
```bash
|
|
59
|
-
nmr test
|
|
60
|
-
nmr build
|
|
61
|
-
nmr check
|
|
43
|
+
nmr test # Run tests for the current package
|
|
44
|
+
nmr build # Compile to .js and .d.ts in one pass
|
|
45
|
+
nmr check # Typecheck, format check, lint check, and tests
|
|
62
46
|
```
|
|
63
47
|
|
|
64
48
|
From the monorepo root:
|
|
65
49
|
|
|
66
50
|
```bash
|
|
67
|
-
nmr test
|
|
68
|
-
nmr build
|
|
69
|
-
nmr ci
|
|
51
|
+
nmr test # Run root tests + recursive workspace tests
|
|
52
|
+
nmr build # Build all packages
|
|
53
|
+
nmr ci # Run build && check:strict && audit (full CI pipeline)
|
|
70
54
|
```
|
|
71
55
|
|
|
72
56
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -75,11 +59,11 @@ nmr detects where you are and selects the right scripts automatically — see [c
|
|
|
75
59
|
|
|
76
60
|
nmr's key feature is that the same command runs different scripts depending on where you invoke it. It walks up from your current directory to find `pnpm-workspace.yaml`, then checks whether your CWD is inside a workspace package directory.
|
|
77
61
|
|
|
78
|
-
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs
|
|
79
|
-
| --------------------------------- | ----------------- | ----------------- |
|
|
80
|
-
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test`
|
|
81
|
-
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest` (
|
|
82
|
-
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test`
|
|
62
|
+
| Where you run `nmr` | Registry used | Working directory | `nmr test` runs |
|
|
63
|
+
| --------------------------------- | ----------------- | ----------------- | --------------------------------------------------------------- |
|
|
64
|
+
| Monorepo root | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
65
|
+
| Inside a workspace package | Workspace scripts | The package root | `pnpm exec vitest --project '!integration'` (that package only) |
|
|
66
|
+
| Anywhere, with `--workspace-root` | Root scripts | Monorepo root | Root tests + `pnpm --recursive exec nmr test` |
|
|
83
67
|
|
|
84
68
|
Relative paths in a script resolve against that working directory, not the invocation directory.
|
|
85
69
|
|
|
@@ -227,9 +211,9 @@ The second example calls the bin directly, which sidesteps the workspace-versus-
|
|
|
227
211
|
The upgrade tool ([taze](https://github.com/antfu-collective/taze)) arrives with nmr, so your repo declares no dependency on it. Everything after the command name is passed through, including the range mode:
|
|
228
212
|
|
|
229
213
|
```bash
|
|
230
|
-
nmr upgrade
|
|
231
|
-
nmr upgrade major
|
|
232
|
-
nmr upgrade --write
|
|
214
|
+
nmr upgrade # upgrades available within each package's version ceilings
|
|
215
|
+
nmr upgrade major # major upgrades, still inside the ceilings
|
|
216
|
+
nmr upgrade --write # apply the proposals to package.json
|
|
233
217
|
```
|
|
234
218
|
|
|
235
219
|
### Configuring upgrades
|
|
@@ -259,38 +243,48 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
259
243
|
|
|
260
244
|
### Workspace scripts
|
|
261
245
|
|
|
262
|
-
| Command
|
|
263
|
-
|
|
|
264
|
-
| `build`
|
|
265
|
-
| `check`
|
|
266
|
-
| `check:strict`
|
|
267
|
-
| `clean`
|
|
268
|
-
| `compile`
|
|
269
|
-
| `fix`
|
|
270
|
-
| `fix:check`
|
|
271
|
-
| `fmt`
|
|
272
|
-
| `fmt:check`
|
|
273
|
-
| `lint`
|
|
274
|
-
| `lint:check`
|
|
275
|
-
| `lint:strict`
|
|
276
|
-
| `test`
|
|
277
|
-
| `test:
|
|
278
|
-
| `test:
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
246
|
+
| Command | Runs |
|
|
247
|
+
| ------------------ | -------------------------------------------------------- |
|
|
248
|
+
| `build` | `compile` |
|
|
249
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
250
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
251
|
+
| `clean` | `nmr-clean` |
|
|
252
|
+
| `compile` | `nmr-compile` |
|
|
253
|
+
| `fix` | `lint`, `fmt` |
|
|
254
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
255
|
+
| `fmt` | `nmr-fmt --write` |
|
|
256
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
257
|
+
| `lint` | `eslint --fix .` |
|
|
258
|
+
| `lint:check` | `eslint .` |
|
|
259
|
+
| `lint:strict` | `strict-lint` |
|
|
260
|
+
| `test` | `pnpm exec vitest --project '!integration'` |
|
|
261
|
+
| `test:all` | `pnpm exec vitest` |
|
|
262
|
+
| `test:coverage` | `pnpm exec vitest --project '!integration' --coverage` |
|
|
263
|
+
| `test:integration` | `pnpm exec vitest --project integration` |
|
|
264
|
+
| `test:watch` | `pnpm exec vitest --project '!integration' --watch` |
|
|
265
|
+
| `typecheck` | `tsgo --noEmit` |
|
|
266
|
+
| `upgrade` | `nmr-taze --include-locked` |
|
|
267
|
+
| `view-coverage` | `open coverage/index.html` |
|
|
268
|
+
|
|
269
|
+
`fmt` and `fmt:check` select their files through git rather than by walking the directory, which is what makes a package-level run and a root-level run apply the same ignore rules — see [file selection](#file-selection).
|
|
270
|
+
|
|
271
|
+
#### Test selections
|
|
272
|
+
|
|
273
|
+
Every package resolves the same five test commands. Nothing is detected on disk: the commands select [Vitest projects](#shared-vitest-config), so a package separates its integration tests by naming them `*.int.test.ts`, not by carrying extra config files.
|
|
274
|
+
|
|
275
|
+
| To run | Command |
|
|
276
|
+
| ----------------------------------- | ------------------ |
|
|
277
|
+
| Everything except integration tests | `test` |
|
|
278
|
+
| Integration tests alone | `test:integration` |
|
|
279
|
+
| Every project | `test:all` |
|
|
280
|
+
|
|
281
|
+
`test` negates `integration` rather than naming the code-only projects, so a category added later joins the default run instead of being dropped from it.
|
|
282
|
+
|
|
283
|
+
`test:coverage` and `test:watch` are the same selection as `test` in a different mode. There are no `test:integration --coverage` equivalents because none are needed: everything after a command name is forwarded untouched, so `nmr test:integration --coverage` already works.
|
|
284
|
+
|
|
285
|
+
To narrow to a single project, go through `test:all`: `nmr test:all --project app`. Narrowing through `test` does not work, because a second `--project` widens the filter rather than restricting it.
|
|
286
|
+
|
|
287
|
+
A run that collects no test files passes. That is what lets `nmr test:integration` fan out across a monorepo in which most packages have no integration tests, and it means a package with no tests at all needs no `"test": ""` override.
|
|
294
288
|
|
|
295
289
|
### Root scripts
|
|
296
290
|
|
|
@@ -318,11 +312,19 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
318
312
|
|
|
319
313
|
#### Test
|
|
320
314
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
|
324
|
-
|
|
|
325
|
-
| `test
|
|
315
|
+
The same five names the workspace registry carries, so a command means the same thing from the monorepo root as from inside a package. Each fans out to root-level files and every workspace package.
|
|
316
|
+
|
|
317
|
+
| Command | Runs |
|
|
318
|
+
| ------------------ | ------------------------------------------------------------------------- |
|
|
319
|
+
| `test` | `nmr root:test && pnpm --recursive exec nmr test` |
|
|
320
|
+
| `test:all` | `nmr root:test:all && pnpm --recursive exec nmr test:all` |
|
|
321
|
+
| `test:coverage` | `nmr root:test && pnpm --recursive exec nmr test:coverage` |
|
|
322
|
+
| `test:integration` | `nmr root:test:integration && pnpm --recursive exec nmr test:integration` |
|
|
323
|
+
| `test:watch` | `vitest --project '!integration' --watch` |
|
|
324
|
+
|
|
325
|
+
`test:coverage` chains `root:test` rather than a `root:test:coverage`, because the root config reports no coverage of its own; packages cover their own sources.
|
|
326
|
+
|
|
327
|
+
`test:watch` is the exception to the fan-out shape, and deliberately omits `--config`: bare `vitest` at the monorepo root resolves the root `vitest.config.ts`, whose projects then cover the whole tree in one process. A chain like the others would never advance past its first watcher. To watch root-level files alone, run `nmr root:test --watch`.
|
|
326
328
|
|
|
327
329
|
#### Typecheck
|
|
328
330
|
|
|
@@ -340,12 +342,16 @@ A package gets this variant automatically when it contains a `vitest.integration
|
|
|
340
342
|
|
|
341
343
|
#### Format
|
|
342
344
|
|
|
343
|
-
| Command | Runs
|
|
344
|
-
| ----------- |
|
|
345
|
-
| `fmt` | `
|
|
346
|
-
| `fmt:
|
|
347
|
-
|
|
348
|
-
|
|
345
|
+
| Command | Runs |
|
|
346
|
+
| ----------- | ----------------- |
|
|
347
|
+
| `fmt` | `nmr-fmt --write` |
|
|
348
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
349
|
+
|
|
350
|
+
#### File selection
|
|
351
|
+
|
|
352
|
+
`fmt` and `fmt:check` format the files git reports: tracked files, plus untracked files git does not ignore. Prettier reads `.gitignore` and `.prettierignore` from the working directory alone, so a pattern in `packages/<pkg>/.gitignore` never reaches a run started at the monorepo root. git knows the whole hierarchy, including `.git/info/exclude` and `core.excludesFile`, and every `.prettierignore` in the tree is discovered from the repository root, so package-level exclusions apply from any directory as well.
|
|
353
|
+
|
|
354
|
+
A file git ignores is never formatted, even when named directly. Trailing arguments are git pathspecs rather than Prettier flags, so `nmr fmt:check packages/nmr` narrows the run, while an unrecognized option and a pathspec matching no formattable file both fail it. Running outside a git repository fails rather than reporting a clean run.
|
|
349
355
|
|
|
350
356
|
#### Audit
|
|
351
357
|
|
|
@@ -367,15 +373,17 @@ See [dependency upgrades](#dependency-upgrades) for the workflow and its configu
|
|
|
367
373
|
|
|
368
374
|
These scripts operate on root-level code only (not workspace packages):
|
|
369
375
|
|
|
370
|
-
| Command
|
|
371
|
-
|
|
|
372
|
-
| `root:check`
|
|
373
|
-
| `root:lint`
|
|
374
|
-
| `root:lint:check`
|
|
375
|
-
| `root:lint:strict`
|
|
376
|
-
| `root:test`
|
|
377
|
-
| `root:
|
|
378
|
-
| `root:
|
|
376
|
+
| Command | Runs |
|
|
377
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
378
|
+
| `root:check` | `root:typecheck`, `fmt:check`, `root:lint:check`, `root:test` |
|
|
379
|
+
| `root:lint` | `eslint --fix --ignore-pattern 'packages/**' .` |
|
|
380
|
+
| `root:lint:check` | `eslint --ignore-pattern 'packages/**' .` |
|
|
381
|
+
| `root:lint:strict` | `strict-lint --ignore-pattern 'packages/**' .` |
|
|
382
|
+
| `root:test` | `vitest --config ./vitest.root.config.ts --project '!integration'` |
|
|
383
|
+
| `root:test:all` | `vitest --config ./vitest.root.config.ts` |
|
|
384
|
+
| `root:test:integration` | `vitest --config ./vitest.root.config.ts --project integration` |
|
|
385
|
+
| `root:typecheck` | `tsgo --noEmit` |
|
|
386
|
+
| `root:upgrade` | `nmr-taze --include-locked` |
|
|
379
387
|
|
|
380
388
|
#### Utilities
|
|
381
389
|
|
|
@@ -407,19 +415,19 @@ Position determines ownership: flags before the command name are nmr's own, and
|
|
|
407
415
|
|
|
408
416
|
```bash
|
|
409
417
|
# From a package directory
|
|
410
|
-
nmr test
|
|
411
|
-
nmr build
|
|
418
|
+
nmr test # Run workspace test script
|
|
419
|
+
nmr build # Compile to .js and .d.ts in one pass
|
|
412
420
|
|
|
413
421
|
# From the monorepo root
|
|
414
|
-
nmr test
|
|
415
|
-
nmr ci
|
|
422
|
+
nmr test # Root tests + recursive workspace tests
|
|
423
|
+
nmr ci # build + check:strict + audit
|
|
416
424
|
|
|
417
425
|
# Target specific packages
|
|
418
|
-
nmr --filter core test
|
|
419
|
-
nmr --recursive lint
|
|
426
|
+
nmr --filter core test # Test only the core package
|
|
427
|
+
nmr --recursive lint # Lint all workspace packages
|
|
420
428
|
|
|
421
429
|
# Force root context from anywhere
|
|
422
|
-
nmr --workspace-root check
|
|
430
|
+
nmr --workspace-root check # Run root check from a package dir
|
|
423
431
|
```
|
|
424
432
|
|
|
425
433
|
## Additional subcommands
|
|
@@ -439,8 +447,8 @@ nmr report-overrides
|
|
|
439
447
|
Sync the agent-facing guidance shipped with nmr into the consuming repo.
|
|
440
448
|
|
|
441
449
|
```bash
|
|
442
|
-
nmr sync-agent-files
|
|
443
|
-
nmr sync-agent-files --check
|
|
450
|
+
nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
|
|
451
|
+
nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
|
|
444
452
|
```
|
|
445
453
|
|
|
446
454
|
Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
|
|
@@ -484,6 +492,23 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
484
492
|
nmr-compile
|
|
485
493
|
```
|
|
486
494
|
|
|
495
|
+
### `nmr-fmt`
|
|
496
|
+
|
|
497
|
+
Format, or check the formatting of, the files 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 it rewrote. This is what `fmt` and `fmt:check` resolve to, in both registries.
|
|
498
|
+
|
|
499
|
+
The selection is tracked files plus untracked files git does not ignore, scoped to the working directory, with every `.prettierignore` in the tree discovered from the repository root and passed to Prettier explicitly — see [file selection](#file-selection) for what that buys. Paths the index names but the filesystem does not have (a file deleted but not yet staged) and submodule gitlinks are both dropped, so neither reaches Prettier.
|
|
500
|
+
|
|
501
|
+
Trailing arguments are git pathspecs, not Prettier flags: they narrow the selection, an unrecognized option is rejected rather than forwarded, and pathspecs that match no formattable file fail rather than passing quietly.
|
|
502
|
+
|
|
503
|
+
`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 the repo declares and not whichever `prettier` happens to come first. A repository's formatter has to be the one 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.
|
|
504
|
+
|
|
505
|
+
The floor is a currency policy, not a capability boundary. What the design requires is `--ignore-path` honouring every flag rather than only the last, so 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 sits at the current release because every consuming repo tracks it; lowering it to `>=3.0.0` would cost correctness nothing.
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
nmr-fmt --check
|
|
509
|
+
nmr-fmt --write packages/nmr
|
|
510
|
+
```
|
|
511
|
+
|
|
487
512
|
### `nmr-taze`
|
|
488
513
|
|
|
489
514
|
Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what `root:upgrade` resolves to, and what the root `upgrade` script ends with — see [dependency upgrades](#dependency-upgrades) for the workflow.
|
|
@@ -537,8 +562,144 @@ for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
|
537
562
|
|
|
538
563
|
`findMonorepoRoot(startDir?)` walks up from `startDir`, defaulting to `process.cwd()`, until it reaches a directory containing `pnpm-workspace.yaml`. It throws if it runs out of parent directories without finding one.
|
|
539
564
|
|
|
540
|
-
`getWorkspacePackageDirs(monorepoRoot)` reads the workspace patterns from that repo's `pnpm-workspace.yaml` and resolves them to absolute package directories, sorted and free of duplicates. Patterns carry pnpm's own semantics: `packages/*`, deeper globs such as `packages/**`, exact paths such as `tools/cli`, and `!`-prefixed exclusions such as `!packages/legacy` or `!**/test/**`, which filter every directory the positive patterns matched regardless of where they appear in the list. Nothing under `node_modules` is ever returned.
|
|
565
|
+
`getWorkspacePackageDirs(monorepoRoot)` reads the workspace patterns from that repo's `pnpm-workspace.yaml` and resolves them to absolute package directories, sorted and free of duplicates. It throws if `monorepoRoot` holds no `pnpm-workspace.yaml`. Patterns carry pnpm's own semantics: `packages/*`, deeper globs such as `packages/**`, exact paths such as `tools/cli`, and `!`-prefixed exclusions such as `!packages/legacy` or `!**/test/**`, which filter every directory the positive patterns matched regardless of where they appear in the list. Nothing under `node_modules` is ever returned.
|
|
541
566
|
|
|
542
567
|
One divergence from pnpm: a directory counts as a package only if it holds a `package.json`, not a `package.yaml` or `package.json5`.
|
|
543
568
|
|
|
544
569
|
Quote exclusion patterns in the manifest — `- '!packages/legacy'`. An unquoted `!` opens a YAML tag rather than a string, so the entry never reaches nmr (or pnpm) as a pattern.
|
|
570
|
+
|
|
571
|
+
## Shared Prettier config
|
|
572
|
+
|
|
573
|
+
Every repo consuming nmr otherwise maintains its own copy of the house Prettier options, and wires up shell formatting itself. The `@williamthorsen/nmr/prettier` subpath publishes both as a factory, so a repo declares only what it customizes:
|
|
574
|
+
|
|
575
|
+
```js
|
|
576
|
+
// .prettierrc.js
|
|
577
|
+
import { definePrettierConfig } from '@williamthorsen/nmr/prettier';
|
|
578
|
+
|
|
579
|
+
export default definePrettierConfig();
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
`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.
|
|
583
|
+
|
|
584
|
+
### What it formats
|
|
585
|
+
|
|
586
|
+
Beyond everything Prettier already handles, this config formats `.sh`, `.bash`, `.zsh`, and the other extensions and dotfile names of the Shell language, plus `Dockerfile` and `Containerfile`. `nmr fmt` therefore covers shell scripts with no extra script, glob, or workflow entry.
|
|
587
|
+
|
|
588
|
+
Shell output matches `shfmt` run with no flags: `binaryNextLine`, `spaceRedirects`, and `switchCaseIndent` are pinned to shfmt's CLI defaults, which the underlying plugin inverts. A repo whose scripts shfmt already formatted sees no diff on adoption.
|
|
589
|
+
|
|
590
|
+
### Language scoping
|
|
591
|
+
|
|
592
|
+
`prettier-plugin-sh` routes 20 further file types to the same shell parser, among them `.gitignore`, `.env`, `.csh`, `.nu`, `.properties`, `.ics`, `.vcf`, `CODEOWNERS`, and `hosts`. Since `nmr fmt` hands git's whole file list to Prettier, registering the plugin as shipped would make every one of them formattable — and the shell parser either fails on them or silently rewrites them. A `.gitignore` pattern such as `a(b)c` fails to parse, and an unquoted `&` in a `.env` or `.properties` value is split across two lines.
|
|
593
|
+
|
|
594
|
+
This config registers a narrowed plugin, so Prettier infers a parser for the Shell and Dockerfile languages alone. `.flaskenv`, `gradlew`, and `mvnw` are dropped from Shell as well: the first holds dotenv content the plugin misfiles as shell, and the other two are vendored wrappers their generators would overwrite.
|
|
595
|
+
|
|
596
|
+
The parsers themselves stay registered, so a repo that wants a dropped language back assigns it explicitly:
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
export default definePrettierConfig({
|
|
600
|
+
additionalOverrides: [{ files: ['APKBUILD', '*.ebuild'], options: { parser: 'sh' } }],
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Customizing
|
|
605
|
+
|
|
606
|
+
Any Prettier option passed to the factory spreads over the defaults:
|
|
607
|
+
|
|
608
|
+
```js
|
|
609
|
+
export default definePrettierConfig({
|
|
610
|
+
printWidth: 100,
|
|
611
|
+
additionalPlugins: [await import('prettier-plugin-tailwindcss')],
|
|
612
|
+
additionalOverrides: [{ files: ['*.md'], options: { proseWrap: 'always' } }],
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`additionalPlugins` and `additionalOverrides` append to what the config declares, so a later override wins over nmr's own and no seam can drop shell support. Passing `plugins` or `overrides` throws, and both are typed `never` — replacement is inexpressible rather than merely discouraged, because a silently ignored key would produce no diff to notice.
|
|
617
|
+
|
|
618
|
+
### Adoption caveats
|
|
619
|
+
|
|
620
|
+
- **Indentation follows `.editorconfig`**, as it did under shfmt: Prettier maps `indent_style` and `indent_size` to `useTabs` and `tabWidth`, and the plugin honours both. With no `.editorconfig`, the two disagree — shfmt indents shell with tabs, Prettier with two spaces. Add `indent_style = tab` under `[*.sh]` to keep tabs.
|
|
621
|
+
- **Shell fences in Markdown are formatted**, which collapses hand-aligned comment columns in documentation. Prettier keeps the original text when an embedded snippet fails to parse, so illustrative or truncated fences are safe. Use `<!-- prettier-ignore -->` before a fence whose alignment matters.
|
|
622
|
+
- **Adopting the house options reformats the repo.** `singleQuote`, `trailingComma`, and the rest apply on the first run; review that diff separately from the shell one.
|
|
623
|
+
|
|
624
|
+
## Shared Vitest config
|
|
625
|
+
|
|
626
|
+
Every repo consuming nmr otherwise writes and maintains its own Vitest config. The `@williamthorsen/nmr/vitest` subpath publishes that config as a factory, so a repo declares only what it customizes:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
// vitest.config.ts
|
|
630
|
+
import { defineVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
631
|
+
|
|
632
|
+
export default defineVitestConfig();
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
`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.
|
|
636
|
+
|
|
637
|
+
### Test categories
|
|
638
|
+
|
|
639
|
+
The config declares three projects, named for what the tests cover:
|
|
640
|
+
|
|
641
|
+
| Project | Matches | Covers |
|
|
642
|
+
| ------------- | -------------------------------------- | ---------------------------------------- |
|
|
643
|
+
| `unit` | every test file the others don't claim | source code |
|
|
644
|
+
| `integration` | `*.int.test.{ts,tsx}` | behavior beyond source code |
|
|
645
|
+
| `app` | `*.app.test.{ts,tsx}` | the app's own tooling, e.g. drift checks |
|
|
646
|
+
|
|
647
|
+
All three match only under a `__tests__` directory. Select them at run time with `--project`, which accepts negation:
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
vitest --project '!integration' # everything except integration tests
|
|
651
|
+
vitest --project integration # integration tests alone
|
|
652
|
+
vitest # every project
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
`unit` is defined by subtracting the other categories rather than by an allow-list of suffixes, so a file such as `parser.smoke.test.ts` runs under `unit` instead of being silently dropped.
|
|
656
|
+
|
|
657
|
+
### Customizing by scope
|
|
658
|
+
|
|
659
|
+
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 two separate override surfaces instead of one merged config:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
export default defineVitestConfig({
|
|
663
|
+
// Vite-level options, plus the test options Vitest honours only at the root.
|
|
664
|
+
root: { resolve: { conditions: ['development'] } },
|
|
665
|
+
// Applied to every project.
|
|
666
|
+
project: { setupFiles: ['./vitest.setup.ts'] },
|
|
667
|
+
});
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
`root` is typed to accept only the options that work at the root, so writing a per-project option there is a compile error rather than a setting that never runs. Both surfaces merge into the generated config rather than replacing it, so overriding one coverage field leaves the rest intact.
|
|
671
|
+
|
|
672
|
+
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and neither seam can narrow `include` or drop a default exclusion. Adding an `include` pattern through `project` widens all three projects at once, so a file matching it is collected by each and runs three times.
|
|
673
|
+
|
|
674
|
+
### Root-scoped tests
|
|
675
|
+
|
|
676
|
+
A monorepo's own root-level tests need a second config, because the package config is found by walking up from a package directory:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
// vitest.root.config.ts
|
|
680
|
+
import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
681
|
+
|
|
682
|
+
export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
This variant reads `pnpm-workspace.yaml` and excludes every workspace package from all three projects, so a root run covers only root-level files. It reports no coverage of its own — packages cover their own sources.
|
|
686
|
+
|
|
687
|
+
`monorepoRoot` is required, and because the config sits at the monorepo root, it is always `import.meta.dirname`. Stating the root rather than searching for it is what makes the exclusions describe this repo: a search from the working directory would resolve whichever monorepo the run started in, and every project is then pinned to that one. A directory holding no `pnpm-workspace.yaml` throws, naming the directory.
|
|
688
|
+
|
|
689
|
+
### Migrating from the config-file variants
|
|
690
|
+
|
|
691
|
+
nmr once selected a package's test scripts by looking for a `vitest.integration.config.ts` on disk, which meant three config files per package that separated its integration tests. Those files are no longer consulted. **Migrate the config and the scripts together**: one without the other leaves the repo in one of the two states below.
|
|
692
|
+
|
|
693
|
+
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig({ monorepoRoot: import.meta.dirname })`.
|
|
694
|
+
2. Delete every `vitest.integration.config.ts` and `vitest.standalone.config.ts`, plus any per-package `vitest.config.ts` that only re-exports an ancestor. Vitest resolves config by walking up from the run root, so those are redundant.
|
|
695
|
+
3. Rename integration tests to `*.int.test.ts` and tooling or drift tests to `*.app.test.ts`.
|
|
696
|
+
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
697
|
+
|
|
698
|
+
Until step 1 is done, an upgraded nmr behaves as follows against a config that declares no projects:
|
|
699
|
+
|
|
700
|
+
| Command | Result |
|
|
701
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
702
|
+
| `nmr test` / `test:coverage` | **Succeeds, and runs integration tests too.** The separation is lost with no error, because `--project '!integration'` matches nothing to exclude. |
|
|
703
|
+
| `nmr test:integration` | **Fails** with `No projects matched the filter "integration"`. |
|
|
704
|
+
|
|
705
|
+
The second is the reliable signal that migration is incomplete. The first is silent, so do not treat a green `nmr test` as evidence that the move succeeded.
|
package/bin/nmr-fmt.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
await import('../dist/esm/cli-fmt.js');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
+
process.stderr.write('nmr-fmt: build output not found — run `pnpm run build` first\n');
|
|
7
|
+
} else {
|
|
8
|
+
process.stderr.write(`nmr-fmt: failed to load: ${error.message}\n`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,7 +2,7 @@ import { rm } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { findContainingPackageDir } from "../context.js";
|
|
5
|
-
import { applyDevBin, buildWorkspaceRegistry,
|
|
5
|
+
import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
|
|
6
6
|
import { runCommand } from "../runner.js";
|
|
7
7
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
8
8
|
import { resolveBuildCachePath } from "./build.js";
|
|
@@ -34,8 +34,8 @@ export async function runClean(cwd = process.cwd()) {
|
|
|
34
34
|
}
|
|
35
35
|
async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
36
36
|
const config = await loadConfig(monorepoRoot);
|
|
37
|
+
const registry = buildWorkspaceRegistry(config);
|
|
37
38
|
for (const packageDir of workspacePackageDirs) {
|
|
38
|
-
const registry = buildWorkspaceRegistry(config, hasIntegrationTestConfig(packageDir));
|
|
39
39
|
const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
40
40
|
if (resolved === undefined || resolved.command === '') {
|
|
41
41
|
continue;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type FormatMode = 'check' | 'write';
|
|
2
|
+
export interface FormatTargets {
|
|
3
|
+
files: string[];
|
|
4
|
+
ignorePaths: string[];
|
|
5
|
+
}
|
|
6
|
+
export type ResolveTargetsResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
targets: FormatTargets;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function runFmt(argv: string[], cwd?: string): number;
|
|
14
|
+
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
15
|
+
export interface RunPrettierOptions {
|
|
16
|
+
cliPath: string;
|
|
17
|
+
mode: FormatMode;
|
|
18
|
+
files: string[];
|
|
19
|
+
ignorePaths: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
budgetBytes?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function runPrettier(options: RunPrettierOptions): number;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
8
|
+
import { isObject } from "../helpers/type-guards.js";
|
|
9
|
+
const IGNORE_FILENAME = '.prettierignore';
|
|
10
|
+
const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
11
|
+
const IGNORE_FILE_PATHSPEC = `*${IGNORE_FILENAME}`;
|
|
12
|
+
const PRETTIER_PACKAGE = 'prettier';
|
|
13
|
+
const PRETTIER_RANGE = '>=3.9.5 <4';
|
|
14
|
+
const ARGUMENT_BUDGET_BYTES = 100_000;
|
|
15
|
+
const USAGE = 'Usage: nmr-fmt (--check | --write) [pathspec...]';
|
|
16
|
+
const MODE_ARGS = {
|
|
17
|
+
check: ['--check'],
|
|
18
|
+
write: ['--list-different', '--write'],
|
|
19
|
+
};
|
|
20
|
+
export function runFmt(argv, cwd = process.cwd()) {
|
|
21
|
+
const parsed = parseFmtArgs(argv);
|
|
22
|
+
if (!parsed.ok) {
|
|
23
|
+
reportError(`nmr-fmt: ${parsed.error}`);
|
|
24
|
+
process.stderr.write(`${USAGE}\n`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const resolved = resolveFormatTargets(cwd, parsed.pathspecs);
|
|
28
|
+
if (!resolved.ok) {
|
|
29
|
+
reportError(`nmr-fmt: ${resolved.error}`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const { files, ignorePaths } = resolved.targets;
|
|
33
|
+
if (files.length === 0) {
|
|
34
|
+
if (parsed.pathspecs.length > 0) {
|
|
35
|
+
reportError(`nmr-fmt: no formattable files matched: ${parsed.pathspecs.join(', ')}`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
const cli = resolvePrettierCli();
|
|
41
|
+
if (!cli.ok) {
|
|
42
|
+
reportError(`nmr-fmt: ${cli.error}`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
return runPrettier({ cliPath: cli.cliPath, mode: parsed.mode, files, ignorePaths, cwd });
|
|
46
|
+
}
|
|
47
|
+
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
48
|
+
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
49
|
+
if (!toplevel.ok)
|
|
50
|
+
return toplevel;
|
|
51
|
+
const repositoryRoot = toplevel.stdout.trim();
|
|
52
|
+
const listed = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
53
|
+
if (!listed.ok)
|
|
54
|
+
return listed;
|
|
55
|
+
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
56
|
+
if (!ignoreFiles.ok)
|
|
57
|
+
return ignoreFiles;
|
|
58
|
+
const discovered = splitNulSeparated(ignoreFiles.stdout)
|
|
59
|
+
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
60
|
+
.map((file) => path.resolve(repositoryRoot, file));
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
targets: {
|
|
64
|
+
files: dedupe(splitNulSeparated(listed.stdout))
|
|
65
|
+
.filter((file) => isFormattableFile(cwd, file))
|
|
66
|
+
.toSorted(),
|
|
67
|
+
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discovered]),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function isFormattableFile(cwd, file) {
|
|
72
|
+
return statSync(path.resolve(cwd, file), { throwIfNoEntry: false })?.isFile() === true;
|
|
73
|
+
}
|
|
74
|
+
function parseFmtArgs(argv) {
|
|
75
|
+
let mode;
|
|
76
|
+
const pathspecs = [];
|
|
77
|
+
for (const arg of argv) {
|
|
78
|
+
if (arg === '--check' || arg === '--write') {
|
|
79
|
+
const requested = arg === '--check' ? 'check' : 'write';
|
|
80
|
+
if (mode !== undefined && mode !== requested) {
|
|
81
|
+
return { ok: false, error: 'Pass --check or --write, not both.' };
|
|
82
|
+
}
|
|
83
|
+
mode = requested;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith('-')) {
|
|
87
|
+
return { ok: false, error: `Unknown option: ${arg}` };
|
|
88
|
+
}
|
|
89
|
+
pathspecs.push(arg);
|
|
90
|
+
}
|
|
91
|
+
if (mode === undefined) {
|
|
92
|
+
return { ok: false, error: 'Pass --check or --write.' };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, mode, pathspecs };
|
|
95
|
+
}
|
|
96
|
+
function resolvePrettierCli() {
|
|
97
|
+
const missing = {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: `\`${PRETTIER_PACKAGE}\` (${PRETTIER_RANGE}) could not be resolved. Install it in this repository.`,
|
|
100
|
+
};
|
|
101
|
+
let manifestPath;
|
|
102
|
+
try {
|
|
103
|
+
manifestPath = createRequire(import.meta.url).resolve(`${PRETTIER_PACKAGE}/package.json`);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return missing;
|
|
107
|
+
}
|
|
108
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
109
|
+
if (!isObject(manifest))
|
|
110
|
+
return missing;
|
|
111
|
+
const bin = isObject(manifest.bin) ? manifest.bin[PRETTIER_PACKAGE] : manifest.bin;
|
|
112
|
+
if (typeof bin !== 'string')
|
|
113
|
+
return missing;
|
|
114
|
+
return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
|
|
115
|
+
}
|
|
116
|
+
export function runPrettier(options) {
|
|
117
|
+
const { cliPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES } = options;
|
|
118
|
+
const prettierArgs = [
|
|
119
|
+
cliPath,
|
|
120
|
+
'--ignore-unknown',
|
|
121
|
+
...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
|
|
122
|
+
...MODE_ARGS[mode],
|
|
123
|
+
];
|
|
124
|
+
let firstFailure = 0;
|
|
125
|
+
for (const batch of batchWithinBudget(files, budgetBytes)) {
|
|
126
|
+
const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
|
|
127
|
+
if (result.error) {
|
|
128
|
+
reportError(`nmr-fmt: could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const status = result.status ?? 1;
|
|
132
|
+
if (status !== 0 && firstFailure === 0) {
|
|
133
|
+
firstFailure = status;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return firstFailure;
|
|
137
|
+
}
|
|
138
|
+
function batchWithinBudget(files, budget) {
|
|
139
|
+
const batches = [];
|
|
140
|
+
let batch = [];
|
|
141
|
+
let size = 0;
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
const cost = Buffer.byteLength(file) + 1;
|
|
144
|
+
if (batch.length > 0 && size + cost > budget) {
|
|
145
|
+
batches.push(batch);
|
|
146
|
+
batch = [];
|
|
147
|
+
size = 0;
|
|
148
|
+
}
|
|
149
|
+
batch.push(file);
|
|
150
|
+
size += cost;
|
|
151
|
+
}
|
|
152
|
+
if (batch.length > 0)
|
|
153
|
+
batches.push(batch);
|
|
154
|
+
return batches;
|
|
155
|
+
}
|
|
156
|
+
function runGit(args, cwd) {
|
|
157
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
158
|
+
if (result.error) {
|
|
159
|
+
return { ok: false, error: result.error.message };
|
|
160
|
+
}
|
|
161
|
+
if (result.status !== 0) {
|
|
162
|
+
const stderr = result.stderr.trim();
|
|
163
|
+
return { ok: false, error: stderr || `\`git ${args.join(' ')}\` failed with exit code ${result.status}.` };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, stdout: result.stdout };
|
|
166
|
+
}
|
|
167
|
+
function splitNulSeparated(output) {
|
|
168
|
+
return output.split('\0').filter((entry) => entry !== '');
|
|
169
|
+
}
|
|
170
|
+
function dedupe(items) {
|
|
171
|
+
return [...new Set(items)];
|
|
172
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export type ScriptValue = string | string[];
|
|
2
2
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const integrationTestScripts: ScriptRegistry;
|
|
5
|
-
export declare const standardTestScripts: ScriptRegistry;
|
|
3
|
+
export declare const workspaceScripts: ScriptRegistry;
|
|
6
4
|
export declare const rootScripts: ScriptRegistry;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const workspaceScripts = {
|
|
2
2
|
build: ['compile'],
|
|
3
3
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
4
4
|
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
@@ -6,27 +6,20 @@ export const commonWorkspaceScripts = {
|
|
|
6
6
|
compile: 'nmr-compile',
|
|
7
7
|
fix: ['lint', 'fmt'],
|
|
8
8
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
9
|
-
fmt: '
|
|
10
|
-
'fmt:check': '
|
|
9
|
+
fmt: 'nmr-fmt --write',
|
|
10
|
+
'fmt:check': 'nmr-fmt --check',
|
|
11
11
|
lint: 'eslint --fix .',
|
|
12
12
|
'lint:check': 'eslint .',
|
|
13
13
|
'lint:strict': 'strict-lint',
|
|
14
|
+
test: "pnpm exec vitest --project '!integration'",
|
|
15
|
+
'test:all': 'pnpm exec vitest',
|
|
16
|
+
'test:coverage': "pnpm exec vitest --project '!integration' --coverage",
|
|
17
|
+
'test:integration': 'pnpm exec vitest --project integration',
|
|
18
|
+
'test:watch': "pnpm exec vitest --project '!integration' --watch",
|
|
14
19
|
typecheck: 'tsgo --noEmit',
|
|
15
20
|
upgrade: 'nmr-taze --include-locked',
|
|
16
21
|
'view-coverage': 'open coverage/index.html',
|
|
17
22
|
};
|
|
18
|
-
export const integrationTestScripts = {
|
|
19
|
-
test: 'pnpm exec vitest --config=vitest.standalone.config.ts',
|
|
20
|
-
'test:all': 'pnpm exec vitest',
|
|
21
|
-
'test:coverage': 'pnpm exec vitest --config=vitest.standalone.config.ts --coverage',
|
|
22
|
-
'test:integration': 'pnpm exec vitest --config=vitest.integration.config.ts',
|
|
23
|
-
'test:watch': 'pnpm exec vitest --config=vitest.standalone.config.ts --watch',
|
|
24
|
-
};
|
|
25
|
-
export const standardTestScripts = {
|
|
26
|
-
test: 'pnpm exec vitest',
|
|
27
|
-
'test:coverage': 'pnpm exec vitest --coverage',
|
|
28
|
-
'test:watch': 'pnpm exec vitest --watch',
|
|
29
|
-
};
|
|
30
23
|
export const rootScripts = {
|
|
31
24
|
audit: ['audit:prod', 'audit:dev'],
|
|
32
25
|
'audit:dev': 'pnpm exec v11y --dev',
|
|
@@ -39,10 +32,8 @@ export const rootScripts = {
|
|
|
39
32
|
clean: 'nmr-clean',
|
|
40
33
|
fix: ['lint', 'fmt'],
|
|
41
34
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
42
|
-
fmt: '
|
|
43
|
-
'fmt:
|
|
44
|
-
'fmt:check': 'sh -c \'prettier --check "${@:-.}"\' --',
|
|
45
|
-
'fmt:sh': 'shfmt --write **/*.sh',
|
|
35
|
+
fmt: 'nmr-fmt --write',
|
|
36
|
+
'fmt:check': 'nmr-fmt --check',
|
|
46
37
|
lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
|
|
47
38
|
'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
|
|
48
39
|
'lint:strict': 'nmr root:lint:strict && pnpm --recursive exec nmr lint:strict',
|
|
@@ -51,13 +42,17 @@ export const rootScripts = {
|
|
|
51
42
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
52
43
|
'root:lint:check': "eslint --ignore-pattern 'packages/**' .",
|
|
53
44
|
'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
|
|
54
|
-
'root:test':
|
|
45
|
+
'root:test': "vitest --config ./vitest.root.config.ts --project '!integration'",
|
|
46
|
+
'root:test:all': 'vitest --config ./vitest.root.config.ts',
|
|
47
|
+
'root:test:integration': 'vitest --config ./vitest.root.config.ts --project integration',
|
|
55
48
|
'root:typecheck': 'tsgo --noEmit',
|
|
56
49
|
'root:upgrade': 'nmr-taze --include-locked',
|
|
57
50
|
'sync-agent-files': 'nmr-sync-agent-files',
|
|
58
51
|
test: 'nmr root:test && pnpm --recursive exec nmr test',
|
|
52
|
+
'test:all': 'nmr root:test:all && pnpm --recursive exec nmr test:all',
|
|
59
53
|
'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
|
|
60
|
-
'test:
|
|
54
|
+
'test:integration': 'nmr root:test:integration && pnpm --recursive exec nmr test:integration',
|
|
55
|
+
'test:watch': "vitest --project '!integration' --watch",
|
|
61
56
|
typecheck: 'nmr root:typecheck && pnpm --recursive exec nmr typecheck',
|
|
62
57
|
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
63
58
|
};
|
package/dist/esm/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
-
import { buildRootRegistry, buildWorkspaceRegistry, describeScript,
|
|
2
|
+
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
3
3
|
export function generateHelp(config, packageDir, useRoot) {
|
|
4
4
|
const lines = [
|
|
5
5
|
'Usage: nmr [flags] <command> [args...]',
|
|
@@ -16,8 +16,7 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
16
16
|
];
|
|
17
17
|
const overrides = packageDir === undefined ? {} : collectOverrides(packageDir);
|
|
18
18
|
let hadOverride = false;
|
|
19
|
-
const
|
|
20
|
-
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config, useIntTests));
|
|
19
|
+
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config));
|
|
21
20
|
const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
|
|
22
21
|
if (workspaceMarked.size > 0)
|
|
23
22
|
hadOverride = true;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Config } from 'prettier';
|
|
2
|
+
export interface PrettierConfigOptions extends Omit<Config, 'overrides' | 'plugins'> {
|
|
3
|
+
additionalPlugins?: NonNullable<Config['plugins']>;
|
|
4
|
+
additionalOverrides?: NonNullable<Config['overrides']>;
|
|
5
|
+
overrides?: never;
|
|
6
|
+
plugins?: never;
|
|
7
|
+
}
|
|
8
|
+
export declare function definePrettierConfig(options?: PrettierConfigOptions): Config;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as shPlugin from 'prettier-plugin-sh';
|
|
2
|
+
const INFERRED_LANGUAGES = new Set(['Dockerfile', 'Shell']);
|
|
3
|
+
const EXCLUDED_FILENAMES = new Set([
|
|
4
|
+
'.bash_history',
|
|
5
|
+
'.cshrc',
|
|
6
|
+
'.flaskenv',
|
|
7
|
+
'.login',
|
|
8
|
+
'cshrc',
|
|
9
|
+
'gradlew',
|
|
10
|
+
'login',
|
|
11
|
+
'mvnw',
|
|
12
|
+
]);
|
|
13
|
+
const DEFAULT_OPTIONS = {
|
|
14
|
+
binaryNextLine: false,
|
|
15
|
+
checkIgnorePragma: true,
|
|
16
|
+
jsxSingleQuote: false,
|
|
17
|
+
singleQuote: true,
|
|
18
|
+
spaceRedirects: false,
|
|
19
|
+
switchCaseIndent: false,
|
|
20
|
+
trailingComma: 'all',
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_OVERRIDES = [
|
|
23
|
+
{
|
|
24
|
+
files: ['*.json5', '*.jsonc', 'tsconfig.json', 'tsconfig.*.json'],
|
|
25
|
+
options: { parser: 'jsonc', singleQuote: false, trailingComma: 'all' },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
export function definePrettierConfig(options = {}) {
|
|
29
|
+
assertNoReplacement(options);
|
|
30
|
+
const { additionalOverrides = [], additionalPlugins = [], ...overrideOptions } = options;
|
|
31
|
+
return {
|
|
32
|
+
...DEFAULT_OPTIONS,
|
|
33
|
+
...overrideOptions,
|
|
34
|
+
overrides: [...DEFAULT_OVERRIDES, ...additionalOverrides],
|
|
35
|
+
plugins: [buildShellPlugin(), ...additionalPlugins],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function buildShellPlugin() {
|
|
39
|
+
return {
|
|
40
|
+
...shPlugin,
|
|
41
|
+
languages: shPlugin.languages.filter(isInferredLanguage).map(withoutExcludedFilenames),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isInferredLanguage(language) {
|
|
45
|
+
return INFERRED_LANGUAGES.has(language.name);
|
|
46
|
+
}
|
|
47
|
+
function withoutExcludedFilenames(language) {
|
|
48
|
+
if (language.filenames === undefined)
|
|
49
|
+
return language;
|
|
50
|
+
return { ...language, filenames: language.filenames.filter((name) => !EXCLUDED_FILENAMES.has(name)) };
|
|
51
|
+
}
|
|
52
|
+
function assertNoReplacement(options) {
|
|
53
|
+
if ('overrides' in options)
|
|
54
|
+
throw ownedKeyError('overrides', 'additionalOverrides');
|
|
55
|
+
if ('plugins' in options)
|
|
56
|
+
throw ownedKeyError('plugins', 'additionalPlugins');
|
|
57
|
+
}
|
|
58
|
+
function ownedKeyError(key, seam) {
|
|
59
|
+
return new TypeError(`definePrettierConfig: \`${key}\` is owned by this config. Use \`${seam}\` to append to it.`);
|
|
60
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ScriptRegistry } from './default-scripts.js';
|
|
2
2
|
export type { ScriptRegistry, ScriptValue } from './default-scripts.js';
|
|
3
|
-
export declare function getDefaultWorkspaceScripts(
|
|
3
|
+
export declare function getDefaultWorkspaceScripts(): ScriptRegistry;
|
|
4
4
|
export declare function getDefaultRootScripts(): ScriptRegistry;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function getDefaultWorkspaceScripts(
|
|
3
|
-
return {
|
|
4
|
-
...commonWorkspaceScripts,
|
|
5
|
-
...(useIntTests ? integrationTestScripts : standardTestScripts),
|
|
6
|
-
};
|
|
1
|
+
import { rootScripts, workspaceScripts } from "./default-scripts.js";
|
|
2
|
+
export function getDefaultWorkspaceScripts() {
|
|
3
|
+
return { ...workspaceScripts };
|
|
7
4
|
}
|
|
8
5
|
export function getDefaultRootScripts() {
|
|
9
6
|
return { ...rootScripts };
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -8,8 +8,7 @@ export interface ResolvedScript {
|
|
|
8
8
|
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): string;
|
|
9
9
|
export declare function describeScript(script: ScriptValue): string;
|
|
10
10
|
export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function buildWorkspaceRegistry(config: NmrConfig, useIntTests: boolean): ScriptRegistry;
|
|
11
|
+
export declare function buildWorkspaceRegistry(config: NmrConfig): ScriptRegistry;
|
|
13
12
|
export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
|
|
14
13
|
export declare function isSelfReferential(script: string, commandName: string): boolean;
|
|
15
14
|
export declare function resolveScript(commandName: string, registry: ScriptRegistry, packageDir: string | undefined, workspaceRoot: boolean): ResolvedScript | undefined;
|
package/dist/esm/resolver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isObject } from "./helpers/type-guards.js";
|
|
4
4
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
@@ -63,12 +63,9 @@ export function readPackageJsonScripts(packageDir) {
|
|
|
63
63
|
throw error;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function
|
|
67
|
-
return existsSync(path.join(packageDir, 'vitest.integration.config.ts'));
|
|
68
|
-
}
|
|
69
|
-
export function buildWorkspaceRegistry(config, useIntTests) {
|
|
66
|
+
export function buildWorkspaceRegistry(config) {
|
|
70
67
|
return {
|
|
71
|
-
...getDefaultWorkspaceScripts(
|
|
68
|
+
...getDefaultWorkspaceScripts(),
|
|
72
69
|
...config.workspaceScripts,
|
|
73
70
|
};
|
|
74
71
|
}
|
package/dist/esm/runCli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
|
3
3
|
import { resolveContext } from "./context.js";
|
|
4
4
|
import { generateHelp } from "./help.js";
|
|
5
5
|
import { isHookName } from "./helpers/hook-name.js";
|
|
6
|
-
import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry,
|
|
6
|
+
import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
7
7
|
import { runCommand } from "./runner.js";
|
|
8
8
|
const VERSION = readPackageVersion(import.meta.url);
|
|
9
9
|
export async function runCli(options) {
|
|
@@ -39,9 +39,7 @@ export async function runCli(options) {
|
|
|
39
39
|
const exitCode = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: childEnv });
|
|
40
40
|
return { exitCode };
|
|
41
41
|
}
|
|
42
|
-
const registry = useRoot
|
|
43
|
-
? buildRootRegistry(context.config)
|
|
44
|
-
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(anchorDir));
|
|
42
|
+
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
45
43
|
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
46
44
|
if (!resolved) {
|
|
47
45
|
if (env.NMR_RUN_IF_PRESENT === '1') {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ViteUserConfig } from 'vitest/config';
|
|
2
|
+
import type { InlineConfig, ProjectConfig } from 'vitest/node';
|
|
3
|
+
export type RootTestOptions = Omit<InlineConfig, keyof ProjectConfig>;
|
|
4
|
+
export type RootOverrides = Omit<ViteUserConfig, 'test'> & {
|
|
5
|
+
test?: RootTestOptions;
|
|
6
|
+
};
|
|
7
|
+
export interface VitestConfigOptions {
|
|
8
|
+
root?: RootOverrides;
|
|
9
|
+
project?: ProjectConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface RootVitestConfigOptions extends VitestConfigOptions {
|
|
12
|
+
monorepoRoot: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function defineVitestConfig(options?: VitestConfigOptions): ViteUserConfig;
|
|
15
|
+
export declare function defineRootVitestConfig(options: RootVitestConfigOptions | undefined): ViteUserConfig;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
3
|
+
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
4
|
+
const APP_PATTERNS = ['**/__tests__/**/*.app.test.{ts,tsx}'];
|
|
5
|
+
const INTEGRATION_PATTERNS = ['**/__tests__/**/*.int.test.{ts,tsx}'];
|
|
6
|
+
const ALL_TEST_PATTERNS = ['**/__tests__/**/*.test.{ts,tsx}'];
|
|
7
|
+
const COVERAGE_EXCLUDE = [
|
|
8
|
+
'**/__{mocks,tests}__/*',
|
|
9
|
+
'**/index.ts',
|
|
10
|
+
'**/mock*.{ts,tsx}',
|
|
11
|
+
'**/*.d.ts',
|
|
12
|
+
'**/*.types.ts',
|
|
13
|
+
];
|
|
14
|
+
const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
|
|
15
|
+
const MISSING_MONOREPO_ROOT = 'defineRootVitestConfig requires `monorepoRoot`, an absolute path to the directory holding pnpm-workspace.yaml. Pass `import.meta.dirname` from the root config.';
|
|
16
|
+
export function defineVitestConfig(options = {}) {
|
|
17
|
+
return buildConfig(options, { coverageInclude: PACKAGE_COVERAGE_INCLUDE });
|
|
18
|
+
}
|
|
19
|
+
export function defineRootVitestConfig(options) {
|
|
20
|
+
if (options === undefined) {
|
|
21
|
+
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
22
|
+
}
|
|
23
|
+
const monorepoRoot = options.monorepoRoot;
|
|
24
|
+
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
25
|
+
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
26
|
+
}
|
|
27
|
+
return buildConfig(options, {
|
|
28
|
+
coverageInclude: [],
|
|
29
|
+
projectExclude: getWorkspaceExcludePatterns(monorepoRoot),
|
|
30
|
+
projectRoot: monorepoRoot,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function buildConfig(options, { coverageInclude, projectExclude = [], projectRoot }) {
|
|
34
|
+
const config = {
|
|
35
|
+
test: {
|
|
36
|
+
coverage: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
exclude: COVERAGE_EXCLUDE,
|
|
39
|
+
include: coverageInclude,
|
|
40
|
+
provider: 'v8',
|
|
41
|
+
},
|
|
42
|
+
passWithNoTests: true,
|
|
43
|
+
projects: buildProjects(options.project, projectExclude, projectRoot),
|
|
44
|
+
silent: 'passed-only',
|
|
45
|
+
watch: false,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return options.root ? mergeConfig(config, options.root) : config;
|
|
49
|
+
}
|
|
50
|
+
function buildProjects(overrides, extraExclude, projectRoot) {
|
|
51
|
+
const categories = [
|
|
52
|
+
{ exclude: [], include: APP_PATTERNS, name: 'app' },
|
|
53
|
+
{ exclude: [], include: INTEGRATION_PATTERNS, name: 'integration' },
|
|
54
|
+
{ exclude: [...APP_PATTERNS, ...INTEGRATION_PATTERNS], include: ALL_TEST_PATTERNS, name: 'unit' },
|
|
55
|
+
];
|
|
56
|
+
return categories.map(({ exclude, include, name }) => {
|
|
57
|
+
const project = {
|
|
58
|
+
extends: true,
|
|
59
|
+
...(projectRoot !== undefined && { root: projectRoot }),
|
|
60
|
+
test: { exclude: [...defaultExclude, ...exclude, ...extraExclude], include, name },
|
|
61
|
+
};
|
|
62
|
+
return overrides ? mergeConfig(project, { test: overrides }) : project;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function getWorkspaceExcludePatterns(monorepoRoot) {
|
|
66
|
+
return getWorkspacePackageDirs(monorepoRoot)
|
|
67
|
+
.map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
|
|
68
|
+
.toSorted();
|
|
69
|
+
}
|
package/dist/esm/workspace.js
CHANGED
|
@@ -3,21 +3,25 @@ import path from 'node:path';
|
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { isObject } from "./helpers/type-guards.js";
|
|
5
5
|
import { resolvePackageDirs } from "./helpers/workspace-patterns.js";
|
|
6
|
+
const WORKSPACE_MANIFEST = 'pnpm-workspace.yaml';
|
|
6
7
|
export function findMonorepoRoot(startDir) {
|
|
7
8
|
let dir = path.resolve(startDir ?? process.cwd());
|
|
8
9
|
for (;;) {
|
|
9
|
-
if (existsSync(path.join(dir,
|
|
10
|
+
if (existsSync(path.join(dir, WORKSPACE_MANIFEST))) {
|
|
10
11
|
return dir;
|
|
11
12
|
}
|
|
12
13
|
const parent = path.dirname(dir);
|
|
13
14
|
if (parent === dir) {
|
|
14
|
-
throw new Error(
|
|
15
|
+
throw new Error(`Could not find monorepo root: no ${WORKSPACE_MANIFEST} found in any parent directory`);
|
|
15
16
|
}
|
|
16
17
|
dir = parent;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
export function getWorkspacePackageDirs(monorepoRoot) {
|
|
20
|
-
const workspaceFile = path.join(monorepoRoot,
|
|
21
|
+
const workspaceFile = path.join(monorepoRoot, WORKSPACE_MANIFEST);
|
|
22
|
+
if (!existsSync(workspaceFile)) {
|
|
23
|
+
throw new Error(`Not a monorepo root: no ${WORKSPACE_MANIFEST} in ${monorepoRoot}`);
|
|
24
|
+
}
|
|
21
25
|
const content = readFileSync(workspaceFile, 'utf8');
|
|
22
26
|
const parsed = parse(content);
|
|
23
27
|
const packages = getPackagesFromParsedYaml(parsed);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"types": "./dist/esm/index.d.ts",
|
|
27
27
|
"default": "./dist/esm/index.js"
|
|
28
28
|
},
|
|
29
|
+
"./prettier": {
|
|
30
|
+
"types": "./dist/esm/prettier.d.ts",
|
|
31
|
+
"default": "./dist/esm/prettier.js"
|
|
32
|
+
},
|
|
29
33
|
"./scripts": {
|
|
30
34
|
"types": "./dist/esm/scripts.d.ts",
|
|
31
35
|
"default": "./dist/esm/scripts.js"
|
|
@@ -34,6 +38,10 @@
|
|
|
34
38
|
"types": "./dist/esm/taze.d.ts",
|
|
35
39
|
"default": "./dist/esm/taze.js"
|
|
36
40
|
},
|
|
41
|
+
"./vitest": {
|
|
42
|
+
"types": "./dist/esm/vitest.d.ts",
|
|
43
|
+
"default": "./dist/esm/vitest.js"
|
|
44
|
+
},
|
|
37
45
|
"./workspace": {
|
|
38
46
|
"types": "./dist/esm/workspace.d.ts",
|
|
39
47
|
"default": "./dist/esm/workspace.js"
|
|
@@ -44,6 +52,7 @@
|
|
|
44
52
|
"nmr": "bin/nmr.js",
|
|
45
53
|
"nmr-clean": "bin/nmr-clean.js",
|
|
46
54
|
"nmr-compile": "bin/nmr-compile.js",
|
|
55
|
+
"nmr-fmt": "bin/nmr-fmt.js",
|
|
47
56
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
48
57
|
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
49
58
|
"nmr-taze": "bin/nmr-taze.js"
|
|
@@ -56,12 +65,28 @@
|
|
|
56
65
|
"dependencies": {
|
|
57
66
|
"glob": "13.0.6",
|
|
58
67
|
"jiti": "2.7.0",
|
|
59
|
-
"
|
|
68
|
+
"prettier-plugin-sh": "0.19.0",
|
|
69
|
+
"taze": "19.16.0",
|
|
60
70
|
"yaml": "2.9.0",
|
|
61
|
-
"@williamthorsen/nmr-core": "0.7.
|
|
71
|
+
"@williamthorsen/nmr-core": "0.7.3"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"prettier": "3.9.6",
|
|
75
|
+
"tinyglobby": "0.2.17",
|
|
76
|
+
"vitest": "4.1.10"
|
|
62
77
|
},
|
|
63
78
|
"peerDependencies": {
|
|
64
|
-
"
|
|
79
|
+
"prettier": ">=3.9.5 <4",
|
|
80
|
+
"typescript": ">=5.7.0 <7",
|
|
81
|
+
"vitest": ">=4.0.0 <5"
|
|
82
|
+
},
|
|
83
|
+
"peerDependenciesMeta": {
|
|
84
|
+
"prettier": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"vitest": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
65
90
|
},
|
|
66
91
|
"engines": {
|
|
67
92
|
"node": ">=24.16.0"
|