@williamthorsen/nmr 0.21.0 → 0.23.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 +105 -44
- package/dist/esm/commands/build.js +1 -1
- package/dist/esm/default-scripts.js +0 -2
- package/dist/esm/prettier.d.ts +8 -0
- package/dist/esm/prettier.js +79 -0
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +13 -5
- package/dist/esm/workspace.js +7 -3
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -3,23 +3,19 @@
|
|
|
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.23.0 (2026-08-01)
|
|
7
7
|
|
|
8
8
|
### 🎉 Features
|
|
9
9
|
|
|
10
|
-
-
|
|
10
|
+
- 🚨 **Breaking:** Close the shared Vitest config's path gaps (#546)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- 🚨 **Breaking:** Retire the Vitest config-file variants from nmr's defaults (#533)
|
|
15
|
-
|
|
16
|
-
Packages no longer need extra Vitest config files to keep integration tests out of the default test run. Integration tests are now recognized by an `*.int.test.ts` suffix, and every package gets the same five commands: `test`, `test:all`, `test:coverage`, `test:integration`, and `test:watch`. The same five commands work from the monorepo root. A Vitest run that collects no test files now passes instead of failing. Consumers should migrate the Vitest config when upgrading.
|
|
12
|
+
Defines common patterns to determine which files are considered test files and which are subject to code coverage. Consumers that use `defineVitestConfig` benefit from these patterns if they follow naming and file-location conventions.
|
|
17
13
|
|
|
18
14
|
### 🐛 Bug fixes
|
|
19
15
|
|
|
20
|
-
-
|
|
16
|
+
- Stop rewriting documented commands inside Markdown fences (#549)
|
|
21
17
|
|
|
22
|
-
Fixes an issue where `nmr fmt`
|
|
18
|
+
Fixes an issue where `nmr fmt` silently rewrote shell commands documented in a repository's Markdown, leaving usage examples that no longer ran as written. Code fences inside Markdown are now left untouched in any language, withdrawing the fence formatting added in 0.22.0. Fence formatting can be reenabled on selected paths via the config.
|
|
23
19
|
<!-- /section:release-notes -->
|
|
24
20
|
|
|
25
21
|
## Installation
|
|
@@ -46,17 +42,17 @@ nmr works out of the box with no configuration. It ships with built-in scripts f
|
|
|
46
42
|
From a package directory:
|
|
47
43
|
|
|
48
44
|
```bash
|
|
49
|
-
nmr test
|
|
50
|
-
nmr build
|
|
51
|
-
nmr check
|
|
45
|
+
nmr test # Run tests for the current package
|
|
46
|
+
nmr build # Compile to .js and .d.ts in one pass
|
|
47
|
+
nmr check # Typecheck, format check, lint check, and tests
|
|
52
48
|
```
|
|
53
49
|
|
|
54
50
|
From the monorepo root:
|
|
55
51
|
|
|
56
52
|
```bash
|
|
57
|
-
nmr test
|
|
58
|
-
nmr build
|
|
59
|
-
nmr ci
|
|
53
|
+
nmr test # Run root tests + recursive workspace tests
|
|
54
|
+
nmr build # Build all packages
|
|
55
|
+
nmr ci # Run build && check:strict && audit (full CI pipeline)
|
|
60
56
|
```
|
|
61
57
|
|
|
62
58
|
nmr detects where you are and selects the right scripts automatically — see [context-aware resolution](#context-aware-resolution) below.
|
|
@@ -217,9 +213,9 @@ The second example calls the bin directly, which sidesteps the workspace-versus-
|
|
|
217
213
|
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:
|
|
218
214
|
|
|
219
215
|
```bash
|
|
220
|
-
nmr upgrade
|
|
221
|
-
nmr upgrade major
|
|
222
|
-
nmr upgrade --write
|
|
216
|
+
nmr upgrade # upgrades available within each package's version ceilings
|
|
217
|
+
nmr upgrade major # major upgrades, still inside the ceilings
|
|
218
|
+
nmr upgrade --write # apply the proposals to package.json
|
|
223
219
|
```
|
|
224
220
|
|
|
225
221
|
### Configuring upgrades
|
|
@@ -348,12 +344,10 @@ The same five names the workspace registry carries, so a command means the same
|
|
|
348
344
|
|
|
349
345
|
#### Format
|
|
350
346
|
|
|
351
|
-
| Command | Runs
|
|
352
|
-
| ----------- |
|
|
353
|
-
| `fmt` | `nmr-fmt --write`
|
|
354
|
-
| `fmt:
|
|
355
|
-
| `fmt:check` | `nmr-fmt --check` |
|
|
356
|
-
| `fmt:sh` | `shfmt --write **/*.sh` |
|
|
347
|
+
| Command | Runs |
|
|
348
|
+
| ----------- | ----------------- |
|
|
349
|
+
| `fmt` | `nmr-fmt --write` |
|
|
350
|
+
| `fmt:check` | `nmr-fmt --check` |
|
|
357
351
|
|
|
358
352
|
#### File selection
|
|
359
353
|
|
|
@@ -423,19 +417,19 @@ Position determines ownership: flags before the command name are nmr's own, and
|
|
|
423
417
|
|
|
424
418
|
```bash
|
|
425
419
|
# From a package directory
|
|
426
|
-
nmr test
|
|
427
|
-
nmr build
|
|
420
|
+
nmr test # Run workspace test script
|
|
421
|
+
nmr build # Compile to .js and .d.ts in one pass
|
|
428
422
|
|
|
429
423
|
# From the monorepo root
|
|
430
|
-
nmr test
|
|
431
|
-
nmr ci
|
|
424
|
+
nmr test # Root tests + recursive workspace tests
|
|
425
|
+
nmr ci # build + check:strict + audit
|
|
432
426
|
|
|
433
427
|
# Target specific packages
|
|
434
|
-
nmr --filter core test
|
|
435
|
-
nmr --recursive lint
|
|
428
|
+
nmr --filter core test # Test only the core package
|
|
429
|
+
nmr --recursive lint # Lint all workspace packages
|
|
436
430
|
|
|
437
431
|
# Force root context from anywhere
|
|
438
|
-
nmr --workspace-root check
|
|
432
|
+
nmr --workspace-root check # Run root check from a package dir
|
|
439
433
|
```
|
|
440
434
|
|
|
441
435
|
## Additional subcommands
|
|
@@ -455,8 +449,8 @@ nmr report-overrides
|
|
|
455
449
|
Sync the agent-facing guidance shipped with nmr into the consuming repo.
|
|
456
450
|
|
|
457
451
|
```bash
|
|
458
|
-
nmr sync-agent-files
|
|
459
|
-
nmr sync-agent-files --check
|
|
452
|
+
nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
|
|
453
|
+
nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
|
|
460
454
|
```
|
|
461
455
|
|
|
462
456
|
Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
|
|
@@ -570,12 +564,67 @@ for (const packageDir of getWorkspacePackageDirs(monorepoRoot)) {
|
|
|
570
564
|
|
|
571
565
|
`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.
|
|
572
566
|
|
|
573
|
-
`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.
|
|
567
|
+
`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.
|
|
574
568
|
|
|
575
569
|
One divergence from pnpm: a directory counts as a package only if it holds a `package.json`, not a `package.yaml` or `package.json5`.
|
|
576
570
|
|
|
577
571
|
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.
|
|
578
572
|
|
|
573
|
+
## Shared Prettier config
|
|
574
|
+
|
|
575
|
+
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:
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
// .prettierrc.js
|
|
579
|
+
import { definePrettierConfig } from '@williamthorsen/nmr/prettier';
|
|
580
|
+
|
|
581
|
+
export default definePrettierConfig();
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
`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.
|
|
585
|
+
|
|
586
|
+
### What it formats
|
|
587
|
+
|
|
588
|
+
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.
|
|
589
|
+
|
|
590
|
+
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.
|
|
591
|
+
|
|
592
|
+
Code fences inside Markdown are left alone, in every language rather than shell alone. Registering the shell plugin also routes a fence tagged `bash` to shfmt, where a documented command's angle-bracket placeholders are valid redirections: `cmd --type <type> --harness claude` is reprinted as `cmd --type claude <type >--harness`, which still runs but no longer does what it documents. It cannot be narrowed back to shell, because Prettier matches a fence tag against the same `extensions` its file inference reads, so the `.bash` routing a script also claims the tag `bash`.
|
|
593
|
+
|
|
594
|
+
### Language scoping
|
|
595
|
+
|
|
596
|
+
`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.
|
|
597
|
+
|
|
598
|
+
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.
|
|
599
|
+
|
|
600
|
+
The parsers themselves stay registered, so a repo that wants a dropped language back assigns it explicitly:
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
export default definePrettierConfig({
|
|
604
|
+
additionalOverrides: [{ files: ['APKBUILD', '*.ebuild'], options: { parser: 'sh' } }],
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Customizing
|
|
609
|
+
|
|
610
|
+
Any Prettier option passed to the factory spreads over the defaults:
|
|
611
|
+
|
|
612
|
+
```js
|
|
613
|
+
export default definePrettierConfig({
|
|
614
|
+
printWidth: 100,
|
|
615
|
+
additionalPlugins: [await import('prettier-plugin-tailwindcss')],
|
|
616
|
+
additionalOverrides: [{ files: ['*.md'], options: { proseWrap: 'always' } }],
|
|
617
|
+
});
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`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.
|
|
621
|
+
|
|
622
|
+
### Adoption caveats
|
|
623
|
+
|
|
624
|
+
- **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.
|
|
625
|
+
- **Markdown fences stop being tidied.** The carve-out that protects documented shell commands costs the `ts`, `json`, and `yaml` fences their formatting too. Fences already formatted stay as they are, so nothing churns on adoption; new ones are simply left as written. A repo whose docs hold no shell can take them back for the Markdown paths it actually uses: `additionalOverrides: [{ files: ['*.md'], options: { embeddedLanguageFormatting: 'auto' } }]` restores `.md` alone, and the carve-out still covers every other path Prettier reads as Markdown.
|
|
626
|
+
- **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.
|
|
627
|
+
|
|
579
628
|
## Shared Vitest config
|
|
580
629
|
|
|
581
630
|
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:
|
|
@@ -602,9 +651,9 @@ The config declares three projects, named for what the tests cover:
|
|
|
602
651
|
All three match only under a `__tests__` directory. Select them at run time with `--project`, which accepts negation:
|
|
603
652
|
|
|
604
653
|
```bash
|
|
605
|
-
vitest --project '!integration'
|
|
606
|
-
vitest --project integration
|
|
607
|
-
vitest
|
|
654
|
+
vitest --project '!integration' # everything except integration tests
|
|
655
|
+
vitest --project integration # integration tests alone
|
|
656
|
+
vitest # every project
|
|
608
657
|
```
|
|
609
658
|
|
|
610
659
|
`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.
|
|
@@ -626,6 +675,22 @@ export default defineVitestConfig({
|
|
|
626
675
|
|
|
627
676
|
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.
|
|
628
677
|
|
|
678
|
+
### What the config excludes
|
|
679
|
+
|
|
680
|
+
Collection skips `**/node_modules/**`, `**/.git/**`, and `**/dist/**`. Coverage skips `**/__{fixtures,mocks,tests}__/**`, `**/index.ts`, `**/mock*.{ts,tsx}`, `**/*.d.ts`, and `**/*.types.ts`.
|
|
681
|
+
|
|
682
|
+
Because both seams concatenate, a consumer can add an exclusion but never remove one. A pattern therefore earns its place in these lists only by preventing a _silent_ failure, one a consumer cannot self-diagnose. A visible failure, such as a stray file sitting at 0% in the coverage report, is left to the consumer's own `project` seam.
|
|
683
|
+
|
|
684
|
+
Excluding build output from collection is the clearest case. A build that copies `.ts` sources rather than compiling them puts a second copy of the suite under `dist/`, where it is collected and passes green against stale code, and nothing in the run says so. (`nmr build` emits only `.js` and `.d.ts`, neither of which the include matches, so an nmr-built package was never exposed.) `dist/` is deliberately absent from the coverage list, because build output reaching a coverage report shows up as a diagnosable 0% entry.
|
|
685
|
+
|
|
686
|
+
#### Where to put fixtures
|
|
687
|
+
|
|
688
|
+
A `__fixtures__/` directory is excluded from coverage at any depth, so fixture data stops counting against a package's numbers whether or not a test imports it.
|
|
689
|
+
|
|
690
|
+
Placing it outside `__tests__/` settles a second problem: anything named `*.test.{ts,tsx}` under `__tests__/` is collected and run, whatever it holds, so a fixture carrying that name becomes a failing test. `src/__fixtures__/` resolves both halves; `src/__tests__/__fixtures__/` resolves the coverage half alone.
|
|
691
|
+
|
|
692
|
+
No collection pattern names fixtures, and that asymmetry is intentional. A coverage exclusion cannot hide a real test, whereas a collection exclusion can; since it could not be removed, a consumer who legitimately keeps a test under `__fixtures__/` would have no recourse.
|
|
693
|
+
|
|
629
694
|
### Root-scoped tests
|
|
630
695
|
|
|
631
696
|
A monorepo's own root-level tests need a second config, because the package config is found by walking up from a package directory:
|
|
@@ -634,22 +699,18 @@ A monorepo's own root-level tests need a second config, because the package conf
|
|
|
634
699
|
// vitest.root.config.ts
|
|
635
700
|
import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest';
|
|
636
701
|
|
|
637
|
-
export default defineRootVitestConfig();
|
|
702
|
+
export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });
|
|
638
703
|
```
|
|
639
704
|
|
|
640
705
|
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.
|
|
641
706
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
export default defineRootVitestConfig({ startDir: import.meta.dirname });
|
|
646
|
-
```
|
|
707
|
+
`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.
|
|
647
708
|
|
|
648
709
|
### Migrating from the config-file variants
|
|
649
710
|
|
|
650
711
|
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.
|
|
651
712
|
|
|
652
|
-
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig()`.
|
|
713
|
+
1. Replace the repo's root `vitest.config.ts` with `defineVitestConfig()`, and its root-scoped config with `defineRootVitestConfig({ monorepoRoot: import.meta.dirname })`.
|
|
653
714
|
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.
|
|
654
715
|
3. Rename integration tests to `*.int.test.ts` and tooling or drift tests to `*.app.test.ts`.
|
|
655
716
|
4. Drop any hand-copied `test*` entries from `package.json`, which now shadow the defaults.
|
|
@@ -33,7 +33,7 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
export async function computeBuildHash(packageDir, files, emitConfig, compilerVersion) {
|
|
35
35
|
const hash = createHash('sha256');
|
|
36
|
-
for (const file of
|
|
36
|
+
for (const file of files.toSorted()) {
|
|
37
37
|
hash.update(file);
|
|
38
38
|
hash.update('\0');
|
|
39
39
|
hash.update(await readFile(path.join(packageDir, file)));
|
|
@@ -33,9 +33,7 @@ export const rootScripts = {
|
|
|
33
33
|
fix: ['lint', 'fmt'],
|
|
34
34
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
35
35
|
fmt: 'nmr-fmt --write',
|
|
36
|
-
'fmt:all': ['fmt', 'fmt:sh'],
|
|
37
36
|
'fmt:check': 'nmr-fmt --check',
|
|
38
|
-
'fmt:sh': 'shfmt --write **/*.sh',
|
|
39
37
|
lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
|
|
40
38
|
'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
|
|
41
39
|
'lint:strict': 'nmr root:lint:strict && pnpm --recursive exec nmr lint:strict',
|
|
@@ -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,79 @@
|
|
|
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
|
+
files: [
|
|
29
|
+
'*.livemd',
|
|
30
|
+
'*.markdown',
|
|
31
|
+
'*.md',
|
|
32
|
+
'*.mdown',
|
|
33
|
+
'*.mdwn',
|
|
34
|
+
'*.mdx',
|
|
35
|
+
'*.mkd',
|
|
36
|
+
'*.mkdn',
|
|
37
|
+
'*.mkdown',
|
|
38
|
+
'*.ronn',
|
|
39
|
+
'*.scd',
|
|
40
|
+
'*.workbook',
|
|
41
|
+
'README',
|
|
42
|
+
'contents.lr',
|
|
43
|
+
],
|
|
44
|
+
options: { embeddedLanguageFormatting: 'off' },
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
export function definePrettierConfig(options = {}) {
|
|
48
|
+
assertNoReplacement(options);
|
|
49
|
+
const { additionalOverrides = [], additionalPlugins = [], ...overrideOptions } = options;
|
|
50
|
+
return {
|
|
51
|
+
...DEFAULT_OPTIONS,
|
|
52
|
+
...overrideOptions,
|
|
53
|
+
overrides: [...DEFAULT_OVERRIDES, ...additionalOverrides],
|
|
54
|
+
plugins: [buildShellPlugin(), ...additionalPlugins],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function buildShellPlugin() {
|
|
58
|
+
return {
|
|
59
|
+
...shPlugin,
|
|
60
|
+
languages: shPlugin.languages.filter(isInferredLanguage).map(withoutExcludedFilenames),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isInferredLanguage(language) {
|
|
64
|
+
return INFERRED_LANGUAGES.has(language.name);
|
|
65
|
+
}
|
|
66
|
+
function withoutExcludedFilenames(language) {
|
|
67
|
+
if (language.filenames === undefined)
|
|
68
|
+
return language;
|
|
69
|
+
return { ...language, filenames: language.filenames.filter((name) => !EXCLUDED_FILENAMES.has(name)) };
|
|
70
|
+
}
|
|
71
|
+
function assertNoReplacement(options) {
|
|
72
|
+
if ('overrides' in options)
|
|
73
|
+
throw ownedKeyError('overrides', 'additionalOverrides');
|
|
74
|
+
if ('plugins' in options)
|
|
75
|
+
throw ownedKeyError('plugins', 'additionalPlugins');
|
|
76
|
+
}
|
|
77
|
+
function ownedKeyError(key, seam) {
|
|
78
|
+
return new TypeError(`definePrettierConfig: \`${key}\` is owned by this config. Use \`${seam}\` to append to it.`);
|
|
79
|
+
}
|
package/dist/esm/vitest.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface VitestConfigOptions {
|
|
|
9
9
|
project?: ProjectConfig;
|
|
10
10
|
}
|
|
11
11
|
export interface RootVitestConfigOptions extends VitestConfigOptions {
|
|
12
|
-
|
|
12
|
+
monorepoRoot: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function defineVitestConfig(options?: VitestConfigOptions): ViteUserConfig;
|
|
15
|
-
export declare function defineRootVitestConfig(options
|
|
15
|
+
export declare function defineRootVitestConfig(options: RootVitestConfigOptions | undefined): ViteUserConfig;
|
package/dist/esm/vitest.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
3
|
-
import {
|
|
3
|
+
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
4
4
|
const APP_PATTERNS = ['**/__tests__/**/*.app.test.{ts,tsx}'];
|
|
5
5
|
const INTEGRATION_PATTERNS = ['**/__tests__/**/*.int.test.{ts,tsx}'];
|
|
6
6
|
const ALL_TEST_PATTERNS = ['**/__tests__/**/*.test.{ts,tsx}'];
|
|
7
7
|
const COVERAGE_EXCLUDE = [
|
|
8
|
-
'**/__{mocks,tests}__
|
|
8
|
+
'**/__{fixtures,mocks,tests}__/**',
|
|
9
9
|
'**/index.ts',
|
|
10
10
|
'**/mock*.{ts,tsx}',
|
|
11
11
|
'**/*.d.ts',
|
|
12
12
|
'**/*.types.ts',
|
|
13
13
|
];
|
|
14
|
+
const BUILD_OUTPUT_EXCLUDE = ['**/dist/**'];
|
|
14
15
|
const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
|
|
16
|
+
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.';
|
|
15
17
|
export function defineVitestConfig(options = {}) {
|
|
16
18
|
return buildConfig(options, { coverageInclude: PACKAGE_COVERAGE_INCLUDE });
|
|
17
19
|
}
|
|
18
|
-
export function defineRootVitestConfig(options
|
|
19
|
-
|
|
20
|
+
export function defineRootVitestConfig(options) {
|
|
21
|
+
if (options === undefined) {
|
|
22
|
+
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
23
|
+
}
|
|
24
|
+
const monorepoRoot = options.monorepoRoot;
|
|
25
|
+
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
26
|
+
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
27
|
+
}
|
|
20
28
|
return buildConfig(options, {
|
|
21
29
|
coverageInclude: [],
|
|
22
30
|
projectExclude: getWorkspaceExcludePatterns(monorepoRoot),
|
|
@@ -50,7 +58,7 @@ function buildProjects(overrides, extraExclude, projectRoot) {
|
|
|
50
58
|
const project = {
|
|
51
59
|
extends: true,
|
|
52
60
|
...(projectRoot !== undefined && { root: projectRoot }),
|
|
53
|
-
test: { exclude: [...defaultExclude, ...exclude, ...extraExclude], include, name },
|
|
61
|
+
test: { exclude: [...defaultExclude, ...BUILD_OUTPUT_EXCLUDE, ...exclude, ...extraExclude], include, name },
|
|
54
62
|
};
|
|
55
63
|
return overrides ? mergeConfig(project, { test: overrides }) : project;
|
|
56
64
|
});
|
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.23.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"
|
|
@@ -61,6 +65,7 @@
|
|
|
61
65
|
"dependencies": {
|
|
62
66
|
"glob": "13.0.6",
|
|
63
67
|
"jiti": "2.7.0",
|
|
68
|
+
"prettier-plugin-sh": "0.19.0",
|
|
64
69
|
"taze": "19.16.0",
|
|
65
70
|
"yaml": "2.9.0",
|
|
66
71
|
"@williamthorsen/nmr-core": "0.7.3"
|