@williamthorsen/nmr 0.18.2 → 0.20.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 -84
- package/README.md +187 -46
- package/bin/nmr-taze.js +11 -0
- package/dist/esm/cli-ensure-prepublish-hooks.js +1 -1
- package/dist/esm/cli-report-overrides.js +1 -1
- package/dist/esm/cli-sync-agent-files.js +1 -1
- package/dist/esm/cli-taze.js +10 -0
- package/dist/esm/commands/build.d.ts +1 -1
- package/dist/esm/commands/build.js +8 -6
- package/dist/esm/commands/clean.js +2 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +1 -1
- package/dist/esm/commands/taze.d.ts +12 -0
- package/dist/esm/commands/taze.js +32 -0
- package/dist/esm/context.d.ts +0 -2
- package/dist/esm/context.js +8 -61
- package/dist/esm/default-scripts.js +3 -5
- package/dist/esm/help.js +9 -6
- 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/runCli.js +16 -16
- package/dist/esm/taze.d.ts +5 -0
- package/dist/esm/taze.js +6 -0
- package/dist/esm/workspace.d.ts +2 -0
- package/dist/esm/workspace.js +38 -0
- package/package.json +16 -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-taze.d.ts} +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
5
|
+
const TAZE_CLI_SPECIFIER = 'taze/cli';
|
|
6
|
+
export function runTaze(args, options = {}) {
|
|
7
|
+
const stderr = options.stderr ?? process.stderr;
|
|
8
|
+
const resolveCliPath = options.resolveCliPath ?? resolveTazeCliPath;
|
|
9
|
+
const spawn = options.spawn ?? spawnNode;
|
|
10
|
+
let cliPath;
|
|
11
|
+
try {
|
|
12
|
+
cliPath = resolveCliPath();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
16
|
+
reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const outcome = spawn(process.execPath, [cliPath, ...args]);
|
|
20
|
+
if (outcome.error) {
|
|
21
|
+
reportError(`Failed to run taze: ${outcome.error.message}`, stderr);
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
return outcome.status ?? 1;
|
|
25
|
+
}
|
|
26
|
+
export function resolveTazeCliPath() {
|
|
27
|
+
return fileURLToPath(import.meta.resolve(TAZE_CLI_SPECIFIER));
|
|
28
|
+
}
|
|
29
|
+
function spawnNode(nodePath, argv) {
|
|
30
|
+
const result = spawnSync(nodePath, argv, { stdio: 'inherit' });
|
|
31
|
+
return { status: result.status, error: result.error };
|
|
32
|
+
}
|
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());
|
|
@@ -76,7 +23,7 @@ export async function resolveContext(cwd) {
|
|
|
76
23
|
return {
|
|
77
24
|
monorepoRoot,
|
|
78
25
|
isRoot: packageDir === undefined,
|
|
79
|
-
...(packageDir
|
|
26
|
+
...(packageDir !== undefined && { packageDir }),
|
|
80
27
|
config,
|
|
81
28
|
};
|
|
82
29
|
}
|
|
@@ -12,6 +12,7 @@ export const commonWorkspaceScripts = {
|
|
|
12
12
|
'lint:check': 'eslint .',
|
|
13
13
|
'lint:strict': 'strict-lint',
|
|
14
14
|
typecheck: 'tsgo --noEmit',
|
|
15
|
+
upgrade: 'nmr-taze --include-locked',
|
|
15
16
|
'view-coverage': 'open coverage/index.html',
|
|
16
17
|
};
|
|
17
18
|
export const integrationTestScripts = {
|
|
@@ -45,8 +46,6 @@ export const rootScripts = {
|
|
|
45
46
|
lint: 'nmr root:lint && pnpm --recursive exec nmr lint',
|
|
46
47
|
'lint:check': 'nmr root:lint:check && pnpm --recursive exec nmr lint:check',
|
|
47
48
|
'lint:strict': 'nmr root:lint:strict && pnpm --recursive exec nmr lint:strict',
|
|
48
|
-
outdated: 'pnpm outdated --compatible --recursive',
|
|
49
|
-
'outdated:latest': 'pnpm outdated --recursive',
|
|
50
49
|
'report-overrides': 'nmr-report-overrides',
|
|
51
50
|
'root:check': ['root:typecheck', 'fmt:check', 'root:lint:check', 'root:test'],
|
|
52
51
|
'root:lint': "eslint --fix --ignore-pattern 'packages/**' .",
|
|
@@ -54,12 +53,11 @@ export const rootScripts = {
|
|
|
54
53
|
'root:lint:strict': "strict-lint --ignore-pattern 'packages/**' .",
|
|
55
54
|
'root:test': 'vitest --config ./vitest.root.config.ts',
|
|
56
55
|
'root:typecheck': 'tsgo --noEmit',
|
|
56
|
+
'root:upgrade': 'nmr-taze --include-locked',
|
|
57
57
|
'sync-agent-files': 'nmr-sync-agent-files',
|
|
58
|
-
'sync-pnpm-version': 'nmr-sync-pnpm-version',
|
|
59
58
|
test: 'nmr root:test && pnpm --recursive exec nmr test',
|
|
60
59
|
'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
|
|
61
60
|
'test:watch': 'vitest --watch',
|
|
62
61
|
typecheck: 'nmr root:typecheck && pnpm --recursive exec nmr typecheck',
|
|
63
|
-
|
|
64
|
-
'update:latest': 'pnpm update --latest --recursive',
|
|
62
|
+
upgrade: 'nmr-report-overrides && nmr-taze --include-locked --recursive',
|
|
65
63
|
};
|
package/dist/esm/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
-
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
2
|
+
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, hasIntegrationTestConfig, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
3
3
|
export function generateHelp(config, packageDir, useRoot) {
|
|
4
4
|
const lines = [
|
|
5
5
|
'Usage: nmr [flags] <command> [args...]',
|
|
@@ -7,15 +7,17 @@ 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
|
|
19
|
+
const useIntTests = !useRoot && packageDir !== undefined && hasIntegrationTestConfig(packageDir);
|
|
20
|
+
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config, useIntTests));
|
|
19
21
|
const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
|
|
20
22
|
if (workspaceMarked.size > 0)
|
|
21
23
|
hadOverride = true;
|
|
@@ -48,10 +50,11 @@ function collectOverrides(packageDir) {
|
|
|
48
50
|
function applyOverrides(registry, overrides) {
|
|
49
51
|
const marked = new Set();
|
|
50
52
|
for (const [name, value] of Object.entries(overrides)) {
|
|
51
|
-
if (name in registry) {
|
|
52
|
-
|
|
53
|
-
marked.add(name);
|
|
53
|
+
if (!(name in registry)) {
|
|
54
|
+
continue;
|
|
54
55
|
}
|
|
56
|
+
registry[name] = value;
|
|
57
|
+
marked.add(name);
|
|
55
58
|
}
|
|
56
59
|
return marked;
|
|
57
60
|
}
|
|
@@ -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
package/dist/esm/runCli.js
CHANGED
|
@@ -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;
|
|
@@ -41,8 +41,8 @@ export async function runCli(options) {
|
|
|
41
41
|
}
|
|
42
42
|
const registry = useRoot
|
|
43
43
|
? buildRootRegistry(context.config)
|
|
44
|
-
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(
|
|
45
|
-
const resolved = resolveScript(command, registry,
|
|
44
|
+
: buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(anchorDir));
|
|
45
|
+
const resolved = resolveScript(command, registry, anchorDir, parsed.workspaceRoot);
|
|
46
46
|
if (!resolved) {
|
|
47
47
|
if (env.NMR_RUN_IF_PRESENT === '1') {
|
|
48
48
|
return { exitCode: 0 };
|
|
@@ -50,20 +50,20 @@ export async function runCli(options) {
|
|
|
50
50
|
reportError(`Unknown command: ${command}`, stderr);
|
|
51
51
|
return { exitCode: 1 };
|
|
52
52
|
}
|
|
53
|
-
const skipExitCode = handleSkipMessage(resolved.command,
|
|
53
|
+
const skipExitCode = handleSkipMessage(resolved.command, anchorDir, parsed.quiet, stdout);
|
|
54
54
|
if (skipExitCode !== undefined) {
|
|
55
55
|
return { exitCode: skipExitCode };
|
|
56
56
|
}
|
|
57
57
|
if (resolved.source === 'package' && !parsed.quiet && registry[command] !== undefined) {
|
|
58
|
-
stdout.write(`📦 ${path.basename(
|
|
58
|
+
stdout.write(`📦 ${path.basename(anchorDir)}: Using override script: ${resolved.command}\n`);
|
|
59
59
|
}
|
|
60
60
|
const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
|
|
61
61
|
const mainCommand = substitutedCommand + passthrough;
|
|
62
62
|
const isHookInvocation = isHookName(command);
|
|
63
63
|
const fullCommand = isHookInvocation
|
|
64
64
|
? mainCommand
|
|
65
|
-
: wrapWithHooks(command, mainCommand, registry,
|
|
66
|
-
const exitCode = runCommand(fullCommand,
|
|
65
|
+
: wrapWithHooks(command, mainCommand, registry, anchorDir, parsed.workspaceRoot);
|
|
66
|
+
const exitCode = runCommand(fullCommand, anchorDir, runOptions);
|
|
67
67
|
return { exitCode };
|
|
68
68
|
}
|
|
69
69
|
function parseArgs(args) {
|
|
@@ -121,35 +121,35 @@ function parseArgs(args) {
|
|
|
121
121
|
}
|
|
122
122
|
return { ok: true, parsed };
|
|
123
123
|
}
|
|
124
|
-
function handleSkipMessage(resolvedCommand,
|
|
124
|
+
function handleSkipMessage(resolvedCommand, anchorDir, quiet, stdout) {
|
|
125
125
|
if (resolvedCommand === '') {
|
|
126
126
|
if (!quiet) {
|
|
127
|
-
stdout.write(`⛔ ${path.basename(
|
|
127
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is defined but empty. Skipping.\n`);
|
|
128
128
|
}
|
|
129
129
|
return 0;
|
|
130
130
|
}
|
|
131
131
|
if (resolvedCommand === ':') {
|
|
132
132
|
if (!quiet) {
|
|
133
|
-
stdout.write(`⛔ ${path.basename(
|
|
133
|
+
stdout.write(`⛔ ${path.basename(anchorDir)}: Override script is a no-op. Skipping.\n`);
|
|
134
134
|
}
|
|
135
135
|
return 0;
|
|
136
136
|
}
|
|
137
137
|
return undefined;
|
|
138
138
|
}
|
|
139
|
-
function wrapWithHooks(command, mainCommand, registry,
|
|
139
|
+
function wrapWithHooks(command, mainCommand, registry, anchorDir, workspaceRoot) {
|
|
140
140
|
const segments = [];
|
|
141
141
|
const flag = workspaceRoot ? '-w ' : '';
|
|
142
|
-
if (hasRunnableHook(`${command}:pre`, registry,
|
|
142
|
+
if (hasRunnableHook(`${command}:pre`, registry, anchorDir, workspaceRoot)) {
|
|
143
143
|
segments.push(`nmr ${flag}${command}:pre`);
|
|
144
144
|
}
|
|
145
145
|
segments.push(mainCommand);
|
|
146
|
-
if (hasRunnableHook(`${command}:post`, registry,
|
|
146
|
+
if (hasRunnableHook(`${command}:post`, registry, anchorDir, workspaceRoot)) {
|
|
147
147
|
segments.push(`nmr ${flag}${command}:post`);
|
|
148
148
|
}
|
|
149
149
|
return segments.join(' && ');
|
|
150
150
|
}
|
|
151
|
-
function hasRunnableHook(hookName, registry,
|
|
152
|
-
const resolved = resolveScript(hookName, registry,
|
|
151
|
+
function hasRunnableHook(hookName, registry, anchorDir, workspaceRoot) {
|
|
152
|
+
const resolved = resolveScript(hookName, registry, anchorDir, workspaceRoot);
|
|
153
153
|
if (!resolved)
|
|
154
154
|
return false;
|
|
155
155
|
return resolved.command !== '' && resolved.command !== ':';
|
package/dist/esm/taze.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
5
|
+
import { resolvePackageDirs } from "./helpers/workspace-patterns.js";
|
|
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
|
+
return resolvePackageDirs(monorepoRoot, packages);
|
|
28
|
+
}
|
|
29
|
+
function getPackagesFromParsedYaml(parsed) {
|
|
30
|
+
if (!isObject(parsed))
|
|
31
|
+
return undefined;
|
|
32
|
+
const packages = parsed.packages;
|
|
33
|
+
if (!Array.isArray(packages))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (!packages.every((p) => typeof p === 'string'))
|
|
36
|
+
return undefined;
|
|
37
|
+
return packages;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Context-aware script runner for
|
|
5
|
+
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"monorepo",
|
|
8
8
|
"pnpm",
|
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
"types": "./dist/esm/scripts.d.ts",
|
|
31
31
|
"default": "./dist/esm/scripts.js"
|
|
32
32
|
},
|
|
33
|
-
"./
|
|
34
|
-
"types": "./dist/esm/
|
|
35
|
-
"default": "./dist/esm/
|
|
33
|
+
"./taze": {
|
|
34
|
+
"types": "./dist/esm/taze.d.ts",
|
|
35
|
+
"default": "./dist/esm/taze.js"
|
|
36
|
+
},
|
|
37
|
+
"./workspace": {
|
|
38
|
+
"types": "./dist/esm/workspace.d.ts",
|
|
39
|
+
"default": "./dist/esm/workspace.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"bin": {
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
"nmr-compile": "bin/nmr-compile.js",
|
|
43
47
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
44
48
|
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
45
|
-
"nmr-
|
|
49
|
+
"nmr-taze": "bin/nmr-taze.js"
|
|
46
50
|
},
|
|
47
51
|
"files": [
|
|
48
52
|
"AGENTS.md",
|
|
@@ -52,15 +56,15 @@
|
|
|
52
56
|
"dependencies": {
|
|
53
57
|
"glob": "13.0.6",
|
|
54
58
|
"jiti": "2.7.0",
|
|
59
|
+
"taze": "19.15.1",
|
|
55
60
|
"yaml": "2.9.0",
|
|
56
|
-
"
|
|
57
|
-
"@williamthorsen/nmr-core": "0.7.0"
|
|
58
|
-
},
|
|
59
|
-
"devDependencies": {
|
|
60
|
-
"@williamthorsen/toolbelt.strings": "3.1.4"
|
|
61
|
+
"@williamthorsen/nmr-core": "0.7.2"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"typescript": ">=5.7.0"
|
|
64
|
+
"typescript": ">=5.7.0 <7"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=24.16.0"
|
|
64
68
|
},
|
|
65
69
|
"publishConfig": {
|
|
66
70
|
"access": "public"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
try {
|
|
3
|
-
await import('../dist/esm/cli-sync-pnpm-version.js');
|
|
4
|
-
} catch (error) {
|
|
5
|
-
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
-
process.stderr.write('nmr-sync-pnpm-version: build output not found — run `pnpm run build` first\n');
|
|
7
|
-
} else {
|
|
8
|
-
process.stderr.write(`nmr-sync-pnpm-version: failed to load: ${error.message}\n`);
|
|
9
|
-
}
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
-
import { syncPnpmVersion } from "./commands/sync-pnpm-version.js";
|
|
3
|
-
import { findMonorepoRoot } from "./context.js";
|
|
4
|
-
try {
|
|
5
|
-
const monorepoRoot = findMonorepoRoot();
|
|
6
|
-
syncPnpmVersion(monorepoRoot);
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
reportError(error instanceof Error ? error.message : String(error));
|
|
10
|
-
process.exitCode = 1;
|
|
11
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parseDocument } from 'yaml';
|
|
4
|
-
import { CodeQualityPnpmWorkflowSchema, getPnpmVersionNodes } from "../helpers/code-quality-pnpm-action.js";
|
|
5
|
-
import { readPackageJson } from "../helpers/package-json.js";
|
|
6
|
-
const WORKFLOW_RELATIVE_PATH = '.github/workflows/code-quality.yaml';
|
|
7
|
-
function extractPnpmVersion(packageManager) {
|
|
8
|
-
if (!packageManager) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
const match = /^pnpm@(.+)$/.exec(packageManager);
|
|
12
|
-
return match?.[1] ?? null;
|
|
13
|
-
}
|
|
14
|
-
export function syncPnpmVersion(monorepoRoot) {
|
|
15
|
-
console.info('Synchronizing pnpm version in GitHub workflow...');
|
|
16
|
-
const pkg = readPackageJson(monorepoRoot);
|
|
17
|
-
const pnpmVersion = extractPnpmVersion(pkg.packageManager);
|
|
18
|
-
if (!pnpmVersion) {
|
|
19
|
-
throw new Error(`Could not extract pnpm version from package.json packageManager field\npackageManager field: ${pkg.packageManager ?? '(not set)'}`);
|
|
20
|
-
}
|
|
21
|
-
console.info(`Package.json pnpm version: ${pnpmVersion}`);
|
|
22
|
-
const workflowPath = path.join(monorepoRoot, WORKFLOW_RELATIVE_PATH);
|
|
23
|
-
const doc = parseDocument(readFileSync(workflowPath, 'utf8'));
|
|
24
|
-
const [parseError] = doc.errors;
|
|
25
|
-
if (parseError) {
|
|
26
|
-
throw new Error(`Failed to parse workflow file: ${workflowPath}\n${parseError.message}`);
|
|
27
|
-
}
|
|
28
|
-
CodeQualityPnpmWorkflowSchema.parse(doc.toJS());
|
|
29
|
-
const outdatedNodes = getPnpmVersionNodes(doc).filter((node) => String(node.value) !== pnpmVersion);
|
|
30
|
-
if (outdatedNodes.length === 0) {
|
|
31
|
-
console.info('Workflow pnpm version is already up to date');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
for (const node of outdatedNodes) {
|
|
35
|
-
node.value = pnpmVersion;
|
|
36
|
-
}
|
|
37
|
-
writeFileSync(workflowPath, doc.toString(), 'utf8');
|
|
38
|
-
console.info(`✓ Updated ${outdatedNodes.length} pnpm-version occurrence(s) → ${pnpmVersion}`);
|
|
39
|
-
}
|
|
40
|
-
export { extractPnpmVersion };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type Document, type Scalar } from 'yaml';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
export declare const CodeQualityPnpmWorkflowSchema: z.ZodObject<{
|
|
4
|
-
jobs: z.ZodObject<{
|
|
5
|
-
'code-quality': z.ZodObject<{
|
|
6
|
-
with: z.ZodObject<{
|
|
7
|
-
'pnpm-version': z.ZodString;
|
|
8
|
-
}, z.core.$loose>;
|
|
9
|
-
}, z.core.$loose>;
|
|
10
|
-
}, z.core.$loose>;
|
|
11
|
-
}, z.core.$loose>;
|
|
12
|
-
export type CodeQualityPnpmWorkflow = z.infer<typeof CodeQualityPnpmWorkflowSchema>;
|
|
13
|
-
export declare function getPnpmVersionNodes(doc: Document): Scalar[];
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { isScalar, visit } from 'yaml';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
export const CodeQualityPnpmWorkflowSchema = z.looseObject({
|
|
4
|
-
jobs: z.looseObject({
|
|
5
|
-
'code-quality': z.looseObject({
|
|
6
|
-
with: z.looseObject({
|
|
7
|
-
'pnpm-version': z.string(),
|
|
8
|
-
}),
|
|
9
|
-
}),
|
|
10
|
-
}),
|
|
11
|
-
});
|
|
12
|
-
export function getPnpmVersionNodes(doc) {
|
|
13
|
-
const nodes = [];
|
|
14
|
-
visit(doc, {
|
|
15
|
-
Pair(_, pair) {
|
|
16
|
-
if (isScalar(pair.key) && pair.key.value === 'pnpm-version' && isScalar(pair.value)) {
|
|
17
|
-
nodes.push(pair.value);
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
return nodes;
|
|
22
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { findMonorepoRoot } from '../context.js';
|
|
2
|
-
export declare function checkPnpmVersionConsistency(monorepoRoot: string): void;
|
|
3
|
-
export declare function checkNodeVersionConsistency(monorepoRoot: string): void;
|
|
4
|
-
export declare function getPnpmVersionFromAction(monorepoRoot: string): Promise<string>;
|
|
5
|
-
export declare function getPnpmVersionFromPackageJson(monorepoRoot: string): string;
|
|
6
|
-
export declare function getNodeVersionFromAction(monorepoRoot: string): Promise<string>;
|
|
7
|
-
export declare function runConsistencyChecks(): void;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { findMonorepoRoot } from "../context.js";
|
|
5
|
-
export { findMonorepoRoot } from "../context.js";
|
|
6
|
-
import { getRuntimeVersionFromAsdf } from "./helpers/get-runtime-version-from-asdf.js";
|
|
7
|
-
import { getStringFromYamlFile } from "./helpers/get-string-from-yaml-file.js";
|
|
8
|
-
import { getValueAtPathOrThrow } from "./helpers/get-value-at-path.js";
|
|
9
|
-
const GITHUB_ACTION_FILE_PATH = '.github/workflows/code-quality.yaml';
|
|
10
|
-
export function checkPnpmVersionConsistency(monorepoRoot) {
|
|
11
|
-
describe('pnpm version consistency', () => {
|
|
12
|
-
it('pnpm version is the same in GitHub action and package.json', async () => {
|
|
13
|
-
const actionVersion = await getPnpmVersionFromAction(monorepoRoot);
|
|
14
|
-
const packageJsonVersion = getPnpmVersionFromPackageJson(monorepoRoot);
|
|
15
|
-
expect(actionVersion).toBe(packageJsonVersion);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
export function checkNodeVersionConsistency(monorepoRoot) {
|
|
20
|
-
describe('Node.js version consistency', () => {
|
|
21
|
-
it('version is the same in GitHub action and .tool-versions', async () => {
|
|
22
|
-
const toolVersion = await getRuntimeVersionFromAsdf('nodejs', monorepoRoot);
|
|
23
|
-
const actionVersion = await getNodeVersionFromAction(monorepoRoot);
|
|
24
|
-
expect(toolVersion).toBe(actionVersion);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
export async function getPnpmVersionFromAction(monorepoRoot) {
|
|
29
|
-
const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
|
|
30
|
-
return getStringFromYamlFile(actionPath, 'jobs.code-quality.with.pnpm-version', 'pnpm version');
|
|
31
|
-
}
|
|
32
|
-
export function getPnpmVersionFromPackageJson(monorepoRoot) {
|
|
33
|
-
const pkgPath = path.join(monorepoRoot, 'package.json');
|
|
34
|
-
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
35
|
-
const pkg = JSON.parse(raw);
|
|
36
|
-
const pm = getValueAtPathOrThrow(pkg, 'packageManager');
|
|
37
|
-
if (typeof pm !== 'string') {
|
|
38
|
-
throw new TypeError('"packageManager" field missing or not a string in package.json.');
|
|
39
|
-
}
|
|
40
|
-
const [name, version] = pm.split('@');
|
|
41
|
-
if (name !== 'pnpm') {
|
|
42
|
-
throw new Error('packageManager is not pnpm.');
|
|
43
|
-
}
|
|
44
|
-
if (!version) {
|
|
45
|
-
throw new Error('pnpm version missing in package.json.');
|
|
46
|
-
}
|
|
47
|
-
return version;
|
|
48
|
-
}
|
|
49
|
-
export async function getNodeVersionFromAction(monorepoRoot) {
|
|
50
|
-
const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
|
|
51
|
-
return getStringFromYamlFile(actionPath, 'jobs.code-quality.with.node-version', 'Node.js version');
|
|
52
|
-
}
|
|
53
|
-
export function runConsistencyChecks() {
|
|
54
|
-
const monorepoRoot = findMonorepoRoot();
|
|
55
|
-
checkPnpmVersionConsistency(monorepoRoot);
|
|
56
|
-
checkNodeVersionConsistency(monorepoRoot);
|
|
57
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getRuntimeVersionFromAsdf(runtime: string, monorepoRoot: string): Promise<string>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
export async function getRuntimeVersionFromAsdf(runtime, monorepoRoot) {
|
|
5
|
-
const toolVersionsPath = path.join(monorepoRoot, '.tool-versions');
|
|
6
|
-
const toolVersions = await fs.promises.readFile(toolVersionsPath, { encoding: 'utf8' });
|
|
7
|
-
const versionLine = toolVersions.split('\n').find((line) => line.trim().startsWith(runtime));
|
|
8
|
-
assert.ok(versionLine, `${runtime} not found in .tool-versions`);
|
|
9
|
-
const [, version] = versionLine.trim().split(/\s+/);
|
|
10
|
-
assert.ok(version, `${runtime} version missing in .tool-versions`);
|
|
11
|
-
return version;
|
|
12
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getStringFromYamlFile(filePath: string, keyPath: string, label: string): Promise<string>;
|