@williamthorsen/nmr 0.33.0 → 0.34.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.
@@ -1,10 +1,10 @@
1
1
  /** @noformat — @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.30.0";
3
+ export const __readyupVersion = "0.32.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
7
- import { existsSync, globSync, readdirSync as readdirSync2, readFileSync } from "node:fs";
7
+ import { existsSync, globSync, readdirSync as readdirSync2 } from "node:fs";
8
8
  import { basename, join, sep } from "node:path";
9
9
  import { defineRdyKit } from "readyup";
10
10
  import {
@@ -278,6 +278,13 @@ var default_default = defineRdyKit({
278
278
  check: () => tazeConfigBuildsOnSharedConfig(),
279
279
  fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); \u2014 nmr's upgrade policy reaches a repo only through this file, so without it `nmr upgrade` reports nothing where dependencies are pinned to exact versions"
280
280
  },
281
+ {
282
+ name: "taze config declares no option taze discards",
283
+ severity: "warn",
284
+ quiet: true,
285
+ check: () => tazeConfigAvoidsClobberedOptions(),
286
+ fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script \u2014 taze's CLI writes a default for each of them over whatever the config file declares, so the value there never reaches taze (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
287
+ },
281
288
  // -- Audit dependency --------------------------------------------------------
282
289
  {
283
290
  name: "v11y-check in devDependencies",
@@ -311,6 +318,13 @@ var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
311
318
  var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
312
319
  var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
313
320
  var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
321
+ var CLOBBERED_TAZE_OPTIONS = [
322
+ { key: "concurrency", pattern: /\bconcurrency\s*:/ },
323
+ { key: "githubActions", pattern: /\bgithubActions\s*:\s*(?:false|\{)/ },
324
+ { key: "ignoreOtherWorkspaces", pattern: /\bignoreOtherWorkspaces\s*:\s*false/ },
325
+ { key: "nodeVersion", pattern: /\bnodeVersion\s*:\s*false/ },
326
+ { key: "requestTimeout", pattern: /\brequestTimeout\s*:/ }
327
+ ];
314
328
  var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
315
329
  var MIN_ESLINT_VERSION = "10.0.0";
316
330
  var MIN_STRICT_LINT_VERSION = "9.3.0";
@@ -402,7 +416,7 @@ function formatPaths(paths) {
402
416
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
403
417
  }
404
418
  function getMinVersion() {
405
- const picked = { "version": "0.33.0" };
419
+ const picked = { "version": "0.34.0" };
406
420
  if (typeof picked["version"] !== "string") {
407
421
  throw new TypeError("nmr/package.json: 'version' must be a string");
408
422
  }
@@ -482,11 +496,7 @@ function noWorkspaceRunScriptReferences() {
482
496
  };
483
497
  }
484
498
  function readFileIn(cwd, relativePath) {
485
- try {
486
- return readFileSync(join(cwd, relativePath), "utf8");
487
- } catch {
488
- return void 0;
489
- }
499
+ return readFile(join(cwd, relativePath));
490
500
  }
491
501
  function readPnpmFieldKeys(content) {
492
502
  if (content === void 0) return void 0;
@@ -514,6 +524,18 @@ function tazeConfigBuildsOnSharedConfig(cwd = process.cwd()) {
514
524
  if (stale.length === 0) return true;
515
525
  return { ok: false, detail: `does not import defineConfig from ${SHARED_TAZE_MODULE}: ${stale.join(", ")}` };
516
526
  }
527
+ function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
528
+ const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
529
+ const findings = [];
530
+ for (const relativePath of configs) {
531
+ const content = readFileIn(cwd, relativePath);
532
+ if (content === void 0) continue;
533
+ const discarded = CLOBBERED_TAZE_OPTIONS.filter(({ pattern }) => pattern.test(content)).map(({ key }) => key);
534
+ if (discarded.length > 0) findings.push(`${relativePath}: ${discarded.join(", ")}`);
535
+ }
536
+ if (findings.length === 0) return true;
537
+ return { ok: false, detail: formatPaths(findings) };
538
+ }
517
539
  function describeMissingTazeConfig(cwd) {
518
540
  const inert = findFiles(INERT_TAZE_CONFIGS, cwd);
519
541
  if (inert.length > 0) return `holds no code to call the factory: ${inert.join(", ")}`;
@@ -539,6 +561,7 @@ export {
539
561
  noReExportOnlyVitestConfigs,
540
562
  noRetiredVitestConfigs,
541
563
  prettierConfigBuildsOnSharedConfig,
564
+ tazeConfigAvoidsClobberedOptions,
542
565
  tazeConfigBuildsOnSharedConfig,
543
566
  vitestConfigBuildsOnSharedConfig,
544
567
  vitestRootConfigBuildsOnSharedConfig
@@ -8,12 +8,12 @@
8
8
  "esbuildVersion": "0.28.1",
9
9
  "inputs": [
10
10
  {
11
- "hash": "904ac753",
11
+ "hash": "f58e8934",
12
12
  "kind": "module",
13
13
  "path": "kits/default.ts"
14
14
  },
15
15
  {
16
- "hash": "1cd4c58d",
16
+ "hash": "cbbd2a2c",
17
17
  "kind": "inline",
18
18
  "path": "../package.json",
19
19
  "paths": [
@@ -38,10 +38,10 @@
38
38
  ],
39
39
  "name": "default",
40
40
  "path": "kits/default.js",
41
- "readyupVersion": "0.30.0",
41
+ "readyupVersion": "0.32.0",
42
42
  "source": "kits/default.ts",
43
- "sourceHash": "904ac753",
44
- "targetHash": "2b35e5fd"
43
+ "sourceHash": "f58e8934",
44
+ "targetHash": "debdcb6e"
45
45
  }
46
46
  ]
47
47
  }
package/README.md CHANGED
@@ -3,15 +3,25 @@
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.33.0 (2026-08-20)
6
+ ## Release notes — v0.34.0 (2026-08-24)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - Drop the coverage step from the `check:strict` gate (#727)
10
+ - 🚨 **Breaking:** Make defineVitestConfig resolve from source and isolate git (#749)
11
11
 
12
- Drops the coverage step from `check:strict` in both the workspace and root registries: The gate now runs `test` rather than `test:coverage`.
12
+ Adds two defaults to `defineVitestConfig` and `defineRootVitestConfig` in `@williamthorsen/nmr/vitest`: Workspace packages resolve through their `source` export condition, so a suite runs without a prior build; and every project loads a setup file that keeps the developer's git configuration out of the subprocesses a test spawns.
13
13
 
14
- Coverage is still available as `nmr test:coverage`, run as a separate step.
14
+ Each is individually opt-out-able through the new `isolateGit` and `resolveFromSource` options. A consumer's own `resolve.conditions` and `setupFiles` compose alongside what the factory supplies.
15
+
16
+ Migration: A workspace package declaring a `source` condition is resolved from source in tests that previously read its `dist`. A package that publishes only its build output takes a bespoke condition name instead, as `@williamthorsen/nmr-core` does with `nmr-source`.
17
+
18
+ ### 🐛 Bug fixes
19
+
20
+ - Stop nmr upgrade from timing out against a proxying registry (#744)
21
+
22
+ Fixes an issue where `nmr upgrade` timed out intermittently against a registry that proxies npm. Every `upgrade` script now calls taze with a 30-second request timeout in place of the 5-second default; a different request timeout specified by the caller is respected.
23
+
24
+ The ReadyUp kit bundled with nmr now warns when a taze config sets a value that the taze CLI will not use.
15
25
  <!-- /section:release-notes -->
16
26
 
17
27
  ## Installation
@@ -556,7 +566,7 @@ export default defineConfig({
556
566
 
557
567
  `defineConfig` supplies nmr's shared upgrade policy — a seven-day quarantine on brand-new releases, and the pair of settings that report a dependency pinned to an exact version: locked dependencies are included, and the range searched is `minor` — so your config carries only what is specific to your repo. Any setting you declare wins over nmr's default. Passing `undefined` clears a default rather than falling back to it, which is how you hand the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode` is the exception: the upgrade tool carries a default of its own for it, and clearing nmr's leaves no valid range to search, so name a mode instead.
558
568
 
559
- Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here.
569
+ Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that reach the upgrade tool from no config file at all: `concurrency`, `githubActions`, `ignoreOtherWorkspaces`, `nodeVersion`, and `requestTimeout`. The tool's CLI writes a default for each of them over whatever the config declares ([taze#317](https://github.com/antfu-collective/taze/issues/317)), so the command line is the only place they can be set. nmr's ReadyUp kit warns when your config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
560
570
 
561
571
  > **Note:** This file is what activates nmr's upgrade policy at all. A repo without one gets neither the release quarantine nor the settings that report a dependency pinned to an exact version, and `nmr upgrade` reports nothing in a repo using pnpm's `savePrefix: ''`. nmr's ReadyUp kit warns when the file is missing.
562
572
 
@@ -565,6 +575,32 @@ Two consequences of the `minor` range mode the policy declares:
565
575
  - A `packageMode` entry is honored only by the pass whose mode matches it, or by a `default` pass; every other pass drops that dependency rather than narrowing it. Under the policy's `minor` mode, a `patch` ceiling hides its dependency until you run `nmr upgrade patch`, and a `minor` ceiling hides it from `nmr upgrade major`.
566
576
  - A `~` range is searched as `^`, so minor upgrades are proposed for it and `--write` applies them under the original `~`. To hold one dependency to patches, give it a `packageMode` entry of `patch` and read it from `nmr upgrade patch`; to hold the whole repo there, declare `mode: 'patch'`.
567
577
 
578
+ ### Request timeout
579
+
580
+ Every `upgrade` script reaches the upgrade tool with a 30-second request timeout, in place of the tool's own 5 seconds.
581
+
582
+ Five seconds is too tight for a registry that proxies npm. The tool fetches a full packument whenever the resolved registry is not `registry.npmjs.org`, and a large one runs to tens of megabytes; the budget covers the whole retry chain rather than a single attempt, so one slow response ends the run with `Timeout requesting "<package>"`. The symptom is an upgrade pass that succeeds or fails according to how warm the tool's cache happens to be.
583
+
584
+ Pass a different value for one run:
585
+
586
+ ```bash
587
+ nmr upgrade --request-timeout 90000
588
+ ```
589
+
590
+ To change it for every run, override the `upgrade` script in `.config/nmr.config.ts`, keeping the rest of the default. This is nmr's own config, not the `taze.config.ts` above; taze ignores a `rootScripts` key, and nothing reports it:
591
+
592
+ ```ts
593
+ import { defineConfig } from '@williamthorsen/nmr/config';
594
+
595
+ export default defineConfig({
596
+ rootScripts: {
597
+ upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000',
598
+ },
599
+ });
600
+ ```
601
+
602
+ The override is one command string rather than a list of steps, because a composite element names an nmr command and both halves here are binaries.
603
+
568
604
  ## Default script registries
569
605
 
570
606
  These scripts are available out of the box. Repo-wide config (tier 2) and per-package overrides (tier 3) can add to or replace any of them.
@@ -877,7 +913,7 @@ nmr-fmt --write packages/nmr
877
913
 
878
914
  ### `nmr-taze`
879
915
 
880
- Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow.
916
+ Run the [taze](https://github.com/antfu-collective/taze) dependency-upgrade tool, forwarding every argument to it untouched and adding one of its own: a 30-second request timeout, unless the invocation already carries one. This is what every `upgrade` chain ends with, `root:upgrade` included — see [dependency upgrades](#dependency-upgrades) for the workflow, and [request timeout](#request-timeout) for why the timeout is set here.
881
917
 
882
918
  Under pnpm's isolated `node_modules`, a transitive package's binary is absent from the consuming repo's `node_modules/.bin`, so a repo that depends on nmr cannot run `taze` directly. `nmr-taze` can, because nmr is a direct dependency, and it resolves the tool from the tree nmr controls.
883
919
 
@@ -1058,6 +1094,25 @@ Every tier above `unit` carries a 30-second `testTimeout` and `hookTimeout`, whe
1058
1094
 
1059
1095
  To raise a budget for one tier and no other, use the `tiers` seam below. The `project` seam merges over both budgets as well, but like every option passed through it the value reaches all four projects at once -- raising a tier's budget raises `unit`'s with it. To lift the ceiling for a single file rather than a whole tier, pass a timeout to the individual test or hook, which stays the narrower tool.
1060
1096
 
1097
+ ### What the config supplies
1098
+
1099
+ Two settings a workspace test runner needs almost universally are part of what the factory produces, so a config file declares neither:
1100
+
1101
+ - **Workspace packages resolve from source.** `source` leads the emitted `resolve.conditions` and `ssr.resolve.conditions`, so a cross-package import reaches the dependency's `.ts` rather than its `dist/` and a suite runs without a prior build. A package declaring no such condition is unaffected.
1102
+ - **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device and sets `GIT_CONFIG_NOSYSTEM`, so a test that spawns `git` reads neither the developer's identity nor a signing config that can block on a passphrase. It leads each project's `setupFiles`, ahead of every entry the layers add, because a shared setup establishes the environment the rest run in.
1103
+
1104
+ Both defaults exist because omitting either is silent rather than loud: the tiers still exist and the suite still runs green, testing `dist/` and the developer's git identity instead. Turn either off through its own option, which folds across layers like any other, the last layer to declare it winning:
1105
+
1106
+ ```ts
1107
+ export default defineVitestConfig({ isolateGit: false, resolveFromSource: false });
1108
+ ```
1109
+
1110
+ `resolveFromSource: false` is for a repo that resolves a `source` condition it does not want in tests; `isolateGit: false` is for a suite meant to read the developer's own git configuration.
1111
+
1112
+ **Vite replaces its condition defaults rather than extending them.** A config writing `conditions: ['source']` by hand therefore drops `module`, so a dependency exposing a `module` entry falls through to whatever its `exports` lists next. A replaced list still resolves `node` and `development` under Vitest, which is why nothing in a test run reports the loss. The factory emits `source` alongside Vite's defaults for each environment, so a config declaring no conditions of its own starts from a complete list.
1113
+
1114
+ **A package declaring a `source` condition must ship its sources.** Every nmr consumer now resolves that condition, so a published package whose `files` carries `dist` alone resolves to a path that is not in the tarball. A package that publishes only its build output takes a bespoke condition name instead, the way `@williamthorsen/nmr-core` uses `nmr-source` for the build's own bootstrap.
1115
+
1061
1116
  ### Customizing by scope
1062
1117
 
1063
1118
  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 separate override surfaces instead of one merged config:
@@ -1065,7 +1120,7 @@ Vitest applies some options at the root of a `projects` config and others per pr
1065
1120
  ```ts
1066
1121
  export default defineVitestConfig({
1067
1122
  // Vite-level options, plus the test options Vitest honours only at the root.
1068
- root: { resolve: { conditions: ['development'] } },
1123
+ root: { ssr: { resolve: { conditions: ['my-condition'] } } },
1069
1124
  // Applied to every project.
1070
1125
  project: { setupFiles: ['./vitest.setup.ts'] },
1071
1126
  // Applied to one tier, after the `project` block above.
@@ -1079,7 +1134,9 @@ export default defineVitestConfig({
1079
1134
 
1080
1135
  Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and no surface can narrow `include` or drop a default exclusion. Adding an `include` pattern through `project` widens all four projects at once, so a file matching it is collected by each and runs four times.
1081
1136
 
1082
- `resolve.conditions` concatenates too, but layer order carries no meaning there: Vite consumes conditions as a set, and which one wins is decided by the key order of the consumed package's own `exports`. A later layer can add a condition and can never remove or outrank one an earlier layer contributed. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
1137
+ `resolve.conditions` concatenates too, onto the `source` condition and the Vite defaults the factory already emits, but layer order carries no meaning there: Vite consumes conditions as a set, and which one wins is decided by the key order of the consumed package's own `exports`. A later layer can add a condition and can never remove or outrank one an earlier layer contributed; removing `source` is what `resolveFromSource: false` is for. `resolve.alias` is the one key that merges override-first, so a later alias takes precedence over an earlier one.
1138
+
1139
+ **A condition for the tests' own resolution goes under `ssr`.** `resolve` is per-environment, and Vitest resolves a test's imports through the server environment, so `root: { ssr: { resolve: { conditions: ['my-condition'] } } }` is the seam that reaches them. A top-level `root: { resolve: { conditions: [...] } }` entry reaches the client environment, which only browser-mode tests resolve through; it composes into that array and never reports that a node test did not see it. `resolve.alias` is not per-environment, so a top-level alias reaches both.
1083
1140
 
1084
1141
  ### Sharing options across config files
1085
1142
 
@@ -1092,7 +1149,7 @@ import { fileURLToPath } from 'node:url';
1092
1149
  import type { VitestConfigOptions } from '@williamthorsen/nmr/vitest';
1093
1150
 
1094
1151
  export const shared: VitestConfigOptions = {
1095
- root: { resolve: { conditions: ['source'] } },
1152
+ root: { resolve: { alias: { '~': fileURLToPath(new URL('.', import.meta.url)) } } },
1096
1153
  project: { setupFiles: [fileURLToPath(new URL('./vitest.setup.ts', import.meta.url))] },
1097
1154
  };
1098
1155
  ```
@@ -1114,7 +1171,7 @@ Order is the point where `setupFiles` is concerned, since a shared setup file es
1114
1171
 
1115
1172
  **Do not merge two returned configs.** `mergeConfig(defineVitestConfig(), defineVitestConfig(mine))` looks like the idiomatic recovery and fails at startup: both sides declare the same four project names, which Vitest rejects with `Project name "unit" ... is not unique`. Layers merge the factory's inputs instead, which is why they yield four projects however many fold.
1116
1173
 
1117
- A config file that omits the shared layer still loses those settings, silently -- Vitest's own resolution contract, not something the factory can intercept. Guard it with a test that fails when a package's suite goes missing, which also catches a shared `exclude` pattern swallowing one package's tests.
1174
+ A config file that omits the shared layer still loses those settings, silently -- Vitest's own resolution contract, not something the factory can intercept. Guard it with a test that fails when a package's suite goes missing, which also catches a shared `exclude` pattern swallowing one package's tests. Source resolution and git isolation need no such guard: the factory supplies both, so a config file cannot omit them by forgetting a layer.
1118
1175
 
1119
1176
  ### What the config excludes
1120
1177
 
@@ -1171,3 +1228,17 @@ nmr once selected a package's test scripts by looking for a `vitest.integration.
1171
1228
  Until step 1 is done, every test command that names a tier fails against a config declaring no projects, with `Error: No projects matched the filter "unit", "tool".` at startup. `test:all` is the exception: it names no project, so it runs everything the config collects.
1172
1229
 
1173
1230
  That is a change for the better. While `test` selected by negation, an unmigrated config produced a _green_ run, because `--project '!integration'` had nothing to exclude, so the separation was lost with no error anywhere. Positive selection turns the same state into a startup failure that names the missing projects.
1231
+
1232
+ ### Migrating a repo that layered source resolution or git isolation by hand
1233
+
1234
+ The factory supplies both, so the layer a repo wrote for them, and any guard test that checked every config file imported it, can go.
1235
+
1236
+ 1. Delete the shared module's `resolve.conditions` entry for `source` and its `ssr.resolve` twin. Keep the module for whatever else it carries; delete it outright once it carries nothing.
1237
+ 2. Delete the git-isolation setup file and its `setupFiles` entry. nmr's runs in its place, ahead of every entry a layer adds.
1238
+ 3. Delete the test that asserted each config file imports the shared module, where the module is gone.
1239
+
1240
+ Two behaviors change on upgrade even in a repo that layered neither.
1241
+
1242
+ A suite whose tests run `git commit` while relying on the developer's global identity now fails with git's own `Please tell me who you are`. Set the identity locally in the fixture repo the test builds -- `git -C <repo> config user.email`, `user.name`, and `commit.gpgsign false` -- which is what makes the test report the same result on every machine.
1243
+
1244
+ A workspace package declaring a `source` condition is now resolved from source in tests that previously read its `dist`. That is the point of the default, and it means a suite stops exercising build output it may have been exercising by accident.
@@ -4,6 +4,10 @@ import { fileURLToPath } from 'node:url';
4
4
  import { reportError } from '@williamthorsen/nmr-core';
5
5
  import { describeError } from '@williamthorsen/toolbelt.errors';
6
6
  const TAZE_CLI_SPECIFIER = 'taze/cli';
7
+ const REQUEST_TIMEOUT_FLAG = '--request-timeout';
8
+ const REQUEST_TIMEOUT_FLAGS = [REQUEST_TIMEOUT_FLAG, '--requestTimeout'];
9
+ const REQUEST_TIMEOUT_MS = 30_000;
10
+ const ARGUMENT_TERMINATOR = '--';
7
11
  export function runTaze(args, options = {}) {
8
12
  const stderr = options.stderr ?? process.stderr;
9
13
  const resolveCliPath = options.resolveCliPath ?? resolveTazeCliPath;
@@ -17,7 +21,7 @@ export function runTaze(args, options = {}) {
17
21
  reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
18
22
  return 1;
19
23
  }
20
- const outcome = spawn(process.execPath, [cliPath, ...args]);
24
+ const outcome = spawn(process.execPath, [cliPath, ...composeRequestTimeoutArgs(args), ...args]);
21
25
  if (outcome.error) {
22
26
  reportError(`Failed to run taze: ${outcome.error.message}`, stderr);
23
27
  return 1;
@@ -27,6 +31,18 @@ export function runTaze(args, options = {}) {
27
31
  export function resolveTazeCliPath() {
28
32
  return fileURLToPath(import.meta.resolve(TAZE_CLI_SPECIFIER));
29
33
  }
34
+ function carriesRequestTimeout(args) {
35
+ for (const arg of args) {
36
+ if (arg === ARGUMENT_TERMINATOR)
37
+ return false;
38
+ if (REQUEST_TIMEOUT_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)))
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ function composeRequestTimeoutArgs(args) {
44
+ return carriesRequestTimeout(args) ? [] : [REQUEST_TIMEOUT_FLAG, String(REQUEST_TIMEOUT_MS)];
45
+ }
30
46
  function spawnNode(nodePath, argv) {
31
47
  const result = spawnSync(nodePath, argv, { stdio: 'inherit' });
32
48
  return { status: result.status, error: result.error };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import os from 'node:os';
2
+ process.env['GIT_CONFIG_GLOBAL'] = os.devNull;
3
+ process.env['GIT_CONFIG_SYSTEM'] = os.devNull;
4
+ process.env['GIT_CONFIG_NOSYSTEM'] = '1';
@@ -10,6 +10,8 @@ export interface VitestConfigOptions {
10
10
  root?: RootOverrides;
11
11
  project?: ProjectConfig;
12
12
  tiers?: Partial<Record<TierName, ProjectConfig>>;
13
+ isolateGit?: boolean;
14
+ resolveFromSource?: boolean;
13
15
  }
14
16
  export interface RootVitestConfigOptions extends VitestConfigOptions {
15
17
  monorepoRoot: string;
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
2
3
  import { defaultExclude, mergeConfig } from 'vitest/config';
3
4
  import { isObject } from "./helpers/type-guards.js";
4
5
  import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
@@ -6,6 +7,9 @@ import { getWorkspacePackageDirs } from "./workspace.js";
6
7
  const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
7
8
  const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
8
9
  const TIER_TIMEOUT = 30_000;
10
+ const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
11
+ const SOURCE_CLIENT_CONDITIONS = ['source', 'module', 'browser', 'development|production'];
12
+ const SOURCE_SERVER_CONDITIONS = ['source', 'module', 'node', 'development|production'];
9
13
  const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
10
14
  const COLLECTION_EXCLUDE = [...new Set([...defaultExclude, ...TEST_COLLECTION_EXCLUDE.map((dir) => `**/${dir}/**`)])];
11
15
  const PACKAGE_COVERAGE_INCLUDE = ['**/src/**/*.{ts,tsx}'];
