@williamthorsen/nmr 0.22.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 CHANGED
@@ -3,17 +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.22.0 (2026-07-31)
6
+ ## Release notes — v0.23.0 (2026-08-01)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - 🚨 **Breaking:** Ship shell and Dockerfile formatting from a shared nmr Prettier config (#540)
10
+ - 🚨 **Breaking:** Close the shared Vitest config's path gaps (#546)
11
11
 
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.
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.
13
13
 
14
- - 🚨 **Breaking:** Require the monorepo root in the shared root Vitest config (#541)
14
+ ### 🐛 Bug fixes
15
15
 
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.
16
+ - Stop rewriting documented commands inside Markdown fences (#549)
17
+
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.
17
19
  <!-- /section:release-notes -->
18
20
 
19
21
  ## Installation
@@ -587,6 +589,8 @@ Beyond everything Prettier already handles, this config formats `.sh`, `.bash`,
587
589
 
588
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.
589
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
+
590
594
  ### Language scoping
591
595
 
592
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.
@@ -618,7 +622,7 @@ export default definePrettierConfig({
618
622
  ### Adoption caveats
619
623
 
620
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.
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.
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.
622
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.
623
627
 
624
628
  ## Shared Vitest config
@@ -671,6 +675,22 @@ export default defineVitestConfig({
671
675
 
672
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.
673
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
+
674
694
  ### Root-scoped tests
675
695
 
676
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:
@@ -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 [...files].sort()) {
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)));
@@ -24,6 +24,25 @@ const DEFAULT_OVERRIDES = [
24
24
  files: ['*.json5', '*.jsonc', 'tsconfig.json', 'tsconfig.*.json'],
25
25
  options: { parser: 'jsonc', singleQuote: false, trailingComma: 'all' },
26
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
+ },
27
46
  ];
28
47
  export function definePrettierConfig(options = {}) {
29
48
  assertNoReplacement(options);
@@ -5,12 +5,13 @@ 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}'];
15
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.';
16
17
  export function defineVitestConfig(options = {}) {
@@ -57,7 +58,7 @@ function buildProjects(overrides, extraExclude, projectRoot) {
57
58
  const project = {
58
59
  extends: true,
59
60
  ...(projectRoot !== undefined && { root: projectRoot }),
60
- test: { exclude: [...defaultExclude, ...exclude, ...extraExclude], include, name },
61
+ test: { exclude: [...defaultExclude, ...BUILD_OUTPUT_EXCLUDE, ...exclude, ...extraExclude], include, name },
61
62
  };
62
63
  return overrides ? mergeConfig(project, { test: overrides }) : project;
63
64
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [