@williamthorsen/nmr 0.55.0 → 1.0.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/.meta/changelog.json +124 -0
- package/.readyup/kits/default.js +667 -167
- package/.readyup/manifest.json +77 -46
- package/CHANGELOG.md +40 -0
- package/README.md +1 -14
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-actionlint.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-shellcheck.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr-vitest.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/bins.d.ts +11 -0
- package/dist/esm/bins.js +11 -0
- package/dist/esm/check-cache-entries.js +5 -5
- package/dist/esm/check-cache-gate.js +2 -1
- package/dist/esm/child-env.js +2 -1
- package/dist/esm/child-process.d.ts +2 -0
- package/dist/esm/child-process.js +3 -3
- package/dist/esm/cli-actionlint.js +2 -1
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +29 -16
- package/dist/esm/cli-fmt.js +2 -1
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-shellcheck.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli-vitest.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/actionlint.d.ts +3 -3
- package/dist/esm/commands/actionlint.js +5 -5
- package/dist/esm/commands/build.js +6 -6
- package/dist/esm/commands/clean.js +3 -1
- package/dist/esm/commands/compiler.js +3 -3
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +4 -5
- package/dist/esm/commands/fmt.d.ts +3 -3
- package/dist/esm/commands/fmt.js +33 -52
- package/dist/esm/commands/format-targets.js +1 -13
- package/dist/esm/commands/node-pin.js +3 -4
- package/dist/esm/commands/rewrite-specifiers.js +2 -2
- package/dist/esm/commands/run-vitest.js +5 -5
- package/dist/esm/commands/shellcheck.d.ts +3 -3
- package/dist/esm/commands/shellcheck.js +8 -11
- package/dist/esm/commands/taze.js +5 -4
- package/dist/esm/commands/tsconfig-chain.js +3 -3
- package/dist/esm/commands/workflow-tools.d.ts +12 -3
- package/dist/esm/commands/workflow-tools.js +29 -14
- package/dist/esm/config-validation.js +9 -9
- package/dist/esm/config.js +2 -3
- package/dist/esm/delegate-host.d.ts +1 -1
- package/dist/esm/delegate-host.js +5 -3
- package/dist/esm/entry-chain.js +2 -3
- package/dist/esm/fan-out.d.ts +3 -3
- package/dist/esm/fan-out.js +14 -6
- package/dist/esm/git-ignored-paths.js +3 -3
- package/dist/esm/help.d.ts +14 -15
- package/dist/esm/help.js +29 -54
- package/dist/esm/helpers/filter-selection.js +2 -2
- package/dist/esm/helpers/package-json.js +8 -9
- package/dist/esm/helpers/resolveXdgBaseDir.d.ts +5 -0
- package/dist/esm/helpers/resolveXdgBaseDir.js +12 -0
- package/dist/esm/helpers/type-guards.d.ts +0 -2
- package/dist/esm/helpers/type-guards.js +2 -4
- package/dist/esm/invocation-args.d.ts +0 -32
- package/dist/esm/invocation-args.js +23 -121
- package/dist/esm/invocation.d.ts +3 -1
- package/dist/esm/invocation.js +1 -0
- package/dist/esm/machine-lock.js +3 -4
- package/dist/esm/nmr-flags.d.ts +109 -0
- package/dist/esm/nmr-flags.js +120 -0
- package/dist/esm/output-style.d.ts +1 -1
- package/dist/esm/output-style.js +2 -1
- package/dist/esm/pass-manifest.js +2 -2
- package/dist/esm/presentation.d.ts +1 -1
- package/dist/esm/remote-host-session.d.ts +2 -0
- package/dist/esm/remote-host-session.js +1 -0
- package/dist/esm/remote-probe.js +2 -2
- package/dist/esm/remote-run.d.ts +3 -3
- package/dist/esm/remote-run.js +6 -4
- package/dist/esm/remote-session.js +4 -4
- package/dist/esm/remote-snapshot.js +8 -22
- package/dist/esm/resolver.js +3 -4
- package/dist/esm/route.d.ts +29 -0
- package/dist/esm/route.js +94 -0
- package/dist/esm/run-chain.d.ts +1 -1
- package/dist/esm/run-chain.js +4 -2
- package/dist/esm/runCli.d.ts +2 -0
- package/dist/esm/runCli.js +36 -78
- package/dist/esm/stated-outcome.js +3 -3
- package/dist/esm/steps.js +6 -30
- package/dist/esm/user-config.js +7 -14
- package/dist/esm/vitest-source-resolution.js +5 -6
- package/dist/esm/vitest-temp-isolation.d.ts +2 -0
- package/dist/esm/vitest-temp-isolation.js +44 -0
- package/dist/esm/vitest.d.ts +9 -0
- package/dist/esm/vitest.js +8 -5
- package/docs/reporting.md +3 -3
- package/docs/scripts.md +1 -1
- package/docs/utilities.md +5 -5
- package/docs/vitest.md +11 -6
- package/package.json +4 -4
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { isBuiltin } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
4
|
+
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { findDirectoryChainMatch } from '@williamthorsen/toolbelt.filesystem';
|
|
6
6
|
import { parsePackageJson } from "./helpers/package-json.js";
|
|
7
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
8
7
|
const SHARED_CONDITIONS = ['module', 'import', 'development', 'default'];
|
|
9
8
|
const CLIENT_CONDITION = 'browser';
|
|
10
9
|
const SERVER_CONDITION = 'node';
|
|
@@ -32,7 +31,7 @@ export function resolveSourceTarget(specifier, importer, context) {
|
|
|
32
31
|
if (packageDir === undefined || isInsideNodeModules(packageDir))
|
|
33
32
|
return undefined;
|
|
34
33
|
const manifest = readManifest(packageDir, context.manifests);
|
|
35
|
-
if (!
|
|
34
|
+
if (!isRecord(manifest))
|
|
36
35
|
return undefined;
|
|
37
36
|
const matchedSubpath = matchSubpath(manifest['exports'], parsedSpecifier.subpath);
|
|
38
37
|
if (!matchedSubpath)
|
|
@@ -72,7 +71,7 @@ function findSelfReferenceDir(name, fromDir) {
|
|
|
72
71
|
const file = path.join(dir, 'package.json');
|
|
73
72
|
if (existsSync(file)) {
|
|
74
73
|
const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
|
|
75
|
-
return
|
|
74
|
+
return isRecord(parsedManifest) && parsedManifest['name'] === name && parsedManifest['exports'] !== undefined
|
|
76
75
|
? dir
|
|
77
76
|
: undefined;
|
|
78
77
|
}
|
|
@@ -122,7 +121,7 @@ function isInsideNodeModules(dir) {
|
|
|
122
121
|
function matchSubpath(exportsValue, subpath) {
|
|
123
122
|
if (exportsValue === undefined)
|
|
124
123
|
return undefined;
|
|
125
|
-
if (!
|
|
124
|
+
if (!isRecord(exportsValue) || Object.keys(exportsValue).every((key) => !key.startsWith('.'))) {
|
|
126
125
|
return subpath === '.' ? { value: exportsValue } : undefined;
|
|
127
126
|
}
|
|
128
127
|
const exactValue = exportsValue[subpath];
|
|
@@ -181,7 +180,7 @@ function selectTarget(value, conditions) {
|
|
|
181
180
|
}
|
|
182
181
|
return undefined;
|
|
183
182
|
}
|
|
184
|
-
if (!
|
|
183
|
+
if (!isRecord(value))
|
|
185
184
|
return undefined;
|
|
186
185
|
const source = value[SOURCE_CONDITION];
|
|
187
186
|
if (source !== undefined) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
5
|
+
const RUN_DIR_PREFIX = 'nmr-vitest-';
|
|
6
|
+
const RUN_DIR_PATTERN = /^nmr-vitest-([1-9]\d*)-./;
|
|
7
|
+
export function setup() {
|
|
8
|
+
const baseDir = os.tmpdir();
|
|
9
|
+
removeOrphanedRunDirs(baseDir);
|
|
10
|
+
const runDir = fs.mkdtempSync(path.join(baseDir, `${RUN_DIR_PREFIX}${process.pid}-`));
|
|
11
|
+
const previousTmpdir = process.env['TMPDIR'];
|
|
12
|
+
process.env['TMPDIR'] = runDir;
|
|
13
|
+
return function teardown() {
|
|
14
|
+
if (previousTmpdir === undefined) {
|
|
15
|
+
delete process.env['TMPDIR'];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
process.env['TMPDIR'] = previousTmpdir;
|
|
19
|
+
}
|
|
20
|
+
fs.rmSync(runDir, { force: true, maxRetries: 5, recursive: true });
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isProcessGone(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return hasErrnoCode(error, 'ESRCH');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function removeOrphanedRunDirs(baseDir) {
|
|
33
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const pidText = RUN_DIR_PATTERN.exec(entry.name)?.[1];
|
|
36
|
+
if (!entry.isDirectory() || pidText === undefined || !isProcessGone(Number(pidText)))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
fs.rmSync(path.join(baseDir, entry.name), { force: true, recursive: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/esm/vitest.d.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface VitestConfigOptions {
|
|
|
44
44
|
* Turn it off only when a suite is meant to read the developer's own git configuration.
|
|
45
45
|
*/
|
|
46
46
|
shouldIsolateGit?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Loads nmr's temp-isolation global setup into the root config. Defaults to `true`. The setup gives the run a
|
|
49
|
+
* directory of its own under the temp directory in effect at startup, points `TMPDIR` at it for every worker and
|
|
50
|
+
* every process that a test spawns, and removes it when the run ends, along with any that an interrupted run left
|
|
51
|
+
* behind. Vitest runs a project's own `globalSetup` before the root's, so one that a layer adds through `project`
|
|
52
|
+
* runs outside the run directory. Turn the isolation off only when a suite must leave files in the ambient temp
|
|
53
|
+
* directory after the run.
|
|
54
|
+
*/
|
|
55
|
+
shouldIsolateTempDir?: boolean;
|
|
47
56
|
/**
|
|
48
57
|
* Resolves a package whose files are outside every `node_modules` through its `source` export condition, so a
|
|
49
58
|
* suite runs without a prior build. Defaults to `true`. A package that does not declare a `source` condition is
|
package/dist/esm/vitest.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isRecord } from '@williamthorsen/nmr-core';
|
|
4
5
|
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
5
6
|
import { listGitIgnoredPaths } from "./git-ignored-paths.js";
|
|
6
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
7
7
|
import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
|
|
8
8
|
import { deriveDefaultMaxWorkers, readMaxWorkersFlag } from "./vitest-max-workers.js";
|
|
9
9
|
import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
|
|
@@ -12,7 +12,8 @@ import { toWorkspacePackageGlobs } from "./workspace-globs.js";
|
|
|
12
12
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
13
13
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
14
14
|
const TIER_TIMEOUT_MS = 30_000;
|
|
15
|
-
const GIT_ISOLATION_SETUP_FILE =
|
|
15
|
+
const GIT_ISOLATION_SETUP_FILE = resolveSiblingModule('vitest-git-isolation');
|
|
16
|
+
const TEMP_ISOLATION_SETUP_FILE = resolveSiblingModule('vitest-temp-isolation');
|
|
16
17
|
const CLIENT_CONDITIONS = ['module', 'browser', 'development|production'];
|
|
17
18
|
const SERVER_CONDITIONS = ['module', 'node', 'development|production'];
|
|
18
19
|
const COVERAGE_EXCLUDE = ['**/__{fixtures,mocks,tests}__/**', '**/index.ts', '**/*.d.ts'];
|
|
@@ -22,6 +23,7 @@ const RECOGNIZED_OPTIONS = {
|
|
|
22
23
|
project: true,
|
|
23
24
|
root: true,
|
|
24
25
|
shouldIsolateGit: true,
|
|
26
|
+
shouldIsolateTempDir: true,
|
|
25
27
|
shouldResolveFromSource: true,
|
|
26
28
|
testCollectionExclude: true,
|
|
27
29
|
tiers: true,
|
|
@@ -44,7 +46,7 @@ export function defineVitestConfig(...layers) {
|
|
|
44
46
|
}
|
|
45
47
|
export function defineRootVitestConfig(...layers) {
|
|
46
48
|
const lastLayer = layers.at(-1);
|
|
47
|
-
const monorepoRoot =
|
|
49
|
+
const monorepoRoot = isRecord(lastLayer) ? lastLayer['monorepoRoot'] : undefined;
|
|
48
50
|
if (typeof monorepoRoot !== 'string' || !path.isAbsolute(monorepoRoot)) {
|
|
49
51
|
throw new TypeError(MISSING_MONOREPO_ROOT);
|
|
50
52
|
}
|
|
@@ -74,6 +76,7 @@ function buildConfig(declaredLayers, { coverageInclude, ignoredPathsRoot, projec
|
|
|
74
76
|
include: coverageInclude,
|
|
75
77
|
provider: 'v8',
|
|
76
78
|
},
|
|
79
|
+
...(resolveFlag(layers, 'shouldIsolateTempDir', true) && { globalSetup: [TEMP_ISOLATION_SETUP_FILE] }),
|
|
77
80
|
passWithNoTests: true,
|
|
78
81
|
projects: buildProjects(layers, projectExclude, projectRoot, ignoredPathsRoot),
|
|
79
82
|
silent: 'passed-only',
|
|
@@ -189,7 +192,7 @@ function resolveFlag(layers, name, isOnByDefault) {
|
|
|
189
192
|
}
|
|
190
193
|
return resolvedFlag;
|
|
191
194
|
}
|
|
192
|
-
function
|
|
195
|
+
function resolveSiblingModule(basename) {
|
|
193
196
|
const thisFile = fileURLToPath(import.meta.url);
|
|
194
|
-
return path.join(path.dirname(thisFile),
|
|
197
|
+
return path.join(path.dirname(thisFile), `${basename}${path.extname(thisFile)}`);
|
|
195
198
|
}
|
package/docs/reporting.md
CHANGED
|
@@ -64,13 +64,13 @@ Because the format is nmr's own reporting, and nmr does not tell the commands th
|
|
|
64
64
|
|
|
65
65
|
## Reserved environment variables
|
|
66
66
|
|
|
67
|
-
`NMR_COMMAND_VERBOSITY` is nmr's own, and sets how loudly a run reports the output of the commands that it runs. Its values are `full` and `quiet`; nmr reports any other non-empty value and exits
|
|
67
|
+
`NMR_COMMAND_VERBOSITY` is nmr's own, and sets how loudly a run reports the output of the commands that it runs. Its values are `full` and `quiet`; nmr reports any other non-empty value as a usage error and exits 2, rather than falling back to a mode that nobody chose, and an empty value reads as unset. Each process in a chain suppresses the output of the command that it runs rather than of everything below it, so the failing command's output still prints on a failure. It governs those commands' output alone: nmr's [verdicts](#what-nmr-reports) print in every verbosity, and the override notice is the one message that a quiet run withholds.
|
|
68
68
|
|
|
69
69
|
`nmr -q <command>` sets it for the run and outranks an inherited value. Both values are spelled out so that either direction is expressible: Export `NMR_COMMAND_VERBOSITY=quiet` for a quiet shell, and set `full` on one invocation to opt back out.
|
|
70
70
|
|
|
71
71
|
`NMR_OUTPUT_STYLE` is nmr's own as well, and sets the style in which nmr renders every marker that it prints; see [output style](#output-style) below.
|
|
72
72
|
|
|
73
|
-
`NMR_REPORT_FORMAT` is nmr's own as well, and sets how nmr renders its own verdicts. Its values are `text` and `json`; nmr reports any other non-empty value and exits
|
|
73
|
+
`NMR_REPORT_FORMAT` is nmr's own as well, and sets how nmr renders its own verdicts. Its values are `text` and `json`; nmr reports any other non-empty value as a usage error and exits 2, and an empty value reads as unset. `--json` sets it for the run and outranks an inherited value, and both values are spelled out so that either direction is expressible: Export `NMR_REPORT_FORMAT=json` for a shell whose consumer parses, and set `text` on one invocation to opt back out. The format has two sources, then, whereas a verbosity has four: It does not have a repo-level default or harness detection, because detection would hand every agent JSON in place of the prose that the [bundled guidance](../README.md#agent-guidance) teaches. See [reporting for a machine](#reporting-for-a-machine) for what the objects contain.
|
|
74
74
|
|
|
75
75
|
`NMR_ENTRY_CHAIN` is nmr's own as well. It lists the commands that the current run has entered and the directories in which it entered them, and nmr passes it to every process that it starts, so that a process re-entering a command already running in its directory fails instead of repeating without end; see [self-referential entries](scripts.md#three-tier-override-system). Do not set it: nmr reports a value that it cannot read and exits 1.
|
|
76
76
|
|
|
@@ -78,7 +78,7 @@ Because the format is nmr's own reporting, and nmr does not tell the commands th
|
|
|
78
78
|
|
|
79
79
|
### Output style
|
|
80
80
|
|
|
81
|
-
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code
|
|
81
|
+
`--output-style <auto|plain|rich>` sets the style for one invocation, and `NMR_OUTPUT_STYLE` sets it for a shell: `rich` marks each verdict with an emoji, such as ✅ or ❌, and `plain` prints a word in its place, such as `PASS` or `FAIL`, which a reader of a log can search for. `auto`, the default, defers to detection. Any other value is a usage error, and nmr exits with code 2, whichever source named it. The flag outranks the variable, and both accept `auto` as well as a style, so a shell exporting one style is overridable on one invocation in either direction.
|
|
82
82
|
|
|
83
83
|
Detection decides stdout and stderr separately. A stream is plain when `CI` is set to anything other than an empty string or `false`, when the stream is not a terminal, or when `TERM` is `linux`; otherwise it is rich. `--json` output does not contain a marker in either style.
|
|
84
84
|
|
package/docs/scripts.md
CHANGED
|
@@ -198,7 +198,7 @@ The run never enters a package, so per-package `lint:pre` and `lint:post` hooks
|
|
|
198
198
|
|
|
199
199
|
`lint:workflows`, a root command, lints the GitHub Actions workflows under `.github/workflows/` with [actionlint](https://github.com/rhysd/actionlint), including its shellcheck pass over every `run:` block, and fails on any finding. A repo without `.github/workflows/` passes. The command runs `nmr-actionlint`, which forwards trailing arguments to actionlint, so `nmr lint:workflows .github/workflows/ci.yaml` lints that file alone; inside `check`, `check:strict`, and `root:check`, the step declines them.
|
|
200
200
|
|
|
201
|
-
The result does not depend on what the machine has installed. nmr pins actionlint 1.7.12 and shellcheck 0.11.0, downloads each from its upstream GitHub release on first use, verifies it against the SHA-256 recorded in nmr, and caches it under `node_modules/.cache/nmr-tools/` at the monorepo root. The first run therefore needs access to `github.com`, and a failed download or a checksum mismatch fails the command. The pyflakes integration is off. The pins move only with a release of nmr.
|
|
201
|
+
The result does not depend on what the machine has installed. nmr pins actionlint 1.7.12 and shellcheck 0.11.0, downloads each from its upstream GitHub release on first use, verifies it against the SHA-256 recorded in nmr, and caches it once per machine under `$XDG_CACHE_HOME/nmr/tools/`, or `~/.cache/nmr/tools/` when `XDG_CACHE_HOME` is unset or not absolute, so that every worktree on the machine shares one copy. A run first looks in that cache and then in `node_modules/.cache/nmr-tools/` at the monorepo root. When the tool is in neither, the run installs it into the machine cache, or into the monorepo-root cache when the machine cache cannot be written, as in a sandbox that confines writes to the worktree. The first run on a machine therefore needs access to `github.com`, and a failed download or a checksum mismatch fails the command. The pyflakes integration is off. The pins move only with a release of nmr.
|
|
202
202
|
|
|
203
203
|
The supported platforms are macOS and Linux on x64 and arm64; on any other, the command fails and names the platform. actionlint reads its own configuration from `.github/actionlint.yaml`, through which a repo can ignore a finding.
|
|
204
204
|
|
package/docs/utilities.md
CHANGED
|
@@ -100,8 +100,8 @@ Verify that all publishable workspace packages have a `prepublishOnly` script. E
|
|
|
100
100
|
ensure-prepublish-hooks
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
| Flag | Description | Default
|
|
104
|
-
| --------------------- | ----------------------------- |
|
|
105
|
-
| `--fix` | Add missing hooks | —
|
|
106
|
-
| `--dry-run` | Preview what `--fix` would do | —
|
|
107
|
-
| `--command <command>` | Custom hook command | `"
|
|
103
|
+
| Flag | Description | Default |
|
|
104
|
+
| --------------------- | ----------------------------- | ------------- |
|
|
105
|
+
| `--fix` | Add missing hooks | — |
|
|
106
|
+
| `--dry-run` | Preview what `--fix` would do | — |
|
|
107
|
+
| `--command <command>` | Custom hook command | `"nmr build"` |
|
package/docs/vitest.md
CHANGED
|
@@ -108,18 +108,23 @@ In a repo that has not declared the check, [nmr's readyup kit](../README.md#conf
|
|
|
108
108
|
|
|
109
109
|
## What the config supplies
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
Three settings that a workspace test runner needs almost universally are part of what the factory produces, so a config file declares none of them:
|
|
112
112
|
|
|
113
113
|
- **A package outside `node_modules` resolves from source.** Because a cross-package import resolves to the dependency's `.ts` rather than its `dist/`, through the `source` condition declared by its `exports` map, a suite runs without a prior build. The reach is what the boundary names: a workspace package, or one linked into the repo from elsewhere, whose real path is outside every `node_modules`. A package that does not declare a `source` condition is unaffected, and so is every package under `node_modules`, whatever its `exports` map declares.
|
|
114
114
|
- **Git subprocesses are isolated.** Every project loads a setup file that points `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` at the null device, sets `GIT_CONFIG_NOSYSTEM` and `GIT_ATTR_NOSYSTEM`, and injects `core.excludesFile`, `core.attributesFile`, and `maintenance.auto=false` through `GIT_CONFIG_COUNT` and its key/value pairs, so a test that spawns `git` reads neither the developer's identity, nor a signing config that can block on a passphrase, nor their global ignore rules and attributes, and a command such as `git commit` does not start a detached `git maintenance` process that could still be writing under `.git` when the test removes the repository. Those last two need a step of their own: Because neither path is config-derived, git resolves each from `$XDG_CONFIG_HOME/git/`, or from `~/.config/git/` when that variable is unset, whatever the config variables say. `GIT_CONFIG_GLOBAL` already replaces `$XDG_CONFIG_HOME/git/config`; those two are the rest of what that directory supplies. The system attributes file at `$(prefix)/etc/gitattributes` is not named by any config variable at all, which is what `GIT_ATTR_NOSYSTEM` covers. Redirecting `XDG_CONFIG_HOME` would cover the per-user pair as well, but the setup file does not redirect it, because other tools spawned by a test read that variable for configuration of their own: pnpm fails to start under `XDG_CONFIG_HOME=/dev/null`. An injected key outranks repository-local config, so a repository setting its own `core.excludesFile` is overridden under the isolation; a test needing its own value passes it on the invocation as `git -c core.excludesFile=...`, which outranks the injection. The setup file leads each project's `setupFiles`, ahead of every entry that the layers add, because a shared setup establishes the environment in which the rest run.
|
|
115
|
+
- **Each run has its own temp directory.** The root config loads a global setup that creates a run directory named `nmr-vitest-<pid>-…` under the temp directory in effect at startup, points `TMPDIR` at it, and removes it when the run ends, whether its tests pass or fail. Every worker and every process that a test spawns inherits the variable, so scratch files that a test does not clean up stay out of the shared temp directory, where they would otherwise accumulate across runs and repositories until a tmpfs with a fixed inode count has none left. A run stopped by a signal skips global teardown, so the setup first removes each run directory whose process has exited, and leaves those whose process is still running. Vitest runs a project's own `globalSetup` before the root's, so one that a layer adds through `project` runs outside the run directory.
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
The first two 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. The third exists because a leak shows up only once the temp directory is full, in whatever command next writes to it. Turn any of them off through its own option, which folds across layers like any other, the last layer to declare it winning:
|
|
117
118
|
|
|
118
119
|
```ts
|
|
119
|
-
export default defineVitestConfig({
|
|
120
|
+
export default defineVitestConfig({
|
|
121
|
+
shouldIsolateGit: false,
|
|
122
|
+
shouldIsolateTempDir: false,
|
|
123
|
+
shouldResolveFromSource: false,
|
|
124
|
+
});
|
|
120
125
|
```
|
|
121
126
|
|
|
122
|
-
`shouldResolveFromSource: false` is for a repo whose own packages declare a `source` condition that it does not want in tests; `shouldIsolateGit: false` is for a suite meant to read the developer's own git configuration.
|
|
127
|
+
`shouldResolveFromSource: false` is for a repo whose own packages declare a `source` condition that it does not want in tests; `shouldIsolateGit: false` is for a suite meant to read the developer's own git configuration; `shouldIsolateTempDir: false` is for a suite whose files must outlive the run in the ambient temp directory.
|
|
123
128
|
|
|
124
129
|
**Vite replaces its condition defaults rather than extending them.** A config writing `conditions: ['my-condition']` 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. Because the factory emits Vite's defaults for each environment, whichever way `shouldResolveFromSource` is set, a layer adding a condition extends a complete list rather than replacing one.
|
|
125
130
|
|
|
@@ -157,7 +162,7 @@ export default defineVitestConfig({ tsconfigPaths: true });
|
|
|
157
162
|
|
|
158
163
|
It folds across layers like the other two, the last layer to declare it winning. It does not need an `ssr` counterpart the way a condition does: Because Vite keeps this setting outside its per-environment resolve options, the single key applies to the environment through which a test's own imports resolve.
|
|
159
164
|
|
|
160
|
-
**It is an opt-in because its silent direction is the reverse of the
|
|
165
|
+
**It is an opt-in because its silent direction is the reverse of the defaults'.** Leaving it off fails loudly, with an unresolved import naming the specifier. Turning it on is the silent direction, for a repo that declares `paths` for `tsc` alone: A specifier that resolved through a package's `exports` starts resolving through the alias instead, and nothing in the run reports the switch. A repo that does not declare `paths` is unaffected either way.
|
|
161
166
|
|
|
162
167
|
**It requires Vite 8**, the version that introduced `resolve.tsconfigPaths`. nmr declares `vite` as an optional peer at `>=8.0.0 <9`, so a repo that declares a stale `vite` of its own sees a warning at install time. That check does not apply to a repo that does not declare `vite` itself, whose Vite is the one that Vitest brings in: a dependency under Vitest 4 and an auto-installed peer under Vitest 5. Both majors accept Vite 6 and 7: On those, the emitted key does nothing and the aliased import fails exactly as it would with the flag off.
|
|
163
168
|
|
|
@@ -223,7 +228,7 @@ Order matters for `setupFiles`, because a shared setup file establishes the envi
|
|
|
223
228
|
|
|
224
229
|
**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.
|
|
225
230
|
|
|
226
|
-
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 that excludes one package's tests. Source resolution and
|
|
231
|
+
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 that excludes one package's tests. Source resolution, git isolation, and temp-directory isolation do not need such a guard: The factory supplies all three, so a config file cannot omit them by forgetting a layer.
|
|
227
232
|
|
|
228
233
|
## What the config excludes
|
|
229
234
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -80,10 +80,10 @@
|
|
|
80
80
|
"!docs/*.v*.md"
|
|
81
81
|
],
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@williamthorsen/nmr-core": "0.
|
|
83
|
+
"@williamthorsen/nmr-core": "0.16.0",
|
|
84
84
|
"@williamthorsen/toolbelt.errors": "0.9.0",
|
|
85
85
|
"@williamthorsen/toolbelt.filesystem": "0.10.0",
|
|
86
|
-
"glob": "14.0.
|
|
86
|
+
"glob": "14.0.4",
|
|
87
87
|
"prettier": "3.9.10",
|
|
88
88
|
"prettier-plugin-sh": "0.20.2",
|
|
89
89
|
"taze": "21.3.0",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@williamthorsen/toolbelt.testing": "0.8.0",
|
|
94
|
-
"@williamthorsen/toolbelt.vitest": "0.13.
|
|
94
|
+
"@williamthorsen/toolbelt.vitest": "0.13.2",
|
|
95
95
|
"esbuild": "0.28.2",
|
|
96
96
|
"readyup": "0.45.0",
|
|
97
97
|
"tinyglobby": "0.2.17",
|