@@ -29,6 +33,10 @@ function buildConfig(declaredLayers, { coverageInclude, projectExclude = [], pro
29
33
  const layers = declaredLayers.filter((layer) => layer !== undefined);
30
34
  assertKnownTiers(layers);
31
35
  const config = {
36
+ ...(resolveFlag(layers, 'resolveFromSource') && {
37
+ resolve: { conditions: SOURCE_CLIENT_CONDITIONS },
38
+ ssr: { resolve: { conditions: SOURCE_SERVER_CONDITIONS } },
39
+ }),
32
40
  test: {
33
41
  coverage: {
34
42
  enabled: false,
@@ -59,6 +67,7 @@ function buildProjects(layers, extraExclude, projectRoot) {
59
67
  timeout: TIER_TIMEOUT,
60
68
  })),
61
69
  ];
70
+ const isolateGit = resolveFlag(layers, 'isolateGit');
62
71
  return projectTiers.map(({ exclude, include, name, timeout }) => {
63
72
  const project = {
64
73
  extends: true,
@@ -67,6 +76,7 @@ function buildProjects(layers, extraExclude, projectRoot) {
67
76
  exclude: [...COLLECTION_EXCLUDE, ...exclude, ...extraExclude],
68
77
  include,
69
78
  name,
79
+ ...(isolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
70
80
  ...(timeout !== undefined && { hookTimeout: timeout, testTimeout: timeout }),
71
81
  },
72
82
  };
@@ -98,3 +108,16 @@ function getWorkspaceExcludePatterns(monorepoRoot) {
98
108
  .map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
99
109
  .toSorted();
100
110
  }
111
+ function resolveFlag(layers, name) {
112
+ let resolved = true;
113
+ for (const layer of layers) {
114
+ const declared = layer[name];
115
+ if (declared !== undefined)
116
+ resolved = declared;
117
+ }
118
+ return resolved;
119
+ }
120
+ function resolveGitIsolationSetupFile() {
121
+ const thisFile = fileURLToPath(import.meta.url);
122
+ return path.join(path.dirname(thisFile), `vitest-git-isolation${path.extname(thisFile)}`);
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -21,7 +21,8 @@
21
21
  "license": "ISC",
22
22
  "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
23
23
  "sideEffects": [
24
- "./dist/esm/cli*.js"
24
+ "./dist/esm/cli*.js",
25
+ "./dist/esm/vitest-git-isolation.js"
25
26
  ],
26
27
  "type": "module",
27
28
  "exports": {
@@ -68,21 +69,22 @@
68
69
  "dist"
69
70
  ],
70
71
  "dependencies": {
71
- "@williamthorsen/toolbelt.errors": "0.5.0",
72
+ "@williamthorsen/toolbelt.errors": "0.6.1",
72
73
  "glob": "13.0.6",
73
74
  "prettier-plugin-sh": "0.19.0",
74
- "taze": "20.0.1",
75
+ "taze": "21.1.0",
75
76
  "yaml": "2.9.0",
76
- "@williamthorsen/nmr-core": "0.10.2"
77
+ "@williamthorsen/nmr-core": "0.10.3"
77
78
  },
78
79
  "devDependencies": {
79
- "@williamthorsen/toolbelt.filesystem": "0.6.0",
80
- "@williamthorsen/toolbelt.testing": "0.3.0",
81
- "@williamthorsen/toolbelt.vitest": "0.4.0",
80
+ "@williamthorsen/toolbelt.filesystem": "0.8.0",
81
+ "@williamthorsen/toolbelt.testing": "0.4.0",
82
+ "@williamthorsen/toolbelt.vitest": "0.6.0",
82
83
  "prettier": "3.9.6",
83
- "readyup": "0.30.0",
84
+ "readyup": "0.32.0",
84
85
  "tinyglobby": "0.2.17",
85
86
  "typescript": "6.0.3",
87
+ "vite": "8.2.1",
86
88
  "vitest": "4.1.10"
87
89
  },
88
90
  "peerDependencies": {