@williamthorsen/nmr 0.47.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +206 -0
- package/.readyup/kits/default.js +1534 -1466
- package/.readyup/manifest.json +142 -12
- package/CHANGELOG.md +55 -0
- package/README.md +44 -6
- package/agents/guidance/rulebooks/nmr.md +2 -1
- package/dist/esm/bin-path.js +4 -3
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +178 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +10 -0
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- package/dist/esm/helpers/findClosestName.js +4 -3
- package/dist/esm/helpers/quoteForShell.d.ts +5 -0
- package/dist/esm/helpers/quoteForShell.js +3 -0
- package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +31 -0
- package/dist/esm/remote-probe.js +78 -0
- package/dist/esm/remote-run.d.ts +91 -0
- package/dist/esm/remote-run.js +189 -0
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +41 -895
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +3 -1
- package/dist/esm/verdict.js +4 -3
- package/dist/esm/vitest-source-resolution.js +2 -3
- package/docs/check-cache.md +30 -14
- package/docs/delegate.md +141 -0
- package/docs/machine-lock.md +2 -1
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +5 -5
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
package/.readyup/kits/default.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @noformat -- @generated. Do not edit. Compiled by rdy. */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
export const __readyupVersion = "0.
|
|
3
|
+
export const __readyupVersion = "0.42.0";
|
|
4
4
|
import { createRequire as __rdyCreateRequire } from 'node:module';
|
|
5
5
|
const require = __rdyCreateRequire(import.meta.url);
|
|
6
6
|
|
|
@@ -7270,7 +7270,7 @@ var require_public_api = __commonJS({
|
|
|
7270
7270
|
}
|
|
7271
7271
|
return doc;
|
|
7272
7272
|
}
|
|
7273
|
-
function
|
|
7273
|
+
function parse4(src, reviver, options) {
|
|
7274
7274
|
let _reviver = void 0;
|
|
7275
7275
|
if (typeof reviver === "function") {
|
|
7276
7276
|
_reviver = reviver;
|
|
@@ -7311,7 +7311,7 @@ var require_public_api = __commonJS({
|
|
|
7311
7311
|
return value.toString(options);
|
|
7312
7312
|
return new Document.Document(value, _replacer, options).toString(options);
|
|
7313
7313
|
}
|
|
7314
|
-
exports.parse =
|
|
7314
|
+
exports.parse = parse4;
|
|
7315
7315
|
exports.parseAllDocuments = parseAllDocuments;
|
|
7316
7316
|
exports.parseDocument = parseDocument;
|
|
7317
7317
|
exports.stringify = stringify;
|
|
@@ -7371,9 +7371,16 @@ var require_dist = __commonJS({
|
|
|
7371
7371
|
});
|
|
7372
7372
|
|
|
7373
7373
|
// .readyup/kits/default.ts
|
|
7374
|
-
import {
|
|
7375
|
-
import {
|
|
7376
|
-
|
|
7374
|
+
import { defineRdyKit } from "readyup";
|
|
7375
|
+
import { hasDevDependency as hasDevDependency2 } from "readyup/check-utils";
|
|
7376
|
+
|
|
7377
|
+
// .readyup/lib/bin-targets.ts
|
|
7378
|
+
import { basename as basename2, posix } from "node:path";
|
|
7379
|
+
import { isRecord, listTrackedFiles } from "readyup/check-utils";
|
|
7380
|
+
|
|
7381
|
+
// .readyup/lib/repo-files.ts
|
|
7382
|
+
import { globSync } from "node:fs";
|
|
7383
|
+
import { basename, join, sep } from "node:path";
|
|
7377
7384
|
|
|
7378
7385
|
// ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.3/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
|
|
7379
7386
|
function isError(error) {
|
|
@@ -7392,304 +7399,194 @@ function describeError(error) {
|
|
|
7392
7399
|
}
|
|
7393
7400
|
}
|
|
7394
7401
|
|
|
7395
|
-
// .readyup/
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
hasMinDevDependencyVersion,
|
|
7404
|
-
hasPackageJsonField,
|
|
7405
|
-
isRecord,
|
|
7406
|
-
listTrackedFiles,
|
|
7407
|
-
readFile,
|
|
7408
|
-
readPackageJson
|
|
7409
|
-
} from "readyup/check-utils";
|
|
7410
|
-
|
|
7411
|
-
// src/config.ts
|
|
7412
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
7413
|
-
import { pathToFileURL } from "node:url";
|
|
7414
|
-
|
|
7415
|
-
// src/helpers/config-path.ts
|
|
7416
|
-
import path from "node:path";
|
|
7417
|
-
var CONFIG_DIR = ".config";
|
|
7418
|
-
var CONFIG_FILENAME = "nmr.config.ts";
|
|
7419
|
-
var CONFIG_RELATIVE_PATH = path.join(CONFIG_DIR, CONFIG_FILENAME);
|
|
7420
|
-
function resolveConfigPath(baseDir) {
|
|
7421
|
-
return path.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
|
|
7402
|
+
// .readyup/lib/repo-files.ts
|
|
7403
|
+
import { discoverWorkspaces, readFile } from "readyup/check-utils";
|
|
7404
|
+
var SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([".git", "coverage", "dist", "node_modules"]);
|
|
7405
|
+
var CONFIG_EXTENSIONS = "{ts,mts,cts,js,mjs,cjs}";
|
|
7406
|
+
function checkNoMatchingFiles(patterns, cwd) {
|
|
7407
|
+
const foundFiles = findFiles(patterns, cwd);
|
|
7408
|
+
if (foundFiles.length === 0) return true;
|
|
7409
|
+
return { ok: false, detail: formatPaths(foundFiles) };
|
|
7422
7410
|
}
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
[" ", String.raw`\t`]
|
|
7429
|
-
]);
|
|
7430
|
-
function escapeControlCharacters(text) {
|
|
7431
|
-
let escapedText = text;
|
|
7432
|
-
for (const [char, escape] of NAMED_ESCAPES) {
|
|
7433
|
-
escapedText = escapedText.split(char).join(escape);
|
|
7411
|
+
function discoverWorkspacesOrFailure(filter) {
|
|
7412
|
+
try {
|
|
7413
|
+
return { ok: true, workspaces: discoverWorkspaces(filter === void 0 ? void 0 : { filter }) };
|
|
7414
|
+
} catch (error) {
|
|
7415
|
+
return { ok: false, detail: `cannot enumerate workspaces: ${describeError(error)}` };
|
|
7434
7416
|
}
|
|
7435
|
-
|
|
7417
|
+
}
|
|
7418
|
+
function findFiles(patterns, cwd) {
|
|
7419
|
+
return globSync(patterns, { cwd, exclude: (path7) => SCAN_EXCLUDE_DIRS.has(basename(path7)) }).map((path7) => path7.split(sep).join("/")).toSorted();
|
|
7420
|
+
}
|
|
7421
|
+
function formatPaths(paths) {
|
|
7422
|
+
return `${paths.length} found:
|
|
7423
|
+
${paths.map((path7) => ` ${path7}`).join("\n")}`;
|
|
7424
|
+
}
|
|
7425
|
+
function importsSharedExport(content, exportName, moduleSpecifier) {
|
|
7426
|
+
if (content === void 0) return false;
|
|
7427
|
+
const pattern = new RegExp(String.raw`import\s*\{[^}]*\b${exportName}\b[^}]*\}\s*from\s*['"]${moduleSpecifier}['"]`);
|
|
7428
|
+
return pattern.test(content);
|
|
7429
|
+
}
|
|
7430
|
+
function readFileIn(cwd, relativePath) {
|
|
7431
|
+
return readFile(join(cwd, relativePath));
|
|
7436
7432
|
}
|
|
7437
7433
|
|
|
7438
|
-
//
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7434
|
+
// .readyup/lib/bin-targets.ts
|
|
7435
|
+
var BUILD_OUTPUT_DIR = "dist";
|
|
7436
|
+
var WRAPPER_TARGET_PATTERN = /['"](\.\.?\/[^'"]+)['"]/;
|
|
7437
|
+
var binTargetChecks = [
|
|
7438
|
+
{
|
|
7439
|
+
name: "every bin target is a committed wrapper",
|
|
7440
|
+
severity: "error",
|
|
7441
|
+
check: () => everyBinTargetIsACommittedWrapper(),
|
|
7442
|
+
fix: `Point each listed entry at a committed wrapper under bin/ that loads the build output at runtime. pnpm links a workspace package's bins during the install's link phase, which runs before anything is built, so a target that is not committed does not exist when pnpm tries to link it. pnpm never retries, which leaves the link missing for the life of the node_modules tree`
|
|
7443
|
+
},
|
|
7444
|
+
{
|
|
7445
|
+
name: "every bin wrapper's build-output target is covered by files",
|
|
7446
|
+
severity: "warn",
|
|
7447
|
+
check: () => everyBinWrapperTargetIsCoveredByFiles(),
|
|
7448
|
+
fix: "Add the build output directory to `files` in each listed package. npm and pnpm publish the bin target itself whatever `files` says, so the wrapper is published pointing at build output missing from the tarball"
|
|
7449
7449
|
}
|
|
7450
|
-
|
|
7450
|
+
];
|
|
7451
|
+
async function everyBinTargetIsACommittedWrapper() {
|
|
7452
|
+
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
7453
|
+
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
7454
|
+
const trackedFiles = await listTrackedFiles();
|
|
7455
|
+
const trackedPaths = trackedFiles === void 0 ? void 0 : new Set(trackedFiles);
|
|
7456
|
+
const offenders = workspaceDiscovery.workspaces.flatMap(
|
|
7457
|
+
(workspace) => readBinEntries(workspace).flatMap((entry) => {
|
|
7458
|
+
const defect = describeBinTargetDefect(workspace, entry, trackedPaths);
|
|
7459
|
+
return defect === void 0 ? [] : `${describeBinEntry(workspace, entry)} (${defect})`;
|
|
7460
|
+
})
|
|
7461
|
+
);
|
|
7462
|
+
if (offenders.length === 0) return true;
|
|
7463
|
+
return { ok: false, detail: formatPaths(offenders) };
|
|
7451
7464
|
}
|
|
7452
|
-
function
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
previousRow = completedRow;
|
|
7471
|
-
}
|
|
7472
|
-
return readCell(previousRow, second.length);
|
|
7465
|
+
function everyBinWrapperTargetIsCoveredByFiles() {
|
|
7466
|
+
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
7467
|
+
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
7468
|
+
const cwd = process.cwd();
|
|
7469
|
+
const offenders = workspaceDiscovery.workspaces.flatMap((workspace) => {
|
|
7470
|
+
const files = workspace.packageJson["files"];
|
|
7471
|
+
if (!Array.isArray(files)) return [];
|
|
7472
|
+
const publishedSegments = new Set(
|
|
7473
|
+
files.flatMap((entry) => typeof entry === "string" ? readFirstSegment(entry) : [])
|
|
7474
|
+
);
|
|
7475
|
+
return readBinEntries(workspace).flatMap((entry) => {
|
|
7476
|
+
const target = readWrapperTarget(cwd, workspace, entry);
|
|
7477
|
+
if (target === void 0 || publishedSegments.has(readFirstSegment(target))) return [];
|
|
7478
|
+
return `${describeBinEntry(workspace, entry)} -> ${target}`;
|
|
7479
|
+
});
|
|
7480
|
+
});
|
|
7481
|
+
if (offenders.length === 0) return true;
|
|
7482
|
+
return { ok: false, detail: formatPaths(offenders) };
|
|
7473
7483
|
}
|
|
7474
|
-
function
|
|
7475
|
-
|
|
7476
|
-
if (value === void 0) {
|
|
7477
|
-
throw new RangeError(`No cell at index ${index} in a row of ${row.length}`);
|
|
7478
|
-
}
|
|
7479
|
-
return value;
|
|
7484
|
+
function describeBinEntry(workspace, entry) {
|
|
7485
|
+
return `${workspace.name ?? workspace.dir}:${entry.command} -> ${entry.target}`;
|
|
7480
7486
|
}
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
return
|
|
7487
|
+
function describeBinTargetDefect(workspace, entry, trackedPaths) {
|
|
7488
|
+
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return `names a path under ${BUILD_OUTPUT_DIR}/`;
|
|
7489
|
+
if (trackedPaths === void 0) return void 0;
|
|
7490
|
+
return trackedPaths.has(`${workspace.dir}/${entry.target}`) ? void 0 : "untracked";
|
|
7485
7491
|
}
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
function isObject(value) {
|
|
7489
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7492
|
+
function normalizeBinTarget(target) {
|
|
7493
|
+
return target.replace(/^\.\//, "");
|
|
7490
7494
|
}
|
|
7491
|
-
function
|
|
7492
|
-
|
|
7495
|
+
function readBinEntries(workspace) {
|
|
7496
|
+
const bin = workspace.packageJson["bin"];
|
|
7497
|
+
if (typeof bin === "string") {
|
|
7498
|
+
const command = workspace.name?.split("/").at(-1) ?? basename2(workspace.dir);
|
|
7499
|
+
return [{ command, target: normalizeBinTarget(bin) }];
|
|
7500
|
+
}
|
|
7501
|
+
if (!isRecord(bin)) return [];
|
|
7502
|
+
return Object.entries(bin).flatMap(
|
|
7503
|
+
([command, target]) => typeof target === "string" ? { command, target: normalizeBinTarget(target) } : []
|
|
7504
|
+
);
|
|
7505
|
+
}
|
|
7506
|
+
function readFirstSegment(entry) {
|
|
7507
|
+
return normalizeBinTarget(entry).split("/", 1).at(0) ?? "";
|
|
7508
|
+
}
|
|
7509
|
+
function readWrapperTarget(cwd, workspace, entry) {
|
|
7510
|
+
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return void 0;
|
|
7511
|
+
const content = readFileIn(cwd, `${workspace.dir}/${entry.target}`);
|
|
7512
|
+
if (content === void 0) return void 0;
|
|
7513
|
+
const specifier = WRAPPER_TARGET_PATTERN.exec(content)?.[1];
|
|
7514
|
+
if (specifier === void 0) return void 0;
|
|
7515
|
+
return posix.normalize(posix.join(posix.dirname(entry.target), specifier));
|
|
7493
7516
|
}
|
|
7494
7517
|
|
|
7495
|
-
//
|
|
7518
|
+
// .readyup/lib/root-scripts.ts
|
|
7519
|
+
import { existsSync as existsSync2, readdirSync } from "node:fs";
|
|
7520
|
+
import { join as join2 } from "node:path";
|
|
7521
|
+
import { fileExists, isRecord as isRecord2, readFile as readFile2, readPackageJson } from "readyup/check-utils";
|
|
7522
|
+
|
|
7523
|
+
// src/shell-syntax.ts
|
|
7496
7524
|
var ENV_ASSIGNMENT = /^\w+=/;
|
|
7497
|
-
var LAUNCHERS = /* @__PURE__ */ new Map([
|
|
7498
|
-
["bun", /* @__PURE__ */ new Set(["x"])],
|
|
7499
|
-
["bunx", /* @__PURE__ */ new Set()],
|
|
7500
|
-
["npm", /* @__PURE__ */ new Set(["exec"])],
|
|
7501
|
-
["npx", /* @__PURE__ */ new Set()],
|
|
7502
|
-
["pnpm", /* @__PURE__ */ new Set(["dlx", "exec"])],
|
|
7503
|
-
["yarn", /* @__PURE__ */ new Set(["dlx", "exec"])]
|
|
7504
|
-
]);
|
|
7505
7525
|
var QUOTES = /* @__PURE__ */ new Set(['"', "'"]);
|
|
7506
7526
|
var SEGMENT_SEPARATORS = /* @__PURE__ */ new Set([";", "|", "&", "\n", "\r"]);
|
|
7507
7527
|
var SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
7508
7528
|
var TOKEN_SEPARATORS = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
7509
|
-
function
|
|
7510
|
-
|
|
7529
|
+
function dropLeadingAssignments(tokens) {
|
|
7530
|
+
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
7531
|
+
return start === -1 ? [] : tokens.slice(start);
|
|
7532
|
+
}
|
|
7533
|
+
function isShellSafeToken(token) {
|
|
7534
|
+
return SHELL_SAFE_TOKEN.test(token);
|
|
7511
7535
|
}
|
|
7512
7536
|
function quoteToken(token) {
|
|
7513
|
-
if (
|
|
7537
|
+
if (isShellSafeToken(token)) {
|
|
7514
7538
|
return token;
|
|
7515
7539
|
}
|
|
7516
7540
|
return "'" + token.replaceAll("'", String.raw`'\''`) + "'";
|
|
7517
7541
|
}
|
|
7518
|
-
function
|
|
7519
|
-
const
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7542
|
+
function splitSegments(command) {
|
|
7543
|
+
const segments = [];
|
|
7544
|
+
let currentSegment = "";
|
|
7545
|
+
let quote;
|
|
7546
|
+
let index = 0;
|
|
7547
|
+
while (index < command.length) {
|
|
7548
|
+
const char = command[index] ?? "";
|
|
7549
|
+
if (quote !== void 0) {
|
|
7550
|
+
if (char === "\\" && quote === '"') {
|
|
7551
|
+
currentSegment += char + (command[index + 1] ?? "");
|
|
7552
|
+
index += 2;
|
|
7553
|
+
continue;
|
|
7554
|
+
}
|
|
7555
|
+
currentSegment += char;
|
|
7556
|
+
if (char === quote) quote = void 0;
|
|
7557
|
+
index += 1;
|
|
7558
|
+
continue;
|
|
7526
7559
|
}
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7560
|
+
if (char === "\\") {
|
|
7561
|
+
currentSegment += char + (command[index + 1] ?? "");
|
|
7562
|
+
index += 2;
|
|
7563
|
+
continue;
|
|
7564
|
+
}
|
|
7565
|
+
if (QUOTES.has(char)) {
|
|
7566
|
+
quote = char;
|
|
7567
|
+
currentSegment += char;
|
|
7568
|
+
index += 1;
|
|
7569
|
+
continue;
|
|
7570
|
+
}
|
|
7571
|
+
if (SEGMENT_SEPARATORS.has(char)) {
|
|
7572
|
+
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
7573
|
+
currentSegment += char;
|
|
7574
|
+
index += 1;
|
|
7575
|
+
continue;
|
|
7576
|
+
}
|
|
7577
|
+
index += command[index + 1] === char ? 2 : 1;
|
|
7578
|
+
segments.push(currentSegment);
|
|
7579
|
+
currentSegment = "";
|
|
7580
|
+
continue;
|
|
7581
|
+
}
|
|
7582
|
+
currentSegment += char;
|
|
7583
|
+
index += 1;
|
|
7538
7584
|
}
|
|
7539
|
-
|
|
7585
|
+
segments.push(currentSegment);
|
|
7586
|
+
return segments;
|
|
7540
7587
|
}
|
|
7541
|
-
function
|
|
7542
|
-
|
|
7543
|
-
const doesReenter = elements.some((element) => {
|
|
7544
|
-
const target = readNmrTarget(tokenize(element));
|
|
7545
|
-
return target !== void 0 && isReentry(target, commandName, scope);
|
|
7546
|
-
});
|
|
7547
|
-
if (!doesReenter) {
|
|
7548
|
-
return void 0;
|
|
7549
|
-
}
|
|
7550
|
-
return elements.length === 1 ? "sole" : "chained";
|
|
7551
|
-
}
|
|
7552
|
-
function dropLeadingAssignments(tokens) {
|
|
7553
|
-
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
7554
|
-
return start === -1 ? [] : tokens.slice(start);
|
|
7555
|
-
}
|
|
7556
|
-
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
7557
|
-
if (char !== "&" && char !== "|") {
|
|
7558
|
-
return false;
|
|
7559
|
-
}
|
|
7560
|
-
return char === "&" && nextChar === ">" || /[<>]\s*$/.test(precedingText);
|
|
7561
|
-
}
|
|
7562
|
-
function isReentry(target, commandName, scope) {
|
|
7563
|
-
if (target.command !== commandName) {
|
|
7564
|
-
return false;
|
|
7565
|
-
}
|
|
7566
|
-
switch (scope) {
|
|
7567
|
-
case "root":
|
|
7568
|
-
return !target.isDelegate;
|
|
7569
|
-
case "package":
|
|
7570
|
-
return target.isRecursive || !target.isDelegate && !target.isWorkspaceRoot;
|
|
7571
|
-
case "workspaceRegistry":
|
|
7572
|
-
return target.isDelegate || !target.isWorkspaceRoot;
|
|
7573
|
-
}
|
|
7574
|
-
}
|
|
7575
|
-
function readLauncherScope(launcher, launcherArgs) {
|
|
7576
|
-
const scope = { isDelegate: false, isRecursive: false, isWorkspaceRoot: false };
|
|
7577
|
-
if (launcher !== "pnpm") {
|
|
7578
|
-
return scope;
|
|
7579
|
-
}
|
|
7580
|
-
for (const token of launcherArgs) {
|
|
7581
|
-
if (token === "-r" || token === "--recursive") {
|
|
7582
|
-
scope.isDelegate = true;
|
|
7583
|
-
scope.isRecursive = true;
|
|
7584
|
-
} else if (token === "-F" || token === "--filter" || token.startsWith("--filter=")) {
|
|
7585
|
-
scope.isDelegate = true;
|
|
7586
|
-
} else if (token === "-w" || token === "--workspace-root") {
|
|
7587
|
-
scope.isWorkspaceRoot = true;
|
|
7588
|
-
}
|
|
7589
|
-
}
|
|
7590
|
-
return scope;
|
|
7591
|
-
}
|
|
7592
|
-
function readNmrTail(segment) {
|
|
7593
|
-
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
7594
|
-
if (head === void 0) {
|
|
7595
|
-
return void 0;
|
|
7596
|
-
}
|
|
7597
|
-
if (head === "nmr") {
|
|
7598
|
-
return { launcher: void 0, launcherArgs: [], tail: rest };
|
|
7599
|
-
}
|
|
7600
|
-
const subcommands = LAUNCHERS.get(head);
|
|
7601
|
-
if (subcommands === void 0) {
|
|
7602
|
-
return void 0;
|
|
7603
|
-
}
|
|
7604
|
-
const nmrIndex = subcommands.size === 0 ? rest.findIndex((token) => !token.startsWith("-")) : rest.findIndex((token) => subcommands.has(token)) + 1;
|
|
7605
|
-
if (nmrIndex <= 0 && subcommands.size > 0) {
|
|
7606
|
-
return void 0;
|
|
7607
|
-
}
|
|
7608
|
-
return rest[nmrIndex] === "nmr" ? { launcher: head, launcherArgs: rest.slice(0, nmrIndex), tail: rest.slice(nmrIndex + 1) } : void 0;
|
|
7609
|
-
}
|
|
7610
|
-
function readNmrTarget(tokens) {
|
|
7611
|
-
let isDelegate = false;
|
|
7612
|
-
let isRecursive = false;
|
|
7613
|
-
let isWorkspaceRoot = false;
|
|
7614
|
-
let index = 0;
|
|
7615
|
-
while (index < tokens.length) {
|
|
7616
|
-
const token = tokens[index] ?? "";
|
|
7617
|
-
switch (token) {
|
|
7618
|
-
// The pattern is the flag's value, and reading it as a command name would name whichever package it
|
|
7619
|
-
// selects rather than the command that every selected scope runs.
|
|
7620
|
-
case "-F":
|
|
7621
|
-
case "--filter":
|
|
7622
|
-
isDelegate = true;
|
|
7623
|
-
index += 2;
|
|
7624
|
-
break;
|
|
7625
|
-
case "-R":
|
|
7626
|
-
case "--recursive":
|
|
7627
|
-
isDelegate = true;
|
|
7628
|
-
isRecursive = true;
|
|
7629
|
-
index += 1;
|
|
7630
|
-
break;
|
|
7631
|
-
case "-w":
|
|
7632
|
-
case "--workspace-root":
|
|
7633
|
-
isWorkspaceRoot = true;
|
|
7634
|
-
index += 1;
|
|
7635
|
-
break;
|
|
7636
|
-
default:
|
|
7637
|
-
if (!token.startsWith("-")) {
|
|
7638
|
-
return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
|
|
7639
|
-
}
|
|
7640
|
-
index += 1;
|
|
7641
|
-
}
|
|
7642
|
-
}
|
|
7643
|
-
return void 0;
|
|
7644
|
-
}
|
|
7645
|
-
function splitSegments(command) {
|
|
7646
|
-
const segments = [];
|
|
7647
|
-
let currentSegment = "";
|
|
7648
|
-
let quote;
|
|
7649
|
-
let index = 0;
|
|
7650
|
-
while (index < command.length) {
|
|
7651
|
-
const char = command[index] ?? "";
|
|
7652
|
-
if (quote !== void 0) {
|
|
7653
|
-
if (char === "\\" && quote === '"') {
|
|
7654
|
-
currentSegment += char + (command[index + 1] ?? "");
|
|
7655
|
-
index += 2;
|
|
7656
|
-
continue;
|
|
7657
|
-
}
|
|
7658
|
-
currentSegment += char;
|
|
7659
|
-
if (char === quote) quote = void 0;
|
|
7660
|
-
index += 1;
|
|
7661
|
-
continue;
|
|
7662
|
-
}
|
|
7663
|
-
if (char === "\\") {
|
|
7664
|
-
currentSegment += char + (command[index + 1] ?? "");
|
|
7665
|
-
index += 2;
|
|
7666
|
-
continue;
|
|
7667
|
-
}
|
|
7668
|
-
if (QUOTES.has(char)) {
|
|
7669
|
-
quote = char;
|
|
7670
|
-
currentSegment += char;
|
|
7671
|
-
index += 1;
|
|
7672
|
-
continue;
|
|
7673
|
-
}
|
|
7674
|
-
if (SEGMENT_SEPARATORS.has(char)) {
|
|
7675
|
-
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
7676
|
-
currentSegment += char;
|
|
7677
|
-
index += 1;
|
|
7678
|
-
continue;
|
|
7679
|
-
}
|
|
7680
|
-
index += command[index + 1] === char ? 2 : 1;
|
|
7681
|
-
segments.push(currentSegment);
|
|
7682
|
-
currentSegment = "";
|
|
7683
|
-
continue;
|
|
7684
|
-
}
|
|
7685
|
-
currentSegment += char;
|
|
7686
|
-
index += 1;
|
|
7687
|
-
}
|
|
7688
|
-
segments.push(currentSegment);
|
|
7689
|
-
return segments;
|
|
7690
|
-
}
|
|
7691
|
-
function tokenize(element) {
|
|
7692
|
-
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
7588
|
+
function tokenize(element) {
|
|
7589
|
+
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
7693
7590
|
}
|
|
7694
7591
|
function tokenizeSegment(segment) {
|
|
7695
7592
|
const tokens = [];
|
|
@@ -7736,6 +7633,12 @@ function tokenizeSegment(segment) {
|
|
|
7736
7633
|
}
|
|
7737
7634
|
return tokens;
|
|
7738
7635
|
}
|
|
7636
|
+
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
7637
|
+
if (char !== "&" && char !== "|") {
|
|
7638
|
+
return false;
|
|
7639
|
+
}
|
|
7640
|
+
return char === "&" && nextChar === ">" || /[<>]\s*$/.test(precedingText);
|
|
7641
|
+
}
|
|
7739
7642
|
|
|
7740
7643
|
// src/default-scripts.ts
|
|
7741
7644
|
var GATE_PROJECTS = "--project unit --project tool";
|
|
@@ -7821,12 +7724,12 @@ function buildRootScripts(workspaceGlobs) {
|
|
|
7821
7724
|
|
|
7822
7725
|
// ../nmr-core/dist/esm/workspace.js
|
|
7823
7726
|
import { existsSync, readFileSync } from "node:fs";
|
|
7824
|
-
import
|
|
7727
|
+
import path2 from "node:path";
|
|
7825
7728
|
var import_yaml = __toESM(require_dist(), 1);
|
|
7826
7729
|
|
|
7827
7730
|
// ../nmr-core/dist/esm/helpers/workspace-patterns.js
|
|
7828
|
-
import { globSync } from "node:fs";
|
|
7829
|
-
import
|
|
7731
|
+
import { globSync as globSync2 } from "node:fs";
|
|
7732
|
+
import path from "node:path";
|
|
7830
7733
|
var MANIFEST = "package.json";
|
|
7831
7734
|
var ALWAYS_EXCLUDED = ["**/node_modules/**"];
|
|
7832
7735
|
function matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns) {
|
|
@@ -7835,8 +7738,8 @@ function matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns) {
|
|
|
7835
7738
|
exclude: [...excludedPatterns, ...ALWAYS_EXCLUDED],
|
|
7836
7739
|
followSymlinks: true
|
|
7837
7740
|
};
|
|
7838
|
-
const matches =
|
|
7839
|
-
const dirs = matches.map((match) =>
|
|
7741
|
+
const matches = globSync2([...includedPatterns], options);
|
|
7742
|
+
const dirs = matches.map((match) => path.dirname(path.resolve(monorepoRoot, match)));
|
|
7840
7743
|
return [...new Set(dirs)].toSorted();
|
|
7841
7744
|
}
|
|
7842
7745
|
function resolvePackageDirs(monorepoRoot, patterns) {
|
|
@@ -7865,7 +7768,7 @@ function buildManifestPattern(pattern) {
|
|
|
7865
7768
|
// ../nmr-core/dist/esm/workspace.js
|
|
7866
7769
|
var WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
|
|
7867
7770
|
function isMonorepoRoot(dir) {
|
|
7868
|
-
return existsSync(
|
|
7771
|
+
return existsSync(path2.join(dir, WORKSPACE_MANIFEST));
|
|
7869
7772
|
}
|
|
7870
7773
|
function resolveWorkspace(monorepoRoot) {
|
|
7871
7774
|
const manifestRead = readWorkspaceManifest(monorepoRoot);
|
|
@@ -7898,7 +7801,7 @@ function diagnoseEmptyCause(monorepoRoot, patterns) {
|
|
|
7898
7801
|
return "no-package";
|
|
7899
7802
|
}
|
|
7900
7803
|
function readDeclaredPackages(parsedManifest) {
|
|
7901
|
-
if (!
|
|
7804
|
+
if (!isObject(parsedManifest))
|
|
7902
7805
|
return { kind: "absent" };
|
|
7903
7806
|
const packages = parsedManifest["packages"];
|
|
7904
7807
|
if (packages === void 0 || packages === null)
|
|
@@ -7909,11 +7812,11 @@ function readDeclaredPackages(parsedManifest) {
|
|
|
7909
7812
|
return { kind: "unreadable" };
|
|
7910
7813
|
return packages.length === 0 ? { kind: "absent" } : { kind: "patterns", patterns: packages };
|
|
7911
7814
|
}
|
|
7912
|
-
function
|
|
7815
|
+
function isObject(value) {
|
|
7913
7816
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7914
7817
|
}
|
|
7915
7818
|
function readWorkspaceManifest(monorepoRoot) {
|
|
7916
|
-
const workspaceFile =
|
|
7819
|
+
const workspaceFile = path2.join(monorepoRoot, WORKSPACE_MANIFEST);
|
|
7917
7820
|
if (!existsSync(workspaceFile)) {
|
|
7918
7821
|
return { kind: "absent" };
|
|
7919
7822
|
}
|
|
@@ -7943,9 +7846,9 @@ function getWorkspacePackageDirs(monorepoRoot) {
|
|
|
7943
7846
|
}
|
|
7944
7847
|
|
|
7945
7848
|
// src/workspace-globs.ts
|
|
7946
|
-
import
|
|
7849
|
+
import path3 from "node:path";
|
|
7947
7850
|
function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
|
|
7948
|
-
return packageDirs.map((dir) =>
|
|
7851
|
+
return packageDirs.map((dir) => path3.relative(monorepoRoot, dir).split(path3.sep).join("/")).filter((relativeDir) => relativeDir !== "").map((relativeDir) => `${relativeDir}/**`).toSorted();
|
|
7949
7852
|
}
|
|
7950
7853
|
|
|
7951
7854
|
// src/resolve-scripts.ts
|
|
@@ -7958,233 +7861,399 @@ function getDefaultRootScripts(monorepoRoot) {
|
|
|
7958
7861
|
return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
|
|
7959
7862
|
}
|
|
7960
7863
|
|
|
7961
|
-
//
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7864
|
+
// .readyup/lib/root-scripts.ts
|
|
7865
|
+
var INSTALL_LIFECYCLE_SCRIPTS = [
|
|
7866
|
+
"pnpm:devPreinstall",
|
|
7867
|
+
"preinstall",
|
|
7868
|
+
"install",
|
|
7869
|
+
"postinstall",
|
|
7870
|
+
"preprepare",
|
|
7871
|
+
"prepare",
|
|
7872
|
+
"postprepare"
|
|
7873
|
+
];
|
|
7874
|
+
var LEFTHOOK_CHECK_INSTALL_PATTERN = /\blefthook\s+check-install\b/;
|
|
7875
|
+
var LEFTHOOK_INSTALL_PATTERN = /\blefthook\s+install\b/;
|
|
7876
|
+
var rootScriptChecks = [
|
|
7877
|
+
{
|
|
7878
|
+
name: "root package.json has no nmr-provided scripts",
|
|
7879
|
+
severity: "warn",
|
|
7880
|
+
quiet: true,
|
|
7881
|
+
check: noRedundantRootScripts,
|
|
7882
|
+
fix: "Remove scripts from root package.json that nmr provides as built-in root scripts, and invoke them through nmr directly"
|
|
7976
7883
|
}
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7884
|
+
];
|
|
7885
|
+
var gitHookChecks = [
|
|
7886
|
+
{
|
|
7887
|
+
name: "no root install script runs lefthook install unguarded",
|
|
7888
|
+
severity: "warn",
|
|
7889
|
+
quiet: true,
|
|
7890
|
+
check: () => noUnguardedLefthookInstall(),
|
|
7891
|
+
fix: "Guard each listed script as `lefthook check-install || lefthook install`, which writes hooks only when they are missing or stale. A bare `lefthook install` rewrites `.git/hooks` on every install, so when that directory is not writable, as in an agent's sandbox, `pnpm install` fails and pnpm's `verifyDepsBeforeRun` then reinstalls before every `pnpm exec`"
|
|
7985
7892
|
}
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7893
|
+
];
|
|
7894
|
+
var legacyRunnerChecks = [
|
|
7895
|
+
{
|
|
7896
|
+
name: "scripts/run-workspace-script.ts does not exist",
|
|
7897
|
+
severity: "error",
|
|
7898
|
+
quiet: true,
|
|
7899
|
+
check: () => !fileExists("scripts/run-workspace-script.ts"),
|
|
7900
|
+
fix: "Delete scripts/run-workspace-script.ts, because nmr replaces this custom script runner"
|
|
7901
|
+
},
|
|
7902
|
+
{
|
|
7903
|
+
name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
|
|
7904
|
+
severity: "error",
|
|
7905
|
+
quiet: true,
|
|
7906
|
+
check: noWorkspaceRunScriptReferences,
|
|
7907
|
+
fix: 'Remove "ws" script entries and replace any "pnpm run ws" invocations with nmr in each packages/*/package.json'
|
|
7995
7908
|
}
|
|
7996
|
-
|
|
7997
|
-
|
|
7909
|
+
];
|
|
7910
|
+
function noUnguardedLefthookInstall(cwd = process.cwd()) {
|
|
7911
|
+
const scripts = readRootScripts(cwd);
|
|
7912
|
+
const unguardedScripts = INSTALL_LIFECYCLE_SCRIPTS.flatMap((name) => {
|
|
7913
|
+
const command = scripts[name];
|
|
7914
|
+
if (typeof command !== "string") return [];
|
|
7915
|
+
const isUnguarded = LEFTHOOK_INSTALL_PATTERN.test(command) && !LEFTHOOK_CHECK_INSTALL_PATTERN.test(command);
|
|
7916
|
+
return isUnguarded ? `${name}: ${command}` : [];
|
|
7917
|
+
});
|
|
7918
|
+
if (unguardedScripts.length === 0) return true;
|
|
7919
|
+
return { ok: false, detail: formatPaths(unguardedScripts) };
|
|
7998
7920
|
}
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
7921
|
+
function noRedundantRootScripts() {
|
|
7922
|
+
const pkg = readPackageJson();
|
|
7923
|
+
if (!pkg) return true;
|
|
7924
|
+
const scripts = pkg["scripts"];
|
|
7925
|
+
if (!isRecord2(scripts)) return true;
|
|
7926
|
+
const builtInNames = Object.keys(getDefaultRootScripts(process.cwd()));
|
|
7927
|
+
const redundantNames = Object.keys(scripts).filter((name) => builtInNames.includes(name));
|
|
7928
|
+
if (redundantNames.length === 0) return true;
|
|
7929
|
+
return {
|
|
7930
|
+
ok: false,
|
|
7931
|
+
detail: `redundant: ${redundantNames.join(", ")}`
|
|
7932
|
+
};
|
|
8003
7933
|
}
|
|
8004
|
-
function
|
|
8005
|
-
|
|
8006
|
-
|
|
7934
|
+
function noWorkspaceRunScriptReferences() {
|
|
7935
|
+
const packagesDir = join2(process.cwd(), "packages");
|
|
7936
|
+
if (!existsSync2(packagesDir)) return true;
|
|
7937
|
+
const legacyPattern = /run-workspace-script|"pnpm\s+run\s+ws\b/;
|
|
7938
|
+
const entries = readdirSync(packagesDir, { withFileTypes: true });
|
|
7939
|
+
const matches = [];
|
|
7940
|
+
for (const entry of entries) {
|
|
7941
|
+
if (!entry.isDirectory()) continue;
|
|
7942
|
+
const content = readFile2(`packages/${entry.name}/package.json`);
|
|
7943
|
+
if (content && legacyPattern.test(content)) {
|
|
7944
|
+
matches.push(entry.name);
|
|
7945
|
+
}
|
|
8007
7946
|
}
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
7947
|
+
if (matches.length === 0) return true;
|
|
7948
|
+
return {
|
|
7949
|
+
ok: false,
|
|
7950
|
+
detail: `found in: ${matches.join(", ")}`
|
|
7951
|
+
};
|
|
8013
7952
|
}
|
|
8014
|
-
function
|
|
8015
|
-
const
|
|
8016
|
-
|
|
7953
|
+
function readRootScripts(cwd) {
|
|
7954
|
+
const content = readFileIn(cwd, "package.json");
|
|
7955
|
+
if (content === void 0) return {};
|
|
7956
|
+
let parsedManifest;
|
|
8017
7957
|
try {
|
|
8018
|
-
|
|
8019
|
-
} catch
|
|
8020
|
-
|
|
8021
|
-
return void 0;
|
|
8022
|
-
}
|
|
8023
|
-
throw error;
|
|
7958
|
+
parsedManifest = JSON.parse(content);
|
|
7959
|
+
} catch {
|
|
7960
|
+
return {};
|
|
8024
7961
|
}
|
|
8025
|
-
|
|
8026
|
-
if (!isObject(parsedManifest)) return void 0;
|
|
7962
|
+
if (!isRecord2(parsedManifest)) return {};
|
|
8027
7963
|
const scripts = parsedManifest["scripts"];
|
|
8028
|
-
|
|
8029
|
-
return readScriptRecord(packageDir, scripts);
|
|
7964
|
+
return isRecord2(scripts) ? scripts : {};
|
|
8030
7965
|
}
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
7966
|
+
|
|
7967
|
+
// .readyup/lib/self-hosted-runner.ts
|
|
7968
|
+
import { isRecord as isRecord4 } from "readyup/check-utils";
|
|
7969
|
+
|
|
7970
|
+
// src/config.ts
|
|
7971
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
7972
|
+
import { pathToFileURL } from "node:url";
|
|
7973
|
+
|
|
7974
|
+
// src/helpers/findClosestName.ts
|
|
7975
|
+
function findClosestName(name, candidates) {
|
|
7976
|
+
const ceiling = Math.max(1, Math.floor(name.length / 4));
|
|
7977
|
+
let closestName;
|
|
7978
|
+
let shortestDistance = Infinity;
|
|
7979
|
+
for (const candidate of candidates) {
|
|
7980
|
+
const distance = measureDistance(name, candidate);
|
|
7981
|
+
if (distance >= shortestDistance) {
|
|
7982
|
+
continue;
|
|
7983
|
+
}
|
|
7984
|
+
shortestDistance = distance;
|
|
7985
|
+
closestName = candidate;
|
|
7986
|
+
}
|
|
7987
|
+
return shortestDistance <= ceiling ? closestName : void 0;
|
|
8036
7988
|
}
|
|
8037
|
-
function
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
};
|
|
7989
|
+
function measureDistance(first, second) {
|
|
7990
|
+
if (first.length === 0 || second.length === 0) {
|
|
7991
|
+
return Math.max(first.length, second.length);
|
|
7992
|
+
}
|
|
7993
|
+
let previousRow = Array.from({ length: second.length + 1 }, (_unused, column) => column);
|
|
7994
|
+
let currentRow = Array.from({ length: second.length + 1 }, () => 0);
|
|
7995
|
+
for (let row = 1; row <= first.length; row += 1) {
|
|
7996
|
+
currentRow[0] = row;
|
|
7997
|
+
for (let column = 1; column <= second.length; column += 1) {
|
|
7998
|
+
const substitution = readCell(previousRow, column - 1) + (first[row - 1] === second[column - 1] ? 0 : 1);
|
|
7999
|
+
currentRow[column] = Math.min(
|
|
8000
|
+
readCell(currentRow, column - 1) + 1,
|
|
8001
|
+
readCell(previousRow, column) + 1,
|
|
8002
|
+
substitution
|
|
8003
|
+
);
|
|
8004
|
+
}
|
|
8005
|
+
const completedRow = currentRow;
|
|
8006
|
+
currentRow = previousRow;
|
|
8007
|
+
previousRow = completedRow;
|
|
8008
|
+
}
|
|
8009
|
+
return readCell(previousRow, second.length);
|
|
8010
|
+
}
|
|
8011
|
+
function readCell(row, index) {
|
|
8012
|
+
const value = row[index];
|
|
8013
|
+
if (value === void 0) {
|
|
8014
|
+
throw new RangeError(`No cell at index ${index} in a row of ${row.length}`);
|
|
8015
|
+
}
|
|
8016
|
+
return value;
|
|
8042
8017
|
}
|
|
8043
8018
|
|
|
8044
|
-
// src/
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
return `${source} is \`${value}\`, which is not one of: ${COMMAND_VERBOSITIES.join(", ")}`;
|
|
8019
|
+
// src/helpers/hook-name.ts
|
|
8020
|
+
function isHookName(key) {
|
|
8021
|
+
return key.length > 4 && key.endsWith(":pre") || key.length > 5 && key.endsWith(":post");
|
|
8048
8022
|
}
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8023
|
+
|
|
8024
|
+
// src/resolver.ts
|
|
8025
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8026
|
+
|
|
8027
|
+
// ../nmr-core/dist/esm/hasErrnoCode.js
|
|
8028
|
+
function hasErrnoCode(error, code) {
|
|
8029
|
+
return isError(error) && "code" in error && error.code === code;
|
|
8052
8030
|
}
|
|
8053
8031
|
|
|
8054
|
-
// src/
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
elsewhere: "the monorepo-root config"
|
|
8065
|
-
}
|
|
8066
|
-
};
|
|
8067
|
-
var RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
8068
|
-
var RECOGNIZED_BUILD_KEYS = ["extraIgnorePatterns"];
|
|
8069
|
-
var RECOGNIZED_CHECK_CACHE_KEYS = ["enabled", "excludeCommands", "extraCommands"];
|
|
8070
|
-
var RECOGNIZED_OUTPUT_KEYS = ["commandVerbosity", "extraAgentEnvVars"];
|
|
8071
|
-
var RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ["exempt"];
|
|
8072
|
-
var RECOGNIZED_STEP_KEYS = ["run", "shouldDeclineArguments", "shouldRunInBackground"];
|
|
8073
|
-
var SCRIPT_FIELDS = [
|
|
8074
|
-
{ field: "rootScripts", scope: "root" },
|
|
8075
|
-
{ field: "workspaceScripts", scope: "workspaceRegistry" }
|
|
8076
|
-
];
|
|
8077
|
-
var RETIRED_STEP_KEYS = /* @__PURE__ */ new Map([["declinesArgs", "shouldDeclineArguments"]]);
|
|
8078
|
-
function isScriptRecord(value) {
|
|
8079
|
-
if (!isObject(value)) return false;
|
|
8080
|
-
for (const v of Object.values(value)) {
|
|
8081
|
-
if (typeof v === "string") continue;
|
|
8082
|
-
if (!Array.isArray(v) || !v.every(isScriptElement)) return false;
|
|
8083
|
-
}
|
|
8084
|
-
return true;
|
|
8032
|
+
// src/helpers/package-json.ts
|
|
8033
|
+
import path5 from "node:path";
|
|
8034
|
+
|
|
8035
|
+
// src/helpers/config-path.ts
|
|
8036
|
+
import path4 from "node:path";
|
|
8037
|
+
var CONFIG_DIR = ".config";
|
|
8038
|
+
var CONFIG_FILENAME = "nmr.config.ts";
|
|
8039
|
+
var CONFIG_RELATIVE_PATH = path4.join(CONFIG_DIR, CONFIG_FILENAME);
|
|
8040
|
+
function resolveConfigPath(baseDir) {
|
|
8041
|
+
return path4.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
|
|
8085
8042
|
}
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
return
|
|
8043
|
+
|
|
8044
|
+
// src/helpers/type-guards.ts
|
|
8045
|
+
function isObject2(value) {
|
|
8046
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8090
8047
|
}
|
|
8091
|
-
function
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8048
|
+
function isStringRecord(value) {
|
|
8049
|
+
return isObject2(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
// src/helpers/package-json.ts
|
|
8053
|
+
function parsePackageJson(rawText, file) {
|
|
8054
|
+
try {
|
|
8055
|
+
return JSON.parse(rawText);
|
|
8056
|
+
} catch (error) {
|
|
8057
|
+
throw new UserError(`Invalid package.json at ${file}: ${describeError(error)}`);
|
|
8100
8058
|
}
|
|
8101
|
-
assertValidElements(scripts, fieldName, configPath);
|
|
8102
|
-
return scripts;
|
|
8103
8059
|
}
|
|
8104
|
-
function
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8110
|
-
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8111
|
-
}
|
|
8112
|
-
const element = typeof spec === "string" ? spec : spec.run;
|
|
8113
|
-
const token = findUnexpressibleToken(element);
|
|
8114
|
-
if (token === void 0) continue;
|
|
8115
|
-
throw new UserError(
|
|
8116
|
-
`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains \`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and reference that name here.`
|
|
8117
|
-
);
|
|
8060
|
+
function readScriptRecord(dir, scripts) {
|
|
8061
|
+
const result = {};
|
|
8062
|
+
for (const [key, value] of Object.entries(scripts)) {
|
|
8063
|
+
if (typeof value !== "string") {
|
|
8064
|
+
throw new UserError(formatMalformedScript(dir, key, value));
|
|
8118
8065
|
}
|
|
8066
|
+
result[key] = value;
|
|
8119
8067
|
}
|
|
8068
|
+
return result;
|
|
8120
8069
|
}
|
|
8121
|
-
function
|
|
8122
|
-
return
|
|
8070
|
+
function resolvePackageJsonPath(dir) {
|
|
8071
|
+
return path5.join(dir, "package.json");
|
|
8123
8072
|
}
|
|
8124
|
-
function
|
|
8125
|
-
|
|
8073
|
+
function formatMalformedScript(dir, key, value) {
|
|
8074
|
+
const rejection = `Invalid package.json at ${resolvePackageJsonPath(dir)}: \`scripts.${key}\` must be a string.`;
|
|
8075
|
+
if (!Array.isArray(value)) {
|
|
8076
|
+
return rejection;
|
|
8077
|
+
}
|
|
8078
|
+
const field = isMonorepoRoot(dir) ? "rootScripts" : "workspaceScripts";
|
|
8079
|
+
return `${rejection} A step list belongs in \`${CONFIG_RELATIVE_PATH}\` under \`${field}\`.`;
|
|
8126
8080
|
}
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8081
|
+
|
|
8082
|
+
// src/steps.ts
|
|
8083
|
+
var LAUNCHERS = /* @__PURE__ */ new Map([
|
|
8084
|
+
["bun", /* @__PURE__ */ new Set(["x"])],
|
|
8085
|
+
["bunx", /* @__PURE__ */ new Set()],
|
|
8086
|
+
["npm", /* @__PURE__ */ new Set(["exec"])],
|
|
8087
|
+
["npx", /* @__PURE__ */ new Set()],
|
|
8088
|
+
["pnpm", /* @__PURE__ */ new Set(["dlx", "exec"])],
|
|
8089
|
+
["yarn", /* @__PURE__ */ new Set(["dlx", "exec"])]
|
|
8090
|
+
]);
|
|
8091
|
+
function findUnexpressibleToken(element) {
|
|
8092
|
+
return tokenize(element).find((token) => !isShellSafeToken(token));
|
|
8093
|
+
}
|
|
8094
|
+
function readSelfReference(options) {
|
|
8095
|
+
const { commandName, scope, script } = options;
|
|
8096
|
+
const segments = splitSegments(script).filter((segment) => segment.trim() !== "");
|
|
8097
|
+
const doesReenter = segments.some((segment) => {
|
|
8098
|
+
const invocation = readNmrTail(segment);
|
|
8099
|
+
const target = invocation === void 0 ? void 0 : readNmrTarget(invocation.tail);
|
|
8100
|
+
if (invocation === void 0 || target === void 0) {
|
|
8101
|
+
return false;
|
|
8102
|
+
}
|
|
8103
|
+
const launcherScope = readLauncherScope(invocation.launcher, invocation.launcherArgs);
|
|
8104
|
+
const effectiveTarget = {
|
|
8105
|
+
command: target.command,
|
|
8106
|
+
isDelegate: target.isDelegate || launcherScope.isDelegate,
|
|
8107
|
+
isRecursive: target.isRecursive || launcherScope.isRecursive,
|
|
8108
|
+
isWorkspaceRoot: target.isWorkspaceRoot || launcherScope.isWorkspaceRoot
|
|
8109
|
+
};
|
|
8110
|
+
return isReentry(effectiveTarget, commandName, scope);
|
|
8111
|
+
});
|
|
8112
|
+
if (!doesReenter) {
|
|
8130
8113
|
return void 0;
|
|
8131
8114
|
}
|
|
8132
|
-
|
|
8133
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
8134
|
-
}
|
|
8135
|
-
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, "build.");
|
|
8136
|
-
const extraIgnorePatterns = build["extraIgnorePatterns"];
|
|
8137
|
-
if (extraIgnorePatterns !== void 0 && !isStringArray(extraIgnorePatterns)) {
|
|
8138
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
8139
|
-
}
|
|
8140
|
-
return extraIgnorePatterns === void 0 ? {} : { extraIgnorePatterns };
|
|
8115
|
+
return segments.length === 1 ? "sole" : "chained";
|
|
8141
8116
|
}
|
|
8142
|
-
function
|
|
8143
|
-
const
|
|
8144
|
-
|
|
8117
|
+
function readStepListSelfReference(options) {
|
|
8118
|
+
const { commandName, elements, scope } = options;
|
|
8119
|
+
const doesReenter = elements.some((element) => {
|
|
8120
|
+
const target = readNmrTarget(tokenize(element));
|
|
8121
|
+
return target !== void 0 && isReentry(target, commandName, scope);
|
|
8122
|
+
});
|
|
8123
|
+
if (!doesReenter) {
|
|
8145
8124
|
return void 0;
|
|
8146
8125
|
}
|
|
8147
|
-
|
|
8148
|
-
|
|
8126
|
+
return elements.length === 1 ? "sole" : "chained";
|
|
8127
|
+
}
|
|
8128
|
+
function isReentry(target, commandName, scope) {
|
|
8129
|
+
if (target.command !== commandName) {
|
|
8130
|
+
return false;
|
|
8149
8131
|
}
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
config.enabled = enabledValue;
|
|
8132
|
+
switch (scope) {
|
|
8133
|
+
case "root":
|
|
8134
|
+
return !target.isDelegate;
|
|
8135
|
+
case "package":
|
|
8136
|
+
return target.isRecursive || !target.isDelegate && !target.isWorkspaceRoot;
|
|
8137
|
+
case "workspaceRegistry":
|
|
8138
|
+
return target.isDelegate || !target.isWorkspaceRoot;
|
|
8158
8139
|
}
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8140
|
+
}
|
|
8141
|
+
function readLauncherScope(launcher, launcherArgs) {
|
|
8142
|
+
const scope = { isDelegate: false, isRecursive: false, isWorkspaceRoot: false };
|
|
8143
|
+
if (launcher !== "pnpm") {
|
|
8144
|
+
return scope;
|
|
8145
|
+
}
|
|
8146
|
+
for (const token of launcherArgs) {
|
|
8147
|
+
if (token === "-r" || token === "--recursive") {
|
|
8148
|
+
scope.isDelegate = true;
|
|
8149
|
+
scope.isRecursive = true;
|
|
8150
|
+
} else if (token === "-F" || token === "--filter" || token.startsWith("--filter=")) {
|
|
8151
|
+
scope.isDelegate = true;
|
|
8152
|
+
} else if (token === "-w" || token === "--workspace-root") {
|
|
8153
|
+
scope.isWorkspaceRoot = true;
|
|
8166
8154
|
}
|
|
8167
|
-
config[field] = commands;
|
|
8168
8155
|
}
|
|
8169
|
-
return
|
|
8156
|
+
return scope;
|
|
8170
8157
|
}
|
|
8171
|
-
function
|
|
8172
|
-
const
|
|
8173
|
-
if (
|
|
8158
|
+
function readNmrTail(segment) {
|
|
8159
|
+
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
8160
|
+
if (head === void 0) {
|
|
8174
8161
|
return void 0;
|
|
8175
8162
|
}
|
|
8176
|
-
if (
|
|
8177
|
-
|
|
8163
|
+
if (head === "nmr") {
|
|
8164
|
+
return { launcher: void 0, launcherArgs: [], tail: rest };
|
|
8178
8165
|
}
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
throw new UserError(
|
|
8183
|
-
`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`
|
|
8184
|
-
);
|
|
8166
|
+
const subcommands = LAUNCHERS.get(head);
|
|
8167
|
+
if (subcommands === void 0) {
|
|
8168
|
+
return void 0;
|
|
8185
8169
|
}
|
|
8186
|
-
|
|
8170
|
+
const nmrIndex = subcommands.size === 0 ? rest.findIndex((token) => !token.startsWith("-")) : rest.findIndex((token) => subcommands.has(token)) + 1;
|
|
8171
|
+
if (nmrIndex <= 0 && subcommands.size > 0) {
|
|
8172
|
+
return void 0;
|
|
8173
|
+
}
|
|
8174
|
+
return rest[nmrIndex] === "nmr" ? { launcher: head, launcherArgs: rest.slice(0, nmrIndex), tail: rest.slice(nmrIndex + 1) } : void 0;
|
|
8175
|
+
}
|
|
8176
|
+
function readNmrTarget(tokens) {
|
|
8177
|
+
let isDelegate = false;
|
|
8178
|
+
let isRecursive = false;
|
|
8179
|
+
let isWorkspaceRoot = false;
|
|
8180
|
+
let index = 0;
|
|
8181
|
+
while (index < tokens.length) {
|
|
8182
|
+
const token = tokens[index] ?? "";
|
|
8183
|
+
switch (token) {
|
|
8184
|
+
// The pattern is the flag's value, and reading it as a command name would name whichever package it
|
|
8185
|
+
// selects rather than the command that every selected scope runs.
|
|
8186
|
+
case "-F":
|
|
8187
|
+
case "--filter":
|
|
8188
|
+
isDelegate = true;
|
|
8189
|
+
index += 2;
|
|
8190
|
+
break;
|
|
8191
|
+
case "-R":
|
|
8192
|
+
case "--recursive":
|
|
8193
|
+
isDelegate = true;
|
|
8194
|
+
isRecursive = true;
|
|
8195
|
+
index += 1;
|
|
8196
|
+
break;
|
|
8197
|
+
case "-w":
|
|
8198
|
+
case "--workspace-root":
|
|
8199
|
+
isWorkspaceRoot = true;
|
|
8200
|
+
index += 1;
|
|
8201
|
+
break;
|
|
8202
|
+
default:
|
|
8203
|
+
if (!token.startsWith("-")) {
|
|
8204
|
+
return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
|
|
8205
|
+
}
|
|
8206
|
+
index += 1;
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
return void 0;
|
|
8210
|
+
}
|
|
8211
|
+
|
|
8212
|
+
// src/resolver.ts
|
|
8213
|
+
function describeScript(script) {
|
|
8214
|
+
return typeof script === "string" ? script : `[${script.map(describeElement).join(", ")}]`;
|
|
8215
|
+
}
|
|
8216
|
+
function describeElement(element) {
|
|
8217
|
+
if (typeof element === "string") {
|
|
8218
|
+
return element;
|
|
8219
|
+
}
|
|
8220
|
+
const notes = [
|
|
8221
|
+
...element.shouldRunInBackground === true ? ["background"] : [],
|
|
8222
|
+
...element.shouldDeclineArguments === true ? ["no args"] : []
|
|
8223
|
+
];
|
|
8224
|
+
return notes.length === 0 ? element.run : `${element.run} (${notes.join(", ")})`;
|
|
8187
8225
|
}
|
|
8226
|
+
function readPackageJsonScripts(packageDir) {
|
|
8227
|
+
const file = resolvePackageJsonPath(packageDir);
|
|
8228
|
+
let rawText;
|
|
8229
|
+
try {
|
|
8230
|
+
rawText = readFileSync2(file, "utf8");
|
|
8231
|
+
} catch (error) {
|
|
8232
|
+
if (hasErrnoCode(error, "ENOENT")) {
|
|
8233
|
+
return void 0;
|
|
8234
|
+
}
|
|
8235
|
+
throw error;
|
|
8236
|
+
}
|
|
8237
|
+
const parsedManifest = parsePackageJson(rawText, file);
|
|
8238
|
+
if (!isObject2(parsedManifest)) return void 0;
|
|
8239
|
+
const scripts = parsedManifest["scripts"];
|
|
8240
|
+
if (!isObject2(scripts)) return void 0;
|
|
8241
|
+
return readScriptRecord(packageDir, scripts);
|
|
8242
|
+
}
|
|
8243
|
+
function buildWorkspaceRegistry(config) {
|
|
8244
|
+
return {
|
|
8245
|
+
...getDefaultWorkspaceScripts(),
|
|
8246
|
+
...config.workspaceScripts
|
|
8247
|
+
};
|
|
8248
|
+
}
|
|
8249
|
+
function buildRootRegistry(config, monorepoRoot) {
|
|
8250
|
+
return {
|
|
8251
|
+
...getDefaultRootScripts(monorepoRoot),
|
|
8252
|
+
...config.rootScripts
|
|
8253
|
+
};
|
|
8254
|
+
}
|
|
8255
|
+
|
|
8256
|
+
// src/config-check-cache.ts
|
|
8188
8257
|
function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
8189
8258
|
const { checkCache } = config;
|
|
8190
8259
|
if (checkCache === void 0 || !isMonorepoRoot(baseDir)) {
|
|
@@ -8243,46 +8312,48 @@ function readPackageScripts(packageDir) {
|
|
|
8243
8312
|
throw error;
|
|
8244
8313
|
}
|
|
8245
8314
|
}
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
}
|
|
8251
|
-
if (!isObject(output)) {
|
|
8252
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
8253
|
-
}
|
|
8254
|
-
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, "output.");
|
|
8255
|
-
const config = {};
|
|
8256
|
-
const commandVerbosity = output["commandVerbosity"];
|
|
8257
|
-
if (commandVerbosity !== void 0) {
|
|
8258
|
-
if (typeof commandVerbosity !== "string" || !isCommandVerbosity(commandVerbosity)) {
|
|
8259
|
-
const renderedValue = typeof commandVerbosity === "string" ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
8260
|
-
throw new UserError(
|
|
8261
|
-
`Invalid nmr config at ${configPath}: ${formatVerbosityRejection("`output.commandVerbosity`", renderedValue)}`
|
|
8262
|
-
);
|
|
8263
|
-
}
|
|
8264
|
-
config.commandVerbosity = commandVerbosity;
|
|
8265
|
-
}
|
|
8266
|
-
const extraAgentEnvVars = output["extraAgentEnvVars"];
|
|
8267
|
-
if (extraAgentEnvVars !== void 0) {
|
|
8268
|
-
if (!isStringArray(extraAgentEnvVars)) {
|
|
8269
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
8270
|
-
}
|
|
8271
|
-
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
8272
|
-
}
|
|
8273
|
-
return config;
|
|
8315
|
+
|
|
8316
|
+
// src/verbosity.ts
|
|
8317
|
+
var COMMAND_VERBOSITIES = ["full", "quiet"];
|
|
8318
|
+
function formatVerbosityRejection(source, value) {
|
|
8319
|
+
return `${source} is \`${value}\`, which is not one of: ${COMMAND_VERBOSITIES.join(", ")}`;
|
|
8274
8320
|
}
|
|
8275
|
-
function
|
|
8276
|
-
|
|
8277
|
-
|
|
8321
|
+
function isCommandVerbosity(value) {
|
|
8322
|
+
const names = COMMAND_VERBOSITIES;
|
|
8323
|
+
return names.includes(value);
|
|
8324
|
+
}
|
|
8325
|
+
|
|
8326
|
+
// src/config-validation.ts
|
|
8327
|
+
var CONFIG_TIERS = {
|
|
8328
|
+
root: {
|
|
8329
|
+
label: "a monorepo-root config",
|
|
8330
|
+
honoredKeys: ["checkCache", "delegate", "devBin", "output", "rootScripts", "selfHostedRunner", "workspaceScripts"],
|
|
8331
|
+
elsewhere: "the package's own config"
|
|
8332
|
+
},
|
|
8333
|
+
workspace: {
|
|
8334
|
+
label: "a package config",
|
|
8335
|
+
honoredKeys: ["build"],
|
|
8336
|
+
elsewhere: "the monorepo-root config"
|
|
8278
8337
|
}
|
|
8279
|
-
|
|
8280
|
-
|
|
8338
|
+
};
|
|
8339
|
+
var RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
8340
|
+
var RECOGNIZED_BUILD_KEYS = ["extraIgnorePatterns"];
|
|
8341
|
+
var RECOGNIZED_CHECK_CACHE_KEYS = ["enabled", "excludeCommands", "extraCommands"];
|
|
8342
|
+
var RECOGNIZED_OUTPUT_KEYS = ["commandVerbosity", "extraAgentEnvVars"];
|
|
8343
|
+
var RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ["exempt"];
|
|
8344
|
+
var RECOGNIZED_STEP_KEYS = ["run", "shouldDeclineArguments", "shouldRunInBackground"];
|
|
8345
|
+
var RETIRED_STEP_KEYS = /* @__PURE__ */ new Map([["declinesArgs", "shouldDeclineArguments"]]);
|
|
8346
|
+
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = "") {
|
|
8347
|
+
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
8348
|
+
if (unrecognizedKeys.length === 0) {
|
|
8349
|
+
return;
|
|
8281
8350
|
}
|
|
8282
|
-
|
|
8351
|
+
throw new UserError(
|
|
8352
|
+
`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? "key" : "keys"} ${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`
|
|
8353
|
+
);
|
|
8283
8354
|
}
|
|
8284
8355
|
function validateConfig(value, configPath, baseDir) {
|
|
8285
|
-
if (!
|
|
8356
|
+
if (!isObject2(value)) {
|
|
8286
8357
|
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
8287
8358
|
}
|
|
8288
8359
|
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
@@ -8291,6 +8362,7 @@ function validateConfig(value, configPath, baseDir) {
|
|
|
8291
8362
|
if (build) config.build = build;
|
|
8292
8363
|
const checkCache = validateCheckCacheField(value, configPath);
|
|
8293
8364
|
if (checkCache) config.checkCache = checkCache;
|
|
8365
|
+
if (value["delegate"] !== void 0) config.delegate = validateDelegateField(value["delegate"], configPath);
|
|
8294
8366
|
const devBin = validateStringRecordField(value, "devBin", configPath);
|
|
8295
8367
|
if (devBin) config.devBin = devBin;
|
|
8296
8368
|
const output = validateOutputField(value, configPath);
|
|
@@ -8304,22 +8376,6 @@ function validateConfig(value, configPath, baseDir) {
|
|
|
8304
8376
|
assertResolvableCheckCacheCommands(config, configPath, baseDir);
|
|
8305
8377
|
return config;
|
|
8306
8378
|
}
|
|
8307
|
-
async function loadConfig(baseDir) {
|
|
8308
|
-
const configPath = resolveConfigPath(baseDir);
|
|
8309
|
-
if (!existsSync2(configPath)) {
|
|
8310
|
-
return {};
|
|
8311
|
-
}
|
|
8312
|
-
const importedModule = await import(pathToFileURL(configPath).href);
|
|
8313
|
-
const loadedConfig = isObject(importedModule) ? importedModule["default"] : void 0;
|
|
8314
|
-
return validateConfig(loadedConfig, configPath, baseDir);
|
|
8315
|
-
}
|
|
8316
|
-
async function loadRootConfig(monorepoRoot) {
|
|
8317
|
-
const config = await loadConfig(monorepoRoot);
|
|
8318
|
-
const configPath = resolveConfigPath(monorepoRoot);
|
|
8319
|
-
assertTierKeys(config, CONFIG_TIERS.root, configPath);
|
|
8320
|
-
assertNoSelfReferences(config, configPath, monorepoRoot);
|
|
8321
|
-
return config;
|
|
8322
|
-
}
|
|
8323
8379
|
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
8324
8380
|
for (const key of Object.keys(value)) {
|
|
8325
8381
|
const replacement = retiredKeys.get(key);
|
|
@@ -8329,59 +8385,242 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
|
8329
8385
|
);
|
|
8330
8386
|
}
|
|
8331
8387
|
}
|
|
8332
|
-
function
|
|
8333
|
-
for (const
|
|
8334
|
-
|
|
8335
|
-
for (const
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
const remedy = formatSelfReferenceRemedy({
|
|
8344
|
-
isDefault: !isHookName(key) && Object.hasOwn(defaults, key),
|
|
8345
|
-
key,
|
|
8346
|
-
selfReference
|
|
8347
|
-
});
|
|
8388
|
+
function assertValidElements(scripts, fieldName, configPath) {
|
|
8389
|
+
for (const [command, script] of Object.entries(scripts)) {
|
|
8390
|
+
if (typeof script === "string") continue;
|
|
8391
|
+
for (const spec of script) {
|
|
8392
|
+
if (typeof spec !== "string") {
|
|
8393
|
+
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8394
|
+
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8395
|
+
}
|
|
8396
|
+
const element = typeof spec === "string" ? spec : spec.run;
|
|
8397
|
+
const token = findUnexpressibleToken(element);
|
|
8398
|
+
if (token === void 0) continue;
|
|
8348
8399
|
throw new UserError(
|
|
8349
|
-
`Invalid nmr config at ${configPath}: \`${
|
|
8400
|
+
`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains \`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and reference that name here.`
|
|
8350
8401
|
);
|
|
8351
8402
|
}
|
|
8352
8403
|
}
|
|
8353
8404
|
}
|
|
8354
|
-
function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = "") {
|
|
8355
|
-
const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
|
|
8356
|
-
if (unrecognizedKeys.length === 0) {
|
|
8357
|
-
return;
|
|
8358
|
-
}
|
|
8359
|
-
throw new UserError(
|
|
8360
|
-
`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? "key" : "keys"} ${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`
|
|
8361
|
-
);
|
|
8362
|
-
}
|
|
8363
|
-
function assertTierKeys(config, tier, configPath) {
|
|
8364
|
-
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
8365
|
-
if (unsupportedKeys.length === 0) {
|
|
8366
|
-
return;
|
|
8367
|
-
}
|
|
8368
|
-
throw new UserError(
|
|
8369
|
-
`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(", ")} alone, not ${unsupportedKeys.toSorted().join(", ")}. Move those keys to ${tier.elsewhere}.`
|
|
8370
|
-
);
|
|
8371
|
-
}
|
|
8372
8405
|
function formatKeyList(keys, prefix) {
|
|
8373
8406
|
return keys.toSorted().map((key) => `\`${prefix}${key}\``).join(", ");
|
|
8374
8407
|
}
|
|
8375
|
-
function
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8408
|
+
function isOptionalBoolean(value) {
|
|
8409
|
+
return value === void 0 || typeof value === "boolean";
|
|
8410
|
+
}
|
|
8411
|
+
function isScriptRecord(value) {
|
|
8412
|
+
if (!isObject2(value)) return false;
|
|
8413
|
+
for (const v of Object.values(value)) {
|
|
8414
|
+
if (typeof v === "string") continue;
|
|
8415
|
+
if (!Array.isArray(v) || !v.every(isScriptElement)) return false;
|
|
8416
|
+
}
|
|
8417
|
+
return true;
|
|
8418
|
+
}
|
|
8419
|
+
function isScriptElement(value) {
|
|
8420
|
+
if (typeof value === "string") return true;
|
|
8421
|
+
if (!isObject2(value) || typeof value["run"] !== "string") return false;
|
|
8422
|
+
return isOptionalBoolean(value["shouldDeclineArguments"]) && isOptionalBoolean(value["shouldRunInBackground"]);
|
|
8423
|
+
}
|
|
8424
|
+
function isStringArray(value) {
|
|
8425
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
8426
|
+
}
|
|
8427
|
+
function validateBuildField(value, configPath) {
|
|
8428
|
+
const build = value["build"];
|
|
8429
|
+
if (build === void 0) {
|
|
8430
|
+
return void 0;
|
|
8431
|
+
}
|
|
8432
|
+
if (!isObject2(build)) {
|
|
8433
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
8434
|
+
}
|
|
8435
|
+
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, "build.");
|
|
8436
|
+
const extraIgnorePatterns = build["extraIgnorePatterns"];
|
|
8437
|
+
if (extraIgnorePatterns !== void 0 && !isStringArray(extraIgnorePatterns)) {
|
|
8438
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
8439
|
+
}
|
|
8440
|
+
return extraIgnorePatterns === void 0 ? {} : { extraIgnorePatterns };
|
|
8441
|
+
}
|
|
8442
|
+
function validateCheckCacheField(value, configPath) {
|
|
8443
|
+
const checkCache = value["checkCache"];
|
|
8444
|
+
if (checkCache === void 0) {
|
|
8445
|
+
return void 0;
|
|
8446
|
+
}
|
|
8447
|
+
if (!isObject2(checkCache)) {
|
|
8448
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
8449
|
+
}
|
|
8450
|
+
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, "checkCache.");
|
|
8451
|
+
const config = {};
|
|
8452
|
+
const enabledValue = checkCache["enabled"];
|
|
8453
|
+
if (enabledValue !== void 0) {
|
|
8454
|
+
if (typeof enabledValue !== "boolean") {
|
|
8455
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
8456
|
+
}
|
|
8457
|
+
config.enabled = enabledValue;
|
|
8458
|
+
}
|
|
8459
|
+
for (const field of ["excludeCommands", "extraCommands"]) {
|
|
8460
|
+
const commands = checkCache[field];
|
|
8461
|
+
if (commands === void 0) {
|
|
8462
|
+
continue;
|
|
8463
|
+
}
|
|
8464
|
+
if (!isStringArray(commands)) {
|
|
8465
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
|
|
8466
|
+
}
|
|
8467
|
+
config[field] = commands;
|
|
8468
|
+
}
|
|
8469
|
+
return config;
|
|
8470
|
+
}
|
|
8471
|
+
function validateDelegateField(delegate, configPath) {
|
|
8472
|
+
if (delegate === false) {
|
|
8473
|
+
return false;
|
|
8474
|
+
}
|
|
8475
|
+
throw new UserError(
|
|
8476
|
+
`Invalid nmr config at ${configPath}: \`delegate\` accepts only \`false\`, which keeps the repo's checks on the machine that has the tree. Hosts and \`when\` belong in the user config, \`~/.config/nmr/config.ts\`.`
|
|
8477
|
+
);
|
|
8478
|
+
}
|
|
8479
|
+
function validateOutputField(value, configPath) {
|
|
8480
|
+
const output = value["output"];
|
|
8481
|
+
if (output === void 0) {
|
|
8482
|
+
return void 0;
|
|
8483
|
+
}
|
|
8484
|
+
if (!isObject2(output)) {
|
|
8485
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
8486
|
+
}
|
|
8487
|
+
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, "output.");
|
|
8488
|
+
const config = {};
|
|
8489
|
+
const commandVerbosity = output["commandVerbosity"];
|
|
8490
|
+
if (commandVerbosity !== void 0) {
|
|
8491
|
+
if (typeof commandVerbosity !== "string" || !isCommandVerbosity(commandVerbosity)) {
|
|
8492
|
+
const renderedValue = typeof commandVerbosity === "string" ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
8493
|
+
throw new UserError(
|
|
8494
|
+
`Invalid nmr config at ${configPath}: ${formatVerbosityRejection("`output.commandVerbosity`", renderedValue)}`
|
|
8495
|
+
);
|
|
8496
|
+
}
|
|
8497
|
+
config.commandVerbosity = commandVerbosity;
|
|
8498
|
+
}
|
|
8499
|
+
const extraAgentEnvVars = output["extraAgentEnvVars"];
|
|
8500
|
+
if (extraAgentEnvVars !== void 0) {
|
|
8501
|
+
if (!isStringArray(extraAgentEnvVars)) {
|
|
8502
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
8503
|
+
}
|
|
8504
|
+
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
8505
|
+
}
|
|
8506
|
+
return config;
|
|
8507
|
+
}
|
|
8508
|
+
function validateSelfHostedRunnerField(value, configPath) {
|
|
8509
|
+
const selfHostedRunner = value["selfHostedRunner"];
|
|
8510
|
+
if (selfHostedRunner === void 0) {
|
|
8511
|
+
return void 0;
|
|
8512
|
+
}
|
|
8513
|
+
if (!isObject2(selfHostedRunner)) {
|
|
8514
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
|
|
8515
|
+
}
|
|
8516
|
+
assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, "selfHostedRunner.");
|
|
8517
|
+
const exempt = selfHostedRunner["exempt"];
|
|
8518
|
+
if (typeof exempt !== "string" || exempt.trim() === "") {
|
|
8519
|
+
throw new UserError(
|
|
8520
|
+
`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`
|
|
8521
|
+
);
|
|
8522
|
+
}
|
|
8523
|
+
return { exempt };
|
|
8524
|
+
}
|
|
8525
|
+
function validateScriptField(value, fieldName, configPath) {
|
|
8526
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === void 0) {
|
|
8527
|
+
return void 0;
|
|
8528
|
+
}
|
|
8529
|
+
const scripts = value[fieldName];
|
|
8530
|
+
if (!isScriptRecord(scripts)) {
|
|
8531
|
+
throw new UserError(
|
|
8532
|
+
`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, where an element is a command string or \`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }\``
|
|
8533
|
+
);
|
|
8534
|
+
}
|
|
8535
|
+
assertValidElements(scripts, fieldName, configPath);
|
|
8536
|
+
return scripts;
|
|
8537
|
+
}
|
|
8538
|
+
function validateStringRecordField(value, fieldName, configPath) {
|
|
8539
|
+
if (!Object.hasOwn(value, fieldName) || value[fieldName] === void 0) {
|
|
8540
|
+
return void 0;
|
|
8541
|
+
}
|
|
8542
|
+
if (!isStringRecord(value[fieldName])) {
|
|
8543
|
+
throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
|
|
8544
|
+
}
|
|
8545
|
+
return value[fieldName];
|
|
8546
|
+
}
|
|
8547
|
+
|
|
8548
|
+
// src/helpers/escapeControlCharacters.ts
|
|
8549
|
+
var NAMED_ESCAPES = /* @__PURE__ */ new Map([
|
|
8550
|
+
["\n", String.raw`\n`],
|
|
8551
|
+
["\r", String.raw`\r`],
|
|
8552
|
+
[" ", String.raw`\t`]
|
|
8553
|
+
]);
|
|
8554
|
+
function escapeControlCharacters(text) {
|
|
8555
|
+
let escapedText = text;
|
|
8556
|
+
for (const [char, escape] of NAMED_ESCAPES) {
|
|
8557
|
+
escapedText = escapedText.split(char).join(escape);
|
|
8558
|
+
}
|
|
8559
|
+
return escapedText;
|
|
8560
|
+
}
|
|
8561
|
+
|
|
8562
|
+
// src/config.ts
|
|
8563
|
+
var SCRIPT_FIELDS = [
|
|
8564
|
+
{ field: "rootScripts", scope: "root" },
|
|
8565
|
+
{ field: "workspaceScripts", scope: "workspaceRegistry" }
|
|
8566
|
+
];
|
|
8567
|
+
async function loadConfig(baseDir) {
|
|
8568
|
+
const configPath = resolveConfigPath(baseDir);
|
|
8569
|
+
if (!existsSync3(configPath)) {
|
|
8570
|
+
return {};
|
|
8571
|
+
}
|
|
8572
|
+
const importedModule = await import(pathToFileURL(configPath).href);
|
|
8573
|
+
const loadedConfig = isObject2(importedModule) ? importedModule["default"] : void 0;
|
|
8574
|
+
return validateConfig(loadedConfig, configPath, baseDir);
|
|
8575
|
+
}
|
|
8576
|
+
async function loadRootConfig(monorepoRoot) {
|
|
8577
|
+
const config = await loadConfig(monorepoRoot);
|
|
8578
|
+
const configPath = resolveConfigPath(monorepoRoot);
|
|
8579
|
+
assertTierKeys(config, CONFIG_TIERS.root, configPath);
|
|
8580
|
+
assertNoSelfReferences(config, configPath, monorepoRoot);
|
|
8581
|
+
return config;
|
|
8582
|
+
}
|
|
8583
|
+
function assertNoSelfReferences(config, configPath, monorepoRoot) {
|
|
8584
|
+
for (const { field, scope } of SCRIPT_FIELDS) {
|
|
8585
|
+
const scripts = config[field] ?? {};
|
|
8586
|
+
for (const [key, script] of Object.entries(scripts)) {
|
|
8587
|
+
const selfReference = typeof script === "string" ? readSelfReference({ commandName: key, scope, script }) : readStepListSelfReference({
|
|
8588
|
+
commandName: key,
|
|
8589
|
+
elements: script.map((element) => typeof element === "string" ? element : element.run),
|
|
8590
|
+
scope
|
|
8591
|
+
});
|
|
8592
|
+
if (selfReference === void 0) continue;
|
|
8593
|
+
const defaults = field === "rootScripts" ? getDefaultRootScripts(monorepoRoot) : getDefaultWorkspaceScripts();
|
|
8594
|
+
const remedy = formatSelfReferenceRemedy({
|
|
8595
|
+
isDefault: !isHookName(key) && Object.hasOwn(defaults, key),
|
|
8596
|
+
key,
|
|
8597
|
+
selfReference
|
|
8598
|
+
});
|
|
8599
|
+
throw new UserError(
|
|
8600
|
+
`Invalid nmr config at ${configPath}: \`${field}.${key}\` re-invokes \`${key}\` (\`${escapeControlCharacters(describeScript(script))}\`), which would re-enter nmr without bound. ${remedy}`
|
|
8601
|
+
);
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
function assertTierKeys(config, tier, configPath) {
|
|
8606
|
+
const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
|
|
8607
|
+
if (unsupportedKeys.length === 0) {
|
|
8608
|
+
return;
|
|
8609
|
+
}
|
|
8610
|
+
throw new UserError(
|
|
8611
|
+
`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(", ")} alone, not ${unsupportedKeys.toSorted().join(", ")}. Move those keys to ${tier.elsewhere}.`
|
|
8612
|
+
);
|
|
8613
|
+
}
|
|
8614
|
+
function formatSelfReferenceRemedy(options) {
|
|
8615
|
+
const { isDefault, key, selfReference } = options;
|
|
8616
|
+
if (isDefault) {
|
|
8617
|
+
return selfReference === "sole" ? `Delete the entry: nmr's own \`${key}\` then runs.` : `Delete the entry and move the steps that it adds to a \`${key}:pre\` or \`${key}:post\` script.`;
|
|
8379
8618
|
}
|
|
8380
8619
|
return selfReference === "sole" ? "Delete the entry." : `Delete the re-invocation: \`${key}\` runs the steps beside it.`;
|
|
8381
8620
|
}
|
|
8382
8621
|
|
|
8383
8622
|
// src/tiers.ts
|
|
8384
|
-
import { readdirSync } from "node:fs";
|
|
8623
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
8385
8624
|
import path6 from "node:path";
|
|
8386
8625
|
|
|
8387
8626
|
// src/git-ignored-paths.ts
|
|
@@ -8418,7 +8657,7 @@ function hasTierInfix(filePath) {
|
|
|
8418
8657
|
var TEST_COLLECTION_EXCLUDE = [".git", "coverage", "dist", "node_modules"];
|
|
8419
8658
|
var TIER_NAMES = ["unit", "tool", "localhost", "remote"];
|
|
8420
8659
|
function collectTestFiles(dir, relativeDir, isInTestDir, context) {
|
|
8421
|
-
const entries =
|
|
8660
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
8422
8661
|
for (const entry of entries) {
|
|
8423
8662
|
const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
|
|
8424
8663
|
if (entry.isDirectory()) {
|
|
@@ -8440,600 +8679,373 @@ function walkTestFiles(rootDir, { excludedBasenames = [] }, misplaced) {
|
|
|
8440
8679
|
return context.foundPaths.toSorted();
|
|
8441
8680
|
}
|
|
8442
8681
|
|
|
8443
|
-
// .readyup/
|
|
8444
|
-
var
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
{
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
{
|
|
8511
|
-
name: ".config/nmr.config.ts uses defineConfig",
|
|
8512
|
-
severity: "recommend",
|
|
8513
|
-
skip: () => !fileExists(".config/nmr.config.ts") ? "no nmr config file" : false,
|
|
8514
|
-
check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
|
|
8515
|
-
fix: "Wrap the config export with defineConfig() from @williamthorsen/nmr/config for type safety"
|
|
8516
|
-
},
|
|
8517
|
-
// `error` because falling short produces wrong results rather than a failure. Names and fixes are
|
|
8518
|
-
// getters because their version constants are declared below the kit.
|
|
8519
|
-
{
|
|
8520
|
-
get name() {
|
|
8521
|
-
return `eslint >= ${MIN_ESLINT_VERSION}`;
|
|
8522
|
-
},
|
|
8523
|
-
severity: "error",
|
|
8524
|
-
skip: () => !hasDevDependency("eslint") ? "eslint not installed" : false,
|
|
8525
|
-
check: hasSupportedEslintVersion,
|
|
8526
|
-
get fix() {
|
|
8527
|
-
return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION}. Earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
|
|
8528
|
-
}
|
|
8529
|
-
},
|
|
8530
|
-
{
|
|
8531
|
-
get name() {
|
|
8532
|
-
return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
|
|
8533
|
-
},
|
|
8534
|
-
severity: "error",
|
|
8535
|
-
skip: () => !hasDevDependency("@williamthorsen/strict-lint") ? "strict-lint not installed" : false,
|
|
8536
|
-
check: hasSupportedStrictLintVersion,
|
|
8537
|
-
get fix() {
|
|
8538
|
-
return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION}. Earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
|
|
8539
|
-
}
|
|
8540
|
-
},
|
|
8541
|
-
// -- Root script cleanup -------------------------------------------------
|
|
8542
|
-
{
|
|
8543
|
-
name: "root package.json has no nmr-provided scripts",
|
|
8544
|
-
severity: "warn",
|
|
8545
|
-
quiet: true,
|
|
8546
|
-
check: noRedundantRootScripts,
|
|
8547
|
-
fix: "Remove scripts from root package.json that nmr provides as built-in root scripts, and invoke them through nmr directly"
|
|
8548
|
-
},
|
|
8549
|
-
// -- Git hooks -----------------------------------------------------------
|
|
8550
|
-
{
|
|
8551
|
-
name: "no root install script runs lefthook install unguarded",
|
|
8552
|
-
severity: "warn",
|
|
8553
|
-
quiet: true,
|
|
8554
|
-
check: () => noUnguardedLefthookInstall(),
|
|
8555
|
-
fix: "Guard each listed script as `lefthook check-install || lefthook install`, which writes hooks only when they are missing or stale. A bare `lefthook install` rewrites `.git/hooks` on every install, so when that directory is not writable, as in an agent's sandbox, `pnpm install` fails and pnpm's `verifyDepsBeforeRun` then reinstalls before every `pnpm exec`"
|
|
8556
|
-
},
|
|
8557
|
-
// -- Workspace build readiness -------------------------------------------
|
|
8558
|
-
{
|
|
8559
|
-
name: "all workspace packages can build",
|
|
8560
|
-
severity: "warn",
|
|
8561
|
-
check: allWorkspacePackagesCanBuild,
|
|
8562
|
-
fix: `Add "build": ":" to packages that don't need a build, or ensure packages that use the default nmr build have a tsconfig.json and a src/ directory`
|
|
8563
|
-
},
|
|
8564
|
-
// -- Bin targets ---------------------------------------------------------
|
|
8565
|
-
{
|
|
8566
|
-
name: "every bin target is a committed wrapper",
|
|
8567
|
-
severity: "error",
|
|
8568
|
-
check: () => everyBinTargetIsACommittedWrapper(),
|
|
8569
|
-
fix: `Point each listed entry at a committed wrapper under bin/ that loads the build output at runtime. pnpm links a workspace package's bins during the install's link phase, which runs before anything is built, so a target that is not committed does not exist when pnpm tries to link it. pnpm never retries, which leaves the link missing for the life of the node_modules tree`
|
|
8570
|
-
},
|
|
8571
|
-
{
|
|
8572
|
-
name: "every bin wrapper's build-output target is covered by files",
|
|
8573
|
-
severity: "warn",
|
|
8574
|
-
check: () => everyBinWrapperTargetIsCoveredByFiles(),
|
|
8575
|
-
fix: "Add the build output directory to `files` in each listed package. npm and pnpm publish the bin target itself whatever `files` says, so the wrapper is published pointing at build output missing from the tarball"
|
|
8576
|
-
},
|
|
8577
|
-
// -- Vitest projects -----------------------------------------------------
|
|
8578
|
-
{
|
|
8579
|
-
name: "no retired Vitest config variants",
|
|
8580
|
-
severity: "error",
|
|
8581
|
-
quiet: true,
|
|
8582
|
-
check: () => noRetiredVitestConfigs(),
|
|
8583
|
-
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
8584
|
-
},
|
|
8585
|
-
{
|
|
8586
|
-
name: "every vitest.config builds on @williamthorsen/nmr/vitest",
|
|
8587
|
-
severity: "error",
|
|
8588
|
-
check: () => vitestConfigBuildsOnSharedConfig(),
|
|
8589
|
-
fix: "Replace each listed config with: import { defineVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineVitestConfig(); -- a config that does not call the factory does not declare any projects, so every tier-selecting test command fails against it. Pass the repo's own settings to the factory as layers to keep them"
|
|
8590
|
-
},
|
|
8591
|
-
{
|
|
8592
|
-
name: "vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
|
|
8593
|
-
severity: "error",
|
|
8594
|
-
check: () => vitestRootConfigBuildsOnSharedConfig(),
|
|
8595
|
-
fix: "Replace vitest.root.config.ts with: import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });"
|
|
8596
|
-
},
|
|
8597
|
-
{
|
|
8598
|
-
name: "every workspace with a Vite config has a Vitest config",
|
|
8599
|
-
severity: "error",
|
|
8600
|
-
check: () => everyViteConfigHasVitestConfig(),
|
|
8601
|
-
fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory containing either name, so the Vite config otherwise wins and the projects model is never reached"
|
|
8602
|
-
},
|
|
8603
|
-
{
|
|
8604
|
-
name: "the test suite gates the test-file conventions",
|
|
8605
|
-
severity: "warn",
|
|
8606
|
-
check: () => testSuiteGatesTestFileConventions(),
|
|
8607
|
-
fix: "Add a test of the repo's own under a __tests__ directory that declares the check: import { checkTestFileConventions } from '@williamthorsen/nmr/tests'; checkTestFileConventions(); -- passing `excludedBasenames` the directory names that the repo passes to `testCollectionExclude`. Without it, the repo's test run does not report an untiered or misplaced test file, and this kit reports them in its place against only nmr's built-in exclusions and the paths that git ignores"
|
|
8608
|
-
},
|
|
8609
|
-
{
|
|
8610
|
-
name: "every test file names its isolation tier",
|
|
8611
|
-
severity: "error",
|
|
8612
|
-
skip: () => describeConventionsGuardSkip(),
|
|
8613
|
-
check: () => everyTestFileNamesItsTier(),
|
|
8614
|
-
fix: `Rename each to <subject>[.<aspect>].<tier>.test.ts, naming one of ${TIER_NAMES.join(", ")}. Use tool for a test that reaches a program supplied by the environment, which is the tier for a retired .int. or .integration. file. Only the segment before .test. selects a project, so an untiered file runs under the residual unit project and reports success`
|
|
8615
|
-
},
|
|
8616
|
-
{
|
|
8617
|
-
name: "every test file is under a __tests__ directory",
|
|
8618
|
-
severity: "error",
|
|
8619
|
-
skip: () => describeConventionsGuardSkip(),
|
|
8620
|
-
check: () => everyTestFileSitsUnderTestsDir(),
|
|
8621
|
-
fix: "Move each into a __tests__ directory, the only place from which the shared Vitest config collects. Vitest does not collect a file outside one, so it runs nowhere and reports nothing. For a file that is not a test, declare checkTestFileConventions with its directory in `excludedBasenames` instead, which this check then defers to"
|
|
8622
|
-
},
|
|
8623
|
-
{
|
|
8624
|
-
name: "no package re-exports the ancestor Vitest config",
|
|
8625
|
-
severity: "recommend",
|
|
8626
|
-
quiet: true,
|
|
8627
|
-
check: () => noReExportOnlyVitestConfigs(),
|
|
8628
|
-
fix: "Delete these files. Vitest resolves config by walking up from the run root, so a per-package re-export is redundant"
|
|
8629
|
-
},
|
|
8630
|
-
// -- Shared Prettier config ----------------------------------------------
|
|
8631
|
-
{
|
|
8632
|
-
name: "Prettier config builds on @williamthorsen/nmr/prettier",
|
|
8633
|
-
severity: "error",
|
|
8634
|
-
check: () => prettierConfigBuildsOnSharedConfig(),
|
|
8635
|
-
fix: "Replace the Prettier config with: import { definePrettierConfig } from '@williamthorsen/nmr/prettier'; export default definePrettierConfig();"
|
|
8636
|
-
},
|
|
8637
|
-
// -- Shared upgrade policy -----------------------------------------------
|
|
8638
|
-
{
|
|
8639
|
-
name: "taze.config.ts builds on @williamthorsen/nmr/taze",
|
|
8640
|
-
severity: "warn",
|
|
8641
|
-
check: () => tazeConfigBuildsOnSharedConfig(),
|
|
8642
|
-
fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); -- nmr's upgrade policy applies to a repo only through this file, so without it `nmr upgrade` does not report a dependency pinned to an exact version"
|
|
8643
|
-
},
|
|
8644
|
-
{
|
|
8645
|
-
name: "taze config declares no option taze discards",
|
|
8646
|
-
severity: "warn",
|
|
8647
|
-
quiet: true,
|
|
8648
|
-
check: () => tazeConfigAvoidsClobberedOptions(),
|
|
8649
|
-
fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script. taze's CLI writes a default for each of them over whatever the config file declares, so taze never uses the file's value (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
|
|
8650
|
-
},
|
|
8651
|
-
// -- Audit dependency --------------------------------------------------------
|
|
8652
|
-
{
|
|
8653
|
-
name: "v11y-check in devDependencies",
|
|
8654
|
-
severity: "warn",
|
|
8655
|
-
check: () => hasDevDependency("v11y-check"),
|
|
8656
|
-
fix: "pnpm add --save-dev v11y-check"
|
|
8657
|
-
},
|
|
8658
|
-
// -- Legacy script runner ------------------------------------------------
|
|
8659
|
-
{
|
|
8660
|
-
name: "scripts/run-workspace-script.ts does not exist",
|
|
8661
|
-
severity: "error",
|
|
8662
|
-
quiet: true,
|
|
8663
|
-
check: () => !fileExists("scripts/run-workspace-script.ts"),
|
|
8664
|
-
fix: "Delete scripts/run-workspace-script.ts, because nmr replaces this custom script runner"
|
|
8665
|
-
},
|
|
8666
|
-
{
|
|
8667
|
-
name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
|
|
8668
|
-
severity: "error",
|
|
8669
|
-
quiet: true,
|
|
8670
|
-
check: noWorkspaceRunScriptReferences,
|
|
8671
|
-
fix: 'Remove "ws" script entries and replace any "pnpm run ws" invocations with nmr in each packages/*/package.json'
|
|
8682
|
+
// .readyup/lib/github.ts
|
|
8683
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
8684
|
+
import { execFile } from "node:child_process";
|
|
8685
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
8686
|
+
import { join as join3, posix as posix2 } from "node:path";
|
|
8687
|
+
import { isRecord as isRecord3, readFile as readFile3 } from "readyup/check-utils";
|
|
8688
|
+
var CODE_QUALITY_WORKFLOW = "williamthorsen/.github/.github/workflows/code-quality-pnpm-workflow.yaml";
|
|
8689
|
+
var MIN_CODE_QUALITY_MAJOR = 8;
|
|
8690
|
+
var GH_TIMEOUT_MS = 15e3;
|
|
8691
|
+
var IMAGE_ABSENT_PROGRAM_PATTERN = /(?:^|[\s;&|(])(corepack|gh|node|npm|npx|pnpm)(?=\s|$)/gm;
|
|
8692
|
+
var NEVER_INSTALLED_PROGRAMS = /* @__PURE__ */ new Set(["corepack", "gh"]);
|
|
8693
|
+
function countOnlineSelfHostedRunners(response) {
|
|
8694
|
+
if (!isRecord3(response) || !Array.isArray(response["runners"])) return 0;
|
|
8695
|
+
return response["runners"].filter(
|
|
8696
|
+
(runner) => isRecord3(runner) && runner["status"] === "online" && Array.isArray(runner["labels"]) && runner["labels"].some((label) => isRecord3(label) && label["name"] === "self-hosted")
|
|
8697
|
+
).length;
|
|
8698
|
+
}
|
|
8699
|
+
function describeGhFailure(result, context) {
|
|
8700
|
+
if (result.isMissing) return "gh is not installed";
|
|
8701
|
+
if (/gh auth login|not logged in/i.test(result.stderr)) return "gh is not authenticated";
|
|
8702
|
+
const firstLine = result.stderr.trim().split("\n", 1)[0] ?? "";
|
|
8703
|
+
return firstLine === "" ? `gh ${context}` : `gh ${context}: ${firstLine}`;
|
|
8704
|
+
}
|
|
8705
|
+
function findCodeQualityCallerDefects(workflows) {
|
|
8706
|
+
const defects = [];
|
|
8707
|
+
for (const { file, jobs } of workflows) {
|
|
8708
|
+
for (const [jobName, job] of jobs) {
|
|
8709
|
+
const uses = job["uses"];
|
|
8710
|
+
if (typeof uses !== "string" || !uses.startsWith(`${CODE_QUALITY_WORKFLOW}@`)) continue;
|
|
8711
|
+
const ref = uses.slice(CODE_QUALITY_WORKFLOW.length + 1);
|
|
8712
|
+
const major = /^v(\d+)(?:\.\d+){0,2}$/.exec(ref)?.[1];
|
|
8713
|
+
if (major === void 0) {
|
|
8714
|
+
defects.push(
|
|
8715
|
+
`${file} job ${jobName} pins the code-quality workflow at ${ref}, whose release cannot be determined`
|
|
8716
|
+
);
|
|
8717
|
+
} else if (Number(major) < MIN_CODE_QUALITY_MAJOR) {
|
|
8718
|
+
defects.push(
|
|
8719
|
+
`${file} job ${jobName} pins the code-quality workflow at ${ref}, which predates CI_RUNS_ON; pin v${MIN_CODE_QUALITY_MAJOR} or later`
|
|
8720
|
+
);
|
|
8721
|
+
}
|
|
8722
|
+
const inputs = job["with"];
|
|
8723
|
+
if (isRecord3(inputs) && inputs["runs-on"] !== void 0) {
|
|
8724
|
+
defects.push(`${file} job ${jobName} passes runs-on, which overrides CI_RUNS_ON`);
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
return defects;
|
|
8729
|
+
}
|
|
8730
|
+
function findImageIncompatibleSteps(workflows) {
|
|
8731
|
+
const defects = [];
|
|
8732
|
+
for (const { file, jobs } of workflows) {
|
|
8733
|
+
for (const [jobName, job] of jobs) {
|
|
8734
|
+
if (!/\bvars\.CI_RUNS_ON\b/.test(JSON.stringify(job["runs-on"] ?? ""))) continue;
|
|
8735
|
+
const steps = job["steps"];
|
|
8736
|
+
if (!Array.isArray(steps)) continue;
|
|
8737
|
+
const installed = /* @__PURE__ */ new Set();
|
|
8738
|
+
for (const [index, step] of steps.entries()) {
|
|
8739
|
+
if (!isRecord3(step)) continue;
|
|
8740
|
+
const stepLabel = typeof step["name"] === "string" ? step["name"] : `step ${index + 1}`;
|
|
8741
|
+
const run = step["run"];
|
|
8742
|
+
if (typeof run === "string") {
|
|
8743
|
+
const missing = findUninstalledPrograms(run, installed);
|
|
8744
|
+
if (missing.length > 0) {
|
|
8745
|
+
defects.push(
|
|
8746
|
+
`${file} job ${jobName}, ${stepLabel}: runs ${missing.join(", ")}, which the runner image lacks at that step`
|
|
8747
|
+
);
|
|
8748
|
+
}
|
|
8672
8749
|
}
|
|
8673
|
-
|
|
8674
|
-
|
|
8750
|
+
for (const program of readInstalledPrograms(step)) installed.add(program);
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
return defects;
|
|
8755
|
+
}
|
|
8756
|
+
function isNotFound(result) {
|
|
8757
|
+
return /HTTP 404/.test(result.stderr);
|
|
8758
|
+
}
|
|
8759
|
+
function parseJson(text) {
|
|
8760
|
+
try {
|
|
8761
|
+
return JSON.parse(text);
|
|
8762
|
+
} catch {
|
|
8763
|
+
return void 0;
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
async function readOrganizationValue(nameWithOwner, cwd, runGh) {
|
|
8767
|
+
const result = await runGh(["api", `repos/${nameWithOwner}/actions/organization-variables`], cwd);
|
|
8768
|
+
if (!result.ok) return void 0;
|
|
8769
|
+
const response = parseJson(result.stdout);
|
|
8770
|
+
if (!isRecord3(response) || !Array.isArray(response["variables"])) return void 0;
|
|
8771
|
+
const variable = response["variables"].find(
|
|
8772
|
+
(entry) => isRecord3(entry) && entry["name"] === "CI_RUNS_ON"
|
|
8773
|
+
);
|
|
8774
|
+
return readVariableValue(variable);
|
|
8775
|
+
}
|
|
8776
|
+
function readVariableValue(variable) {
|
|
8777
|
+
return isRecord3(variable) && typeof variable["value"] === "string" ? variable["value"] : void 0;
|
|
8778
|
+
}
|
|
8779
|
+
function readWorkflows(cwd) {
|
|
8780
|
+
const dir = join3(cwd, ".github", "workflows");
|
|
8781
|
+
if (!existsSync4(dir)) return [];
|
|
8782
|
+
const workflows = [];
|
|
8783
|
+
const files = readdirSync3(dir).filter((name) => /\.ya?ml$/.test(name)).toSorted();
|
|
8784
|
+
for (const file of files) {
|
|
8785
|
+
let document;
|
|
8786
|
+
try {
|
|
8787
|
+
document = (0, import_yaml2.parse)(readFile3(join3(dir, file)) ?? "");
|
|
8788
|
+
} catch {
|
|
8789
|
+
continue;
|
|
8790
|
+
}
|
|
8791
|
+
if (!isRecord3(document) || !isRecord3(document["jobs"])) continue;
|
|
8792
|
+
const jobs = Object.entries(document["jobs"]).filter(
|
|
8793
|
+
(entry) => isRecord3(entry[1])
|
|
8794
|
+
);
|
|
8795
|
+
workflows.push({ file: posix2.join(".github/workflows", file), jobs });
|
|
8796
|
+
}
|
|
8797
|
+
return workflows;
|
|
8798
|
+
}
|
|
8799
|
+
function runGhCommand(args, cwd) {
|
|
8800
|
+
return new Promise((resolve) => {
|
|
8801
|
+
void execFile("gh", args, { cwd, timeout: GH_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
8802
|
+
if (error === null) {
|
|
8803
|
+
resolve({ ok: true, stdout });
|
|
8804
|
+
return;
|
|
8805
|
+
}
|
|
8806
|
+
resolve({ ok: false, isMissing: "code" in error && error.code === "ENOENT", stderr: stderr || error.message });
|
|
8807
|
+
});
|
|
8808
|
+
});
|
|
8809
|
+
}
|
|
8810
|
+
function findUninstalledPrograms(run, installed) {
|
|
8811
|
+
const programs = /* @__PURE__ */ new Set();
|
|
8812
|
+
for (const match of run.matchAll(IMAGE_ABSENT_PROGRAM_PATTERN)) {
|
|
8813
|
+
const program = match[1];
|
|
8814
|
+
if (program === void 0) continue;
|
|
8815
|
+
const provider = program === "npm" || program === "npx" ? "node" : program;
|
|
8816
|
+
if (NEVER_INSTALLED_PROGRAMS.has(program) || !installed.has(provider)) programs.add(program);
|
|
8817
|
+
}
|
|
8818
|
+
return [...programs].toSorted();
|
|
8819
|
+
}
|
|
8820
|
+
function readInstalledPrograms(step) {
|
|
8821
|
+
const uses = step["uses"];
|
|
8822
|
+
if (typeof uses !== "string") return [];
|
|
8823
|
+
const action = uses.split("@", 1)[0];
|
|
8824
|
+
if (action === "actions/setup-node") return ["node"];
|
|
8825
|
+
if (action === "pnpm/action-setup") return ["pnpm"];
|
|
8826
|
+
if (action !== "pnpm/setup") return [];
|
|
8827
|
+
const inputs = isRecord3(step["with"]) ? step["with"] : {};
|
|
8828
|
+
const runtime = inputs["runtime"];
|
|
8829
|
+
const installsNode = typeof runtime === "string" ? /^node\b/.test(runtime) : String(inputs["node-version-file"]) !== "false";
|
|
8830
|
+
return installsNode ? ["pnpm", "node"] : ["pnpm"];
|
|
8831
|
+
}
|
|
8832
|
+
|
|
8833
|
+
// .readyup/lib/self-hosted-runner.ts
|
|
8834
|
+
var SET_CI_RUNS_ON_COMMAND = `gh variable set CI_RUNS_ON --body '"self-hosted"'`;
|
|
8835
|
+
var statusByCwd = /* @__PURE__ */ new Map();
|
|
8836
|
+
var selfHostedRunnerChecklist = {
|
|
8837
|
+
name: "self-hosted runner",
|
|
8838
|
+
checks: [
|
|
8675
8839
|
{
|
|
8676
|
-
name: "
|
|
8840
|
+
name: "GitHub reports the repo's runner settings",
|
|
8841
|
+
severity: "recommend",
|
|
8842
|
+
quiet: true,
|
|
8843
|
+
skip: () => describeSelfHostedRunnerSkip(),
|
|
8844
|
+
check: () => true,
|
|
8677
8845
|
checks: [
|
|
8678
8846
|
{
|
|
8679
|
-
name: "
|
|
8847
|
+
name: "CI_RUNS_ON selects the self-hosted runner",
|
|
8680
8848
|
severity: "recommend",
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
check: () => recommendSelfHostedRunner()
|
|
8690
|
-
},
|
|
8691
|
-
{
|
|
8692
|
-
name: "the self-hosted runner can run CI",
|
|
8693
|
-
severity: "warn",
|
|
8694
|
-
skip: () => describeReadinessSkip(),
|
|
8695
|
-
check: () => selfHostedRunnerCanRunCi()
|
|
8696
|
-
}
|
|
8697
|
-
]
|
|
8849
|
+
skip: () => describeRecommendationSkip(),
|
|
8850
|
+
check: () => recommendSelfHostedRunner()
|
|
8851
|
+
},
|
|
8852
|
+
{
|
|
8853
|
+
name: "the self-hosted runner can run CI",
|
|
8854
|
+
severity: "warn",
|
|
8855
|
+
skip: () => describeReadinessSkip(),
|
|
8856
|
+
check: () => selfHostedRunnerCanRunCi()
|
|
8698
8857
|
}
|
|
8699
8858
|
]
|
|
8700
8859
|
}
|
|
8701
8860
|
]
|
|
8702
|
-
}
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
"preprepare",
|
|
8729
|
-
"prepare",
|
|
8730
|
-
"postprepare"
|
|
8731
|
-
];
|
|
8732
|
-
var LEFTHOOK_CHECK_INSTALL_PATTERN = /\blefthook\s+check-install\b/;
|
|
8733
|
-
var LEFTHOOK_INSTALL_PATTERN = /\blefthook\s+install\b/;
|
|
8734
|
-
var MIN_ESLINT_VERSION = "10.0.0";
|
|
8735
|
-
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
8736
|
-
var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
|
|
8737
|
-
var CATALOGUE_CHECKED_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
8738
|
-
var PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
8739
|
-
function allWorkspacePackagesCanBuild() {
|
|
8740
|
-
const packagesDir = join(process.cwd(), "packages");
|
|
8741
|
-
if (!existsSync3(packagesDir)) return true;
|
|
8742
|
-
const entries = readdirSync2(packagesDir, { withFileTypes: true });
|
|
8743
|
-
const failingPackages = [];
|
|
8744
|
-
for (const entry of entries) {
|
|
8745
|
-
if (!entry.isDirectory()) continue;
|
|
8746
|
-
const pkgPath = `packages/${entry.name}/package.json`;
|
|
8747
|
-
const content = readFile(pkgPath);
|
|
8748
|
-
if (!content) continue;
|
|
8749
|
-
const hasBuildOverride = /"build"\s*:/.test(content);
|
|
8750
|
-
const hasDefaultBuildInputs = fileExists(`packages/${entry.name}/tsconfig.json`) && existsSync3(join(packagesDir, entry.name, "src"));
|
|
8751
|
-
if (!hasBuildOverride && !hasDefaultBuildInputs) {
|
|
8752
|
-
failingPackages.push(entry.name);
|
|
8753
|
-
}
|
|
8861
|
+
};
|
|
8862
|
+
async function describeReadinessSkip(cwd = process.cwd(), readFacts) {
|
|
8863
|
+
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
8864
|
+
if ("skip" in status) return status.skip;
|
|
8865
|
+
if (status.selection === "unset") return "CI_RUNS_ON is unset";
|
|
8866
|
+
if (status.selection === "other") return `CI_RUNS_ON selects ${status.facts.ciRunsOn ?? ""}`;
|
|
8867
|
+
return false;
|
|
8868
|
+
}
|
|
8869
|
+
async function describeRecommendationSkip(cwd = process.cwd(), readFacts) {
|
|
8870
|
+
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
8871
|
+
if ("skip" in status) return status.skip;
|
|
8872
|
+
if (status.selection !== "unset") return `CI_RUNS_ON is already set to ${status.facts.ciRunsOn ?? ""}`;
|
|
8873
|
+
if (status.exempt !== void 0) return `the repo is exempt: ${status.exempt}`;
|
|
8874
|
+
if (status.failures.length > 0) return `not ready: ${status.failures.join("; ")}`;
|
|
8875
|
+
return false;
|
|
8876
|
+
}
|
|
8877
|
+
async function describeSelfHostedRunnerSkip(cwd = process.cwd(), readFacts) {
|
|
8878
|
+
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
8879
|
+
return "skip" in status && status.skip;
|
|
8880
|
+
}
|
|
8881
|
+
function findSelfHostedRunnerFailures(facts, cwd) {
|
|
8882
|
+
const failures = [];
|
|
8883
|
+
if (!facts.isPrivate) {
|
|
8884
|
+
failures.push(
|
|
8885
|
+
`${facts.nameWithOwner} is public: A self-hosted runner must never run code from a fork's pull request`
|
|
8886
|
+
);
|
|
8754
8887
|
}
|
|
8755
|
-
if (
|
|
8888
|
+
if (facts.onlineSelfHostedRunners === 0) {
|
|
8889
|
+
failures.push(
|
|
8890
|
+
`${facts.nameWithOwner} does not have an online runner labelled self-hosted. When williamthorsen/hq allocates the repo slots in apps/runners/config/repos.yaml, the runners have not registered yet`
|
|
8891
|
+
);
|
|
8892
|
+
}
|
|
8893
|
+
const workflows = readWorkflows(cwd);
|
|
8894
|
+
failures.push(...findCodeQualityCallerDefects(workflows), ...findImageIncompatibleSteps(workflows));
|
|
8895
|
+
return failures;
|
|
8896
|
+
}
|
|
8897
|
+
async function readGitHubFacts(cwd, runGh = runGhCommand) {
|
|
8898
|
+
const repo = await runGh(["repo", "view", "--json", "nameWithOwner,visibility"], cwd);
|
|
8899
|
+
if (!repo.ok) return { skip: describeGhFailure(repo, "cannot resolve this repo on GitHub") };
|
|
8900
|
+
const repoView = parseJson(repo.stdout);
|
|
8901
|
+
if (!isRecord4(repoView) || typeof repoView["nameWithOwner"] !== "string") {
|
|
8902
|
+
return { skip: "gh repo view returned an unexpected shape" };
|
|
8903
|
+
}
|
|
8904
|
+
const nameWithOwner = repoView["nameWithOwner"];
|
|
8905
|
+
const runners = await runGh(["api", `repos/${nameWithOwner}/actions/runners?per_page=100`], cwd);
|
|
8906
|
+
if (!runners.ok) {
|
|
8907
|
+
return {
|
|
8908
|
+
skip: describeGhFailure(runners, `cannot list the runners of ${nameWithOwner}, which needs admin access`)
|
|
8909
|
+
};
|
|
8910
|
+
}
|
|
8911
|
+
const repoVariable = await runGh(["api", `repos/${nameWithOwner}/actions/variables/CI_RUNS_ON`], cwd);
|
|
8912
|
+
if (!repoVariable.ok && !isNotFound(repoVariable)) {
|
|
8913
|
+
return { skip: describeGhFailure(repoVariable, `cannot read the CI_RUNS_ON variable of ${nameWithOwner}`) };
|
|
8914
|
+
}
|
|
8915
|
+
const repoValue = repoVariable.ok ? readVariableValue(parseJson(repoVariable.stdout)) : void 0;
|
|
8756
8916
|
return {
|
|
8757
|
-
|
|
8758
|
-
|
|
8917
|
+
nameWithOwner,
|
|
8918
|
+
isPrivate: repoView["visibility"] !== "PUBLIC",
|
|
8919
|
+
onlineSelfHostedRunners: countOnlineSelfHostedRunners(parseJson(runners.stdout)),
|
|
8920
|
+
ciRunsOn: repoValue ?? await readOrganizationValue(nameWithOwner, cwd, runGh)
|
|
8759
8921
|
};
|
|
8760
8922
|
}
|
|
8761
|
-
function
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
if (
|
|
8765
|
-
const
|
|
8766
|
-
|
|
8767
|
-
const findings = workspaceDiscovery.workspaces.flatMap((workspace) => findLiteralCataloguedSpecifiers(workspace, catalogRead.names)).toSorted((a, b) => a.manifestPath.localeCompare(b.manifestPath) || a.name.localeCompare(b.name));
|
|
8768
|
-
if (findings.length === 0) return true;
|
|
8769
|
-
const lines = findings.map(({ manifestPath, name, specifier }) => `${manifestPath}: ${name} ${specifier}`);
|
|
8770
|
-
return { ok: false, detail: formatPaths([...new Set(lines)]) };
|
|
8771
|
-
}
|
|
8772
|
-
function checkNoMatchingFiles(patterns, cwd) {
|
|
8773
|
-
const foundFiles = findFiles(patterns, cwd);
|
|
8774
|
-
if (foundFiles.length === 0) return true;
|
|
8775
|
-
return { ok: false, detail: formatPaths(foundFiles) };
|
|
8923
|
+
function readRunnerSelection(value) {
|
|
8924
|
+
if (value === void 0 || value.trim() === "") return "unset";
|
|
8925
|
+
const parsed = parseJson(value);
|
|
8926
|
+
if (parsed === void 0) return "invalid";
|
|
8927
|
+
const labels = Array.isArray(parsed) ? parsed : [parsed];
|
|
8928
|
+
return labels.includes("self-hosted") ? "self-hosted" : "other";
|
|
8776
8929
|
}
|
|
8777
|
-
function
|
|
8778
|
-
const
|
|
8779
|
-
if (
|
|
8780
|
-
|
|
8781
|
-
}
|
|
8782
|
-
const staleConfigs = matches.filter(
|
|
8783
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), exportName, SHARED_VITEST_MODULE)
|
|
8784
|
-
);
|
|
8785
|
-
if (staleConfigs.length === 0) return true;
|
|
8930
|
+
async function recommendSelfHostedRunner(cwd = process.cwd(), readFacts) {
|
|
8931
|
+
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
8932
|
+
if ("skip" in status) return true;
|
|
8933
|
+
const toolTests = status.toolTestFiles.length === 0 ? "The repo does not have any tool-tier tests." : `Before switching, confirm that the image provides every program that these tool-tier tests spawn: ${formatPaths(status.toolTestFiles)}`;
|
|
8786
8934
|
return {
|
|
8787
8935
|
ok: false,
|
|
8788
|
-
detail:
|
|
8936
|
+
detail: `${status.facts.nameWithOwner} can run its CI on the self-hosted runner. ${toolTests}`,
|
|
8937
|
+
fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${status.facts.nameWithOwner}`
|
|
8789
8938
|
};
|
|
8790
8939
|
}
|
|
8791
|
-
function
|
|
8792
|
-
const
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
);
|
|
8797
|
-
|
|
8798
|
-
|
|
8940
|
+
async function selfHostedRunnerCanRunCi(cwd = process.cwd(), readFacts) {
|
|
8941
|
+
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
8942
|
+
if ("skip" in status) return true;
|
|
8943
|
+
const { facts } = status;
|
|
8944
|
+
const conditions = status.exempt === void 0 ? status.failures : [`the repo is exempt: ${status.exempt}`, ...status.failures];
|
|
8945
|
+
if (status.selection !== "invalid" && conditions.length === 0) return true;
|
|
8946
|
+
const invalidValue = `CI_RUNS_ON is ${facts.ciRunsOn ?? ""}, which is not JSON, so every workflow that reads it with fromJSON fails`;
|
|
8947
|
+
if (conditions.length === 0) {
|
|
8948
|
+
return { ok: false, detail: invalidValue, fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${facts.nameWithOwner}` };
|
|
8799
8949
|
}
|
|
8800
|
-
const
|
|
8801
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "definePrettierConfig", SHARED_PRETTIER_MODULE)
|
|
8802
|
-
);
|
|
8803
|
-
if (staleConfigs.length === 0) return true;
|
|
8950
|
+
const problems = status.selection === "invalid" ? [invalidValue, ...conditions] : conditions;
|
|
8804
8951
|
return {
|
|
8805
8952
|
ok: false,
|
|
8806
|
-
detail:
|
|
8953
|
+
detail: problems.map((problem) => ` ${problem}`).join("\n"),
|
|
8954
|
+
fix: `Fix each condition, or return the repo's CI to ubuntu-latest: gh variable delete CI_RUNS_ON --repo ${facts.nameWithOwner}`
|
|
8807
8955
|
};
|
|
8808
8956
|
}
|
|
8809
|
-
function
|
|
8810
|
-
const
|
|
8811
|
-
if (
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
}
|
|
8815
|
-
return ".prettierrc.js is missing";
|
|
8816
|
-
}
|
|
8817
|
-
function hasPrettierConfigKey(cwd) {
|
|
8818
|
-
const manifest = readFileIn(cwd, "package.json");
|
|
8819
|
-
if (manifest === void 0) return false;
|
|
8820
|
-
try {
|
|
8821
|
-
const parsedManifest = JSON.parse(manifest);
|
|
8822
|
-
return isRecord(parsedManifest) && parsedManifest["prettier"] !== void 0;
|
|
8823
|
-
} catch {
|
|
8824
|
-
return false;
|
|
8825
|
-
}
|
|
8826
|
-
}
|
|
8827
|
-
function discoverWorkspacesOrFailure(filter) {
|
|
8957
|
+
async function gatherSelfHostedRunnerStatus(cwd, readFacts) {
|
|
8958
|
+
const facts = await readFacts(cwd);
|
|
8959
|
+
if ("skip" in facts) return facts;
|
|
8960
|
+
const failures = findSelfHostedRunnerFailures(facts, cwd);
|
|
8961
|
+
let exempt;
|
|
8828
8962
|
try {
|
|
8829
|
-
|
|
8963
|
+
exempt = (await loadRootConfig(cwd)).selfHostedRunner?.exempt;
|
|
8830
8964
|
} catch (error) {
|
|
8831
|
-
|
|
8965
|
+
failures.push(`the nmr config cannot be loaded: ${describeError(error)}`);
|
|
8832
8966
|
}
|
|
8967
|
+
return {
|
|
8968
|
+
facts,
|
|
8969
|
+
selection: readRunnerSelection(facts.ciRunsOn),
|
|
8970
|
+
failures,
|
|
8971
|
+
exempt,
|
|
8972
|
+
toolTestFiles: findTestFiles(cwd).filter(
|
|
8973
|
+
(file) => file.endsWith(".tool.test.ts") || file.endsWith(".tool.test.tsx")
|
|
8974
|
+
)
|
|
8975
|
+
};
|
|
8833
8976
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
(workspace) => readBinEntries(workspace).flatMap((entry) => {
|
|
8841
|
-
const defect = describeBinTargetDefect(workspace, entry, trackedPaths);
|
|
8842
|
-
return defect === void 0 ? [] : [`${describeBinEntry(workspace, entry)} (${defect})`];
|
|
8843
|
-
})
|
|
8844
|
-
);
|
|
8845
|
-
if (offenders.length === 0) return true;
|
|
8846
|
-
return { ok: false, detail: formatPaths(offenders) };
|
|
8847
|
-
}
|
|
8848
|
-
function everyBinWrapperTargetIsCoveredByFiles() {
|
|
8849
|
-
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
8850
|
-
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
8851
|
-
const cwd = process.cwd();
|
|
8852
|
-
const offenders = workspaceDiscovery.workspaces.flatMap((workspace) => {
|
|
8853
|
-
const files = workspace.packageJson["files"];
|
|
8854
|
-
if (!Array.isArray(files)) return [];
|
|
8855
|
-
const publishedSegments = new Set(
|
|
8856
|
-
files.flatMap((entry) => typeof entry === "string" ? [readFirstSegment(entry)] : [])
|
|
8857
|
-
);
|
|
8858
|
-
return readBinEntries(workspace).flatMap((entry) => {
|
|
8859
|
-
const target = readWrapperTarget(cwd, workspace, entry);
|
|
8860
|
-
if (target === void 0 || publishedSegments.has(readFirstSegment(target))) return [];
|
|
8861
|
-
return [`${describeBinEntry(workspace, entry)} -> ${target}`];
|
|
8862
|
-
});
|
|
8863
|
-
});
|
|
8864
|
-
if (offenders.length === 0) return true;
|
|
8865
|
-
return { ok: false, detail: formatPaths(offenders) };
|
|
8866
|
-
}
|
|
8867
|
-
function describeBinEntry(workspace, entry) {
|
|
8868
|
-
return `${workspace.name ?? workspace.dir}:${entry.command} -> ${entry.target}`;
|
|
8869
|
-
}
|
|
8870
|
-
function describeBinTargetDefect(workspace, entry, trackedPaths) {
|
|
8871
|
-
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return `names a path under ${BUILD_OUTPUT_DIR}/`;
|
|
8872
|
-
if (trackedPaths === void 0) return void 0;
|
|
8873
|
-
return trackedPaths.has(`${workspace.dir}/${entry.target}`) ? void 0 : "untracked";
|
|
8874
|
-
}
|
|
8875
|
-
function normalizeBinTarget(target) {
|
|
8876
|
-
return target.replace(/^\.\//, "");
|
|
8877
|
-
}
|
|
8878
|
-
function readBinEntries(workspace) {
|
|
8879
|
-
const bin = workspace.packageJson["bin"];
|
|
8880
|
-
if (typeof bin === "string") {
|
|
8881
|
-
const command = workspace.name?.split("/").at(-1) ?? basename(workspace.dir);
|
|
8882
|
-
return [{ command, target: normalizeBinTarget(bin) }];
|
|
8977
|
+
function getSelfHostedRunnerStatus(cwd, readFacts) {
|
|
8978
|
+
if (readFacts !== void 0) return gatherSelfHostedRunnerStatus(cwd, readFacts);
|
|
8979
|
+
let status = statusByCwd.get(cwd);
|
|
8980
|
+
if (status === void 0) {
|
|
8981
|
+
status = gatherSelfHostedRunnerStatus(cwd, (dir) => readGitHubFacts(dir));
|
|
8982
|
+
statusByCwd.set(cwd, status);
|
|
8883
8983
|
}
|
|
8884
|
-
|
|
8885
|
-
return Object.entries(bin).flatMap(
|
|
8886
|
-
([command, target]) => typeof target === "string" ? [{ command, target: normalizeBinTarget(target) }] : []
|
|
8887
|
-
);
|
|
8888
|
-
}
|
|
8889
|
-
function readFirstSegment(entry) {
|
|
8890
|
-
return normalizeBinTarget(entry).split("/", 1).at(0) ?? "";
|
|
8891
|
-
}
|
|
8892
|
-
function readWrapperTarget(cwd, workspace, entry) {
|
|
8893
|
-
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return void 0;
|
|
8894
|
-
const content = readFileIn(cwd, `${workspace.dir}/${entry.target}`);
|
|
8895
|
-
if (content === void 0) return void 0;
|
|
8896
|
-
const specifier = WRAPPER_TARGET_PATTERN.exec(content)?.[1];
|
|
8897
|
-
if (specifier === void 0) return void 0;
|
|
8898
|
-
return posix.normalize(posix.join(posix.dirname(entry.target), specifier));
|
|
8899
|
-
}
|
|
8900
|
-
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
8901
|
-
const untieredFiles = findUntieredTestFiles(cwd);
|
|
8902
|
-
if (untieredFiles.length === 0) return true;
|
|
8903
|
-
return { ok: false, detail: formatPaths(untieredFiles) };
|
|
8904
|
-
}
|
|
8905
|
-
function everyTestFileSitsUnderTestsDir(cwd = process.cwd()) {
|
|
8906
|
-
const misplacedFiles = findMisplacedTestFiles(cwd);
|
|
8907
|
-
if (misplacedFiles.length === 0) return true;
|
|
8908
|
-
return { ok: false, detail: formatPaths(misplacedFiles) };
|
|
8984
|
+
return status;
|
|
8909
8985
|
}
|
|
8910
|
-
|
|
8911
|
-
|
|
8986
|
+
|
|
8987
|
+
// .readyup/lib/setup.ts
|
|
8988
|
+
import {
|
|
8989
|
+
fileContains,
|
|
8990
|
+
fileExists as fileExists2,
|
|
8991
|
+
hasDevDependency,
|
|
8992
|
+
hasMinDevDependencyVersion,
|
|
8993
|
+
hasPackageJsonField,
|
|
8994
|
+
readFile as readFile4
|
|
8995
|
+
} from "readyup/check-utils";
|
|
8996
|
+
|
|
8997
|
+
// .readyup/lib/catalog.ts
|
|
8998
|
+
import { posix as posix3 } from "node:path";
|
|
8999
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
9000
|
+
import { isRecord as isRecord5 } from "readyup/check-utils";
|
|
9001
|
+
var CATALOGUE_CHECKED_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
9002
|
+
var PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
9003
|
+
function cataloguedDependenciesUseCatalog() {
|
|
9004
|
+
const catalogRead = readCataloguedNames(process.cwd());
|
|
9005
|
+
if (!catalogRead.ok) return catalogRead;
|
|
9006
|
+
if (catalogRead.names.size === 0) return true;
|
|
9007
|
+
const workspaceDiscovery = discoverWorkspacesOrFailure();
|
|
8912
9008
|
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
8913
|
-
const
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
});
|
|
8918
|
-
if (unpairedConfigs.length === 0) return true;
|
|
8919
|
-
return { ok: false, detail: formatPaths(unpairedConfigs) };
|
|
8920
|
-
}
|
|
8921
|
-
function findFiles(patterns, cwd) {
|
|
8922
|
-
return globSync2(patterns, { cwd, exclude: (path7) => SCAN_EXCLUDE_DIRS.has(basename(path7)) }).map((path7) => path7.split(sep).join("/")).toSorted();
|
|
9009
|
+
const findings = workspaceDiscovery.workspaces.flatMap((workspace) => findLiteralCataloguedSpecifiers(workspace, catalogRead.names)).toSorted((a, b) => a.manifestPath.localeCompare(b.manifestPath) || a.name.localeCompare(b.name));
|
|
9010
|
+
if (findings.length === 0) return true;
|
|
9011
|
+
const lines = findings.map(({ manifestPath, name, specifier }) => `${manifestPath}: ${name} ${specifier}`);
|
|
9012
|
+
return { ok: false, detail: formatPaths([...new Set(lines)]) };
|
|
8923
9013
|
}
|
|
8924
9014
|
function findLiteralCataloguedSpecifiers(workspace, names) {
|
|
8925
|
-
const manifestPath =
|
|
9015
|
+
const manifestPath = posix3.join(workspace.dir, "package.json");
|
|
8926
9016
|
return CATALOGUE_CHECKED_FIELDS.flatMap((field) => {
|
|
8927
9017
|
const declared = workspace.packageJson[field];
|
|
8928
|
-
if (!
|
|
9018
|
+
if (!isRecord5(declared)) return [];
|
|
8929
9019
|
return Object.entries(declared).flatMap(
|
|
8930
|
-
([name, specifier]) => names.has(name) && typeof specifier === "string" && !PROTOCOL_PATTERN.test(specifier) ?
|
|
9020
|
+
([name, specifier]) => names.has(name) && typeof specifier === "string" && !PROTOCOL_PATTERN.test(specifier) ? { manifestPath, name, specifier } : []
|
|
8931
9021
|
);
|
|
8932
9022
|
});
|
|
8933
9023
|
}
|
|
8934
|
-
function
|
|
8935
|
-
|
|
8936
|
-
}
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
}
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
if (
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
return
|
|
8947
|
-
}
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
});
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
return findFiles([VITE_CONFIG_PATTERN], join(cwd, dirname(relativePath))).length > 0;
|
|
8960
|
-
}
|
|
8961
|
-
function importsSharedExport(content, exportName, moduleSpecifier) {
|
|
8962
|
-
if (content === void 0) return false;
|
|
8963
|
-
const pattern = new RegExp(String.raw`import\s*\{[^}]*\b${exportName}\b[^}]*\}\s*from\s*['"]${moduleSpecifier}['"]`);
|
|
8964
|
-
return pattern.test(content);
|
|
8965
|
-
}
|
|
8966
|
-
function isOwnedByReExportCheck(cwd, relativePath) {
|
|
8967
|
-
return isReExportOnly(readFileIn(cwd, relativePath)) && !hasViteConfigBeside(cwd, relativePath);
|
|
8968
|
-
}
|
|
8969
|
-
function isReExportOnly(content) {
|
|
8970
|
-
if (content === void 0) return false;
|
|
8971
|
-
const statements = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
8972
|
-
return statements.length > 0 && statements.every((line) => RE_EXPORT_LINE_PATTERN.test(line));
|
|
8973
|
-
}
|
|
8974
|
-
function noPnpmFieldInPackageJson(cwd = process.cwd()) {
|
|
8975
|
-
const declaringFiles = findFiles(["**/package.json"], cwd).flatMap((relativePath) => {
|
|
8976
|
-
const keys = readPnpmFieldKeys(readFileIn(cwd, relativePath));
|
|
8977
|
-
if (keys === void 0) return [];
|
|
8978
|
-
return [keys.length > 0 ? `${relativePath} (${keys.join(", ")})` : relativePath];
|
|
8979
|
-
});
|
|
8980
|
-
if (declaringFiles.length === 0) return true;
|
|
8981
|
-
return { ok: false, detail: formatPaths(declaringFiles) };
|
|
8982
|
-
}
|
|
8983
|
-
function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
8984
|
-
const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path7) => path7.includes("/"));
|
|
8985
|
-
const reExports = nonRootConfigs.filter((path7) => isOwnedByReExportCheck(cwd, path7));
|
|
8986
|
-
if (reExports.length === 0) return true;
|
|
8987
|
-
return { ok: false, detail: formatPaths(reExports) };
|
|
8988
|
-
}
|
|
8989
|
-
function noRedundantRootScripts() {
|
|
8990
|
-
const pkg = readPackageJson();
|
|
8991
|
-
if (!pkg) return true;
|
|
8992
|
-
const scripts = pkg["scripts"];
|
|
8993
|
-
if (!isRecord(scripts)) return true;
|
|
8994
|
-
const builtInNames = Object.keys(getDefaultRootScripts(process.cwd()));
|
|
8995
|
-
const redundantNames = Object.keys(scripts).filter((name) => builtInNames.includes(name));
|
|
8996
|
-
if (redundantNames.length === 0) return true;
|
|
8997
|
-
return {
|
|
8998
|
-
ok: false,
|
|
8999
|
-
detail: `redundant: ${redundantNames.join(", ")}`
|
|
9000
|
-
};
|
|
9001
|
-
}
|
|
9002
|
-
function noRetiredVitestConfigs(cwd = process.cwd()) {
|
|
9003
|
-
return checkNoMatchingFiles(
|
|
9004
|
-
[`**/vitest.standalone.config.${CONFIG_EXTENSIONS}`, `**/vitest.integration.config.${CONFIG_EXTENSIONS}`],
|
|
9005
|
-
cwd
|
|
9006
|
-
);
|
|
9007
|
-
}
|
|
9008
|
-
function noUnguardedLefthookInstall(cwd = process.cwd()) {
|
|
9009
|
-
const scripts = readRootScripts(cwd);
|
|
9010
|
-
const unguardedScripts = INSTALL_LIFECYCLE_SCRIPTS.flatMap((name) => {
|
|
9011
|
-
const command = scripts[name];
|
|
9012
|
-
if (typeof command !== "string") return [];
|
|
9013
|
-
const isUnguarded = LEFTHOOK_INSTALL_PATTERN.test(command) && !LEFTHOOK_CHECK_INSTALL_PATTERN.test(command);
|
|
9014
|
-
return isUnguarded ? [`${name}: ${command}`] : [];
|
|
9015
|
-
});
|
|
9016
|
-
if (unguardedScripts.length === 0) return true;
|
|
9017
|
-
return { ok: false, detail: formatPaths(unguardedScripts) };
|
|
9018
|
-
}
|
|
9019
|
-
function noWorkspaceRunScriptReferences() {
|
|
9020
|
-
const packagesDir = join(process.cwd(), "packages");
|
|
9021
|
-
if (!existsSync3(packagesDir)) return true;
|
|
9022
|
-
const legacyPattern = /run-workspace-script|"pnpm\s+run\s+ws\b/;
|
|
9023
|
-
const entries = readdirSync2(packagesDir, { withFileTypes: true });
|
|
9024
|
-
const matches = [];
|
|
9025
|
-
for (const entry of entries) {
|
|
9026
|
-
if (!entry.isDirectory()) continue;
|
|
9027
|
-
const content = readFile(`packages/${entry.name}/package.json`);
|
|
9028
|
-
if (content && legacyPattern.test(content)) {
|
|
9029
|
-
matches.push(entry.name);
|
|
9030
|
-
}
|
|
9031
|
-
}
|
|
9032
|
-
if (matches.length === 0) return true;
|
|
9033
|
-
return {
|
|
9034
|
-
ok: false,
|
|
9035
|
-
detail: `found in: ${matches.join(", ")}`
|
|
9036
|
-
};
|
|
9024
|
+
function readCataloguedNames(cwd) {
|
|
9025
|
+
const content = readFileIn(cwd, "pnpm-workspace.yaml");
|
|
9026
|
+
if (content === void 0) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9027
|
+
let workspaceManifest;
|
|
9028
|
+
try {
|
|
9029
|
+
workspaceManifest = (0, import_yaml3.parse)(content);
|
|
9030
|
+
} catch (error) {
|
|
9031
|
+
return { ok: false, detail: `cannot parse pnpm-workspace.yaml: ${describeError(error)}` };
|
|
9032
|
+
}
|
|
9033
|
+
if (!isRecord5(workspaceManifest)) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9034
|
+
const namedCatalogs = workspaceManifest["catalogs"];
|
|
9035
|
+
const catalogs = [workspaceManifest["catalog"], ...isRecord5(namedCatalogs) ? Object.values(namedCatalogs) : []];
|
|
9036
|
+
return { ok: true, names: new Set(catalogs.filter(isRecord5).flatMap((catalog) => Object.keys(catalog))) };
|
|
9037
|
+
}
|
|
9038
|
+
|
|
9039
|
+
// .readyup/lib/manifest-fields.ts
|
|
9040
|
+
import { isRecord as isRecord6 } from "readyup/check-utils";
|
|
9041
|
+
function noPnpmFieldInPackageJson(cwd = process.cwd()) {
|
|
9042
|
+
const declaringFiles = findFiles(["**/package.json"], cwd).flatMap((relativePath) => {
|
|
9043
|
+
const keys = readPnpmFieldKeys(readFileIn(cwd, relativePath));
|
|
9044
|
+
if (keys === void 0) return [];
|
|
9045
|
+
return keys.length > 0 ? `${relativePath} (${keys.join(", ")})` : relativePath;
|
|
9046
|
+
});
|
|
9047
|
+
if (declaringFiles.length === 0) return true;
|
|
9048
|
+
return { ok: false, detail: formatPaths(declaringFiles) };
|
|
9037
9049
|
}
|
|
9038
9050
|
function noWorkspacesFieldInPackageJson(cwd = process.cwd()) {
|
|
9039
9051
|
const declaringFiles = findFiles(["**/package.json"], cwd).filter(
|
|
@@ -9046,28 +9058,11 @@ function declaresWorkspacesField(content) {
|
|
|
9046
9058
|
if (content === void 0) return false;
|
|
9047
9059
|
try {
|
|
9048
9060
|
const parsedManifest = JSON.parse(content);
|
|
9049
|
-
return
|
|
9061
|
+
return isRecord6(parsedManifest) && "workspaces" in parsedManifest;
|
|
9050
9062
|
} catch {
|
|
9051
9063
|
return false;
|
|
9052
9064
|
}
|
|
9053
9065
|
}
|
|
9054
|
-
function readCataloguedNames(cwd) {
|
|
9055
|
-
const content = readFileIn(cwd, "pnpm-workspace.yaml");
|
|
9056
|
-
if (content === void 0) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9057
|
-
let workspaceManifest;
|
|
9058
|
-
try {
|
|
9059
|
-
workspaceManifest = (0, import_yaml2.parse)(content);
|
|
9060
|
-
} catch (error) {
|
|
9061
|
-
return { ok: false, detail: `cannot parse pnpm-workspace.yaml: ${describeError(error)}` };
|
|
9062
|
-
}
|
|
9063
|
-
if (!isRecord(workspaceManifest)) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9064
|
-
const namedCatalogs = workspaceManifest["catalogs"];
|
|
9065
|
-
const catalogs = [workspaceManifest["catalog"], ...isRecord(namedCatalogs) ? Object.values(namedCatalogs) : []];
|
|
9066
|
-
return { ok: true, names: new Set(catalogs.filter(isRecord).flatMap((catalog) => Object.keys(catalog))) };
|
|
9067
|
-
}
|
|
9068
|
-
function readFileIn(cwd, relativePath) {
|
|
9069
|
-
return readFile(join(cwd, relativePath));
|
|
9070
|
-
}
|
|
9071
9066
|
function readPnpmFieldKeys(content) {
|
|
9072
9067
|
if (content === void 0) return void 0;
|
|
9073
9068
|
let parsedManifest;
|
|
@@ -9076,36 +9071,187 @@ function readPnpmFieldKeys(content) {
|
|
|
9076
9071
|
} catch {
|
|
9077
9072
|
return void 0;
|
|
9078
9073
|
}
|
|
9079
|
-
if (!
|
|
9074
|
+
if (!isRecord6(parsedManifest)) return void 0;
|
|
9080
9075
|
const pnpm = parsedManifest["pnpm"];
|
|
9081
|
-
return
|
|
9076
|
+
return isRecord6(pnpm) ? Object.keys(pnpm).toSorted() : void 0;
|
|
9082
9077
|
}
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9078
|
+
|
|
9079
|
+
// .readyup/lib/setup.ts
|
|
9080
|
+
var MIN_ESLINT_VERSION = "10.0.0";
|
|
9081
|
+
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
9082
|
+
var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
|
|
9083
|
+
var setupChecks = [
|
|
9084
|
+
{
|
|
9085
|
+
name: "@williamthorsen/nmr in devDependencies",
|
|
9086
|
+
severity: "error",
|
|
9087
|
+
check: () => hasDevDependency("@williamthorsen/nmr"),
|
|
9088
|
+
fix: "pnpm add --save-dev @williamthorsen/nmr",
|
|
9089
|
+
checks: [
|
|
9090
|
+
{
|
|
9091
|
+
get name() {
|
|
9092
|
+
return `@williamthorsen/nmr >= ${getMinVersion()}`;
|
|
9093
|
+
},
|
|
9094
|
+
severity: "error",
|
|
9095
|
+
check: () => hasMinDevDependencyVersion("@williamthorsen/nmr", getMinVersion(), {
|
|
9096
|
+
exempt: resolvesVersionViaWorkspace
|
|
9097
|
+
}),
|
|
9098
|
+
get fix() {
|
|
9099
|
+
return `pnpm add --save-dev @williamthorsen/nmr@^${getMinVersion()}`;
|
|
9100
|
+
}
|
|
9101
|
+
}
|
|
9102
|
+
]
|
|
9103
|
+
},
|
|
9104
|
+
{
|
|
9105
|
+
name: "pnpm-workspace.yaml exists",
|
|
9106
|
+
severity: "error",
|
|
9107
|
+
check: () => fileExists2("pnpm-workspace.yaml"),
|
|
9108
|
+
fix: "Create pnpm-workspace.yaml with workspace package globs"
|
|
9109
|
+
},
|
|
9110
|
+
{
|
|
9111
|
+
name: "catalogued dependencies use catalog:",
|
|
9112
|
+
severity: "warn",
|
|
9113
|
+
quiet: true,
|
|
9114
|
+
check: cataloguedDependenciesUseCatalog,
|
|
9115
|
+
fix: "Replace each listed specifier with `catalog:`, so that pnpm-workspace.yaml stays the one place that declares the version. A version that must differ belongs in a named catalog under `catalogs`, referenced as `catalog:<name>`"
|
|
9116
|
+
},
|
|
9117
|
+
{
|
|
9118
|
+
name: "package.json has packageManager field",
|
|
9119
|
+
severity: "warn",
|
|
9120
|
+
check: () => hasPackageJsonField("packageManager"),
|
|
9121
|
+
fix: 'Add "packageManager" field to package.json (e.g., "pnpm@10.33.0")'
|
|
9122
|
+
},
|
|
9123
|
+
{
|
|
9124
|
+
name: ".tool-versions does not list pnpm",
|
|
9125
|
+
severity: "warn",
|
|
9126
|
+
quiet: true,
|
|
9127
|
+
check: toolVersionsHasNoPnpm,
|
|
9128
|
+
fix: "Remove pnpm from .tool-versions, and manage it through the packageManager field"
|
|
9129
|
+
},
|
|
9130
|
+
{
|
|
9131
|
+
name: "no package.json declares a pnpm field",
|
|
9132
|
+
severity: "error",
|
|
9133
|
+
quiet: true,
|
|
9134
|
+
check: () => noPnpmFieldInPackageJson(),
|
|
9135
|
+
fix: "Move these settings into pnpm-workspace.yaml, quoting each version under `overrides`, or run `pnpx codemod run pnpm-v10-to-v11`. pnpm 11 does not read any key from the `pnpm` field, so an override left there pins nothing while an upgrade run with `--write` goes on rewriting it"
|
|
9136
|
+
},
|
|
9137
|
+
{
|
|
9138
|
+
name: "no package.json declares a workspaces field",
|
|
9139
|
+
severity: "warn",
|
|
9140
|
+
quiet: true,
|
|
9141
|
+
check: () => noWorkspacesFieldInPackageJson(),
|
|
9142
|
+
fix: "Delete the `workspaces` field. pnpm reads workspace globs only from pnpm-workspace.yaml, so the field can drift from them without any error"
|
|
9143
|
+
},
|
|
9144
|
+
{
|
|
9145
|
+
name: ".config/nmr.config.ts uses defineConfig",
|
|
9146
|
+
severity: "recommend",
|
|
9147
|
+
skip: () => !fileExists2(".config/nmr.config.ts") && "no nmr config file",
|
|
9148
|
+
check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
|
|
9149
|
+
fix: "Wrap the config export with defineConfig() from @williamthorsen/nmr/config for type safety"
|
|
9150
|
+
},
|
|
9151
|
+
// `error` because falling short produces wrong results rather than a failure. Names and fixes are
|
|
9152
|
+
// getters because their version constants are declared below the kit.
|
|
9153
|
+
{
|
|
9154
|
+
get name() {
|
|
9155
|
+
return `eslint >= ${MIN_ESLINT_VERSION}`;
|
|
9156
|
+
},
|
|
9157
|
+
severity: "error",
|
|
9158
|
+
skip: () => !hasDevDependency("eslint") && "eslint not installed",
|
|
9159
|
+
check: hasSupportedEslintVersion,
|
|
9160
|
+
get fix() {
|
|
9161
|
+
return `pnpm add --save-dev eslint@^${MIN_ESLINT_VERSION}. Earlier releases resolve config from the working directory, so nmr's root lint and lint:check would apply the root config to every package`;
|
|
9162
|
+
}
|
|
9163
|
+
},
|
|
9164
|
+
{
|
|
9165
|
+
get name() {
|
|
9166
|
+
return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
|
|
9167
|
+
},
|
|
9168
|
+
severity: "error",
|
|
9169
|
+
skip: () => !hasDevDependency("@williamthorsen/strict-lint") && "strict-lint not installed",
|
|
9170
|
+
check: hasSupportedStrictLintVersion,
|
|
9171
|
+
get fix() {
|
|
9172
|
+
return `pnpm add --save-dev @williamthorsen/strict-lint@^${MIN_STRICT_LINT_VERSION}. Earlier releases pin ESLint to one config and resolve ceilings from the working directory, so nmr's root lint:strict would report the wrong rules for every package`;
|
|
9173
|
+
}
|
|
9091
9174
|
}
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
return
|
|
9175
|
+
];
|
|
9176
|
+
function hasSupportedEslintVersion() {
|
|
9177
|
+
return hasMinDevDependencyVersion("eslint", MIN_ESLINT_VERSION, {
|
|
9178
|
+
exempt: resolvesVersionViaWorkspace
|
|
9179
|
+
});
|
|
9180
|
+
}
|
|
9181
|
+
function hasSupportedStrictLintVersion() {
|
|
9182
|
+
return hasMinDevDependencyVersion("@williamthorsen/strict-lint", MIN_STRICT_LINT_VERSION, {
|
|
9183
|
+
exempt: resolvesVersionViaWorkspace
|
|
9184
|
+
});
|
|
9185
|
+
}
|
|
9186
|
+
function getMinVersion() {
|
|
9187
|
+
const pickedFields = { "version": "0.49.0" };
|
|
9188
|
+
if (typeof pickedFields["version"] !== "string") {
|
|
9189
|
+
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
9190
|
+
}
|
|
9191
|
+
return pickedFields["version"];
|
|
9095
9192
|
}
|
|
9096
9193
|
function resolvesVersionViaWorkspace(range) {
|
|
9097
9194
|
return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
|
|
9098
9195
|
}
|
|
9099
|
-
function
|
|
9100
|
-
const
|
|
9196
|
+
function toolVersionsHasNoPnpm() {
|
|
9197
|
+
const content = readFile4(".tool-versions");
|
|
9198
|
+
if (content === void 0) return true;
|
|
9199
|
+
return !/^pnpm\s/m.test(content);
|
|
9200
|
+
}
|
|
9201
|
+
|
|
9202
|
+
// .readyup/lib/shared-configs.ts
|
|
9203
|
+
import { isRecord as isRecord7 } from "readyup/check-utils";
|
|
9204
|
+
var SHARED_PRETTIER_MODULE = "@williamthorsen/nmr/prettier";
|
|
9205
|
+
var INERT_PRETTIER_CONFIGS = [".prettierrc", ".prettierrc.{json,json5,yaml,yml,toml}"];
|
|
9206
|
+
var SHARED_TAZE_MODULE = "@williamthorsen/nmr/taze";
|
|
9207
|
+
var INERT_TAZE_CONFIGS = [".tazerc", ".tazerc.json", "taze.config.json"];
|
|
9208
|
+
var CLOBBERED_TAZE_OPTIONS = [
|
|
9209
|
+
{ key: "concurrency", pattern: /\bconcurrency\s*:/ },
|
|
9210
|
+
{ key: "githubActions", pattern: /\bgithubActions\s*:\s*(?:false|\{)/ },
|
|
9211
|
+
{ key: "ignoreOtherWorkspaces", pattern: /\bignoreOtherWorkspaces\s*:\s*false/ },
|
|
9212
|
+
{ key: "nodeVersion", pattern: /\bnodeVersion\s*:\s*false/ },
|
|
9213
|
+
{ key: "requestTimeout", pattern: /\brequestTimeout\s*:/ }
|
|
9214
|
+
];
|
|
9215
|
+
var prettierChecks = [
|
|
9216
|
+
{
|
|
9217
|
+
name: "Prettier config builds on @williamthorsen/nmr/prettier",
|
|
9218
|
+
severity: "error",
|
|
9219
|
+
check: () => prettierConfigBuildsOnSharedConfig(),
|
|
9220
|
+
fix: "Replace the Prettier config with: import { definePrettierConfig } from '@williamthorsen/nmr/prettier'; export default definePrettierConfig();"
|
|
9221
|
+
}
|
|
9222
|
+
];
|
|
9223
|
+
var tazeChecks = [
|
|
9224
|
+
{
|
|
9225
|
+
name: "taze.config.ts builds on @williamthorsen/nmr/taze",
|
|
9226
|
+
severity: "warn",
|
|
9227
|
+
check: () => tazeConfigBuildsOnSharedConfig(),
|
|
9228
|
+
fix: "Replace the taze config with: import { defineConfig } from '@williamthorsen/nmr/taze'; export default defineConfig(); -- nmr's upgrade policy applies to a repo only through this file, so without it `nmr upgrade` does not report a dependency pinned to an exact version"
|
|
9229
|
+
},
|
|
9230
|
+
{
|
|
9231
|
+
name: "taze config declares no option taze discards",
|
|
9232
|
+
severity: "warn",
|
|
9233
|
+
quiet: true,
|
|
9234
|
+
check: () => tazeConfigAvoidsClobberedOptions(),
|
|
9235
|
+
fix: "Set these through the upgrade script instead, as rootScripts: { upgrade: 'nmr-report-overrides && nmr-taze --recursive --request-timeout 90000' } in .config/nmr.config.ts, keeping the rest of the default script. taze's CLI writes a default for each of them over whatever the config file declares, so taze never uses the file's value (antfu-collective/taze#317). nmr already forwards a 30-second request timeout"
|
|
9236
|
+
}
|
|
9237
|
+
];
|
|
9238
|
+
function prettierConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9239
|
+
const configs = findFiles(
|
|
9240
|
+
[`.prettierrc.${CONFIG_EXTENSIONS}`, `prettier.config.${CONFIG_EXTENSIONS}`],
|
|
9241
|
+
//
|
|
9242
|
+
cwd
|
|
9243
|
+
);
|
|
9101
9244
|
if (configs.length === 0) {
|
|
9102
|
-
return { ok: false, detail:
|
|
9245
|
+
return { ok: false, detail: describeMissingPrettierConfig(cwd) };
|
|
9103
9246
|
}
|
|
9104
9247
|
const staleConfigs = configs.filter(
|
|
9105
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "
|
|
9248
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "definePrettierConfig", SHARED_PRETTIER_MODULE)
|
|
9106
9249
|
);
|
|
9107
9250
|
if (staleConfigs.length === 0) return true;
|
|
9108
|
-
return {
|
|
9251
|
+
return {
|
|
9252
|
+
ok: false,
|
|
9253
|
+
detail: `does not import definePrettierConfig from ${SHARED_PRETTIER_MODULE}: ${staleConfigs.join(", ")}`
|
|
9254
|
+
};
|
|
9109
9255
|
}
|
|
9110
9256
|
function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
|
|
9111
9257
|
const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
|
|
@@ -9119,345 +9265,267 @@ function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
|
|
|
9119
9265
|
if (findings.length === 0) return true;
|
|
9120
9266
|
return { ok: false, detail: formatPaths(findings) };
|
|
9121
9267
|
}
|
|
9268
|
+
function tazeConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9269
|
+
const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
|
|
9270
|
+
if (configs.length === 0) {
|
|
9271
|
+
return { ok: false, detail: describeMissingTazeConfig(cwd) };
|
|
9272
|
+
}
|
|
9273
|
+
const staleConfigs = configs.filter(
|
|
9274
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "defineConfig", SHARED_TAZE_MODULE)
|
|
9275
|
+
);
|
|
9276
|
+
if (staleConfigs.length === 0) return true;
|
|
9277
|
+
return { ok: false, detail: `does not import defineConfig from ${SHARED_TAZE_MODULE}: ${staleConfigs.join(", ")}` };
|
|
9278
|
+
}
|
|
9279
|
+
function describeMissingPrettierConfig(cwd) {
|
|
9280
|
+
const inertConfigs = findFiles(INERT_PRETTIER_CONFIGS, cwd);
|
|
9281
|
+
if (inertConfigs.length > 0) return `does not contain code that calls the factory: ${inertConfigs.join(", ")}`;
|
|
9282
|
+
if (hasPrettierConfigKey(cwd)) {
|
|
9283
|
+
return 'does not contain code that calls the factory: the "prettier" key in package.json';
|
|
9284
|
+
}
|
|
9285
|
+
return ".prettierrc.js is missing";
|
|
9286
|
+
}
|
|
9122
9287
|
function describeMissingTazeConfig(cwd) {
|
|
9123
9288
|
const inertConfigs = findFiles(INERT_TAZE_CONFIGS, cwd);
|
|
9124
9289
|
if (inertConfigs.length > 0) return `does not contain code that calls the factory: ${inertConfigs.join(", ")}`;
|
|
9125
9290
|
return "taze.config.ts is missing";
|
|
9126
9291
|
}
|
|
9127
|
-
function
|
|
9128
|
-
|
|
9129
|
-
return
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
}
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
return guard === void 0 ? false : `reported by checkTestFileConventions in ${guard}`;
|
|
9292
|
+
function hasPrettierConfigKey(cwd) {
|
|
9293
|
+
const manifest = readFileIn(cwd, "package.json");
|
|
9294
|
+
if (manifest === void 0) return false;
|
|
9295
|
+
try {
|
|
9296
|
+
const parsedManifest = JSON.parse(manifest);
|
|
9297
|
+
return isRecord7(parsedManifest) && parsedManifest["prettier"] !== void 0;
|
|
9298
|
+
} catch {
|
|
9299
|
+
return false;
|
|
9300
|
+
}
|
|
9137
9301
|
}
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9302
|
+
|
|
9303
|
+
// .readyup/lib/vitest.ts
|
|
9304
|
+
import { dirname, join as join4 } from "node:path";
|
|
9305
|
+
var VITE_CONFIG_PATTERN = `vite.config.${CONFIG_EXTENSIONS}`;
|
|
9306
|
+
var VITEST_CONFIG_PATTERN = `vitest.config.${CONFIG_EXTENSIONS}`;
|
|
9307
|
+
var SHARED_VITEST_MODULE = "@williamthorsen/nmr/vitest";
|
|
9308
|
+
var SHARED_TESTS_MODULE = "@williamthorsen/nmr/tests";
|
|
9309
|
+
var RE_EXPORT_LINE_PATTERN = /^export\s*(?:\{\s*default\s*}|\*)\s*from\s*['"]\.\.\/[^'"]*['"];?$/;
|
|
9310
|
+
var vitestChecks = [
|
|
9311
|
+
{
|
|
9312
|
+
name: "no retired Vitest config variants",
|
|
9313
|
+
severity: "error",
|
|
9314
|
+
quiet: true,
|
|
9315
|
+
check: () => noRetiredVitestConfigs(),
|
|
9316
|
+
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
9317
|
+
},
|
|
9318
|
+
{
|
|
9319
|
+
name: "every vitest.config builds on @williamthorsen/nmr/vitest",
|
|
9320
|
+
severity: "error",
|
|
9321
|
+
check: () => vitestConfigBuildsOnSharedConfig(),
|
|
9322
|
+
fix: "Replace each listed config with: import { defineVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineVitestConfig(); -- a config that does not call the factory does not declare any projects, so every tier-selecting test command fails against it. Pass the repo's own settings to the factory as layers to keep them"
|
|
9323
|
+
},
|
|
9324
|
+
{
|
|
9325
|
+
name: "vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
|
|
9326
|
+
severity: "error",
|
|
9327
|
+
check: () => vitestRootConfigBuildsOnSharedConfig(),
|
|
9328
|
+
fix: "Replace vitest.root.config.ts with: import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });"
|
|
9329
|
+
},
|
|
9330
|
+
{
|
|
9331
|
+
name: "every workspace with a Vite config has a Vitest config",
|
|
9332
|
+
severity: "error",
|
|
9333
|
+
check: () => everyViteConfigHasVitestConfig(),
|
|
9334
|
+
fix: "Add a vitest.config.ts calling defineVitestConfig() from @williamthorsen/nmr/vitest beside each listed vite.config -- Vitest stops its config search at the first directory containing either name, so the Vite config otherwise wins and the projects model is never reached"
|
|
9335
|
+
},
|
|
9336
|
+
{
|
|
9337
|
+
name: "the test suite gates the test-file conventions",
|
|
9338
|
+
severity: "warn",
|
|
9339
|
+
check: () => testSuiteGatesTestFileConventions(),
|
|
9340
|
+
fix: "Add a test of the repo's own under a __tests__ directory that declares the check: import { checkTestFileConventions } from '@williamthorsen/nmr/tests'; checkTestFileConventions(); -- passing `excludedBasenames` the directory names that the repo passes to `testCollectionExclude`. Without it, the repo's test run does not report an untiered or misplaced test file, and this kit reports them in its place against only nmr's built-in exclusions and the paths that git ignores"
|
|
9341
|
+
},
|
|
9342
|
+
{
|
|
9343
|
+
name: "every test file names its isolation tier",
|
|
9344
|
+
severity: "error",
|
|
9345
|
+
skip: () => describeConventionsGuardSkip(),
|
|
9346
|
+
check: () => everyTestFileNamesItsTier(),
|
|
9347
|
+
fix: `Rename each to <subject>[.<aspect>].<tier>.test.ts, naming one of ${TIER_NAMES.join(", ")}. Use tool for a test that reaches a program supplied by the environment, which is the tier for a retired .int. or .integration. file. Only the segment before .test. selects a project, so an untiered file runs under the residual unit project and reports success`
|
|
9348
|
+
},
|
|
9349
|
+
{
|
|
9350
|
+
name: "every test file is under a __tests__ directory",
|
|
9351
|
+
severity: "error",
|
|
9352
|
+
skip: () => describeConventionsGuardSkip(),
|
|
9353
|
+
check: () => everyTestFileSitsUnderTestsDir(),
|
|
9354
|
+
fix: "Move each into a __tests__ directory, the only place from which the shared Vitest config collects. Vitest does not collect a file outside one, so it runs nowhere and reports nothing. For a file that is not a test, declare checkTestFileConventions with its directory in `excludedBasenames` instead, which this check then defers to"
|
|
9355
|
+
},
|
|
9356
|
+
{
|
|
9357
|
+
name: "no package re-exports the ancestor Vitest config",
|
|
9358
|
+
severity: "recommend",
|
|
9359
|
+
quiet: true,
|
|
9360
|
+
check: () => noReExportOnlyVitestConfigs(),
|
|
9361
|
+
fix: "Delete these files. Vitest resolves config by walking up from the run root, so a per-package re-export is redundant"
|
|
9362
|
+
}
|
|
9363
|
+
];
|
|
9364
|
+
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
9365
|
+
const untieredFiles = findUntieredTestFiles(cwd);
|
|
9366
|
+
if (untieredFiles.length === 0) return true;
|
|
9367
|
+
return { ok: false, detail: formatPaths(untieredFiles) };
|
|
9142
9368
|
}
|
|
9143
|
-
function
|
|
9144
|
-
const
|
|
9145
|
-
if (
|
|
9146
|
-
return
|
|
9369
|
+
function everyTestFileSitsUnderTestsDir(cwd = process.cwd()) {
|
|
9370
|
+
const misplacedFiles = findMisplacedTestFiles(cwd);
|
|
9371
|
+
if (misplacedFiles.length === 0) return true;
|
|
9372
|
+
return { ok: false, detail: formatPaths(misplacedFiles) };
|
|
9147
9373
|
}
|
|
9148
|
-
function
|
|
9149
|
-
const cwd = process.cwd();
|
|
9150
|
-
const rootConfigs = findFiles([VITEST_CONFIG_PATTERN], cwd);
|
|
9151
|
-
if (rootConfigs.length === 0) return { ok: false, detail: "vitest.config.ts is missing" };
|
|
9374
|
+
function everyViteConfigHasVitestConfig() {
|
|
9152
9375
|
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
9153
9376
|
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
9154
|
-
const
|
|
9155
|
-
|
|
9156
|
-
(
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
}
|
|
9161
|
-
function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9162
|
-
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
9163
|
-
}
|
|
9164
|
-
var SET_CI_RUNS_ON_COMMAND = `gh variable set CI_RUNS_ON --body '"self-hosted"'`;
|
|
9165
|
-
var CODE_QUALITY_WORKFLOW = "williamthorsen/.github/.github/workflows/code-quality-pnpm-workflow.yaml";
|
|
9166
|
-
var MIN_CODE_QUALITY_MAJOR = 8;
|
|
9167
|
-
var GH_TIMEOUT_MS = 15e3;
|
|
9168
|
-
var IMAGE_ABSENT_PROGRAM_PATTERN = /(?:^|[\s;&|(])(corepack|gh|node|npm|npx|pnpm)(?=\s|$)/gm;
|
|
9169
|
-
var NEVER_INSTALLED_PROGRAMS = /* @__PURE__ */ new Set(["corepack", "gh"]);
|
|
9170
|
-
var statusByCwd = /* @__PURE__ */ new Map();
|
|
9171
|
-
async function describeSelfHostedRunnerSkip(cwd = process.cwd(), readFacts) {
|
|
9172
|
-
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
9173
|
-
return "skip" in status ? status.skip : false;
|
|
9174
|
-
}
|
|
9175
|
-
function findSelfHostedRunnerFailures(facts, cwd) {
|
|
9176
|
-
const failures = [];
|
|
9177
|
-
if (!facts.isPrivate) {
|
|
9178
|
-
failures.push(
|
|
9179
|
-
`${facts.nameWithOwner} is public: A self-hosted runner must never run code from a fork's pull request`
|
|
9180
|
-
);
|
|
9181
|
-
}
|
|
9182
|
-
if (facts.onlineSelfHostedRunners === 0) {
|
|
9183
|
-
failures.push(
|
|
9184
|
-
`${facts.nameWithOwner} does not have an online runner labelled self-hosted. When williamthorsen/hq allocates the repo slots in apps/runners/config/repos.yaml, the runners have not registered yet`
|
|
9185
|
-
);
|
|
9186
|
-
}
|
|
9187
|
-
const workflows = readWorkflows(cwd);
|
|
9188
|
-
failures.push(...findCodeQualityCallerDefects(workflows), ...findImageIncompatibleSteps(workflows));
|
|
9189
|
-
return failures;
|
|
9190
|
-
}
|
|
9191
|
-
async function readGitHubFacts(cwd, runGh = runGhCommand) {
|
|
9192
|
-
const repo = await runGh(["repo", "view", "--json", "nameWithOwner,visibility"], cwd);
|
|
9193
|
-
if (!repo.ok) return { skip: describeGhFailure(repo, "cannot resolve this repo on GitHub") };
|
|
9194
|
-
const repoView = parseJson(repo.stdout);
|
|
9195
|
-
if (!isRecord(repoView) || typeof repoView["nameWithOwner"] !== "string") {
|
|
9196
|
-
return { skip: "gh repo view returned an unexpected shape" };
|
|
9197
|
-
}
|
|
9198
|
-
const nameWithOwner = repoView["nameWithOwner"];
|
|
9199
|
-
const runners = await runGh(["api", `repos/${nameWithOwner}/actions/runners?per_page=100`], cwd);
|
|
9200
|
-
if (!runners.ok) {
|
|
9201
|
-
return {
|
|
9202
|
-
skip: describeGhFailure(runners, `cannot list the runners of ${nameWithOwner}, which needs admin access`)
|
|
9203
|
-
};
|
|
9204
|
-
}
|
|
9205
|
-
const repoVariable = await runGh(["api", `repos/${nameWithOwner}/actions/variables/CI_RUNS_ON`], cwd);
|
|
9206
|
-
if (!repoVariable.ok && !isNotFound(repoVariable)) {
|
|
9207
|
-
return { skip: describeGhFailure(repoVariable, `cannot read the CI_RUNS_ON variable of ${nameWithOwner}`) };
|
|
9208
|
-
}
|
|
9209
|
-
const repoValue = repoVariable.ok ? readVariableValue(parseJson(repoVariable.stdout)) : void 0;
|
|
9210
|
-
return {
|
|
9211
|
-
nameWithOwner,
|
|
9212
|
-
isPrivate: repoView["visibility"] !== "PUBLIC",
|
|
9213
|
-
onlineSelfHostedRunners: countOnlineSelfHostedRunners(parseJson(runners.stdout)),
|
|
9214
|
-
ciRunsOn: repoValue ?? await readOrganizationValue(nameWithOwner, cwd, runGh)
|
|
9215
|
-
};
|
|
9377
|
+
const unpairedConfigs = workspaceDiscovery.workspaces.flatMap((workspace) => {
|
|
9378
|
+
const viteConfigs = findWorkspaceConfigs(workspace, VITE_CONFIG_PATTERN);
|
|
9379
|
+
if (viteConfigs.length === 0) return [];
|
|
9380
|
+
return findWorkspaceConfigs(workspace, VITEST_CONFIG_PATTERN).length > 0 ? [] : viteConfigs;
|
|
9381
|
+
});
|
|
9382
|
+
if (unpairedConfigs.length === 0) return true;
|
|
9383
|
+
return { ok: false, detail: formatPaths(unpairedConfigs) };
|
|
9216
9384
|
}
|
|
9217
|
-
function
|
|
9218
|
-
|
|
9219
|
-
const
|
|
9220
|
-
if (
|
|
9221
|
-
|
|
9222
|
-
return labels.includes("self-hosted") ? "self-hosted" : "other";
|
|
9385
|
+
function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
9386
|
+
const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path7) => path7.includes("/"));
|
|
9387
|
+
const reExports = nonRootConfigs.filter((path7) => isOwnedByReExportCheck(cwd, path7));
|
|
9388
|
+
if (reExports.length === 0) return true;
|
|
9389
|
+
return { ok: false, detail: formatPaths(reExports) };
|
|
9223
9390
|
}
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9391
|
+
function noRetiredVitestConfigs(cwd = process.cwd()) {
|
|
9392
|
+
return checkNoMatchingFiles(
|
|
9393
|
+
[`**/vitest.standalone.config.${CONFIG_EXTENSIONS}`, `**/vitest.integration.config.${CONFIG_EXTENSIONS}`],
|
|
9394
|
+
cwd
|
|
9395
|
+
);
|
|
9396
|
+
}
|
|
9397
|
+
function testSuiteGatesTestFileConventions(cwd = process.cwd()) {
|
|
9398
|
+
if (findConventionsGuard(cwd) !== void 0) return true;
|
|
9228
9399
|
return {
|
|
9229
9400
|
ok: false,
|
|
9230
|
-
detail:
|
|
9231
|
-
fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${status.facts.nameWithOwner}`
|
|
9401
|
+
detail: `the test files under __tests__ do not import checkTestFileConventions from ${SHARED_TESTS_MODULE}`
|
|
9232
9402
|
};
|
|
9233
9403
|
}
|
|
9234
|
-
|
|
9235
|
-
const
|
|
9236
|
-
|
|
9237
|
-
if (
|
|
9238
|
-
|
|
9239
|
-
if (
|
|
9240
|
-
|
|
9404
|
+
function vitestConfigBuildsOnSharedConfig() {
|
|
9405
|
+
const cwd = process.cwd();
|
|
9406
|
+
const rootConfigs = findFiles([VITEST_CONFIG_PATTERN], cwd);
|
|
9407
|
+
if (rootConfigs.length === 0) return { ok: false, detail: "vitest.config.ts is missing" };
|
|
9408
|
+
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
9409
|
+
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
9410
|
+
const workspaceConfigs = workspaceDiscovery.workspaces.flatMap((workspace) => findWorkspaceConfigs(workspace, VITEST_CONFIG_PATTERN)).filter((relativePath) => !isOwnedByReExportCheck(cwd, relativePath));
|
|
9411
|
+
const stale = [...rootConfigs, ...workspaceConfigs].filter(
|
|
9412
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "defineVitestConfig", SHARED_VITEST_MODULE)
|
|
9413
|
+
);
|
|
9414
|
+
if (stale.length === 0) return true;
|
|
9415
|
+
return { ok: false, detail: formatPaths(stale) };
|
|
9241
9416
|
}
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
if ("skip" in status) return status.skip;
|
|
9245
|
-
if (status.selection === "unset") return "CI_RUNS_ON is unset";
|
|
9246
|
-
if (status.selection === "other") return `CI_RUNS_ON selects ${status.facts.ciRunsOn ?? ""}`;
|
|
9247
|
-
return false;
|
|
9417
|
+
function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9418
|
+
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
9248
9419
|
}
|
|
9249
|
-
|
|
9250
|
-
const
|
|
9251
|
-
if (
|
|
9252
|
-
|
|
9253
|
-
const conditions = status.exempt === void 0 ? status.failures : [`the repo is exempt: ${status.exempt}`, ...status.failures];
|
|
9254
|
-
if (status.selection !== "invalid" && conditions.length === 0) return true;
|
|
9255
|
-
const invalidValue = `CI_RUNS_ON is ${facts.ciRunsOn ?? ""}, which is not JSON, so every workflow that reads it with fromJSON fails`;
|
|
9256
|
-
if (conditions.length === 0) {
|
|
9257
|
-
return { ok: false, detail: invalidValue, fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${facts.nameWithOwner}` };
|
|
9420
|
+
function checkRootVitestConfig(baseName, exportName, cwd) {
|
|
9421
|
+
const matches = findFiles([`${baseName}.${CONFIG_EXTENSIONS}`], cwd);
|
|
9422
|
+
if (matches.length === 0) {
|
|
9423
|
+
return { ok: false, detail: `${baseName}.ts is missing` };
|
|
9258
9424
|
}
|
|
9259
|
-
const
|
|
9425
|
+
const staleConfigs = matches.filter(
|
|
9426
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), exportName, SHARED_VITEST_MODULE)
|
|
9427
|
+
);
|
|
9428
|
+
if (staleConfigs.length === 0) return true;
|
|
9260
9429
|
return {
|
|
9261
9430
|
ok: false,
|
|
9262
|
-
detail:
|
|
9263
|
-
fix: `Fix each condition, or return the repo's CI to ubuntu-latest: gh variable delete CI_RUNS_ON --repo ${facts.nameWithOwner}`
|
|
9431
|
+
detail: `does not import ${exportName} from ${SHARED_VITEST_MODULE}: ${staleConfigs.join(", ")}`
|
|
9264
9432
|
};
|
|
9265
9433
|
}
|
|
9266
|
-
function
|
|
9267
|
-
|
|
9268
|
-
return
|
|
9269
|
-
(runner) => isRecord(runner) && runner["status"] === "online" && Array.isArray(runner["labels"]) && runner["labels"].some((label) => isRecord(label) && label["name"] === "self-hosted")
|
|
9270
|
-
).length;
|
|
9271
|
-
}
|
|
9272
|
-
function describeGhFailure(result, context) {
|
|
9273
|
-
if (result.isMissing) return "gh is not installed";
|
|
9274
|
-
if (/gh auth login|not logged in/i.test(result.stderr)) return "gh is not authenticated";
|
|
9275
|
-
const firstLine = result.stderr.trim().split("\n", 1)[0] ?? "";
|
|
9276
|
-
return firstLine === "" ? `gh ${context}` : `gh ${context}: ${firstLine}`;
|
|
9277
|
-
}
|
|
9278
|
-
function findCodeQualityCallerDefects(workflows) {
|
|
9279
|
-
const defects = [];
|
|
9280
|
-
for (const { file, jobs } of workflows) {
|
|
9281
|
-
for (const [jobName, job] of jobs) {
|
|
9282
|
-
const uses = job["uses"];
|
|
9283
|
-
if (typeof uses !== "string" || !uses.startsWith(`${CODE_QUALITY_WORKFLOW}@`)) continue;
|
|
9284
|
-
const ref = uses.slice(CODE_QUALITY_WORKFLOW.length + 1);
|
|
9285
|
-
const major = /^v(\d+)(?:\.\d+){0,2}$/.exec(ref)?.[1];
|
|
9286
|
-
if (major === void 0) {
|
|
9287
|
-
defects.push(
|
|
9288
|
-
`${file} job ${jobName} pins the code-quality workflow at ${ref}, whose release cannot be determined`
|
|
9289
|
-
);
|
|
9290
|
-
} else if (Number(major) < MIN_CODE_QUALITY_MAJOR) {
|
|
9291
|
-
defects.push(
|
|
9292
|
-
`${file} job ${jobName} pins the code-quality workflow at ${ref}, which predates CI_RUNS_ON; pin v${MIN_CODE_QUALITY_MAJOR} or later`
|
|
9293
|
-
);
|
|
9294
|
-
}
|
|
9295
|
-
const inputs = job["with"];
|
|
9296
|
-
if (isRecord(inputs) && inputs["runs-on"] !== void 0) {
|
|
9297
|
-
defects.push(`${file} job ${jobName} passes runs-on, which overrides CI_RUNS_ON`);
|
|
9298
|
-
}
|
|
9299
|
-
}
|
|
9300
|
-
}
|
|
9301
|
-
return defects;
|
|
9434
|
+
function describeConventionsGuardSkip() {
|
|
9435
|
+
const guard = findConventionsGuard(process.cwd());
|
|
9436
|
+
return guard !== void 0 && `reported by checkTestFileConventions in ${guard}`;
|
|
9302
9437
|
}
|
|
9303
|
-
function
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
if (!/\bvars\.CI_RUNS_ON\b/.test(JSON.stringify(job["runs-on"] ?? ""))) continue;
|
|
9308
|
-
const steps = job["steps"];
|
|
9309
|
-
if (!Array.isArray(steps)) continue;
|
|
9310
|
-
const installed = /* @__PURE__ */ new Set();
|
|
9311
|
-
for (const [index, step] of steps.entries()) {
|
|
9312
|
-
if (!isRecord(step)) continue;
|
|
9313
|
-
const stepLabel = typeof step["name"] === "string" ? step["name"] : `step ${index + 1}`;
|
|
9314
|
-
const run = step["run"];
|
|
9315
|
-
if (typeof run === "string") {
|
|
9316
|
-
const missing = findUninstalledPrograms(run, installed);
|
|
9317
|
-
if (missing.length > 0) {
|
|
9318
|
-
defects.push(
|
|
9319
|
-
`${file} job ${jobName}, ${stepLabel}: runs ${missing.join(", ")}, which the runner image lacks at that step`
|
|
9320
|
-
);
|
|
9321
|
-
}
|
|
9322
|
-
}
|
|
9323
|
-
for (const program of readInstalledPrograms(step)) installed.add(program);
|
|
9324
|
-
}
|
|
9325
|
-
}
|
|
9326
|
-
}
|
|
9327
|
-
return defects;
|
|
9438
|
+
function findConventionsGuard(cwd) {
|
|
9439
|
+
return findTestFiles(cwd).find(
|
|
9440
|
+
(relativePath) => importsSharedExport(readFileIn(cwd, relativePath), "checkTestFileConventions", SHARED_TESTS_MODULE)
|
|
9441
|
+
);
|
|
9328
9442
|
}
|
|
9329
|
-
function
|
|
9330
|
-
|
|
9331
|
-
for (const match of run.matchAll(IMAGE_ABSENT_PROGRAM_PATTERN)) {
|
|
9332
|
-
const program = match[1];
|
|
9333
|
-
if (program === void 0) continue;
|
|
9334
|
-
const provider = program === "npm" || program === "npx" ? "node" : program;
|
|
9335
|
-
if (NEVER_INSTALLED_PROGRAMS.has(program) || !installed.has(provider)) programs.add(program);
|
|
9336
|
-
}
|
|
9337
|
-
return [...programs].toSorted();
|
|
9443
|
+
function findWorkspaceConfigs(workspace, pattern) {
|
|
9444
|
+
return findFiles([pattern], workspace.absolutePath).map((name) => `${workspace.dir}/${name}`);
|
|
9338
9445
|
}
|
|
9339
|
-
function
|
|
9340
|
-
|
|
9341
|
-
let status = statusByCwd.get(cwd);
|
|
9342
|
-
if (status === void 0) {
|
|
9343
|
-
status = gatherSelfHostedRunnerStatus(cwd, (dir) => readGitHubFacts(dir));
|
|
9344
|
-
statusByCwd.set(cwd, status);
|
|
9345
|
-
}
|
|
9346
|
-
return status;
|
|
9446
|
+
function hasViteConfigBeside(cwd, relativePath) {
|
|
9447
|
+
return findFiles([VITE_CONFIG_PATTERN], join4(cwd, dirname(relativePath))).length > 0;
|
|
9347
9448
|
}
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
if ("skip" in facts) return facts;
|
|
9351
|
-
const failures = findSelfHostedRunnerFailures(facts, cwd);
|
|
9352
|
-
let exempt;
|
|
9353
|
-
try {
|
|
9354
|
-
exempt = (await loadRootConfig(cwd)).selfHostedRunner?.exempt;
|
|
9355
|
-
} catch (error) {
|
|
9356
|
-
failures.push(`the nmr config cannot be loaded: ${describeError(error)}`);
|
|
9357
|
-
}
|
|
9358
|
-
return {
|
|
9359
|
-
facts,
|
|
9360
|
-
selection: readRunnerSelection(facts.ciRunsOn),
|
|
9361
|
-
failures,
|
|
9362
|
-
exempt,
|
|
9363
|
-
toolTestFiles: findTestFiles(cwd).filter(
|
|
9364
|
-
(file) => file.endsWith(".tool.test.ts") || file.endsWith(".tool.test.tsx")
|
|
9365
|
-
)
|
|
9366
|
-
};
|
|
9449
|
+
function isOwnedByReExportCheck(cwd, relativePath) {
|
|
9450
|
+
return isReExportOnly(readFileIn(cwd, relativePath)) && !hasViteConfigBeside(cwd, relativePath);
|
|
9367
9451
|
}
|
|
9368
|
-
function
|
|
9369
|
-
|
|
9452
|
+
function isReExportOnly(content) {
|
|
9453
|
+
if (content === void 0) return false;
|
|
9454
|
+
const statements = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
9455
|
+
return statements.length > 0 && statements.every((line) => RE_EXPORT_LINE_PATTERN.test(line));
|
|
9370
9456
|
}
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9457
|
+
|
|
9458
|
+
// .readyup/lib/workspace-build.ts
|
|
9459
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
9460
|
+
import { join as join5 } from "node:path";
|
|
9461
|
+
import { fileExists as fileExists3, readFile as readFile5 } from "readyup/check-utils";
|
|
9462
|
+
var buildReadinessChecks = [
|
|
9463
|
+
{
|
|
9464
|
+
name: "all workspace packages can build",
|
|
9465
|
+
severity: "warn",
|
|
9466
|
+
check: allWorkspacePackagesCanBuild,
|
|
9467
|
+
fix: `Add "build": ":" to packages that don't need a build, or ensure packages that use the default nmr build have a tsconfig.json and a src/ directory`
|
|
9376
9468
|
}
|
|
9377
|
-
|
|
9378
|
-
function
|
|
9379
|
-
const
|
|
9380
|
-
if (
|
|
9381
|
-
const
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
if (!result.ok) return void 0;
|
|
9393
|
-
const response = parseJson(result.stdout);
|
|
9394
|
-
if (!isRecord(response) || !Array.isArray(response["variables"])) return void 0;
|
|
9395
|
-
const variable = response["variables"].find(
|
|
9396
|
-
(entry) => isRecord(entry) && entry["name"] === "CI_RUNS_ON"
|
|
9397
|
-
);
|
|
9398
|
-
return readVariableValue(variable);
|
|
9399
|
-
}
|
|
9400
|
-
function readVariableValue(variable) {
|
|
9401
|
-
return isRecord(variable) && typeof variable["value"] === "string" ? variable["value"] : void 0;
|
|
9402
|
-
}
|
|
9403
|
-
function readWorkflows(cwd) {
|
|
9404
|
-
const dir = join(cwd, ".github", "workflows");
|
|
9405
|
-
if (!existsSync3(dir)) return [];
|
|
9406
|
-
const workflows = [];
|
|
9407
|
-
const files = readdirSync2(dir).filter((name) => /\.ya?ml$/.test(name)).toSorted();
|
|
9408
|
-
for (const file of files) {
|
|
9409
|
-
let document;
|
|
9410
|
-
try {
|
|
9411
|
-
document = (0, import_yaml2.parse)(readFile(join(dir, file)) ?? "");
|
|
9412
|
-
} catch {
|
|
9413
|
-
continue;
|
|
9469
|
+
];
|
|
9470
|
+
function allWorkspacePackagesCanBuild() {
|
|
9471
|
+
const packagesDir = join5(process.cwd(), "packages");
|
|
9472
|
+
if (!existsSync5(packagesDir)) return true;
|
|
9473
|
+
const entries = readdirSync4(packagesDir, { withFileTypes: true });
|
|
9474
|
+
const failingPackages = [];
|
|
9475
|
+
for (const entry of entries) {
|
|
9476
|
+
if (!entry.isDirectory()) continue;
|
|
9477
|
+
const pkgPath = `packages/${entry.name}/package.json`;
|
|
9478
|
+
const content = readFile5(pkgPath);
|
|
9479
|
+
if (!content) continue;
|
|
9480
|
+
const hasBuildOverride = /"build"\s*:/.test(content);
|
|
9481
|
+
const hasDefaultBuildInputs = fileExists3(`packages/${entry.name}/tsconfig.json`) && existsSync5(join5(packagesDir, entry.name, "src"));
|
|
9482
|
+
if (!hasBuildOverride && !hasDefaultBuildInputs) {
|
|
9483
|
+
failingPackages.push(entry.name);
|
|
9414
9484
|
}
|
|
9415
|
-
if (!isRecord(document) || !isRecord(document["jobs"])) continue;
|
|
9416
|
-
const jobs = Object.entries(document["jobs"]).filter(
|
|
9417
|
-
(entry) => isRecord(entry[1])
|
|
9418
|
-
);
|
|
9419
|
-
workflows.push({ file: posix.join(".github/workflows", file), jobs });
|
|
9420
9485
|
}
|
|
9421
|
-
return
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
if (error === null) {
|
|
9427
|
-
resolve({ ok: true, stdout });
|
|
9428
|
-
return;
|
|
9429
|
-
}
|
|
9430
|
-
resolve({ ok: false, isMissing: "code" in error && error.code === "ENOENT", stderr: stderr || error.message });
|
|
9431
|
-
});
|
|
9432
|
-
});
|
|
9486
|
+
if (failingPackages.length === 0) return true;
|
|
9487
|
+
return {
|
|
9488
|
+
ok: false,
|
|
9489
|
+
detail: `missing build override or tsconfig.json + src/: ${failingPackages.join(", ")}`
|
|
9490
|
+
};
|
|
9433
9491
|
}
|
|
9492
|
+
|
|
9493
|
+
// .readyup/kits/default.ts
|
|
9494
|
+
var default_default = defineRdyKit({
|
|
9495
|
+
checklists: [
|
|
9496
|
+
{
|
|
9497
|
+
name: "nmr",
|
|
9498
|
+
checks: [
|
|
9499
|
+
// -- Setup --
|
|
9500
|
+
...setupChecks,
|
|
9501
|
+
// -- Root script cleanup --
|
|
9502
|
+
...rootScriptChecks,
|
|
9503
|
+
// -- Git hooks --
|
|
9504
|
+
...gitHookChecks,
|
|
9505
|
+
// -- Workspace build readiness --
|
|
9506
|
+
...buildReadinessChecks,
|
|
9507
|
+
// -- Bin targets --
|
|
9508
|
+
...binTargetChecks,
|
|
9509
|
+
// -- Vitest projects --
|
|
9510
|
+
...vitestChecks,
|
|
9511
|
+
// -- Shared Prettier config --
|
|
9512
|
+
...prettierChecks,
|
|
9513
|
+
// -- Shared upgrade policy --
|
|
9514
|
+
...tazeChecks,
|
|
9515
|
+
// -- Audit dependency --
|
|
9516
|
+
{
|
|
9517
|
+
name: "v11y-check in devDependencies",
|
|
9518
|
+
severity: "warn",
|
|
9519
|
+
check: () => hasDevDependency2("v11y-check"),
|
|
9520
|
+
fix: "pnpm add --save-dev v11y-check"
|
|
9521
|
+
},
|
|
9522
|
+
// -- Legacy script runner --
|
|
9523
|
+
...legacyRunnerChecks
|
|
9524
|
+
]
|
|
9525
|
+
},
|
|
9526
|
+
selfHostedRunnerChecklist
|
|
9527
|
+
]
|
|
9528
|
+
});
|
|
9434
9529
|
export {
|
|
9435
|
-
|
|
9436
|
-
default_default as default,
|
|
9437
|
-
describeReadinessSkip,
|
|
9438
|
-
describeRecommendationSkip,
|
|
9439
|
-
describeSelfHostedRunnerSkip,
|
|
9440
|
-
everyBinTargetIsACommittedWrapper,
|
|
9441
|
-
everyBinWrapperTargetIsCoveredByFiles,
|
|
9442
|
-
everyTestFileNamesItsTier,
|
|
9443
|
-
everyTestFileSitsUnderTestsDir,
|
|
9444
|
-
everyViteConfigHasVitestConfig,
|
|
9445
|
-
findSelfHostedRunnerFailures,
|
|
9446
|
-
hasSupportedEslintVersion,
|
|
9447
|
-
hasSupportedStrictLintVersion,
|
|
9448
|
-
noPnpmFieldInPackageJson,
|
|
9449
|
-
noReExportOnlyVitestConfigs,
|
|
9450
|
-
noRetiredVitestConfigs,
|
|
9451
|
-
noUnguardedLefthookInstall,
|
|
9452
|
-
noWorkspacesFieldInPackageJson,
|
|
9453
|
-
prettierConfigBuildsOnSharedConfig,
|
|
9454
|
-
readGitHubFacts,
|
|
9455
|
-
readRunnerSelection,
|
|
9456
|
-
recommendSelfHostedRunner,
|
|
9457
|
-
selfHostedRunnerCanRunCi,
|
|
9458
|
-
tazeConfigAvoidsClobberedOptions,
|
|
9459
|
-
tazeConfigBuildsOnSharedConfig,
|
|
9460
|
-
testSuiteGatesTestFileConventions,
|
|
9461
|
-
vitestConfigBuildsOnSharedConfig,
|
|
9462
|
-
vitestRootConfigBuildsOnSharedConfig
|
|
9530
|
+
default_default as default
|
|
9463
9531
|
};
|