@williamthorsen/nmr 0.42.0 → 0.44.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 +105 -0
- package/.readyup/kits/default.js +1042 -137
- package/.readyup/manifest.json +151 -8
- package/CHANGELOG.md +33 -0
- package/README.md +20 -11
- package/dist/esm/commands/build-output.js +1 -1
- package/dist/esm/commands/build.js +3 -2
- package/dist/esm/commands/fmt-cache.d.ts +26 -0
- package/dist/esm/commands/fmt-cache.js +71 -0
- package/dist/esm/commands/fmt.d.ts +6 -1
- package/dist/esm/commands/fmt.js +24 -4
- package/dist/esm/config.js +28 -6
- package/dist/esm/default-scripts.d.ts +10 -1
- package/dist/esm/default-scripts.js +45 -39
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +3 -3
- package/dist/esm/helpers/createOutputRecorder.d.ts +17 -0
- package/dist/esm/helpers/createOutputRecorder.js +22 -0
- package/dist/esm/resolve-scripts.d.ts +5 -3
- package/dist/esm/resolve-scripts.js +7 -3
- package/dist/esm/resolver.d.ts +5 -4
- package/dist/esm/resolver.js +11 -4
- package/dist/esm/runCli.js +5 -3
- package/dist/esm/runner.d.ts +7 -0
- package/dist/esm/runner.js +105 -15
- package/dist/esm/steps.d.ts +12 -5
- package/dist/esm/steps.js +5 -3
- package/dist/esm/taze.d.ts +5 -2
- package/dist/esm/taze.js +4 -1
- package/dist/esm/types.d.ts +10 -0
- package/dist/esm/vitest.js +9 -5
- package/dist/esm/workspace-globs.d.ts +5 -0
- package/dist/esm/workspace-globs.js +8 -0
- package/docs/check-cache.md +2 -0
- package/docs/scripts.md +26 -8
- package/docs/upgrades.md +1 -1
- package/docs/utilities.md +17 -3
- package/docs/vitest.md +8 -0
- package/package.json +6 -6
package/dist/esm/vitest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { defaultExclude, mergeConfig } from 'vitest/config';
|
|
@@ -6,9 +7,12 @@ import { isObject } from "./helpers/type-guards.js";
|
|
|
6
7
|
import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
|
|
7
8
|
import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
|
|
8
9
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
10
|
+
import { toWorkspacePackageGlobs } from "./workspace-globs.js";
|
|
9
11
|
const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
|
|
10
12
|
const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
|
|
11
13
|
const TIER_TIMEOUT_MS = 30_000;
|
|
14
|
+
const SMALL_MACHINE_MAX_CORES = 2;
|
|
15
|
+
const SMALL_MACHINE_MAX_WORKERS = 3;
|
|
12
16
|
const GIT_ISOLATION_SETUP_FILE = resolveGitIsolationSetupFile();
|
|
13
17
|
const CLIENT_CONDITIONS = ['module', 'browser', 'development|production'];
|
|
14
18
|
const SERVER_CONDITIONS = ['module', 'node', 'development|production'];
|
|
@@ -49,7 +53,7 @@ export function defineRootVitestConfig(...layers) {
|
|
|
49
53
|
return buildConfig(layers, {
|
|
50
54
|
coverageInclude: [],
|
|
51
55
|
ignoredPathsRoot: monorepoRoot,
|
|
52
|
-
projectExclude:
|
|
56
|
+
projectExclude: toWorkspacePackageGlobs(monorepoRoot, getWorkspacePackageDirs(monorepoRoot)),
|
|
53
57
|
projectRoot: monorepoRoot,
|
|
54
58
|
});
|
|
55
59
|
}
|
|
@@ -95,6 +99,7 @@ function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
|
95
99
|
})),
|
|
96
100
|
];
|
|
97
101
|
const collectionExclude = buildCollectionExclude(layers, ignoredPathsRoot);
|
|
102
|
+
const maxWorkers = resolveDefaultMaxWorkers();
|
|
98
103
|
const shouldIsolateGit = resolveFlag(layers, 'shouldIsolateGit', true);
|
|
99
104
|
return projectTiers.map(({ exclude, include, name, timeoutMs }) => {
|
|
100
105
|
const project = {
|
|
@@ -103,6 +108,7 @@ function buildProjects(layers, extraExclude, projectRoot, ignoredPathsRoot) {
|
|
|
103
108
|
test: {
|
|
104
109
|
exclude: [...collectionExclude, ...exclude, ...extraExclude],
|
|
105
110
|
include,
|
|
111
|
+
...(maxWorkers !== undefined && { maxWorkers }),
|
|
106
112
|
name,
|
|
107
113
|
...(shouldIsolateGit && { setupFiles: [GIT_ISOLATION_SETUP_FILE] }),
|
|
108
114
|
...(timeoutMs !== undefined && { hookTimeout: timeoutMs, testTimeout: timeoutMs }),
|
|
@@ -169,10 +175,8 @@ function formatOptionKeys(keys) {
|
|
|
169
175
|
.map((key) => `\`${key}\``)
|
|
170
176
|
.join(', ');
|
|
171
177
|
}
|
|
172
|
-
function
|
|
173
|
-
return
|
|
174
|
-
.map((dir) => `${path.relative(monorepoRoot, dir).split(path.sep).join('/')}/**`)
|
|
175
|
-
.toSorted();
|
|
178
|
+
function resolveDefaultMaxWorkers() {
|
|
179
|
+
return os.availableParallelism() <= SMALL_MACHINE_MAX_CORES ? SMALL_MACHINE_MAX_WORKERS : undefined;
|
|
176
180
|
}
|
|
177
181
|
function resolveFlag(layers, name, isOnByDefault) {
|
|
178
182
|
let resolvedFlag = isOnByDefault;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders each package directory as a glob, relative to `monorepoRoot`, that matches everything beneath it, sorted. A
|
|
3
|
+
* directory that is the root itself is skipped, since its glob would match the whole repo.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toWorkspacePackageGlobs(monorepoRoot: string, packageDirs: readonly string[]): string[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
|
|
3
|
+
return packageDirs
|
|
4
|
+
.map((dir) => path.relative(monorepoRoot, dir).split(path.sep).join('/'))
|
|
5
|
+
.filter((relativeDir) => relativeDir !== '')
|
|
6
|
+
.map((relativeDir) => `${relativeDir}/**`)
|
|
7
|
+
.toSorted();
|
|
8
|
+
}
|
package/docs/check-cache.md
CHANGED
|
@@ -148,6 +148,8 @@ Try these in order; the first is almost always the right one.
|
|
|
148
148
|
| `rm -rf node_modules/.cache/nmr-check` | Forgets every recorded pass, leaving build output alone. |
|
|
149
149
|
| `nmr clean` | Forgets every recorded pass and removes build output, at any scope. |
|
|
150
150
|
|
|
151
|
+
`--no-cache` and `NMR_NO_CACHE=1` also bypass the Prettier cache that `nmr-fmt` keeps, because nmr exports `NMR_NO_CACHE=1` to the commands that it runs. nmr exports it too when a command is given trailing arguments, so `nmr fmt:check <pathspec>` runs uncached. See [`nmr-fmt`'s cache](utilities.md#cache).
|
|
152
|
+
|
|
151
153
|
`--no-cache` belongs before the command name. After it, it is an argument to the command rather than a flag to nmr; nmr reports this rather than silently ignoring the bypass.
|
|
152
154
|
|
|
153
155
|
## Reserved environment variables
|
package/docs/scripts.md
CHANGED
|
@@ -35,6 +35,21 @@ A script value is a string or an array of steps. Only the built-in defaults and
|
|
|
35
35
|
|
|
36
36
|
Each array element is a command name, optionally preceded by nmr's own flags, split on whitespace: `-R test` runs the element in every package, the way `nmr -R test` does. An element containing a quoted argument or shell syntax is rejected when the config loads: Name that command as a script of its own and reference the name.
|
|
37
37
|
|
|
38
|
+
#### Running a step in the background
|
|
39
|
+
|
|
40
|
+
A composite runs its steps in series. An element declared with `shouldRunInBackground` starts and is left running while the steps after it run:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// "check:strict": [{ run: "lint:strict", shouldRunInBackground: true }, "typecheck", "fmt:check", "test"]
|
|
44
|
+
// nmr lint:strict runs alongside nmr typecheck, then nmr fmt:check, then nmr test
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The background step's output is held rather than streamed, and printed whole at the first step boundary after the step ends, so it never interleaves with another step's output. When nmr writes to a terminal, the step receives `FORCE_COLOR=1`, unless `NO_COLOR` or `FORCE_COLOR` is already set, so that its tools keep their colour. Its stdin is the null device.
|
|
48
|
+
|
|
49
|
+
A failing step in the foreground still ends the sequence, but the composite waits for every background step that has started and prints its output. The composite fails if any step failed, and each step's own verdict line names its failure. While a background step runs, nmr forwards a SIGINT or SIGTERM that it receives to every running step.
|
|
50
|
+
|
|
51
|
+
Put a step in the background when it uses fewer cores than the machine has and the steps after it do not read what it writes. The default `check` and `check:strict` run their lint step this way, because type-aware lint runs on one core while the test fan-out leaves cores idle.
|
|
52
|
+
|
|
38
53
|
#### Where trailing arguments go
|
|
39
54
|
|
|
40
55
|
nmr passes trailing arguments to every step of a composite, so `nmr fix --dry-run` runs `nmr lint --dry-run`, then `nmr fmt --dry-run`. A step whose work the arguments cannot narrow declines them by taking the long form of an element, and runs unnarrowed:
|
|
@@ -91,14 +106,15 @@ A minimal `.config/nmr.config.ts` is in the [README](../README.md#configuration)
|
|
|
91
106
|
|
|
92
107
|
### `defineConfig` fields
|
|
93
108
|
|
|
94
|
-
| Field | Type | Description
|
|
95
|
-
| ------------------ | ----------------------------------------------------------------------------- |
|
|
96
|
-
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only)
|
|
97
|
-
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](check-cache.md#check-result-cache) may skip
|
|
98
|
-
| `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2)
|
|
99
|
-
| `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2)
|
|
100
|
-
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands
|
|
101
|
-
| `output` | `{ commandVerbosity?: CommandVerbosity; extraAgentEnvVars?: string[] }` | How loudly runs report command output; see [configuring](reporting.md#configuring)
|
|
109
|
+
| Field | Type | Description |
|
|
110
|
+
| ------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `build` | `{ extraIgnorePatterns?: string[] }` | Patterns added to the build's ignore set (package config only) |
|
|
112
|
+
| `checkCache` | `{ enabled?: boolean; extraCommands?: string[]; excludeCommands?: string[] }` | Which commands the [check-result cache](check-cache.md#check-result-cache) may skip |
|
|
113
|
+
| `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2) |
|
|
114
|
+
| `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2) |
|
|
115
|
+
| `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
|
|
116
|
+
| `output` | `{ commandVerbosity?: CommandVerbosity; extraAgentEnvVars?: string[] }` | How loudly runs report command output; see [configuring](reporting.md#configuring) |
|
|
117
|
+
| `selfHostedRunner` | `{ exempt: string }` | Why a qualifying repo keeps CI off the self-hosted runners; stops [nmr's readyup kit](../README.md#conformance-checks) from recommending `CI_RUNS_ON` (root config only). The reason is structural: a circular dependency, a platform or capacity the runners lack, or network reach. A missing tool is installed by a workflow step instead |
|
|
102
118
|
|
|
103
119
|
All fields are optional. [Script values](#script-values) describes `ScriptValue` and its step form, and both types are exported from `@williamthorsen/nmr/config`.
|
|
104
120
|
|
|
@@ -153,6 +169,8 @@ Neither is bound to a git hook. `prepush` is named for when a developer runs it,
|
|
|
153
169
|
|
|
154
170
|
### Check and quality
|
|
155
171
|
|
|
172
|
+
`check` and `check:strict` start their lint step first and [run it in the background](#running-a-step-in-the-background), alongside typecheck, the format check, and the tests.
|
|
173
|
+
|
|
156
174
|
Neither gate computes coverage: Instrumentation multiplies the wall time of the run that they exist to keep short. Coverage is [`test:coverage`](vitest.md#test-selections), run as a step of its own; a repo that wants it inside the gate [overrides](#configuration) `check:strict`.
|
|
157
175
|
|
|
158
176
|
### Lint
|
package/docs/upgrades.md
CHANGED
|
@@ -40,7 +40,7 @@ export default defineConfig({
|
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`defineConfig` supplies nmr's shared upgrade policy, so a repo's config contains only what is specific to that repo. The policy is 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`. Any setting that a repo's config declares wins over nmr's default. Passing `undefined` clears a default rather than falling back to it; that is how a config hands the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. `mode`
|
|
43
|
+
`defineConfig` supplies nmr's shared upgrade policy, so a repo's config contains only what is specific to that repo. The policy is 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`. Any setting that a repo's config declares wins over nmr's default. Passing `undefined` clears a default rather than falling back to it; that is how a config hands the quarantine policy back to `pnpm-workspace.yaml`'s `minimumReleaseAge`. Clearing `mode` hands the range back to the upgrade tool's own default, which follows the range that each dependency declares.
|
|
44
44
|
|
|
45
45
|
Everything the [taze configuration](https://github.com/antfu-collective/taze#config-file) accepts is accepted here, with five exceptions that the upgrade tool does not read from any 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 a repo's config declares one, and [request timeout](#request-timeout) below shows how to set it instead.
|
|
46
46
|
|
package/docs/utilities.md
CHANGED
|
@@ -12,9 +12,11 @@ nmr-clean
|
|
|
12
12
|
|
|
13
13
|
## `nmr-compile`
|
|
14
14
|
|
|
15
|
-
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files keep their doc comments either way. Because the compiler parses each source file, every relative import form (static, re-export, dynamic `import()`, and bare side-effect) is rewritten from `.ts` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when every input is unchanged and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means (`nmr clean`, `rm -rf dist`, `git clean`) therefore forces a rebuild rather than a skip. This is the default `compile` script; run it from a package directory.
|
|
15
|
+
Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler API, emitting `.js` from one program and `.d.ts` from a second that reuses it. The package's own `removeComments` governs the `.js` emit; the declarations emit forces it off, so `.d.ts` files keep their doc comments either way. Because the compiler parses each source file, every relative import form (static, re-export, dynamic `import()`, and bare side-effect) is rewritten from `.ts` or `.tsx` to `.js` in both outputs, and `.ts` occurrences inside strings and comments are left intact. tsconfig `paths` aliases are resolved to runnable relative `.js` specifiers in both outputs, sourced from the package's tsconfig. An aliased import whose target resolves outside the package's `src/` and is not resolvable without the alias mapping fails the build with a diagnostic, rather than being emitted verbatim to produce output that fails at runtime. The build is skipped when every input is unchanged and the previous output is still on disk (the key is cached under `node_modules/.cache/nmr-compile/`, outside the published output). Deleting the output by any means (`nmr clean`, `rm -rf dist`, `git clean`) therefore forces a rebuild rather than a skip. This is the default `compile` script; run it from a package directory.
|
|
16
16
|
|
|
17
|
-
**What it compiles.** Entry points are `src/**/*.ts` less the directories that contain test scaffolding rather than published code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: The compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and the compiler never emits a dangling specifier in its importer. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes): Helpers live in `test-utils/` precisely so that they stay covered.
|
|
17
|
+
**What it compiles.** Entry points are `src/**/*.{ts,tsx}` less the directories that contain test scaffolding rather than published code: `__fixtures__/`, `__mocks__/`, `__tests__/`, and `test-utils/`. Add to that list per package with [`build.extraIgnorePatterns`](scripts.md#package-level-configuration). Ignoring a directory drops it as an _entry point_, not from the emit: The compiler still emits whatever the surviving entry points import, so a helper that production code uses is still compiled and the compiler never emits a dangling specifier in its importer. This list is deliberately not the Vitest [coverage exclusions](vitest.md#what-the-config-excludes): Helpers live in `test-utils/` precisely so that they stay covered.
|
|
18
|
+
|
|
19
|
+
**JSX.** A `.tsx` source compiles to `.js` like any other. The package's own tsconfig `jsx` option decides the transform, such as `react-jsx`; the build does not set one.
|
|
18
20
|
|
|
19
21
|
**The build owns its output directory.** Every rebuild replaces `dist/esm` wholesale, so the directory contains exactly the current emit: A source that was deleted, or that a widened ignore set now covers, leaves nothing behind. Assets therefore belong outside the output directory, or in a `build:post` hook, which composes correctly because it runs after the output is published. Because a bare `nmr compile` does not run any hook, assets that a `build:post` step copies in are absent until a full `nmr build` restores them. An `outdir` that does not resolve inside the package is rejected rather than replaced.
|
|
20
22
|
|
|
@@ -22,7 +24,7 @@ Compile a single package's `src` tree to `dist/esm` with the TypeScript compiler
|
|
|
22
24
|
|
|
23
25
|
**What busts the cache.** The key folds each input's content and path, the emit options, the resolved TypeScript version, and a fingerprint of the nmr running the build -- its own build digest in a workspace checkout, its package version otherwise. Upgrading nmr in a consuming repo therefore rebuilds every package that nmr compiles, and an edit to nmr's own build sources rebuilds each sibling once nmr itself has been rebuilt. A build of nmr's own package folds the version rather than the digest, which is the cache entry that this build is about to write.
|
|
24
26
|
|
|
25
|
-
`typescript` is an optional peer dependency (`>=5.7.0 <7`), needed only by `nmr-compile`; the consuming repo provides it, and `nmr-compile` run without it fails with an error naming the peer and the version that it requires. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` extensions for them to be rewritten.
|
|
27
|
+
`typescript` is an optional peer dependency (`>=5.7.0 <7`), needed only by `nmr-compile`; the consuming repo provides it, and `nmr-compile` run without it fails with an error naming the peer and the version that it requires. The floor is what `rewriteRelativeImportExtensions` requires; the ceiling exists because TypeScript 7 does not include a compiler API: Its root export is a version constant, so `nmr-compile` cannot run on it. Relative imports in source must use explicit `.ts` or `.tsx` extensions for them to be rewritten.
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
30
|
nmr-compile
|
|
@@ -49,6 +51,18 @@ A file that git ignores is never formatted, even when named directly. Because tr
|
|
|
49
51
|
|
|
50
52
|
`nmr-fmt` drops paths that the index names but the filesystem does not have (a file deleted but not yet staged), submodule gitlinks, and symlinks whatever they point at, before calling Prettier, which rejects a symlink named explicitly. It also skips a path that it is denied access to, such as a `.envrc` that a sandbox shields, and names each one on stderr, so that a check that passes does not hide a file that it never read. Any other failure to stat a listed path fails the run, with an error that names the path.
|
|
51
53
|
|
|
54
|
+
### Cache
|
|
55
|
+
|
|
56
|
+
`nmr-fmt` passes Prettier's content-keyed cache (`--cache --cache-strategy content`), so a run reformats only the files whose content changed since a run that passed them. The cache files live in `node_modules/.cache/prettier/nmr-fmt/` at the repository root, one per directory from which `nmr-fmt` runs, because Prettier keys each entry by its path relative to that directory. A repository whose root does not contain a `node_modules` directory runs uncached rather than having one created.
|
|
57
|
+
|
|
58
|
+
A cached entry is reused only while the Prettier version, the Node version, the resolved Prettier options, and the repository's lockfile are all unchanged. Prettier's own key covers the first three. It does not cover plugin code, so each cache file's name also contains a fingerprint of the root's lockfile, and a dependency change, a plugin upgrade included, starts a fresh cache. `nmr-fmt` deletes cache files with an outdated fingerprint, and it discards a cache file that is not valid JSON rather than letting Prettier fail on it. A failure to prepare the cache prints a warning and leaves the run uncached; it never fails the run.
|
|
59
|
+
|
|
60
|
+
Pass `--no-cache`, or set `NMR_NO_CACHE=1`, to run uncached. `nmr --no-cache fmt:check` sets that variable, so it bypasses both nmr's [check cache](check-cache.md) and this one. Because the fingerprint makes a cache from an earlier lockfile harmless, CI can restore the whole `node_modules/.cache/prettier/nmr-fmt/` directory under a fallback key.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
nmr-fmt --check --no-cache
|
|
64
|
+
```
|
|
65
|
+
|
|
52
66
|
## `ensure-prepublish-hooks`
|
|
53
67
|
|
|
54
68
|
Verify that all publishable workspace packages have a `prepublishOnly` script. Exits non-zero if any are missing.
|
package/docs/vitest.md
CHANGED
|
@@ -119,6 +119,12 @@ export default defineVitestConfig({ shouldIsolateGit: false, shouldResolveFromSo
|
|
|
119
119
|
|
|
120
120
|
**The boundary is Vite's resolution, not the package's role.** Because Node does not apply the `source` condition, a workspace package that an externalized dependency imports resolves through Node to its build output. Nothing else in a suite resolves that way.
|
|
121
121
|
|
|
122
|
+
### Worker count on small machines
|
|
123
|
+
|
|
124
|
+
On a machine with 2 or fewer cores, the factory gives every project `maxWorkers: 3`. Vitest's run-mode default is one fewer worker than the core count, so a 2-core CI runner, GitHub's standard runner for a private repo, would otherwise run every test file serially on one worker. Three workers are faster there, because the main process transforms files while the workers run them. On a machine with more cores, the projects do not set `maxWorkers`, and Vitest's own default applies.
|
|
125
|
+
|
|
126
|
+
Where the default applies, Vitest's `--maxWorkers` flag has no effect, because Vitest does not forward that flag to projects. Override the default with the `VITEST_MAX_WORKERS` environment variable, with `--no-file-parallelism` for a single worker, or with `maxWorkers` in the `project` seam.
|
|
127
|
+
|
|
122
128
|
## Resolving through tsconfig paths
|
|
123
129
|
|
|
124
130
|
A third resolution setting exists alongside those two, and this one is off. `tsconfigPaths: true` emits Vite's `resolve.tsconfigPaths`, so a test resolves an aliased specifier through the `paths` that its `tsconfig.json` declares, the way `tsc` does:
|
|
@@ -154,6 +160,8 @@ export default defineVitestConfig({
|
|
|
154
160
|
|
|
155
161
|
Arrays concatenate rather than replace. `exclude` and `setupFiles` therefore add to what the config already declares, and a surface can neither narrow `include` nor drop a default exclusion. Adding an `include` pattern through `project` widens all four projects at once: A file matching it is collected by each and runs four times.
|
|
156
162
|
|
|
163
|
+
`maxWorkers` goes under `project`. Vitest requires every project in a run to share one worker count, so a `tiers` target that sets a different `maxWorkers` for one tier makes Vitest throw at startup.
|
|
164
|
+
|
|
157
165
|
`resolve.conditions` concatenates too, onto the Vite defaults that the factory already emits, but layer order does not matter 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 that an earlier layer contributed. Every entry also reaches Node, which resolves each package reached by a test's imports under `node_modules`, so a condition added here is one that the package's `exports` map may match. `resolve.alias` is the one key that merges override-first: A later alias takes precedence over an earlier one.
|
|
158
166
|
|
|
159
167
|
**A condition for the tests' own resolution goes under `ssr`.** Because `resolve` is per-environment and Vitest resolves a test's imports through the server environment, `root: { ssr: { resolve: { conditions: ['my-condition'] } } }` is the seam that applies to them. A top-level `root: { resolve: { conditions: [...] } }` entry applies to 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 applies to both.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -78,21 +78,21 @@
|
|
|
78
78
|
],
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@williamthorsen/nmr-core": "0.13.2",
|
|
81
|
-
"@williamthorsen/toolbelt.errors": "0.8.
|
|
82
|
-
"@williamthorsen/toolbelt.filesystem": "0.9.
|
|
81
|
+
"@williamthorsen/toolbelt.errors": "0.8.3",
|
|
82
|
+
"@williamthorsen/toolbelt.filesystem": "0.9.3",
|
|
83
83
|
"glob": "13.0.6",
|
|
84
84
|
"prettier": "3.9.9",
|
|
85
85
|
"prettier-plugin-sh": "0.20.2",
|
|
86
86
|
"taze": "21.3.0"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@williamthorsen/toolbelt.testing": "0.6.
|
|
90
|
-
"@williamthorsen/toolbelt.vitest": "0.10.
|
|
89
|
+
"@williamthorsen/toolbelt.testing": "0.6.3",
|
|
90
|
+
"@williamthorsen/toolbelt.vitest": "0.10.3",
|
|
91
91
|
"esbuild": "0.28.2",
|
|
92
92
|
"readyup": "0.40.0",
|
|
93
93
|
"tinyglobby": "0.2.17",
|
|
94
94
|
"typescript": "6.0.3",
|
|
95
|
-
"vite": "8.3.
|
|
95
|
+
"vite": "8.3.2",
|
|
96
96
|
"vitest": "4.1.11",
|
|
97
97
|
"yaml": "2.9.1"
|
|
98
98
|
},
|