@williamthorsen/nmr 0.23.0 → 0.25.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/.readyup/kits/default.js +479 -0
- package/.readyup/manifest.json +16 -0
- package/AGENTS.md +3 -1
- package/README.md +343 -114
- package/dist/esm/check-cache.d.ts +76 -0
- package/dist/esm/check-cache.js +224 -0
- package/dist/esm/cli-build.js +8 -2
- package/dist/esm/commands/build-output.d.ts +13 -0
- package/dist/esm/commands/build-output.js +36 -0
- package/dist/esm/commands/build.d.ts +1 -6
- package/dist/esm/commands/build.js +27 -42
- package/dist/esm/commands/clean.js +10 -3
- package/dist/esm/config.d.ts +5 -7
- package/dist/esm/config.js +115 -15
- package/dist/esm/context.d.ts +1 -1
- package/dist/esm/context.js +2 -2
- package/dist/esm/default-scripts.js +14 -9
- package/dist/esm/defineConfig.d.ts +3 -0
- package/dist/esm/defineConfig.js +3 -0
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +1 -0
- package/dist/esm/helpers/duration.d.ts +2 -0
- package/dist/esm/helpers/duration.js +21 -0
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/helpers/type-guards.js +3 -0
- package/dist/esm/resolver.d.ts +1 -1
- package/dist/esm/runCli.js +229 -27
- package/dist/esm/tiers.d.ts +7 -0
- package/dist/esm/tiers.js +35 -0
- package/dist/esm/types.d.ts +15 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vitest.d.ts +6 -2
- package/dist/esm/vitest.js +51 -31
- package/package.json +9 -7
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -1
package/dist/esm/config.js
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { existsSync } from 'node:fs';
|
|
2
10
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
5
13
|
const CONFIG_FILENAME = 'nmr.config.ts';
|
|
6
14
|
const CONFIG_DIR = '.config';
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
const CONFIG_TIERS = {
|
|
16
|
+
root: {
|
|
17
|
+
label: 'a monorepo-root config',
|
|
18
|
+
honoredKeys: ['checkCache', 'devBin', 'rootScripts', 'workspaceScripts'],
|
|
19
|
+
elsewhere: "the package's own config",
|
|
20
|
+
},
|
|
21
|
+
workspace: {
|
|
22
|
+
label: 'a package config',
|
|
23
|
+
honoredKeys: ['build'],
|
|
24
|
+
elsewhere: 'the monorepo-root config',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
28
|
+
const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
|
|
29
|
+
const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
|
|
30
|
+
export function resolveConfigPath(baseDir) {
|
|
31
|
+
return path.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
|
|
9
32
|
}
|
|
10
33
|
function isScriptRecord(value) {
|
|
11
34
|
if (!isObject(value))
|
|
@@ -27,14 +50,52 @@ function validateScriptField(value, fieldName, configPath) {
|
|
|
27
50
|
}
|
|
28
51
|
return value[fieldName];
|
|
29
52
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
function isStringArray(value) {
|
|
54
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
55
|
+
}
|
|
56
|
+
function validateBuildField(value, configPath) {
|
|
57
|
+
const build = value.build;
|
|
58
|
+
if (build === undefined) {
|
|
59
|
+
return undefined;
|
|
36
60
|
}
|
|
37
|
-
|
|
61
|
+
if (!isObject(build)) {
|
|
62
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
63
|
+
}
|
|
64
|
+
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
65
|
+
const extraIgnorePatterns = build.extraIgnorePatterns;
|
|
66
|
+
if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
|
|
67
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
68
|
+
}
|
|
69
|
+
return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
|
|
70
|
+
}
|
|
71
|
+
function validateCheckCacheField(value, configPath) {
|
|
72
|
+
const checkCache = value.checkCache;
|
|
73
|
+
if (checkCache === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (!isObject(checkCache)) {
|
|
77
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
78
|
+
}
|
|
79
|
+
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
80
|
+
const config = {};
|
|
81
|
+
const enabled = checkCache.enabled;
|
|
82
|
+
if (enabled !== undefined) {
|
|
83
|
+
if (typeof enabled !== 'boolean') {
|
|
84
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
85
|
+
}
|
|
86
|
+
config.enabled = enabled;
|
|
87
|
+
}
|
|
88
|
+
for (const field of ['excludeCommands', 'extraCommands']) {
|
|
89
|
+
const commands = checkCache[field];
|
|
90
|
+
if (commands === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!isStringArray(commands)) {
|
|
94
|
+
throw new Error(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
|
|
95
|
+
}
|
|
96
|
+
config[field] = commands;
|
|
97
|
+
}
|
|
98
|
+
return config;
|
|
38
99
|
}
|
|
39
100
|
function validateStringRecordField(value, fieldName, configPath) {
|
|
40
101
|
if (!(fieldName in value) || value[fieldName] === undefined) {
|
|
@@ -49,7 +110,14 @@ function validateConfig(value, configPath) {
|
|
|
49
110
|
if (!isObject(value)) {
|
|
50
111
|
throw new Error(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
51
112
|
}
|
|
113
|
+
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
52
114
|
const config = {};
|
|
115
|
+
const build = validateBuildField(value, configPath);
|
|
116
|
+
if (build)
|
|
117
|
+
config.build = build;
|
|
118
|
+
const checkCache = validateCheckCacheField(value, configPath);
|
|
119
|
+
if (checkCache)
|
|
120
|
+
config.checkCache = checkCache;
|
|
53
121
|
const devBin = validateStringRecordField(value, 'devBin', configPath);
|
|
54
122
|
if (devBin)
|
|
55
123
|
config.devBin = devBin;
|
|
@@ -61,12 +129,44 @@ function validateConfig(value, configPath) {
|
|
|
61
129
|
config.rootScripts = rootScripts;
|
|
62
130
|
return config;
|
|
63
131
|
}
|
|
64
|
-
export async function loadConfig(
|
|
65
|
-
const configPath =
|
|
132
|
+
export async function loadConfig(baseDir) {
|
|
133
|
+
const configPath = resolveConfigPath(baseDir);
|
|
66
134
|
if (!existsSync(configPath)) {
|
|
67
135
|
return {};
|
|
68
136
|
}
|
|
69
|
-
const
|
|
70
|
-
const loaded =
|
|
137
|
+
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
138
|
+
const loaded = isObject(imported) ? imported.default : undefined;
|
|
71
139
|
return validateConfig(loaded, configPath);
|
|
72
140
|
}
|
|
141
|
+
export async function loadRootConfig(monorepoRoot) {
|
|
142
|
+
const config = await loadConfig(monorepoRoot);
|
|
143
|
+
assertTierKeys(config, CONFIG_TIERS.root, resolveConfigPath(monorepoRoot));
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
export async function loadWorkspaceConfig(packageDir) {
|
|
147
|
+
const config = await loadConfig(packageDir);
|
|
148
|
+
assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
|
|
152
|
+
const unrecognized = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
153
|
+
if (unrecognized.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Invalid nmr config at ${configPath}: unrecognized ${unrecognized.length === 1 ? 'key' : 'keys'} ` +
|
|
157
|
+
`${formatKeyList(unrecognized, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
158
|
+
}
|
|
159
|
+
function assertTierKeys(config, tier, configPath) {
|
|
160
|
+
const unsupported = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
161
|
+
if (unsupported.length === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
|
|
165
|
+
`not ${unsupported.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
|
|
166
|
+
}
|
|
167
|
+
function formatKeyList(keys, prefix) {
|
|
168
|
+
return keys
|
|
169
|
+
.toSorted()
|
|
170
|
+
.map((key) => `\`${prefix}${key}\``)
|
|
171
|
+
.join(', ');
|
|
172
|
+
}
|
package/dist/esm/context.d.ts
CHANGED
package/dist/esm/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { loadRootConfig } from "./config.js";
|
|
3
3
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
|
|
4
4
|
export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
5
5
|
const resolved = path.resolve(dir);
|
|
@@ -17,7 +17,7 @@ export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
|
17
17
|
export async function resolveContext(cwd) {
|
|
18
18
|
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
19
19
|
const monorepoRoot = findMonorepoRoot(resolvedCwd);
|
|
20
|
-
const config = await
|
|
20
|
+
const config = await loadRootConfig(monorepoRoot);
|
|
21
21
|
const workspaceDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
22
22
|
const packageDir = findContainingPackageDir(resolvedCwd, workspaceDirs);
|
|
23
23
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const GATE_PROJECTS = '--project unit --project tool';
|
|
1
2
|
export const workspaceScripts = {
|
|
2
3
|
build: ['compile'],
|
|
3
4
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
@@ -11,11 +12,12 @@ export const workspaceScripts = {
|
|
|
11
12
|
lint: 'eslint --fix .',
|
|
12
13
|
'lint:check': 'eslint .',
|
|
13
14
|
'lint:strict': 'strict-lint',
|
|
14
|
-
test:
|
|
15
|
+
test: `pnpm exec vitest ${GATE_PROJECTS}`,
|
|
15
16
|
'test:all': 'pnpm exec vitest',
|
|
16
|
-
'test:coverage':
|
|
17
|
-
'test:
|
|
18
|
-
'test:
|
|
17
|
+
'test:coverage': `pnpm exec vitest ${GATE_PROJECTS} --coverage`,
|
|
18
|
+
'test:tool': 'pnpm exec vitest --project tool',
|
|
19
|
+
'test:unit': 'pnpm exec vitest --project unit',
|
|
20
|
+
'test:watch': `pnpm exec vitest ${GATE_PROJECTS} --watch`,
|
|
19
21
|
typecheck: 'tsgo --noEmit',
|
|
20
22
|
upgrade: 'nmr-taze --include-locked',
|
|
21
23
|
'view-coverage': 'open coverage/index.html',
|
|
@@ -28,7 +30,7 @@ export const rootScripts = {
|
|
|
28
30
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
29
31
|
'check:agent-files': 'nmr-sync-agent-files --check',
|
|
30
32
|
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage', 'check:agent-files'],
|
|
31
|
-
ci: ['build', 'check:strict'
|
|
33
|
+
ci: ['build', 'check:strict'],
|
|
32
34
|
clean: 'nmr-clean',
|
|
33
35
|
fix: ['lint', 'fmt'],
|
|
34
36
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
@@ -37,22 +39,25 @@ export const rootScripts = {
|
|
|
37
39
|
lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
|
|
38
40
|
'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
|
|
39
41
|
'lint:strict': 'nmr root:lint:strict && pnpm --recursive exec nmr lint:strict',
|
|
42
|
+
prepush: ['ci', 'audit'],
|
|
40
43
|
'report-overrides': 'nmr-report-overrides',
|
|
41
44
|
'root:check': ['root:typecheck', 'fmt:check', 'root:lint:check', 'root:test'],
|
|
42
45
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
43
46
|
'root:lint:check': "eslint --ignore-pattern 'packages/**' .",
|
|
44
47
|
'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
|
|
45
|
-
'root:test':
|
|
48
|
+
'root:test': `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
|
|
46
49
|
'root:test:all': 'vitest --config ./vitest.root.config.ts',
|
|
47
|
-
'root:test:
|
|
50
|
+
'root:test:tool': 'vitest --config ./vitest.root.config.ts --project tool',
|
|
51
|
+
'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
|
|
48
52
|
'root:typecheck': 'tsgo --noEmit',
|
|
49
53
|
'root:upgrade': 'nmr-taze --include-locked',
|
|
50
54
|
'sync-agent-files': 'nmr-sync-agent-files',
|
|
51
55
|
test: 'nmr root:test && pnpm --recursive exec nmr test',
|
|
52
56
|
'test:all': 'nmr root:test:all && pnpm --recursive exec nmr test:all',
|
|
53
57
|
'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
|
|
54
|
-
'test:
|
|
55
|
-
'test:
|
|
58
|
+
'test:tool': 'nmr root:test:tool && pnpm --recursive exec nmr test:tool',
|
|
59
|
+
'test:unit': 'nmr root:test:unit && pnpm --recursive exec nmr test:unit',
|
|
60
|
+
'test:watch': `vitest ${GATE_PROJECTS} --watch`,
|
|
56
61
|
typecheck: 'nmr root:typecheck && pnpm --recursive exec nmr typecheck',
|
|
57
62
|
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
58
63
|
};
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { NmrConfig } from './
|
|
1
|
+
import type { NmrConfig } from './types.js';
|
|
2
2
|
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, useRoot: boolean): string;
|
package/dist/esm/help.js
CHANGED
|
@@ -9,6 +9,7 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
9
9
|
' -R, --recursive Run command in all packages',
|
|
10
10
|
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
11
11
|
' -q, --quiet Suppress output on success; show full output on failure',
|
|
12
|
+
' --no-cache Run even if this tree already passed; record the result',
|
|
12
13
|
' -?, --help Show this help',
|
|
13
14
|
' -V, --version Show version number',
|
|
14
15
|
'',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function formatDuration(milliseconds) {
|
|
2
|
+
const seconds = Math.round(milliseconds / MILLISECONDS_PER_SECOND);
|
|
3
|
+
if (seconds < SECONDS_PER_MINUTE) {
|
|
4
|
+
return `${seconds}s`;
|
|
5
|
+
}
|
|
6
|
+
const minutes = Math.round(seconds / SECONDS_PER_MINUTE);
|
|
7
|
+
if (minutes < MINUTES_PER_HOUR) {
|
|
8
|
+
return `${minutes}m`;
|
|
9
|
+
}
|
|
10
|
+
return `${Math.round(minutes / MINUTES_PER_HOUR)}h`;
|
|
11
|
+
}
|
|
12
|
+
export function formatSaving(milliseconds) {
|
|
13
|
+
if (!Number.isFinite(milliseconds) || milliseconds < MILLISECONDS_PER_SECOND) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return `${SPEED_BOOST_ICON} saved ~${formatDuration(milliseconds)}`;
|
|
17
|
+
}
|
|
18
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
19
|
+
const MINUTES_PER_HOUR = 60;
|
|
20
|
+
const SECONDS_PER_MINUTE = 60;
|
|
21
|
+
const SPEED_BOOST_ICON = '🚀';
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { NmrConfig } from './config.js';
|
|
2
1
|
import type { ScriptRegistry, ScriptValue } from './resolve-scripts.js';
|
|
2
|
+
import type { NmrConfig } from './types.js';
|
|
3
3
|
export declare function applyDevBin(command: string, devBin: Record<string, string> | undefined, monorepoRoot: string): string;
|
|
4
4
|
export interface ResolvedScript {
|
|
5
5
|
command: string;
|
package/dist/esm/runCli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { computeCacheKey, CURRENT_RUNTIME, encodeTreeSnapshot, findStaleBuildOutput, formatMisplacedNoCacheWarning, formatSkipLine, NO_CACHE_ENV_VAR, readBuildOutputState, readCheckCacheEntry, resolveCacheableCommands, resolveTreeSnapshot, TREE_SNAPSHOT_ENV_VAR, writeCheckCacheEntry, writeDebugNote, } from "./check-cache.js";
|
|
3
4
|
import { resolveContext } from "./context.js";
|
|
4
5
|
import { generateHelp } from "./help.js";
|
|
5
6
|
import { isHookName } from "./helpers/hook-name.js";
|
|
@@ -27,16 +28,26 @@ export async function runCli(options) {
|
|
|
27
28
|
}
|
|
28
29
|
const { command } = parsed;
|
|
29
30
|
const passthrough = parsed.passthrough.length > 0 ? ' ' + parsed.passthrough.map(shellQuote).join(' ') : '';
|
|
30
|
-
const
|
|
31
|
+
const snapshot = openGate({
|
|
32
|
+
command,
|
|
33
|
+
config: context.config,
|
|
34
|
+
env,
|
|
35
|
+
monorepoRoot: context.monorepoRoot,
|
|
36
|
+
passthrough: parsed.passthrough,
|
|
37
|
+
stderr,
|
|
38
|
+
});
|
|
39
|
+
const noCache = parsed.noCache || env[NO_CACHE_ENV_VAR] === '1';
|
|
40
|
+
const childEnv = buildChildEnv(env, snapshot, noCache);
|
|
41
|
+
const runOptions = { quiet: parsed.quiet, stdout, stderr, env: childEnv };
|
|
31
42
|
if (parsed.filter) {
|
|
32
43
|
const delegateCmd = `pnpm --filter ${shellQuote(parsed.filter)} exec nmr ${command}${passthrough}`;
|
|
33
44
|
const exitCode = runCommand(delegateCmd, context.monorepoRoot, runOptions);
|
|
34
45
|
return { exitCode };
|
|
35
46
|
}
|
|
36
47
|
if (parsed.recursive) {
|
|
37
|
-
const
|
|
48
|
+
const delegateEnv = { ...childEnv, NMR_RUN_IF_PRESENT: '1' };
|
|
38
49
|
const delegateCmd = `pnpm --recursive exec nmr ${command}${passthrough}`;
|
|
39
|
-
const exitCode = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env:
|
|
50
|
+
const exitCode = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: delegateEnv });
|
|
40
51
|
return { exitCode };
|
|
41
52
|
}
|
|
42
53
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
@@ -52,20 +63,122 @@ export async function runCli(options) {
|
|
|
52
63
|
if (skipExitCode !== undefined) {
|
|
53
64
|
return { exitCode: skipExitCode };
|
|
54
65
|
}
|
|
55
|
-
if (resolved.source === 'package' && !parsed.quiet && registry[command] !== undefined) {
|
|
56
|
-
stdout.write(`📦 ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`);
|
|
57
|
-
}
|
|
58
66
|
const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
|
|
59
67
|
const mainCommand = substitutedCommand + passthrough;
|
|
60
68
|
const isHookInvocation = isHookName(command);
|
|
61
69
|
const fullCommand = isHookInvocation
|
|
62
70
|
? mainCommand
|
|
63
71
|
: wrapWithHooks(command, mainCommand, registry, anchorDir, parsed.workspaceRoot);
|
|
64
|
-
const
|
|
72
|
+
const key = resolveCacheKey({
|
|
73
|
+
anchorDir,
|
|
74
|
+
command,
|
|
75
|
+
commandString: fullCommand,
|
|
76
|
+
env,
|
|
77
|
+
monorepoRoot: context.monorepoRoot,
|
|
78
|
+
snapshot,
|
|
79
|
+
stderr,
|
|
80
|
+
substitution: substitutedCommand === resolved.command ? undefined : substitutedCommand,
|
|
81
|
+
});
|
|
82
|
+
const exitCode = await runGated({
|
|
83
|
+
anchorDir,
|
|
84
|
+
command,
|
|
85
|
+
commandString: fullCommand,
|
|
86
|
+
config: context.config,
|
|
87
|
+
env,
|
|
88
|
+
key,
|
|
89
|
+
monorepoRoot: context.monorepoRoot,
|
|
90
|
+
noCache,
|
|
91
|
+
overrideNotice: formatOverrideNotice(resolved, registry, command, anchorDir, parsed.quiet),
|
|
92
|
+
quiet: parsed.quiet,
|
|
93
|
+
runOptions,
|
|
94
|
+
snapshot,
|
|
95
|
+
stderr,
|
|
96
|
+
stdout,
|
|
97
|
+
});
|
|
65
98
|
return { exitCode };
|
|
66
99
|
}
|
|
100
|
+
function buildChildEnv(env, snapshot, noCache) {
|
|
101
|
+
return {
|
|
102
|
+
...env,
|
|
103
|
+
...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
|
|
104
|
+
...(noCache && { [NO_CACHE_ENV_VAR]: '1' }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function formatOverrideNotice(resolved, registry, command, anchorDir, quiet) {
|
|
108
|
+
if (quiet || resolved.source !== 'package' || registry[command] === undefined) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return `📦 ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`;
|
|
112
|
+
}
|
|
113
|
+
function handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout) {
|
|
114
|
+
if (resolvedCommand === '') {
|
|
115
|
+
if (!quiet) {
|
|
116
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is defined but empty. Skipping.\n`);
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (resolvedCommand === ':') {
|
|
121
|
+
if (!quiet) {
|
|
122
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is a no-op. Skipping.\n`);
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
129
|
+
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
130
|
+
if (!resolved)
|
|
131
|
+
return false;
|
|
132
|
+
return resolved.command !== '' && resolved.command !== ':';
|
|
133
|
+
}
|
|
134
|
+
async function lookUpRecordedPass(options) {
|
|
135
|
+
const { anchorDir, command, env, key, monorepoRoot, stderr } = options;
|
|
136
|
+
const entry = await readCheckCacheEntry({ anchorDir, command, monorepoRoot });
|
|
137
|
+
if (entry === undefined) {
|
|
138
|
+
writeDebugNote(`running ${command}: no pass recorded for this scope`, env, stderr);
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
if (entry.key !== key) {
|
|
142
|
+
writeDebugNote(`running ${command}: the tree or its inputs changed since the last pass`, env, stderr);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const output = await readBuildOutputState(monorepoRoot, options.config);
|
|
146
|
+
const [missing] = output.missing;
|
|
147
|
+
if (missing !== undefined) {
|
|
148
|
+
writeDebugNote(`running ${command}: ${missing} has no build output`, env, stderr);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const stale = findStaleBuildOutput(entry.buildDigests, output.digests);
|
|
152
|
+
if (stale !== undefined) {
|
|
153
|
+
writeDebugNote(`running ${command}: ${stale}'s build output came from a different tree`, env, stderr);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return formatSkipLine(command, entry, Date.now());
|
|
157
|
+
}
|
|
158
|
+
function openGate(options) {
|
|
159
|
+
const { command, config, env, monorepoRoot, passthrough, stderr } = options;
|
|
160
|
+
const covered = !isHookName(command) &&
|
|
161
|
+
config.checkCache?.enabled !== false &&
|
|
162
|
+
resolveCacheableCommands(config.checkCache).has(command);
|
|
163
|
+
if (!covered) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (passthrough.includes('--no-cache')) {
|
|
167
|
+
stderr.write(`${formatMisplacedNoCacheWarning(command)}\n`);
|
|
168
|
+
}
|
|
169
|
+
if (passthrough.length > 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const snapshot = resolveTreeSnapshot({ monorepoRoot, env });
|
|
173
|
+
if (!snapshot.ok) {
|
|
174
|
+
writeDebugNote(`gate disabled: ${snapshot.reason}`, env, stderr);
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return snapshot.snapshot;
|
|
178
|
+
}
|
|
67
179
|
function parseArgs(args) {
|
|
68
180
|
const parsed = {
|
|
181
|
+
noCache: false,
|
|
69
182
|
quiet: false,
|
|
70
183
|
recursive: false,
|
|
71
184
|
workspaceRoot: false,
|
|
@@ -113,26 +226,124 @@ function parseArgs(args) {
|
|
|
113
226
|
i++;
|
|
114
227
|
continue;
|
|
115
228
|
}
|
|
229
|
+
if (arg === '--no-cache') {
|
|
230
|
+
parsed.noCache = true;
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
116
234
|
parsed.command = arg;
|
|
117
235
|
parsed.passthrough = args.slice(i + 1);
|
|
118
236
|
break;
|
|
119
237
|
}
|
|
120
238
|
return { ok: true, parsed };
|
|
121
239
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
return
|
|
240
|
+
async function recordPass(options) {
|
|
241
|
+
const { anchorDir, command, env, monorepoRoot, snapshot, stderr } = options;
|
|
242
|
+
const current = resolveTreeSnapshot({ monorepoRoot, env: {} });
|
|
243
|
+
if (!current.ok) {
|
|
244
|
+
writeDebugNote(`not recording ${command}: ${current.reason}`, env, stderr);
|
|
245
|
+
return;
|
|
128
246
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
247
|
+
if (current.snapshot.hash !== snapshot.hash) {
|
|
248
|
+
writeDebugNote(`not recording ${command}: the tree changed while it ran`, env, stderr);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const output = await readBuildOutputState(monorepoRoot, options.config);
|
|
252
|
+
const [missing] = output.missing;
|
|
253
|
+
if (missing !== undefined) {
|
|
254
|
+
writeDebugNote(`not recording ${command}: ${missing} has no build output`, env, stderr);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await writeCheckCacheEntry({
|
|
259
|
+
anchorDir,
|
|
260
|
+
command,
|
|
261
|
+
monorepoRoot,
|
|
262
|
+
entry: {
|
|
263
|
+
key: options.key,
|
|
264
|
+
treeHash: snapshot.hash,
|
|
265
|
+
headSha: snapshot.headSha,
|
|
266
|
+
commandString: options.commandString,
|
|
267
|
+
nmrVersion: VERSION,
|
|
268
|
+
nodeVersion: CURRENT_RUNTIME.nodeVersion,
|
|
269
|
+
durationMs: options.durationMs,
|
|
270
|
+
recordedAt: new Date().toISOString(),
|
|
271
|
+
buildDigests: output.digests,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
writeDebugNote(`could not record ${command}: ${message}`, env, stderr);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function resolveCacheKey(options) {
|
|
281
|
+
const { env, snapshot, stderr, substitution } = options;
|
|
282
|
+
if (snapshot === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
if (substitution !== undefined) {
|
|
286
|
+
writeDebugNote(`gate disabled: devBin substituted \`${substitution}\``, env, stderr);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const result = computeCacheKey({
|
|
290
|
+
anchorDir: options.anchorDir,
|
|
291
|
+
command: options.command,
|
|
292
|
+
commandString: options.commandString,
|
|
293
|
+
env,
|
|
294
|
+
monorepoRoot: options.monorepoRoot,
|
|
295
|
+
nmrVersion: VERSION,
|
|
296
|
+
snapshot,
|
|
297
|
+
});
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
writeDebugNote(`gate disabled: ${result.reason}`, env, stderr);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return result.key;
|
|
303
|
+
}
|
|
304
|
+
async function runGated(options) {
|
|
305
|
+
const { anchorDir, command, commandString, env, key, monorepoRoot, snapshot, stderr, stdout } = options;
|
|
306
|
+
const gated = key !== undefined && snapshot !== undefined;
|
|
307
|
+
if (gated && !options.noCache) {
|
|
308
|
+
const skipLine = await lookUpRecordedPass({
|
|
309
|
+
anchorDir,
|
|
310
|
+
command,
|
|
311
|
+
config: options.config,
|
|
312
|
+
env,
|
|
313
|
+
key,
|
|
314
|
+
monorepoRoot,
|
|
315
|
+
stderr,
|
|
316
|
+
});
|
|
317
|
+
if (skipLine !== undefined) {
|
|
318
|
+
if (!options.quiet) {
|
|
319
|
+
stdout.write(`${skipLine}\n`);
|
|
320
|
+
}
|
|
321
|
+
return 0;
|
|
132
322
|
}
|
|
133
|
-
return 0;
|
|
134
323
|
}
|
|
135
|
-
|
|
324
|
+
if (options.overrideNotice !== undefined) {
|
|
325
|
+
stdout.write(options.overrideNotice);
|
|
326
|
+
}
|
|
327
|
+
const startedAt = Date.now();
|
|
328
|
+
const exitCode = runCommand(commandString, anchorDir, options.runOptions);
|
|
329
|
+
if (exitCode === 0 && gated) {
|
|
330
|
+
await recordPass({
|
|
331
|
+
anchorDir,
|
|
332
|
+
command,
|
|
333
|
+
commandString,
|
|
334
|
+
config: options.config,
|
|
335
|
+
durationMs: Date.now() - startedAt,
|
|
336
|
+
env,
|
|
337
|
+
key,
|
|
338
|
+
monorepoRoot,
|
|
339
|
+
snapshot,
|
|
340
|
+
stderr,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return exitCode;
|
|
344
|
+
}
|
|
345
|
+
function shellQuote(arg) {
|
|
346
|
+
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
136
347
|
}
|
|
137
348
|
function wrapWithHooks(command, mainCommand, registry, anchorDir, workspaceRoot) {
|
|
138
349
|
const segments = [];
|
|
@@ -146,12 +357,3 @@ function wrapWithHooks(command, mainCommand, registry, anchorDir, workspaceRoot)
|
|
|
146
357
|
}
|
|
147
358
|
return segments.join(' && ');
|
|
148
359
|
}
|
|
149
|
-
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
150
|
-
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
151
|
-
if (!resolved)
|
|
152
|
-
return false;
|
|
153
|
-
return resolved.command !== '' && resolved.command !== ':';
|
|
154
|
-
}
|
|
155
|
-
function shellQuote(arg) {
|
|
156
|
-
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
157
|
-
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const ALL_TEST_PATTERNS: string[];
|
|
2
|
+
export declare function buildTierPatterns(tier: string): string[];
|
|
3
|
+
export declare function findTestFiles(rootDir: string): string[];
|
|
4
|
+
export declare function hasTierInfix(filePath: string): boolean;
|
|
5
|
+
export declare const TEST_COLLECTION_EXCLUDE: string[];
|
|
6
|
+
export declare const TIER_NAMES: readonly ["unit", "tool", "localhost", "remote"];
|
|
7
|
+
export type TierName = (typeof TIER_NAMES)[number];
|