@williamthorsen/nmr 0.19.0 โ 0.21.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/AGENTS.md +5 -96
- package/README.md +303 -99
- package/bin/nmr-fmt.js +11 -0
- package/dist/esm/cli-ensure-prepublish-hooks.js +1 -1
- package/dist/esm/cli-fmt.js +3 -0
- package/dist/esm/cli-report-overrides.js +1 -1
- package/dist/esm/cli-sync-agent-files.js +1 -1
- package/dist/esm/commands/clean.js +4 -3
- package/dist/esm/commands/ensure-prepublish-hooks.js +1 -1
- package/dist/esm/commands/fmt.d.ts +23 -0
- package/dist/esm/commands/fmt.js +172 -0
- package/dist/esm/context.d.ts +0 -2
- package/dist/esm/context.js +7 -60
- package/dist/esm/default-scripts.d.ts +1 -3
- package/dist/esm/default-scripts.js +17 -21
- package/dist/esm/help.js +3 -2
- package/dist/esm/helpers/workspace-patterns.d.ts +1 -0
- package/dist/esm/helpers/workspace-patterns.js +29 -0
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/resolve-scripts.d.ts +1 -1
- package/dist/esm/resolve-scripts.js +3 -6
- package/dist/esm/resolver.d.ts +1 -2
- package/dist/esm/resolver.js +3 -6
- package/dist/esm/runCli.js +17 -19
- package/dist/esm/vitest.d.ts +15 -0
- package/dist/esm/vitest.js +62 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +38 -0
- package/package.json +27 -12
- package/bin/nmr-sync-pnpm-version.js +0 -11
- package/dist/esm/cli-sync-pnpm-version.js +0 -11
- package/dist/esm/commands/sync-pnpm-version.d.ts +0 -3
- package/dist/esm/commands/sync-pnpm-version.js +0 -40
- package/dist/esm/helpers/code-quality-pnpm-action.d.ts +0 -13
- package/dist/esm/helpers/code-quality-pnpm-action.js +0 -22
- package/dist/esm/tests/consistency.d.ts +0 -7
- package/dist/esm/tests/consistency.js +0 -57
- package/dist/esm/tests/helpers/get-runtime-version-from-asdf.d.ts +0 -1
- package/dist/esm/tests/helpers/get-runtime-version-from-asdf.js +0 -12
- package/dist/esm/tests/helpers/get-string-from-yaml-file.d.ts +0 -1
- package/dist/esm/tests/helpers/get-string-from-yaml-file.js +0 -12
- package/dist/esm/tests/helpers/get-value-at-path.d.ts +0 -1
- package/dist/esm/tests/helpers/get-value-at-path.js +0 -34
- /package/dist/esm/{cli-sync-pnpm-version.d.ts โ cli-fmt.d.ts} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
|
-
import { findContainingPackageDir
|
|
5
|
-
import { applyDevBin, buildWorkspaceRegistry,
|
|
4
|
+
import { findContainingPackageDir } from "../context.js";
|
|
5
|
+
import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
|
|
6
6
|
import { runCommand } from "../runner.js";
|
|
7
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
7
8
|
import { resolveBuildCachePath } from "./build.js";
|
|
8
9
|
const CLEAN_ICON = '๐งน';
|
|
9
10
|
const CLEAN_COMMAND = 'clean';
|
|
@@ -33,8 +34,8 @@ export async function runClean(cwd = process.cwd()) {
|
|
|
33
34
|
}
|
|
34
35
|
async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
35
36
|
const config = await loadConfig(monorepoRoot);
|
|
37
|
+
const registry = buildWorkspaceRegistry(config);
|
|
36
38
|
for (const packageDir of workspacePackageDirs) {
|
|
37
|
-
const registry = buildWorkspaceRegistry(config, hasIntegrationTestConfig(packageDir));
|
|
38
39
|
const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
39
40
|
if (resolved === undefined || resolved.command === '') {
|
|
40
41
|
continue;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getWorkspacePackageDirs } from "../context.js";
|
|
4
3
|
import { readPackageJson } from "../helpers/package-json.js";
|
|
5
4
|
import { isObject } from "../helpers/type-guards.js";
|
|
5
|
+
import { getWorkspacePackageDirs } from "../workspace.js";
|
|
6
6
|
export const DEFAULT_HOOK = 'npm run build';
|
|
7
7
|
export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
8
8
|
const hookCommand = options.command ?? DEFAULT_HOOK;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type FormatMode = 'check' | 'write';
|
|
2
|
+
export interface FormatTargets {
|
|
3
|
+
files: string[];
|
|
4
|
+
ignorePaths: string[];
|
|
5
|
+
}
|
|
6
|
+
export type ResolveTargetsResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
targets: FormatTargets;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function runFmt(argv: string[], cwd?: string): number;
|
|
14
|
+
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
15
|
+
export interface RunPrettierOptions {
|
|
16
|
+
cliPath: string;
|
|
17
|
+
mode: FormatMode;
|
|
18
|
+
files: string[];
|
|
19
|
+
ignorePaths: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
budgetBytes?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function runPrettier(options: RunPrettierOptions): number;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
8
|
+
import { isObject } from "../helpers/type-guards.js";
|
|
9
|
+
const IGNORE_FILENAME = '.prettierignore';
|
|
10
|
+
const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
11
|
+
const IGNORE_FILE_PATHSPEC = `*${IGNORE_FILENAME}`;
|
|
12
|
+
const PRETTIER_PACKAGE = 'prettier';
|
|
13
|
+
const PRETTIER_RANGE = '>=3.9.5 <4';
|
|
14
|
+
const ARGUMENT_BUDGET_BYTES = 100_000;
|
|
15
|
+
const USAGE = 'Usage: nmr-fmt (--check | --write) [pathspec...]';
|
|
16
|
+
const MODE_ARGS = {
|
|
17
|
+
check: ['--check'],
|
|
18
|
+
write: ['--list-different', '--write'],
|
|
19
|
+
};
|
|
20
|
+
export function runFmt(argv, cwd = process.cwd()) {
|
|
21
|
+
const parsed = parseFmtArgs(argv);
|
|
22
|
+
if (!parsed.ok) {
|
|
23
|
+
reportError(`nmr-fmt: ${parsed.error}`);
|
|
24
|
+
process.stderr.write(`${USAGE}\n`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const resolved = resolveFormatTargets(cwd, parsed.pathspecs);
|
|
28
|
+
if (!resolved.ok) {
|
|
29
|
+
reportError(`nmr-fmt: ${resolved.error}`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const { files, ignorePaths } = resolved.targets;
|
|
33
|
+
if (files.length === 0) {
|
|
34
|
+
if (parsed.pathspecs.length > 0) {
|
|
35
|
+
reportError(`nmr-fmt: no formattable files matched: ${parsed.pathspecs.join(', ')}`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
const cli = resolvePrettierCli();
|
|
41
|
+
if (!cli.ok) {
|
|
42
|
+
reportError(`nmr-fmt: ${cli.error}`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
return runPrettier({ cliPath: cli.cliPath, mode: parsed.mode, files, ignorePaths, cwd });
|
|
46
|
+
}
|
|
47
|
+
export function resolveFormatTargets(cwd, pathspecs = []) {
|
|
48
|
+
const toplevel = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
49
|
+
if (!toplevel.ok)
|
|
50
|
+
return toplevel;
|
|
51
|
+
const repositoryRoot = toplevel.stdout.trim();
|
|
52
|
+
const listed = runGit([...LIST_FILES_ARGS, '--', ...pathspecs], cwd);
|
|
53
|
+
if (!listed.ok)
|
|
54
|
+
return listed;
|
|
55
|
+
const ignoreFiles = runGit([...LIST_FILES_ARGS, '--', IGNORE_FILE_PATHSPEC], repositoryRoot);
|
|
56
|
+
if (!ignoreFiles.ok)
|
|
57
|
+
return ignoreFiles;
|
|
58
|
+
const discovered = splitNulSeparated(ignoreFiles.stdout)
|
|
59
|
+
.filter((file) => path.basename(file) === IGNORE_FILENAME)
|
|
60
|
+
.map((file) => path.resolve(repositoryRoot, file));
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
targets: {
|
|
64
|
+
files: dedupe(splitNulSeparated(listed.stdout))
|
|
65
|
+
.filter((file) => isFormattableFile(cwd, file))
|
|
66
|
+
.toSorted(),
|
|
67
|
+
ignorePaths: dedupe([path.join(repositoryRoot, IGNORE_FILENAME), ...discovered]),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function isFormattableFile(cwd, file) {
|
|
72
|
+
return statSync(path.resolve(cwd, file), { throwIfNoEntry: false })?.isFile() === true;
|
|
73
|
+
}
|
|
74
|
+
function parseFmtArgs(argv) {
|
|
75
|
+
let mode;
|
|
76
|
+
const pathspecs = [];
|
|
77
|
+
for (const arg of argv) {
|
|
78
|
+
if (arg === '--check' || arg === '--write') {
|
|
79
|
+
const requested = arg === '--check' ? 'check' : 'write';
|
|
80
|
+
if (mode !== undefined && mode !== requested) {
|
|
81
|
+
return { ok: false, error: 'Pass --check or --write, not both.' };
|
|
82
|
+
}
|
|
83
|
+
mode = requested;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg.startsWith('-')) {
|
|
87
|
+
return { ok: false, error: `Unknown option: ${arg}` };
|
|
88
|
+
}
|
|
89
|
+
pathspecs.push(arg);
|
|
90
|
+
}
|
|
91
|
+
if (mode === undefined) {
|
|
92
|
+
return { ok: false, error: 'Pass --check or --write.' };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, mode, pathspecs };
|
|
95
|
+
}
|
|
96
|
+
function resolvePrettierCli() {
|
|
97
|
+
const missing = {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: `\`${PRETTIER_PACKAGE}\` (${PRETTIER_RANGE}) could not be resolved. Install it in this repository.`,
|
|
100
|
+
};
|
|
101
|
+
let manifestPath;
|
|
102
|
+
try {
|
|
103
|
+
manifestPath = createRequire(import.meta.url).resolve(`${PRETTIER_PACKAGE}/package.json`);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return missing;
|
|
107
|
+
}
|
|
108
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
109
|
+
if (!isObject(manifest))
|
|
110
|
+
return missing;
|
|
111
|
+
const bin = isObject(manifest.bin) ? manifest.bin[PRETTIER_PACKAGE] : manifest.bin;
|
|
112
|
+
if (typeof bin !== 'string')
|
|
113
|
+
return missing;
|
|
114
|
+
return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin) };
|
|
115
|
+
}
|
|
116
|
+
export function runPrettier(options) {
|
|
117
|
+
const { cliPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES } = options;
|
|
118
|
+
const prettierArgs = [
|
|
119
|
+
cliPath,
|
|
120
|
+
'--ignore-unknown',
|
|
121
|
+
...ignorePaths.flatMap((ignorePath) => ['--ignore-path', ignorePath]),
|
|
122
|
+
...MODE_ARGS[mode],
|
|
123
|
+
];
|
|
124
|
+
let firstFailure = 0;
|
|
125
|
+
for (const batch of batchWithinBudget(files, budgetBytes)) {
|
|
126
|
+
const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
|
|
127
|
+
if (result.error) {
|
|
128
|
+
reportError(`nmr-fmt: could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const status = result.status ?? 1;
|
|
132
|
+
if (status !== 0 && firstFailure === 0) {
|
|
133
|
+
firstFailure = status;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return firstFailure;
|
|
137
|
+
}
|
|
138
|
+
function batchWithinBudget(files, budget) {
|
|
139
|
+
const batches = [];
|
|
140
|
+
let batch = [];
|
|
141
|
+
let size = 0;
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
const cost = Buffer.byteLength(file) + 1;
|
|
144
|
+
if (batch.length > 0 && size + cost > budget) {
|
|
145
|
+
batches.push(batch);
|
|
146
|
+
batch = [];
|
|
147
|
+
size = 0;
|
|
148
|
+
}
|
|
149
|
+
batch.push(file);
|
|
150
|
+
size += cost;
|
|
151
|
+
}
|
|
152
|
+
if (batch.length > 0)
|
|
153
|
+
batches.push(batch);
|
|
154
|
+
return batches;
|
|
155
|
+
}
|
|
156
|
+
function runGit(args, cwd) {
|
|
157
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
158
|
+
if (result.error) {
|
|
159
|
+
return { ok: false, error: result.error.message };
|
|
160
|
+
}
|
|
161
|
+
if (result.status !== 0) {
|
|
162
|
+
const stderr = result.stderr.trim();
|
|
163
|
+
return { ok: false, error: stderr || `\`git ${args.join(' ')}\` failed with exit code ${result.status}.` };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, stdout: result.stdout };
|
|
166
|
+
}
|
|
167
|
+
function splitNulSeparated(output) {
|
|
168
|
+
return output.split('\0').filter((entry) => entry !== '');
|
|
169
|
+
}
|
|
170
|
+
function dedupe(items) {
|
|
171
|
+
return [...new Set(items)];
|
|
172
|
+
}
|
package/dist/esm/context.d.ts
CHANGED
|
@@ -5,7 +5,5 @@ export interface ResolvedContext {
|
|
|
5
5
|
packageDir?: string;
|
|
6
6
|
config: NmrConfig;
|
|
7
7
|
}
|
|
8
|
-
export declare function findMonorepoRoot(startDir?: string): string;
|
|
9
|
-
export declare function getWorkspacePackageDirs(monorepoRoot: string): string[];
|
|
10
8
|
export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
|
|
11
9
|
export declare function resolveContext(cwd?: string): Promise<ResolvedContext>;
|
package/dist/esm/context.js
CHANGED
|
@@ -1,71 +1,18 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
2
|
import { loadConfig } from "./config.js";
|
|
5
|
-
import {
|
|
6
|
-
export function findMonorepoRoot(startDir) {
|
|
7
|
-
let dir = path.resolve(startDir ?? process.cwd());
|
|
8
|
-
for (;;) {
|
|
9
|
-
if (existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
10
|
-
return dir;
|
|
11
|
-
}
|
|
12
|
-
const parent = path.dirname(dir);
|
|
13
|
-
if (parent === dir) {
|
|
14
|
-
throw new Error('Could not find monorepo root: no pnpm-workspace.yaml found in any parent directory');
|
|
15
|
-
}
|
|
16
|
-
dir = parent;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function getWorkspacePackageDirs(monorepoRoot) {
|
|
20
|
-
const workspaceFile = path.join(monorepoRoot, 'pnpm-workspace.yaml');
|
|
21
|
-
const content = readFileSync(workspaceFile, 'utf8');
|
|
22
|
-
const parsed = parse(content);
|
|
23
|
-
const packages = getPackagesFromParsedYaml(parsed);
|
|
24
|
-
if (!packages) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
const dirs = [];
|
|
28
|
-
for (const pattern of packages) {
|
|
29
|
-
if (pattern.endsWith('/*')) {
|
|
30
|
-
const prefix = pattern.slice(0, -2);
|
|
31
|
-
const prefixDir = path.resolve(monorepoRoot, prefix);
|
|
32
|
-
if (existsSync(prefixDir)) {
|
|
33
|
-
for (const entry of readdirSync(prefixDir)) {
|
|
34
|
-
const fullPath = path.join(prefixDir, entry);
|
|
35
|
-
if (statSync(fullPath).isDirectory() && existsSync(path.join(fullPath, 'package.json'))) {
|
|
36
|
-
dirs.push(fullPath);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else if (!pattern.includes('*')) {
|
|
42
|
-
const fullPath = path.resolve(monorepoRoot, pattern);
|
|
43
|
-
if (existsSync(fullPath) && existsSync(path.join(fullPath, 'package.json'))) {
|
|
44
|
-
dirs.push(fullPath);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return dirs;
|
|
49
|
-
}
|
|
3
|
+
import { findMonorepoRoot, getWorkspacePackageDirs } from "./workspace.js";
|
|
50
4
|
export function findContainingPackageDir(dir, workspacePackageDirs) {
|
|
51
5
|
const resolved = path.resolve(dir);
|
|
6
|
+
let nearest;
|
|
52
7
|
for (const pkgDir of workspacePackageDirs) {
|
|
53
8
|
const resolvedPkgDir = path.resolve(pkgDir);
|
|
54
|
-
if (resolved
|
|
55
|
-
|
|
9
|
+
if (resolved !== resolvedPkgDir && !resolved.startsWith(resolvedPkgDir + path.sep))
|
|
10
|
+
continue;
|
|
11
|
+
if (nearest === undefined || resolvedPkgDir.length > nearest.length) {
|
|
12
|
+
nearest = resolvedPkgDir;
|
|
56
13
|
}
|
|
57
14
|
}
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
function getPackagesFromParsedYaml(parsed) {
|
|
61
|
-
if (!isObject(parsed))
|
|
62
|
-
return undefined;
|
|
63
|
-
const packages = parsed.packages;
|
|
64
|
-
if (!Array.isArray(packages))
|
|
65
|
-
return undefined;
|
|
66
|
-
if (!packages.every((p) => typeof p === 'string'))
|
|
67
|
-
return undefined;
|
|
68
|
-
return packages;
|
|
15
|
+
return nearest;
|
|
69
16
|
}
|
|
70
17
|
export async function resolveContext(cwd) {
|
|
71
18
|
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export type ScriptValue = string | string[];
|
|
2
2
|
export type ScriptRegistry = Record<string, ScriptValue>;
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const integrationTestScripts: ScriptRegistry;
|
|
5
|
-
export declare const standardTestScripts: ScriptRegistry;
|
|
3
|
+
export declare const workspaceScripts: ScriptRegistry;
|
|
6
4
|
export declare const rootScripts: ScriptRegistry;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const workspaceScripts = {
|
|
2
2
|
build: ['compile'],
|
|
3
3
|
check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
|
|
4
4
|
'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
|
|
@@ -6,27 +6,20 @@ export const commonWorkspaceScripts = {
|
|
|
6
6
|
compile: 'nmr-compile',
|
|
7
7
|
fix: ['lint', 'fmt'],
|
|
8
8
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
9
|
-
fmt: '
|
|
10
|
-
'fmt:check': '
|
|
9
|
+
fmt: 'nmr-fmt --write',
|
|
10
|
+
'fmt:check': 'nmr-fmt --check',
|
|
11
11
|
lint: 'eslint --fix .',
|
|
12
12
|
'lint:check': 'eslint .',
|
|
13
13
|
'lint:strict': 'strict-lint',
|
|
14
|
+
test: "pnpm exec vitest --project '!integration'",
|
|
15
|
+
'test:all': 'pnpm exec vitest',
|
|
16
|
+
'test:coverage': "pnpm exec vitest --project '!integration' --coverage",
|
|
17
|
+
'test:integration': 'pnpm exec vitest --project integration',
|
|
18
|
+
'test:watch': "pnpm exec vitest --project '!integration' --watch",
|
|
14
19
|
typecheck: 'tsgo --noEmit',
|
|
15
20
|
upgrade: 'nmr-taze --include-locked',
|
|
16
21
|
'view-coverage': 'open coverage/index.html',
|
|
17
22
|
};
|
|
18
|
-
export const integrationTestScripts = {
|
|
19
|
-
test: 'pnpm exec vitest --config=vitest.standalone.config.ts',
|
|
20
|
-
'test:all': 'pnpm exec vitest',
|
|
21
|
-
'test:coverage': 'pnpm exec vitest --config=vitest.standalone.config.ts --coverage',
|
|
22
|
-
'test:integration': 'pnpm exec vitest --config=vitest.integration.config.ts',
|
|
23
|
-
'test:watch': 'pnpm exec vitest --config=vitest.standalone.config.ts --watch',
|
|
24
|
-
};
|
|
25
|
-
export const standardTestScripts = {
|
|
26
|
-
test: 'pnpm exec vitest',
|
|
27
|
-
'test:coverage': 'pnpm exec vitest --coverage',
|
|
28
|
-
'test:watch': 'pnpm exec vitest --watch',
|
|
29
|
-
};
|
|
30
23
|
export const rootScripts = {
|
|
31
24
|
audit: ['audit:prod', 'audit:dev'],
|
|
32
25
|
'audit:dev': 'pnpm exec v11y --dev',
|
|
@@ -39,9 +32,9 @@ export const rootScripts = {
|
|
|
39
32
|
clean: 'nmr-clean',
|
|
40
33
|
fix: ['lint', 'fmt'],
|
|
41
34
|
'fix:check': ['fmt:check', 'lint:check'],
|
|
42
|
-
fmt: '
|
|
35
|
+
fmt: 'nmr-fmt --write',
|
|
43
36
|
'fmt:all': ['fmt', 'fmt:sh'],
|
|
44
|
-
'fmt:check': '
|
|
37
|
+
'fmt:check': 'nmr-fmt --check',
|
|
45
38
|
'fmt:sh': 'shfmt --write **/*.sh',
|
|
46
39
|
lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
|
|
47
40
|
'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
|
|
@@ -51,14 +44,17 @@ export const rootScripts = {
|
|
|
51
44
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
52
45
|
'root:lint:check': "eslint --ignore-pattern 'packages/**' .",
|
|
53
46
|
'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
|
|
54
|
-
'root:test':
|
|
47
|
+
'root:test': "vitest --config ./vitest.root.config.ts --project '!integration'",
|
|
48
|
+
'root:test:all': 'vitest --config ./vitest.root.config.ts',
|
|
49
|
+
'root:test:integration': 'vitest --config ./vitest.root.config.ts --project integration',
|
|
55
50
|
'root:typecheck': 'tsgo --noEmit',
|
|
56
51
|
'root:upgrade': 'nmr-taze --include-locked',
|
|
57
52
|
'sync-agent-files': 'nmr-sync-agent-files',
|
|
58
|
-
'sync-pnpm-version': 'nmr-sync-pnpm-version',
|
|
59
53
|
test: 'nmr root:test && pnpm --recursive exec nmr test',
|
|
54
|
+
'test:all': 'nmr root:test:all && pnpm --recursive exec nmr test:all',
|
|
60
55
|
'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
|
|
61
|
-
'test:
|
|
56
|
+
'test:integration': 'nmr root:test:integration && pnpm --recursive exec nmr test:integration',
|
|
57
|
+
'test:watch': "vitest --project '!integration' --watch",
|
|
62
58
|
typecheck: 'nmr root:typecheck && pnpm --recursive exec nmr typecheck',
|
|
63
|
-
upgrade: 'nmr-taze --include-locked --recursive',
|
|
59
|
+
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
64
60
|
};
|
package/dist/esm/help.js
CHANGED
|
@@ -7,15 +7,16 @@ export function generateHelp(config, packageDir, useRoot) {
|
|
|
7
7
|
'Flags:',
|
|
8
8
|
' -F, --filter <pattern> Run command in matching packages',
|
|
9
9
|
' -R, --recursive Run command in all packages',
|
|
10
|
-
' -w, --workspace-root
|
|
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
12
|
' -?, --help Show this help',
|
|
13
|
+
' -V, --version Show version number',
|
|
13
14
|
'',
|
|
14
15
|
'Workspace commands:',
|
|
15
16
|
];
|
|
16
17
|
const overrides = packageDir === undefined ? {} : collectOverrides(packageDir);
|
|
17
18
|
let hadOverride = false;
|
|
18
|
-
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config
|
|
19
|
+
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config));
|
|
19
20
|
const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
|
|
20
21
|
if (workspaceMarked.size > 0)
|
|
21
22
|
hadOverride = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolvePackageDirs(monorepoRoot: string, patterns: string[]): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { globSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MANIFEST = 'package.json';
|
|
4
|
+
const ALWAYS_EXCLUDED = ['**/node_modules/**'];
|
|
5
|
+
export function resolvePackageDirs(monorepoRoot, patterns) {
|
|
6
|
+
const included = [];
|
|
7
|
+
const excluded = [];
|
|
8
|
+
for (const pattern of patterns) {
|
|
9
|
+
if (pattern.trim() === '')
|
|
10
|
+
continue;
|
|
11
|
+
const isNegated = pattern.startsWith('!');
|
|
12
|
+
const target = isNegated ? excluded : included;
|
|
13
|
+
target.push(toManifestPattern(isNegated ? pattern.slice(1) : pattern));
|
|
14
|
+
}
|
|
15
|
+
if (included.length === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const options = {
|
|
19
|
+
cwd: monorepoRoot,
|
|
20
|
+
exclude: [...excluded, ...ALWAYS_EXCLUDED],
|
|
21
|
+
followSymlinks: true,
|
|
22
|
+
};
|
|
23
|
+
const matches = globSync(included, options);
|
|
24
|
+
const dirs = matches.map((match) => path.dirname(path.resolve(monorepoRoot, match)));
|
|
25
|
+
return [...new Set(dirs)].toSorted();
|
|
26
|
+
}
|
|
27
|
+
function toManifestPattern(pattern) {
|
|
28
|
+
return `${pattern.replace(/\/?$/, '')}/${MANIFEST}`;
|
|
29
|
+
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ScriptRegistry } from './default-scripts.js';
|
|
2
2
|
export type { ScriptRegistry, ScriptValue } from './default-scripts.js';
|
|
3
|
-
export declare function getDefaultWorkspaceScripts(
|
|
3
|
+
export declare function getDefaultWorkspaceScripts(): ScriptRegistry;
|
|
4
4
|
export declare function getDefaultRootScripts(): ScriptRegistry;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function getDefaultWorkspaceScripts(
|
|
3
|
-
return {
|
|
4
|
-
...commonWorkspaceScripts,
|
|
5
|
-
...(useIntTests ? integrationTestScripts : standardTestScripts),
|
|
6
|
-
};
|
|
1
|
+
import { rootScripts, workspaceScripts } from "./default-scripts.js";
|
|
2
|
+
export function getDefaultWorkspaceScripts() {
|
|
3
|
+
return { ...workspaceScripts };
|
|
7
4
|
}
|
|
8
5
|
export function getDefaultRootScripts() {
|
|
9
6
|
return { ...rootScripts };
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -8,8 +8,7 @@ export interface ResolvedScript {
|
|
|
8
8
|
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): string;
|
|
9
9
|
export declare function describeScript(script: ScriptValue): string;
|
|
10
10
|
export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function buildWorkspaceRegistry(config: NmrConfig, useIntTests: boolean): ScriptRegistry;
|
|
11
|
+
export declare function buildWorkspaceRegistry(config: NmrConfig): ScriptRegistry;
|
|
13
12
|
export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
|
|
14
13
|
export declare function isSelfReferential(script: string, commandName: string): boolean;
|
|
15
14
|
export declare function resolveScript(commandName: string, registry: ScriptRegistry, packageDir: string | undefined, workspaceRoot: boolean): ResolvedScript | undefined;
|
package/dist/esm/resolver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isObject } from "./helpers/type-guards.js";
|
|
4
4
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
@@ -63,12 +63,9 @@ export function readPackageJsonScripts(packageDir) {
|
|
|
63
63
|
throw error;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function
|
|
67
|
-
return existsSync(path.join(packageDir, 'vitest.integration.config.ts'));
|
|
68
|
-
}
|
|
69
|
-
export function buildWorkspaceRegistry(config, useIntTests) {
|
|
66
|
+
export function buildWorkspaceRegistry(config) {
|
|
70
67
|
return {
|
|
71
|
-
...getDefaultWorkspaceScripts(
|
|
68
|
+
...getDefaultWorkspaceScripts(),
|
|
72
69
|
...config.workspaceScripts,
|
|
73
70
|
};
|
|
74
71
|
}
|
package/dist/esm/runCli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readPackageVersion, reportError } from '@williamthorsen/nmr-core';
|
|
|
3
3
|
import { resolveContext } from "./context.js";
|
|
4
4
|
import { generateHelp } from "./help.js";
|
|
5
5
|
import { isHookName } from "./helpers/hook-name.js";
|
|
6
|
-
import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry,
|
|
6
|
+
import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
7
7
|
import { runCommand } from "./runner.js";
|
|
8
8
|
const VERSION = readPackageVersion(import.meta.url);
|
|
9
9
|
export async function runCli(options) {
|
|
@@ -20,9 +20,9 @@ export async function runCli(options) {
|
|
|
20
20
|
}
|
|
21
21
|
const context = await resolveContext(cwd);
|
|
22
22
|
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
23
|
-
const
|
|
23
|
+
const anchorDir = useRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
24
24
|
if (parsed.help || !parsed.command) {
|
|
25
|
-
stdout.write(`${generateHelp(context.config,
|
|
25
|
+
stdout.write(`${generateHelp(context.config, anchorDir, useRoot)}\n`);
|
|
26
26
|
return { exitCode: 0 };
|
|
27
27
|
}
|
|
28
28
|
const { command } = parsed;
|
|
@@ -39,10 +39,8 @@ export async function runCli(options) {
|
|
|
39
39
|
const exitCode = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: childEnv });
|
|
40
40
|
return { exitCode };
|
|
41
41
|
}
|
|
42
|
-
const registry = useRoot
|
|
43
|
-
|
|
44
|
-
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(packageDir));
|
|
45
|
-
const resolved = resolveScript(command, registry, packageDir, parsed.workspaceRoot);
|
|
42
|
+
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
43
|
+
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
46
44
|
if (!resolved) {
|
|
47
45
|
if (env.NMR_RUN_IF_PRESENT === '1') {
|
|
48
46
|
return { exitCode: 0 };
|
|
@@ -50,20 +48,20 @@ export async function runCli(options) {
|
|
|
50
48
|
reportError(`Unknown command: ${command}`, stderr);
|
|
51
49
|
return { exitCode: 1 };
|
|
52
50
|
}
|
|
53
|
-
const skipExitCode = handleSkipMessage(resolved.command,
|
|
51
|
+
const skipExitCode = handleSkipMessage(resolved.command, anchorDir, parsed.quiet, stdout);
|
|
54
52
|
if (skipExitCode !== undefined) {
|
|
55
53
|
return { exitCode: skipExitCode };
|
|
56
54
|
}
|
|
57
55
|
if (resolved.source === 'package' && !parsed.quiet && registry[command] !== undefined) {
|
|
58
|
-
stdout.write(`๐ฆ ${path.basename(
|
|
56
|
+
stdout.write(`๐ฆ ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`);
|
|
59
57
|
}
|
|
60
58
|
const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
|
|
61
59
|
const mainCommand = substitutedCommand + passthrough;
|
|
62
60
|
const isHookInvocation = isHookName(command);
|
|
63
61
|
const fullCommand = isHookInvocation
|
|
64
62
|
? mainCommand
|
|
65
|
-
: wrapWithHooks(command, mainCommand, registry,
|
|
66
|
-
const exitCode = runCommand(fullCommand,
|
|
63
|
+
: wrapWithHooks(command, mainCommand, registry, anchorDir, parsed.workspaceRoot);
|
|
64
|
+
const exitCode = runCommand(fullCommand, anchorDir, runOptions);
|
|
67
65
|
return { exitCode };
|
|
68
66
|
}
|
|
69
67
|
function parseArgs(args) {
|
|
@@ -121,35 +119,35 @@ function parseArgs(args) {
|
|
|
121
119
|
}
|
|
122
120
|
return { ok: true, parsed };
|
|
123
121
|
}
|
|
124
|
-
function handleSkipMessage(resolvedCommand,
|
|
122
|
+
function handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout) {
|
|
125
123
|
if (resolvedCommand === '') {
|
|
126
124
|
if (!quiet) {
|
|
127
|
-
stdout.write(`โ ${path.basename(
|
|
125
|
+
stdout.write(`โ ${path.basename(anchorDir)}: Override script is defined but empty. Skipping.\n`);
|
|
128
126
|
}
|
|
129
127
|
return 0;
|
|
130
128
|
}
|
|
131
129
|
if (resolvedCommand === ':') {
|
|
132
130
|
if (!quiet) {
|
|
133
|
-
stdout.write(`โ ${path.basename(
|
|
131
|
+
stdout.write(`โ ${path.basename(anchorDir)}: Override script is a no-op. Skipping.\n`);
|
|
134
132
|
}
|
|
135
133
|
return 0;
|
|
136
134
|
}
|
|
137
135
|
return undefined;
|
|
138
136
|
}
|
|
139
|
-
function wrapWithHooks(command, mainCommand, registry,
|
|
137
|
+
function wrapWithHooks(command, mainCommand, registry, anchorDir, workspaceRoot) {
|
|
140
138
|
const segments = [];
|
|
141
139
|
const flag = workspaceRoot ? '-w ' : '';
|
|
142
|
-
if (hasRunnableHook(`${command}:pre`, registry,
|
|
140
|
+
if (hasRunnableHook(`${command}:pre`, registry, anchorDir, workspaceRoot)) {
|
|
143
141
|
segments.push(`nmr ${flag}${command}:pre`);
|
|
144
142
|
}
|
|
145
143
|
segments.push(mainCommand);
|
|
146
|
-
if (hasRunnableHook(`${command}:post`, registry,
|
|
144
|
+
if (hasRunnableHook(`${command}:post`, registry, anchorDir, workspaceRoot)) {
|
|
147
145
|
segments.push(`nmr ${flag}${command}:post`);
|
|
148
146
|
}
|
|
149
147
|
return segments.join(' && ');
|
|
150
148
|
}
|
|
151
|
-
function hasRunnableHook(hookName, registry,
|
|
152
|
-
const resolved = resolveScript(hookName, registry,
|
|
149
|
+
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
150
|
+
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
153
151
|
if (!resolved)
|
|
154
152
|
return false;
|
|
155
153
|
return resolved.command !== '' && resolved.command !== ':';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ViteUserConfig } from 'vitest/config';
|
|
2
|
+
import type { InlineConfig, ProjectConfig } from 'vitest/node';
|
|
3
|
+
export type RootTestOptions = Omit<InlineConfig, keyof ProjectConfig>;
|
|
4
|
+
export type RootOverrides = Omit<ViteUserConfig, 'test'> & {
|
|
5
|
+
test?: RootTestOptions;
|
|
6
|
+
};
|
|
7
|
+
export interface VitestConfigOptions {
|
|
8
|
+
root?: RootOverrides;
|
|
9
|
+
project?: ProjectConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface RootVitestConfigOptions extends VitestConfigOptions {
|
|
12
|
+
startDir?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function defineVitestConfig(options?: VitestConfigOptions): ViteUserConfig;
|
|
15
|
+
export declare function defineRootVitestConfig(options?: RootVitestConfigOptions): ViteUserConfig;
|