@williamthorsen/nmr 0.48.0 → 0.50.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 +218 -0
- package/.readyup/kits/default.js +1527 -1460
- package/.readyup/manifest.json +96 -11
- package/CHANGELOG.md +65 -0
- package/README.md +19 -8
- package/agents/guidance/rulebooks/nmr.md +2 -2
- 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 +191 -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 -3
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- 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 +63 -0
- package/dist/esm/remote-probe.js +88 -0
- package/dist/esm/remote-run.d.ts +45 -65
- package/dist/esm/remote-run.js +128 -162
- 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 +39 -1002
- 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 +1 -1
- package/dist/esm/verdict.js +1 -1
- package/docs/check-cache.md +28 -12
- package/docs/delegate.md +103 -17
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +4 -4
- /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,302 +7399,191 @@ 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
|
-
|
|
7449
|
-
|
|
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"
|
|
7450
7449
|
}
|
|
7451
|
-
|
|
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) };
|
|
7452
7464
|
}
|
|
7453
|
-
function
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
previousRow = completedRow;
|
|
7472
|
-
}
|
|
7473
|
-
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) };
|
|
7474
7483
|
}
|
|
7475
|
-
function
|
|
7476
|
-
|
|
7477
|
-
if (value === void 0) {
|
|
7478
|
-
throw new RangeError(`No cell at index ${index} in a row of ${row.length}`);
|
|
7479
|
-
}
|
|
7480
|
-
return value;
|
|
7484
|
+
function describeBinEntry(workspace, entry) {
|
|
7485
|
+
return `${workspace.name ?? workspace.dir}:${entry.command} -> ${entry.target}`;
|
|
7481
7486
|
}
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
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";
|
|
7486
7491
|
}
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
function isObject(value) {
|
|
7490
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7492
|
+
function normalizeBinTarget(target) {
|
|
7493
|
+
return target.replace(/^\.\//, "");
|
|
7491
7494
|
}
|
|
7492
|
-
function
|
|
7493
|
-
|
|
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));
|
|
7494
7516
|
}
|
|
7495
7517
|
|
|
7496
|
-
//
|
|
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
|
|
7497
7524
|
var ENV_ASSIGNMENT = /^\w+=/;
|
|
7498
|
-
var LAUNCHERS = /* @__PURE__ */ new Map([
|
|
7499
|
-
["bun", /* @__PURE__ */ new Set(["x"])],
|
|
7500
|
-
["bunx", /* @__PURE__ */ new Set()],
|
|
7501
|
-
["npm", /* @__PURE__ */ new Set(["exec"])],
|
|
7502
|
-
["npx", /* @__PURE__ */ new Set()],
|
|
7503
|
-
["pnpm", /* @__PURE__ */ new Set(["dlx", "exec"])],
|
|
7504
|
-
["yarn", /* @__PURE__ */ new Set(["dlx", "exec"])]
|
|
7505
|
-
]);
|
|
7506
7525
|
var QUOTES = /* @__PURE__ */ new Set(['"', "'"]);
|
|
7507
7526
|
var SEGMENT_SEPARATORS = /* @__PURE__ */ new Set([";", "|", "&", "\n", "\r"]);
|
|
7508
7527
|
var SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
|
|
7509
7528
|
var TOKEN_SEPARATORS = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
7510
|
-
function
|
|
7511
|
-
|
|
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);
|
|
7512
7535
|
}
|
|
7513
7536
|
function quoteToken(token) {
|
|
7514
|
-
if (
|
|
7537
|
+
if (isShellSafeToken(token)) {
|
|
7515
7538
|
return token;
|
|
7516
7539
|
}
|
|
7517
7540
|
return "'" + token.replaceAll("'", String.raw`'\''`) + "'";
|
|
7518
7541
|
}
|
|
7519
|
-
function
|
|
7520
|
-
const
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
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;
|
|
7527
7559
|
}
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
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;
|
|
7539
7584
|
}
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
function readStepListSelfReference(options) {
|
|
7543
|
-
const { commandName, elements, scope } = options;
|
|
7544
|
-
const doesReenter = elements.some((element) => {
|
|
7545
|
-
const target = readNmrTarget(tokenize(element));
|
|
7546
|
-
return target !== void 0 && isReentry(target, commandName, scope);
|
|
7547
|
-
});
|
|
7548
|
-
if (!doesReenter) {
|
|
7549
|
-
return void 0;
|
|
7550
|
-
}
|
|
7551
|
-
return elements.length === 1 ? "sole" : "chained";
|
|
7552
|
-
}
|
|
7553
|
-
function dropLeadingAssignments(tokens) {
|
|
7554
|
-
const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
|
|
7555
|
-
return start === -1 ? [] : tokens.slice(start);
|
|
7556
|
-
}
|
|
7557
|
-
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
7558
|
-
if (char !== "&" && char !== "|") {
|
|
7559
|
-
return false;
|
|
7560
|
-
}
|
|
7561
|
-
return char === "&" && nextChar === ">" || /[<>]\s*$/.test(precedingText);
|
|
7562
|
-
}
|
|
7563
|
-
function isReentry(target, commandName, scope) {
|
|
7564
|
-
if (target.command !== commandName) {
|
|
7565
|
-
return false;
|
|
7566
|
-
}
|
|
7567
|
-
switch (scope) {
|
|
7568
|
-
case "root":
|
|
7569
|
-
return !target.isDelegate;
|
|
7570
|
-
case "package":
|
|
7571
|
-
return target.isRecursive || !target.isDelegate && !target.isWorkspaceRoot;
|
|
7572
|
-
case "workspaceRegistry":
|
|
7573
|
-
return target.isDelegate || !target.isWorkspaceRoot;
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
function readLauncherScope(launcher, launcherArgs) {
|
|
7577
|
-
const scope = { isDelegate: false, isRecursive: false, isWorkspaceRoot: false };
|
|
7578
|
-
if (launcher !== "pnpm") {
|
|
7579
|
-
return scope;
|
|
7580
|
-
}
|
|
7581
|
-
for (const token of launcherArgs) {
|
|
7582
|
-
if (token === "-r" || token === "--recursive") {
|
|
7583
|
-
scope.isDelegate = true;
|
|
7584
|
-
scope.isRecursive = true;
|
|
7585
|
-
} else if (token === "-F" || token === "--filter" || token.startsWith("--filter=")) {
|
|
7586
|
-
scope.isDelegate = true;
|
|
7587
|
-
} else if (token === "-w" || token === "--workspace-root") {
|
|
7588
|
-
scope.isWorkspaceRoot = true;
|
|
7589
|
-
}
|
|
7590
|
-
}
|
|
7591
|
-
return scope;
|
|
7592
|
-
}
|
|
7593
|
-
function readNmrTail(segment) {
|
|
7594
|
-
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
7595
|
-
if (head === void 0) {
|
|
7596
|
-
return void 0;
|
|
7597
|
-
}
|
|
7598
|
-
if (head === "nmr") {
|
|
7599
|
-
return { launcher: void 0, launcherArgs: [], tail: rest };
|
|
7600
|
-
}
|
|
7601
|
-
const subcommands = LAUNCHERS.get(head);
|
|
7602
|
-
if (subcommands === void 0) {
|
|
7603
|
-
return void 0;
|
|
7604
|
-
}
|
|
7605
|
-
const nmrIndex = subcommands.size === 0 ? rest.findIndex((token) => !token.startsWith("-")) : rest.findIndex((token) => subcommands.has(token)) + 1;
|
|
7606
|
-
if (nmrIndex <= 0 && subcommands.size > 0) {
|
|
7607
|
-
return void 0;
|
|
7608
|
-
}
|
|
7609
|
-
return rest[nmrIndex] === "nmr" ? { launcher: head, launcherArgs: rest.slice(0, nmrIndex), tail: rest.slice(nmrIndex + 1) } : void 0;
|
|
7610
|
-
}
|
|
7611
|
-
function readNmrTarget(tokens) {
|
|
7612
|
-
let isDelegate = false;
|
|
7613
|
-
let isRecursive = false;
|
|
7614
|
-
let isWorkspaceRoot = false;
|
|
7615
|
-
let index = 0;
|
|
7616
|
-
while (index < tokens.length) {
|
|
7617
|
-
const token = tokens[index] ?? "";
|
|
7618
|
-
switch (token) {
|
|
7619
|
-
// The pattern is the flag's value, and reading it as a command name would name whichever package it
|
|
7620
|
-
// selects rather than the command that every selected scope runs.
|
|
7621
|
-
case "-F":
|
|
7622
|
-
case "--filter":
|
|
7623
|
-
isDelegate = true;
|
|
7624
|
-
index += 2;
|
|
7625
|
-
break;
|
|
7626
|
-
case "-R":
|
|
7627
|
-
case "--recursive":
|
|
7628
|
-
isDelegate = true;
|
|
7629
|
-
isRecursive = true;
|
|
7630
|
-
index += 1;
|
|
7631
|
-
break;
|
|
7632
|
-
case "-w":
|
|
7633
|
-
case "--workspace-root":
|
|
7634
|
-
isWorkspaceRoot = true;
|
|
7635
|
-
index += 1;
|
|
7636
|
-
break;
|
|
7637
|
-
default:
|
|
7638
|
-
if (!token.startsWith("-")) {
|
|
7639
|
-
return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
|
|
7640
|
-
}
|
|
7641
|
-
index += 1;
|
|
7642
|
-
}
|
|
7643
|
-
}
|
|
7644
|
-
return void 0;
|
|
7645
|
-
}
|
|
7646
|
-
function splitSegments(command) {
|
|
7647
|
-
const segments = [];
|
|
7648
|
-
let currentSegment = "";
|
|
7649
|
-
let quote;
|
|
7650
|
-
let index = 0;
|
|
7651
|
-
while (index < command.length) {
|
|
7652
|
-
const char = command[index] ?? "";
|
|
7653
|
-
if (quote !== void 0) {
|
|
7654
|
-
if (char === "\\" && quote === '"') {
|
|
7655
|
-
currentSegment += char + (command[index + 1] ?? "");
|
|
7656
|
-
index += 2;
|
|
7657
|
-
continue;
|
|
7658
|
-
}
|
|
7659
|
-
currentSegment += char;
|
|
7660
|
-
if (char === quote) quote = void 0;
|
|
7661
|
-
index += 1;
|
|
7662
|
-
continue;
|
|
7663
|
-
}
|
|
7664
|
-
if (char === "\\") {
|
|
7665
|
-
currentSegment += char + (command[index + 1] ?? "");
|
|
7666
|
-
index += 2;
|
|
7667
|
-
continue;
|
|
7668
|
-
}
|
|
7669
|
-
if (QUOTES.has(char)) {
|
|
7670
|
-
quote = char;
|
|
7671
|
-
currentSegment += char;
|
|
7672
|
-
index += 1;
|
|
7673
|
-
continue;
|
|
7674
|
-
}
|
|
7675
|
-
if (SEGMENT_SEPARATORS.has(char)) {
|
|
7676
|
-
if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
|
|
7677
|
-
currentSegment += char;
|
|
7678
|
-
index += 1;
|
|
7679
|
-
continue;
|
|
7680
|
-
}
|
|
7681
|
-
index += command[index + 1] === char ? 2 : 1;
|
|
7682
|
-
segments.push(currentSegment);
|
|
7683
|
-
currentSegment = "";
|
|
7684
|
-
continue;
|
|
7685
|
-
}
|
|
7686
|
-
currentSegment += char;
|
|
7687
|
-
index += 1;
|
|
7688
|
-
}
|
|
7689
|
-
segments.push(currentSegment);
|
|
7690
|
-
return segments;
|
|
7585
|
+
segments.push(currentSegment);
|
|
7586
|
+
return segments;
|
|
7691
7587
|
}
|
|
7692
7588
|
function tokenize(element) {
|
|
7693
7589
|
return element.split(/\s+/).filter((token) => token.length > 0);
|
|
@@ -7737,6 +7633,12 @@ function tokenizeSegment(segment) {
|
|
|
7737
7633
|
}
|
|
7738
7634
|
return tokens;
|
|
7739
7635
|
}
|
|
7636
|
+
function isRedirectionOperator(char, precedingText, nextChar) {
|
|
7637
|
+
if (char !== "&" && char !== "|") {
|
|
7638
|
+
return false;
|
|
7639
|
+
}
|
|
7640
|
+
return char === "&" && nextChar === ">" || /[<>]\s*$/.test(precedingText);
|
|
7641
|
+
}
|
|
7740
7642
|
|
|
7741
7643
|
// src/default-scripts.ts
|
|
7742
7644
|
var GATE_PROJECTS = "--project unit --project tool";
|
|
@@ -7822,12 +7724,12 @@ function buildRootScripts(workspaceGlobs) {
|
|
|
7822
7724
|
|
|
7823
7725
|
// ../nmr-core/dist/esm/workspace.js
|
|
7824
7726
|
import { existsSync, readFileSync } from "node:fs";
|
|
7825
|
-
import
|
|
7727
|
+
import path2 from "node:path";
|
|
7826
7728
|
var import_yaml = __toESM(require_dist(), 1);
|
|
7827
7729
|
|
|
7828
7730
|
// ../nmr-core/dist/esm/helpers/workspace-patterns.js
|
|
7829
|
-
import { globSync } from "node:fs";
|
|
7830
|
-
import
|
|
7731
|
+
import { globSync as globSync2 } from "node:fs";
|
|
7732
|
+
import path from "node:path";
|
|
7831
7733
|
var MANIFEST = "package.json";
|
|
7832
7734
|
var ALWAYS_EXCLUDED = ["**/node_modules/**"];
|
|
7833
7735
|
function matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns) {
|
|
@@ -7836,8 +7738,8 @@ function matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns) {
|
|
|
7836
7738
|
exclude: [...excludedPatterns, ...ALWAYS_EXCLUDED],
|
|
7837
7739
|
followSymlinks: true
|
|
7838
7740
|
};
|
|
7839
|
-
const matches =
|
|
7840
|
-
const dirs = matches.map((match) =>
|
|
7741
|
+
const matches = globSync2([...includedPatterns], options);
|
|
7742
|
+
const dirs = matches.map((match) => path.dirname(path.resolve(monorepoRoot, match)));
|
|
7841
7743
|
return [...new Set(dirs)].toSorted();
|
|
7842
7744
|
}
|
|
7843
7745
|
function resolvePackageDirs(monorepoRoot, patterns) {
|
|
@@ -7866,7 +7768,7 @@ function buildManifestPattern(pattern) {
|
|
|
7866
7768
|
// ../nmr-core/dist/esm/workspace.js
|
|
7867
7769
|
var WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
|
|
7868
7770
|
function isMonorepoRoot(dir) {
|
|
7869
|
-
return existsSync(
|
|
7771
|
+
return existsSync(path2.join(dir, WORKSPACE_MANIFEST));
|
|
7870
7772
|
}
|
|
7871
7773
|
function resolveWorkspace(monorepoRoot) {
|
|
7872
7774
|
const manifestRead = readWorkspaceManifest(monorepoRoot);
|
|
@@ -7899,7 +7801,7 @@ function diagnoseEmptyCause(monorepoRoot, patterns) {
|
|
|
7899
7801
|
return "no-package";
|
|
7900
7802
|
}
|
|
7901
7803
|
function readDeclaredPackages(parsedManifest) {
|
|
7902
|
-
if (!
|
|
7804
|
+
if (!isObject(parsedManifest))
|
|
7903
7805
|
return { kind: "absent" };
|
|
7904
7806
|
const packages = parsedManifest["packages"];
|
|
7905
7807
|
if (packages === void 0 || packages === null)
|
|
@@ -7910,11 +7812,11 @@ function readDeclaredPackages(parsedManifest) {
|
|
|
7910
7812
|
return { kind: "unreadable" };
|
|
7911
7813
|
return packages.length === 0 ? { kind: "absent" } : { kind: "patterns", patterns: packages };
|
|
7912
7814
|
}
|
|
7913
|
-
function
|
|
7815
|
+
function isObject(value) {
|
|
7914
7816
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7915
7817
|
}
|
|
7916
7818
|
function readWorkspaceManifest(monorepoRoot) {
|
|
7917
|
-
const workspaceFile =
|
|
7819
|
+
const workspaceFile = path2.join(monorepoRoot, WORKSPACE_MANIFEST);
|
|
7918
7820
|
if (!existsSync(workspaceFile)) {
|
|
7919
7821
|
return { kind: "absent" };
|
|
7920
7822
|
}
|
|
@@ -7944,9 +7846,9 @@ function getWorkspacePackageDirs(monorepoRoot) {
|
|
|
7944
7846
|
}
|
|
7945
7847
|
|
|
7946
7848
|
// src/workspace-globs.ts
|
|
7947
|
-
import
|
|
7849
|
+
import path3 from "node:path";
|
|
7948
7850
|
function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
|
|
7949
|
-
return packageDirs.map((dir) =>
|
|
7851
|
+
return packageDirs.map((dir) => path3.relative(monorepoRoot, dir).split(path3.sep).join("/")).filter((relativeDir) => relativeDir !== "").map((relativeDir) => `${relativeDir}/**`).toSorted();
|
|
7950
7852
|
}
|
|
7951
7853
|
|
|
7952
7854
|
// src/resolve-scripts.ts
|
|
@@ -7959,233 +7861,399 @@ function getDefaultRootScripts(monorepoRoot) {
|
|
|
7959
7861
|
return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
|
|
7960
7862
|
}
|
|
7961
7863
|
|
|
7962
|
-
//
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
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"
|
|
7977
7883
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
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`"
|
|
7986
7892
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
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'
|
|
7996
7908
|
}
|
|
7997
|
-
|
|
7998
|
-
|
|
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) };
|
|
7999
7920
|
}
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
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
|
+
};
|
|
8004
7933
|
}
|
|
8005
|
-
function
|
|
8006
|
-
|
|
8007
|
-
|
|
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
|
+
}
|
|
8008
7946
|
}
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
7947
|
+
if (matches.length === 0) return true;
|
|
7948
|
+
return {
|
|
7949
|
+
ok: false,
|
|
7950
|
+
detail: `found in: ${matches.join(", ")}`
|
|
7951
|
+
};
|
|
8014
7952
|
}
|
|
8015
|
-
function
|
|
8016
|
-
const
|
|
8017
|
-
|
|
7953
|
+
function readRootScripts(cwd) {
|
|
7954
|
+
const content = readFileIn(cwd, "package.json");
|
|
7955
|
+
if (content === void 0) return {};
|
|
7956
|
+
let parsedManifest;
|
|
8018
7957
|
try {
|
|
8019
|
-
|
|
8020
|
-
} catch
|
|
8021
|
-
|
|
8022
|
-
return void 0;
|
|
8023
|
-
}
|
|
8024
|
-
throw error;
|
|
7958
|
+
parsedManifest = JSON.parse(content);
|
|
7959
|
+
} catch {
|
|
7960
|
+
return {};
|
|
8025
7961
|
}
|
|
8026
|
-
|
|
8027
|
-
if (!isObject(parsedManifest)) return void 0;
|
|
7962
|
+
if (!isRecord2(parsedManifest)) return {};
|
|
8028
7963
|
const scripts = parsedManifest["scripts"];
|
|
8029
|
-
|
|
8030
|
-
return readScriptRecord(packageDir, scripts);
|
|
7964
|
+
return isRecord2(scripts) ? scripts : {};
|
|
8031
7965
|
}
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
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;
|
|
8037
7988
|
}
|
|
8038
|
-
function
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
};
|
|
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;
|
|
8043
8017
|
}
|
|
8044
8018
|
|
|
8045
|
-
// src/
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
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");
|
|
8049
8022
|
}
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
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;
|
|
8053
8030
|
}
|
|
8054
8031
|
|
|
8055
|
-
// src/
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
elsewhere: "the monorepo-root config"
|
|
8066
|
-
}
|
|
8067
|
-
};
|
|
8068
|
-
var RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
|
|
8069
|
-
var RECOGNIZED_BUILD_KEYS = ["extraIgnorePatterns"];
|
|
8070
|
-
var RECOGNIZED_CHECK_CACHE_KEYS = ["enabled", "excludeCommands", "extraCommands"];
|
|
8071
|
-
var RECOGNIZED_OUTPUT_KEYS = ["commandVerbosity", "extraAgentEnvVars"];
|
|
8072
|
-
var RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ["exempt"];
|
|
8073
|
-
var RECOGNIZED_STEP_KEYS = ["run", "shouldDeclineArguments", "shouldRunInBackground"];
|
|
8074
|
-
var SCRIPT_FIELDS = [
|
|
8075
|
-
{ field: "rootScripts", scope: "root" },
|
|
8076
|
-
{ field: "workspaceScripts", scope: "workspaceRegistry" }
|
|
8077
|
-
];
|
|
8078
|
-
var RETIRED_STEP_KEYS = /* @__PURE__ */ new Map([["declinesArgs", "shouldDeclineArguments"]]);
|
|
8079
|
-
function isScriptRecord(value) {
|
|
8080
|
-
if (!isObject(value)) return false;
|
|
8081
|
-
for (const v of Object.values(value)) {
|
|
8082
|
-
if (typeof v === "string") continue;
|
|
8083
|
-
if (!Array.isArray(v) || !v.every(isScriptElement)) return false;
|
|
8084
|
-
}
|
|
8085
|
-
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);
|
|
8086
8042
|
}
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
return
|
|
8043
|
+
|
|
8044
|
+
// src/helpers/type-guards.ts
|
|
8045
|
+
function isObject2(value) {
|
|
8046
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8091
8047
|
}
|
|
8092
|
-
function
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
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)}`);
|
|
8101
8058
|
}
|
|
8102
|
-
assertValidElements(scripts, fieldName, configPath);
|
|
8103
|
-
return scripts;
|
|
8104
8059
|
}
|
|
8105
|
-
function
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8111
|
-
assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
|
|
8112
|
-
}
|
|
8113
|
-
const element = typeof spec === "string" ? spec : spec.run;
|
|
8114
|
-
const token = findUnexpressibleToken(element);
|
|
8115
|
-
if (token === void 0) continue;
|
|
8116
|
-
throw new UserError(
|
|
8117
|
-
`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.`
|
|
8118
|
-
);
|
|
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));
|
|
8119
8065
|
}
|
|
8066
|
+
result[key] = value;
|
|
8120
8067
|
}
|
|
8068
|
+
return result;
|
|
8121
8069
|
}
|
|
8122
|
-
function
|
|
8123
|
-
return
|
|
8070
|
+
function resolvePackageJsonPath(dir) {
|
|
8071
|
+
return path5.join(dir, "package.json");
|
|
8124
8072
|
}
|
|
8125
|
-
function
|
|
8126
|
-
|
|
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}\`.`;
|
|
8127
8080
|
}
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
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) {
|
|
8131
8113
|
return void 0;
|
|
8132
8114
|
}
|
|
8133
|
-
|
|
8134
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
8135
|
-
}
|
|
8136
|
-
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, "build.");
|
|
8137
|
-
const extraIgnorePatterns = build["extraIgnorePatterns"];
|
|
8138
|
-
if (extraIgnorePatterns !== void 0 && !isStringArray(extraIgnorePatterns)) {
|
|
8139
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
|
|
8140
|
-
}
|
|
8141
|
-
return extraIgnorePatterns === void 0 ? {} : { extraIgnorePatterns };
|
|
8115
|
+
return segments.length === 1 ? "sole" : "chained";
|
|
8142
8116
|
}
|
|
8143
|
-
function
|
|
8144
|
-
const
|
|
8145
|
-
|
|
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) {
|
|
8146
8124
|
return void 0;
|
|
8147
8125
|
}
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
const enabledValue = checkCache["enabled"];
|
|
8154
|
-
if (enabledValue !== void 0) {
|
|
8155
|
-
if (typeof enabledValue !== "boolean") {
|
|
8156
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
|
|
8157
|
-
}
|
|
8158
|
-
config.enabled = enabledValue;
|
|
8126
|
+
return elements.length === 1 ? "sole" : "chained";
|
|
8127
|
+
}
|
|
8128
|
+
function isReentry(target, commandName, scope) {
|
|
8129
|
+
if (target.command !== commandName) {
|
|
8130
|
+
return false;
|
|
8159
8131
|
}
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
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;
|
|
8139
|
+
}
|
|
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;
|
|
8167
8154
|
}
|
|
8168
|
-
config[field] = commands;
|
|
8169
8155
|
}
|
|
8170
|
-
return
|
|
8156
|
+
return scope;
|
|
8171
8157
|
}
|
|
8172
|
-
function
|
|
8173
|
-
const
|
|
8174
|
-
if (
|
|
8158
|
+
function readNmrTail(segment) {
|
|
8159
|
+
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
8160
|
+
if (head === void 0) {
|
|
8175
8161
|
return void 0;
|
|
8176
8162
|
}
|
|
8177
|
-
if (
|
|
8178
|
-
|
|
8163
|
+
if (head === "nmr") {
|
|
8164
|
+
return { launcher: void 0, launcherArgs: [], tail: rest };
|
|
8179
8165
|
}
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
throw new UserError(
|
|
8184
|
-
`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`
|
|
8185
|
-
);
|
|
8166
|
+
const subcommands = LAUNCHERS.get(head);
|
|
8167
|
+
if (subcommands === void 0) {
|
|
8168
|
+
return void 0;
|
|
8186
8169
|
}
|
|
8187
|
-
|
|
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(", ")})`;
|
|
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
|
+
};
|
|
8188
8254
|
}
|
|
8255
|
+
|
|
8256
|
+
// src/config-check-cache.ts
|
|
8189
8257
|
function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
|
|
8190
8258
|
const { checkCache } = config;
|
|
8191
8259
|
if (checkCache === void 0 || !isMonorepoRoot(baseDir)) {
|
|
@@ -8244,46 +8312,48 @@ function readPackageScripts(packageDir) {
|
|
|
8244
8312
|
throw error;
|
|
8245
8313
|
}
|
|
8246
8314
|
}
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
}
|
|
8252
|
-
if (!isObject(output)) {
|
|
8253
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
8254
|
-
}
|
|
8255
|
-
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, "output.");
|
|
8256
|
-
const config = {};
|
|
8257
|
-
const commandVerbosity = output["commandVerbosity"];
|
|
8258
|
-
if (commandVerbosity !== void 0) {
|
|
8259
|
-
if (typeof commandVerbosity !== "string" || !isCommandVerbosity(commandVerbosity)) {
|
|
8260
|
-
const renderedValue = typeof commandVerbosity === "string" ? commandVerbosity : JSON.stringify(commandVerbosity);
|
|
8261
|
-
throw new UserError(
|
|
8262
|
-
`Invalid nmr config at ${configPath}: ${formatVerbosityRejection("`output.commandVerbosity`", renderedValue)}`
|
|
8263
|
-
);
|
|
8264
|
-
}
|
|
8265
|
-
config.commandVerbosity = commandVerbosity;
|
|
8266
|
-
}
|
|
8267
|
-
const extraAgentEnvVars = output["extraAgentEnvVars"];
|
|
8268
|
-
if (extraAgentEnvVars !== void 0) {
|
|
8269
|
-
if (!isStringArray(extraAgentEnvVars)) {
|
|
8270
|
-
throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
|
|
8271
|
-
}
|
|
8272
|
-
config.extraAgentEnvVars = extraAgentEnvVars;
|
|
8273
|
-
}
|
|
8274
|
-
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(", ")}`;
|
|
8275
8320
|
}
|
|
8276
|
-
function
|
|
8277
|
-
|
|
8278
|
-
|
|
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"
|
|
8279
8337
|
}
|
|
8280
|
-
|
|
8281
|
-
|
|
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;
|
|
8282
8350
|
}
|
|
8283
|
-
|
|
8351
|
+
throw new UserError(
|
|
8352
|
+
`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? "key" : "keys"} ${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`
|
|
8353
|
+
);
|
|
8284
8354
|
}
|
|
8285
8355
|
function validateConfig(value, configPath, baseDir) {
|
|
8286
|
-
if (!
|
|
8356
|
+
if (!isObject2(value)) {
|
|
8287
8357
|
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
8288
8358
|
}
|
|
8289
8359
|
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
@@ -8292,6 +8362,7 @@ function validateConfig(value, configPath, baseDir) {
|
|
|
8292
8362
|
if (build) config.build = build;
|
|
8293
8363
|
const checkCache = validateCheckCacheField(value, configPath);
|
|
8294
8364
|
if (checkCache) config.checkCache = checkCache;
|
|
8365
|
+
if (value["delegate"] !== void 0) config.delegate = validateDelegateField(value["delegate"], configPath);
|
|
8295
8366
|
const devBin = validateStringRecordField(value, "devBin", configPath);
|
|
8296
8367
|
if (devBin) config.devBin = devBin;
|
|
8297
8368
|
const output = validateOutputField(value, configPath);
|
|
@@ -8305,22 +8376,6 @@ function validateConfig(value, configPath, baseDir) {
|
|
|
8305
8376
|
assertResolvableCheckCacheCommands(config, configPath, baseDir);
|
|
8306
8377
|
return config;
|
|
8307
8378
|
}
|
|
8308
|
-
async function loadConfig(baseDir) {
|
|
8309
|
-
const configPath = resolveConfigPath(baseDir);
|
|
8310
|
-
if (!existsSync2(configPath)) {
|
|
8311
|
-
return {};
|
|
8312
|
-
}
|
|
8313
|
-
const importedModule = await import(pathToFileURL(configPath).href);
|
|
8314
|
-
const loadedConfig = isObject(importedModule) ? importedModule["default"] : void 0;
|
|
8315
|
-
return validateConfig(loadedConfig, configPath, baseDir);
|
|
8316
|
-
}
|
|
8317
|
-
async function loadRootConfig(monorepoRoot) {
|
|
8318
|
-
const config = await loadConfig(monorepoRoot);
|
|
8319
|
-
const configPath = resolveConfigPath(monorepoRoot);
|
|
8320
|
-
assertTierKeys(config, CONFIG_TIERS.root, configPath);
|
|
8321
|
-
assertNoSelfReferences(config, configPath, monorepoRoot);
|
|
8322
|
-
return config;
|
|
8323
|
-
}
|
|
8324
8379
|
function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
8325
8380
|
for (const key of Object.keys(value)) {
|
|
8326
8381
|
const replacement = retiredKeys.get(key);
|
|
@@ -8330,48 +8385,231 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
|
|
|
8330
8385
|
);
|
|
8331
8386
|
}
|
|
8332
8387
|
}
|
|
8333
|
-
function
|
|
8334
|
-
for (const
|
|
8335
|
-
|
|
8336
|
-
for (const
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
const remedy = formatSelfReferenceRemedy({
|
|
8345
|
-
isDefault: !isHookName(key) && Object.hasOwn(defaults, key),
|
|
8346
|
-
key,
|
|
8347
|
-
selfReference
|
|
8348
|
-
});
|
|
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;
|
|
8349
8399
|
throw new UserError(
|
|
8350
|
-
`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.`
|
|
8351
8401
|
);
|
|
8352
8402
|
}
|
|
8353
8403
|
}
|
|
8354
8404
|
}
|
|
8355
|
-
function
|
|
8356
|
-
|
|
8357
|
-
if (unrecognizedKeys.length === 0) {
|
|
8358
|
-
return;
|
|
8359
|
-
}
|
|
8360
|
-
throw new UserError(
|
|
8361
|
-
`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? "key" : "keys"} ${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`
|
|
8362
|
-
);
|
|
8405
|
+
function formatKeyList(keys, prefix) {
|
|
8406
|
+
return keys.toSorted().map((key) => `\`${prefix}${key}\``).join(", ");
|
|
8363
8407
|
}
|
|
8364
|
-
function
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
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\`.`
|
|
8371
8477
|
);
|
|
8372
8478
|
}
|
|
8373
|
-
function
|
|
8374
|
-
|
|
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
|
+
);
|
|
8375
8613
|
}
|
|
8376
8614
|
function formatSelfReferenceRemedy(options) {
|
|
8377
8615
|
const { isDefault, key, selfReference } = options;
|
|
@@ -8382,7 +8620,7 @@ function formatSelfReferenceRemedy(options) {
|
|
|
8382
8620
|
}
|
|
8383
8621
|
|
|
8384
8622
|
// src/tiers.ts
|
|
8385
|
-
import { readdirSync } from "node:fs";
|
|
8623
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
8386
8624
|
import path6 from "node:path";
|
|
8387
8625
|
|
|
8388
8626
|
// src/git-ignored-paths.ts
|
|
@@ -8419,7 +8657,7 @@ function hasTierInfix(filePath) {
|
|
|
8419
8657
|
var TEST_COLLECTION_EXCLUDE = [".git", "coverage", "dist", "node_modules"];
|
|
8420
8658
|
var TIER_NAMES = ["unit", "tool", "localhost", "remote"];
|
|
8421
8659
|
function collectTestFiles(dir, relativeDir, isInTestDir, context) {
|
|
8422
|
-
const entries =
|
|
8660
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
8423
8661
|
for (const entry of entries) {
|
|
8424
8662
|
const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
|
|
8425
8663
|
if (entry.isDirectory()) {
|
|
@@ -8441,600 +8679,373 @@ function walkTestFiles(rootDir, { excludedBasenames = [] }, misplaced) {
|
|
|
8441
8679
|
return context.foundPaths.toSorted();
|
|
8442
8680
|
}
|
|
8443
8681
|
|
|
8444
|
-
// .readyup/
|
|
8445
|
-
var
|
|
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
|
-
{
|
|
8512
|
-
name: ".config/nmr.config.ts uses defineConfig",
|
|
8513
|
-
severity: "recommend",
|
|
8514
|
-
skip: () => !fileExists(".config/nmr.config.ts") && "no nmr config file",
|
|
8515
|
-
check: () => fileContains(".config/nmr.config.ts", /defineConfig/),
|
|
8516
|
-
fix: "Wrap the config export with defineConfig() from @williamthorsen/nmr/config for type safety"
|
|
8517
|
-
},
|
|
8518
|
-
// `error` because falling short produces wrong results rather than a failure. Names and fixes are
|
|
8519
|
-
// getters because their version constants are declared below the kit.
|
|
8520
|
-
{
|
|
8521
|
-
get name() {
|
|
8522
|
-
return `eslint >= ${MIN_ESLINT_VERSION}`;
|
|
8523
|
-
},
|
|
8524
|
-
severity: "error",
|
|
8525
|
-
skip: () => !hasDevDependency("eslint") && "eslint not installed",
|
|
8526
|
-
check: hasSupportedEslintVersion,
|
|
8527
|
-
get fix() {
|
|
8528
|
-
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`;
|
|
8529
|
-
}
|
|
8530
|
-
},
|
|
8531
|
-
{
|
|
8532
|
-
get name() {
|
|
8533
|
-
return `@williamthorsen/strict-lint >= ${MIN_STRICT_LINT_VERSION}`;
|
|
8534
|
-
},
|
|
8535
|
-
severity: "error",
|
|
8536
|
-
skip: () => !hasDevDependency("@williamthorsen/strict-lint") && "strict-lint not installed",
|
|
8537
|
-
check: hasSupportedStrictLintVersion,
|
|
8538
|
-
get fix() {
|
|
8539
|
-
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`;
|
|
8540
|
-
}
|
|
8541
|
-
},
|
|
8542
|
-
// -- Root script cleanup -------------------------------------------------
|
|
8543
|
-
{
|
|
8544
|
-
name: "root package.json has no nmr-provided scripts",
|
|
8545
|
-
severity: "warn",
|
|
8546
|
-
quiet: true,
|
|
8547
|
-
check: noRedundantRootScripts,
|
|
8548
|
-
fix: "Remove scripts from root package.json that nmr provides as built-in root scripts, and invoke them through nmr directly"
|
|
8549
|
-
},
|
|
8550
|
-
// -- Git hooks -----------------------------------------------------------
|
|
8551
|
-
{
|
|
8552
|
-
name: "no root install script runs lefthook install unguarded",
|
|
8553
|
-
severity: "warn",
|
|
8554
|
-
quiet: true,
|
|
8555
|
-
check: () => noUnguardedLefthookInstall(),
|
|
8556
|
-
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`"
|
|
8557
|
-
},
|
|
8558
|
-
// -- Workspace build readiness -------------------------------------------
|
|
8559
|
-
{
|
|
8560
|
-
name: "all workspace packages can build",
|
|
8561
|
-
severity: "warn",
|
|
8562
|
-
check: allWorkspacePackagesCanBuild,
|
|
8563
|
-
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`
|
|
8564
|
-
},
|
|
8565
|
-
// -- Bin targets ---------------------------------------------------------
|
|
8566
|
-
{
|
|
8567
|
-
name: "every bin target is a committed wrapper",
|
|
8568
|
-
severity: "error",
|
|
8569
|
-
check: () => everyBinTargetIsACommittedWrapper(),
|
|
8570
|
-
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`
|
|
8571
|
-
},
|
|
8572
|
-
{
|
|
8573
|
-
name: "every bin wrapper's build-output target is covered by files",
|
|
8574
|
-
severity: "warn",
|
|
8575
|
-
check: () => everyBinWrapperTargetIsCoveredByFiles(),
|
|
8576
|
-
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"
|
|
8577
|
-
},
|
|
8578
|
-
// -- Vitest projects -----------------------------------------------------
|
|
8579
|
-
{
|
|
8580
|
-
name: "no retired Vitest config variants",
|
|
8581
|
-
severity: "error",
|
|
8582
|
-
quiet: true,
|
|
8583
|
-
check: () => noRetiredVitestConfigs(),
|
|
8584
|
-
fix: "Delete every vitest.standalone.config.* and vitest.integration.config.*. nmr's test scripts select Vitest projects instead of naming config files"
|
|
8585
|
-
},
|
|
8586
|
-
{
|
|
8587
|
-
name: "every vitest.config builds on @williamthorsen/nmr/vitest",
|
|
8588
|
-
severity: "error",
|
|
8589
|
-
check: () => vitestConfigBuildsOnSharedConfig(),
|
|
8590
|
-
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"
|
|
8591
|
-
},
|
|
8592
|
-
{
|
|
8593
|
-
name: "vitest.root.config.ts builds on @williamthorsen/nmr/vitest",
|
|
8594
|
-
severity: "error",
|
|
8595
|
-
check: () => vitestRootConfigBuildsOnSharedConfig(),
|
|
8596
|
-
fix: "Replace vitest.root.config.ts with: import { defineRootVitestConfig } from '@williamthorsen/nmr/vitest'; export default defineRootVitestConfig({ monorepoRoot: import.meta.dirname });"
|
|
8597
|
-
},
|
|
8598
|
-
{
|
|
8599
|
-
name: "every workspace with a Vite config has a Vitest config",
|
|
8600
|
-
severity: "error",
|
|
8601
|
-
check: () => everyViteConfigHasVitestConfig(),
|
|
8602
|
-
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"
|
|
8603
|
-
},
|
|
8604
|
-
{
|
|
8605
|
-
name: "the test suite gates the test-file conventions",
|
|
8606
|
-
severity: "warn",
|
|
8607
|
-
check: () => testSuiteGatesTestFileConventions(),
|
|
8608
|
-
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"
|
|
8609
|
-
},
|
|
8610
|
-
{
|
|
8611
|
-
name: "every test file names its isolation tier",
|
|
8612
|
-
severity: "error",
|
|
8613
|
-
skip: () => describeConventionsGuardSkip(),
|
|
8614
|
-
check: () => everyTestFileNamesItsTier(),
|
|
8615
|
-
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`
|
|
8616
|
-
},
|
|
8617
|
-
{
|
|
8618
|
-
name: "every test file is under a __tests__ directory",
|
|
8619
|
-
severity: "error",
|
|
8620
|
-
skip: () => describeConventionsGuardSkip(),
|
|
8621
|
-
check: () => everyTestFileSitsUnderTestsDir(),
|
|
8622
|
-
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"
|
|
8623
|
-
},
|
|
8624
|
-
{
|
|
8625
|
-
name: "no package re-exports the ancestor Vitest config",
|
|
8626
|
-
severity: "recommend",
|
|
8627
|
-
quiet: true,
|
|
8628
|
-
check: () => noReExportOnlyVitestConfigs(),
|
|
8629
|
-
fix: "Delete these files. Vitest resolves config by walking up from the run root, so a per-package re-export is redundant"
|
|
8630
|
-
},
|
|
8631
|
-
// -- Shared Prettier config ----------------------------------------------
|
|
8632
|
-
{
|
|
8633
|
-
name: "Prettier config builds on @williamthorsen/nmr/prettier",
|
|
8634
|
-
severity: "error",
|
|
8635
|
-
check: () => prettierConfigBuildsOnSharedConfig(),
|
|
8636
|
-
fix: "Replace the Prettier config with: import { definePrettierConfig } from '@williamthorsen/nmr/prettier'; export default definePrettierConfig();"
|
|
8637
|
-
},
|
|
8638
|
-
// -- Shared upgrade policy -----------------------------------------------
|
|
8639
|
-
{
|
|
8640
|
-
name: "taze.config.ts builds on @williamthorsen/nmr/taze",
|
|
8641
|
-
severity: "warn",
|
|
8642
|
-
check: () => tazeConfigBuildsOnSharedConfig(),
|
|
8643
|
-
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"
|
|
8644
|
-
},
|
|
8645
|
-
{
|
|
8646
|
-
name: "taze config declares no option taze discards",
|
|
8647
|
-
severity: "warn",
|
|
8648
|
-
quiet: true,
|
|
8649
|
-
check: () => tazeConfigAvoidsClobberedOptions(),
|
|
8650
|
-
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"
|
|
8651
|
-
},
|
|
8652
|
-
// -- Audit dependency --------------------------------------------------------
|
|
8653
|
-
{
|
|
8654
|
-
name: "v11y-check in devDependencies",
|
|
8655
|
-
severity: "warn",
|
|
8656
|
-
check: () => hasDevDependency("v11y-check"),
|
|
8657
|
-
fix: "pnpm add --save-dev v11y-check"
|
|
8658
|
-
},
|
|
8659
|
-
// -- Legacy script runner ------------------------------------------------
|
|
8660
|
-
{
|
|
8661
|
-
name: "scripts/run-workspace-script.ts does not exist",
|
|
8662
|
-
severity: "error",
|
|
8663
|
-
quiet: true,
|
|
8664
|
-
check: () => !fileExists("scripts/run-workspace-script.ts"),
|
|
8665
|
-
fix: "Delete scripts/run-workspace-script.ts, because nmr replaces this custom script runner"
|
|
8666
|
-
},
|
|
8667
|
-
{
|
|
8668
|
-
name: 'no workspace packages reference run-workspace-script or "pnpm run ws"',
|
|
8669
|
-
severity: "error",
|
|
8670
|
-
quiet: true,
|
|
8671
|
-
check: noWorkspaceRunScriptReferences,
|
|
8672
|
-
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
|
+
}
|
|
8673
8749
|
}
|
|
8674
|
-
|
|
8675
|
-
|
|
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: [
|
|
8676
8839
|
{
|
|
8677
|
-
name: "
|
|
8840
|
+
name: "GitHub reports the repo's runner settings",
|
|
8841
|
+
severity: "recommend",
|
|
8842
|
+
quiet: true,
|
|
8843
|
+
skip: () => describeSelfHostedRunnerSkip(),
|
|
8844
|
+
check: () => true,
|
|
8678
8845
|
checks: [
|
|
8679
8846
|
{
|
|
8680
|
-
name: "
|
|
8847
|
+
name: "CI_RUNS_ON selects the self-hosted runner",
|
|
8681
8848
|
severity: "recommend",
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
check: () => recommendSelfHostedRunner()
|
|
8691
|
-
},
|
|
8692
|
-
{
|
|
8693
|
-
name: "the self-hosted runner can run CI",
|
|
8694
|
-
severity: "warn",
|
|
8695
|
-
skip: () => describeReadinessSkip(),
|
|
8696
|
-
check: () => selfHostedRunnerCanRunCi()
|
|
8697
|
-
}
|
|
8698
|
-
]
|
|
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()
|
|
8699
8857
|
}
|
|
8700
8858
|
]
|
|
8701
8859
|
}
|
|
8702
8860
|
]
|
|
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
|
-
|
|
8729
|
-
"preprepare",
|
|
8730
|
-
"prepare",
|
|
8731
|
-
"postprepare"
|
|
8732
|
-
];
|
|
8733
|
-
var LEFTHOOK_CHECK_INSTALL_PATTERN = /\blefthook\s+check-install\b/;
|
|
8734
|
-
var LEFTHOOK_INSTALL_PATTERN = /\blefthook\s+install\b/;
|
|
8735
|
-
var MIN_ESLINT_VERSION = "10.0.0";
|
|
8736
|
-
var MIN_STRICT_LINT_VERSION = "9.3.0";
|
|
8737
|
-
var WORKSPACE_VERSION_MARKERS = ["catalog:", "workspace:"];
|
|
8738
|
-
var CATALOGUE_CHECKED_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
8739
|
-
var PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
8740
|
-
function allWorkspacePackagesCanBuild() {
|
|
8741
|
-
const packagesDir = join(process.cwd(), "packages");
|
|
8742
|
-
if (!existsSync3(packagesDir)) return true;
|
|
8743
|
-
const entries = readdirSync2(packagesDir, { withFileTypes: true });
|
|
8744
|
-
const failingPackages = [];
|
|
8745
|
-
for (const entry of entries) {
|
|
8746
|
-
if (!entry.isDirectory()) continue;
|
|
8747
|
-
const pkgPath = `packages/${entry.name}/package.json`;
|
|
8748
|
-
const content = readFile(pkgPath);
|
|
8749
|
-
if (!content) continue;
|
|
8750
|
-
const hasBuildOverride = /"build"\s*:/.test(content);
|
|
8751
|
-
const hasDefaultBuildInputs = fileExists(`packages/${entry.name}/tsconfig.json`) && existsSync3(join(packagesDir, entry.name, "src"));
|
|
8752
|
-
if (!hasBuildOverride && !hasDefaultBuildInputs) {
|
|
8753
|
-
failingPackages.push(entry.name);
|
|
8754
|
-
}
|
|
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
|
+
);
|
|
8755
8887
|
}
|
|
8756
|
-
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;
|
|
8757
8916
|
return {
|
|
8758
|
-
|
|
8759
|
-
|
|
8917
|
+
nameWithOwner,
|
|
8918
|
+
isPrivate: repoView["visibility"] !== "PUBLIC",
|
|
8919
|
+
onlineSelfHostedRunners: countOnlineSelfHostedRunners(parseJson(runners.stdout)),
|
|
8920
|
+
ciRunsOn: repoValue ?? await readOrganizationValue(nameWithOwner, cwd, runGh)
|
|
8760
8921
|
};
|
|
8761
8922
|
}
|
|
8762
|
-
function
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
if (
|
|
8766
|
-
const
|
|
8767
|
-
|
|
8768
|
-
const findings = workspaceDiscovery.workspaces.flatMap((workspace) => findLiteralCataloguedSpecifiers(workspace, catalogRead.names)).toSorted((a, b) => a.manifestPath.localeCompare(b.manifestPath) || a.name.localeCompare(b.name));
|
|
8769
|
-
if (findings.length === 0) return true;
|
|
8770
|
-
const lines = findings.map(({ manifestPath, name, specifier }) => `${manifestPath}: ${name} ${specifier}`);
|
|
8771
|
-
return { ok: false, detail: formatPaths([...new Set(lines)]) };
|
|
8772
|
-
}
|
|
8773
|
-
function checkNoMatchingFiles(patterns, cwd) {
|
|
8774
|
-
const foundFiles = findFiles(patterns, cwd);
|
|
8775
|
-
if (foundFiles.length === 0) return true;
|
|
8776
|
-
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";
|
|
8777
8929
|
}
|
|
8778
|
-
function
|
|
8779
|
-
const
|
|
8780
|
-
if (
|
|
8781
|
-
|
|
8782
|
-
}
|
|
8783
|
-
const staleConfigs = matches.filter(
|
|
8784
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), exportName, SHARED_VITEST_MODULE)
|
|
8785
|
-
);
|
|
8786
|
-
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)}`;
|
|
8787
8934
|
return {
|
|
8788
8935
|
ok: false,
|
|
8789
|
-
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}`
|
|
8790
8938
|
};
|
|
8791
8939
|
}
|
|
8792
|
-
function
|
|
8793
|
-
const
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
);
|
|
8798
|
-
|
|
8799
|
-
|
|
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}` };
|
|
8800
8949
|
}
|
|
8801
|
-
const
|
|
8802
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "definePrettierConfig", SHARED_PRETTIER_MODULE)
|
|
8803
|
-
);
|
|
8804
|
-
if (staleConfigs.length === 0) return true;
|
|
8950
|
+
const problems = status.selection === "invalid" ? [invalidValue, ...conditions] : conditions;
|
|
8805
8951
|
return {
|
|
8806
8952
|
ok: false,
|
|
8807
|
-
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}`
|
|
8808
8955
|
};
|
|
8809
8956
|
}
|
|
8810
|
-
function
|
|
8811
|
-
const
|
|
8812
|
-
if (
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
}
|
|
8816
|
-
return ".prettierrc.js is missing";
|
|
8817
|
-
}
|
|
8818
|
-
function hasPrettierConfigKey(cwd) {
|
|
8819
|
-
const manifest = readFileIn(cwd, "package.json");
|
|
8820
|
-
if (manifest === void 0) return false;
|
|
8821
|
-
try {
|
|
8822
|
-
const parsedManifest = JSON.parse(manifest);
|
|
8823
|
-
return isRecord(parsedManifest) && parsedManifest["prettier"] !== void 0;
|
|
8824
|
-
} catch {
|
|
8825
|
-
return false;
|
|
8826
|
-
}
|
|
8827
|
-
}
|
|
8828
|
-
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;
|
|
8829
8962
|
try {
|
|
8830
|
-
|
|
8963
|
+
exempt = (await loadRootConfig(cwd)).selfHostedRunner?.exempt;
|
|
8831
8964
|
} catch (error) {
|
|
8832
|
-
|
|
8965
|
+
failures.push(`the nmr config cannot be loaded: ${describeError(error)}`);
|
|
8833
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
|
+
};
|
|
8834
8976
|
}
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
(workspace) => readBinEntries(workspace).flatMap((entry) => {
|
|
8842
|
-
const defect = describeBinTargetDefect(workspace, entry, trackedPaths);
|
|
8843
|
-
return defect === void 0 ? [] : [`${describeBinEntry(workspace, entry)} (${defect})`];
|
|
8844
|
-
})
|
|
8845
|
-
);
|
|
8846
|
-
if (offenders.length === 0) return true;
|
|
8847
|
-
return { ok: false, detail: formatPaths(offenders) };
|
|
8848
|
-
}
|
|
8849
|
-
function everyBinWrapperTargetIsCoveredByFiles() {
|
|
8850
|
-
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
8851
|
-
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
8852
|
-
const cwd = process.cwd();
|
|
8853
|
-
const offenders = workspaceDiscovery.workspaces.flatMap((workspace) => {
|
|
8854
|
-
const files = workspace.packageJson["files"];
|
|
8855
|
-
if (!Array.isArray(files)) return [];
|
|
8856
|
-
const publishedSegments = new Set(
|
|
8857
|
-
files.flatMap((entry) => typeof entry === "string" ? [readFirstSegment(entry)] : [])
|
|
8858
|
-
);
|
|
8859
|
-
return readBinEntries(workspace).flatMap((entry) => {
|
|
8860
|
-
const target = readWrapperTarget(cwd, workspace, entry);
|
|
8861
|
-
if (target === void 0 || publishedSegments.has(readFirstSegment(target))) return [];
|
|
8862
|
-
return [`${describeBinEntry(workspace, entry)} -> ${target}`];
|
|
8863
|
-
});
|
|
8864
|
-
});
|
|
8865
|
-
if (offenders.length === 0) return true;
|
|
8866
|
-
return { ok: false, detail: formatPaths(offenders) };
|
|
8867
|
-
}
|
|
8868
|
-
function describeBinEntry(workspace, entry) {
|
|
8869
|
-
return `${workspace.name ?? workspace.dir}:${entry.command} -> ${entry.target}`;
|
|
8870
|
-
}
|
|
8871
|
-
function describeBinTargetDefect(workspace, entry, trackedPaths) {
|
|
8872
|
-
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return `names a path under ${BUILD_OUTPUT_DIR}/`;
|
|
8873
|
-
if (trackedPaths === void 0) return void 0;
|
|
8874
|
-
return trackedPaths.has(`${workspace.dir}/${entry.target}`) ? void 0 : "untracked";
|
|
8875
|
-
}
|
|
8876
|
-
function normalizeBinTarget(target) {
|
|
8877
|
-
return target.replace(/^\.\//, "");
|
|
8878
|
-
}
|
|
8879
|
-
function readBinEntries(workspace) {
|
|
8880
|
-
const bin = workspace.packageJson["bin"];
|
|
8881
|
-
if (typeof bin === "string") {
|
|
8882
|
-
const command = workspace.name?.split("/").at(-1) ?? basename(workspace.dir);
|
|
8883
|
-
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);
|
|
8884
8983
|
}
|
|
8885
|
-
|
|
8886
|
-
return Object.entries(bin).flatMap(
|
|
8887
|
-
([command, target]) => typeof target === "string" ? [{ command, target: normalizeBinTarget(target) }] : []
|
|
8888
|
-
);
|
|
8889
|
-
}
|
|
8890
|
-
function readFirstSegment(entry) {
|
|
8891
|
-
return normalizeBinTarget(entry).split("/", 1).at(0) ?? "";
|
|
8892
|
-
}
|
|
8893
|
-
function readWrapperTarget(cwd, workspace, entry) {
|
|
8894
|
-
if (readFirstSegment(entry.target) === BUILD_OUTPUT_DIR) return void 0;
|
|
8895
|
-
const content = readFileIn(cwd, `${workspace.dir}/${entry.target}`);
|
|
8896
|
-
if (content === void 0) return void 0;
|
|
8897
|
-
const specifier = WRAPPER_TARGET_PATTERN.exec(content)?.[1];
|
|
8898
|
-
if (specifier === void 0) return void 0;
|
|
8899
|
-
return posix.normalize(posix.join(posix.dirname(entry.target), specifier));
|
|
8900
|
-
}
|
|
8901
|
-
function everyTestFileNamesItsTier(cwd = process.cwd()) {
|
|
8902
|
-
const untieredFiles = findUntieredTestFiles(cwd);
|
|
8903
|
-
if (untieredFiles.length === 0) return true;
|
|
8904
|
-
return { ok: false, detail: formatPaths(untieredFiles) };
|
|
8905
|
-
}
|
|
8906
|
-
function everyTestFileSitsUnderTestsDir(cwd = process.cwd()) {
|
|
8907
|
-
const misplacedFiles = findMisplacedTestFiles(cwd);
|
|
8908
|
-
if (misplacedFiles.length === 0) return true;
|
|
8909
|
-
return { ok: false, detail: formatPaths(misplacedFiles) };
|
|
8984
|
+
return status;
|
|
8910
8985
|
}
|
|
8911
|
-
|
|
8912
|
-
|
|
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();
|
|
8913
9008
|
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
8914
|
-
const
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
});
|
|
8919
|
-
if (unpairedConfigs.length === 0) return true;
|
|
8920
|
-
return { ok: false, detail: formatPaths(unpairedConfigs) };
|
|
8921
|
-
}
|
|
8922
|
-
function findFiles(patterns, cwd) {
|
|
8923
|
-
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)]) };
|
|
8924
9013
|
}
|
|
8925
9014
|
function findLiteralCataloguedSpecifiers(workspace, names) {
|
|
8926
|
-
const manifestPath =
|
|
9015
|
+
const manifestPath = posix3.join(workspace.dir, "package.json");
|
|
8927
9016
|
return CATALOGUE_CHECKED_FIELDS.flatMap((field) => {
|
|
8928
9017
|
const declared = workspace.packageJson[field];
|
|
8929
|
-
if (!
|
|
9018
|
+
if (!isRecord5(declared)) return [];
|
|
8930
9019
|
return Object.entries(declared).flatMap(
|
|
8931
|
-
([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 } : []
|
|
8932
9021
|
);
|
|
8933
9022
|
});
|
|
8934
9023
|
}
|
|
8935
|
-
function
|
|
8936
|
-
|
|
8937
|
-
}
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
}
|
|
8942
|
-
|
|
8943
|
-
const pickedFields = { "version": "0.48.0" };
|
|
8944
|
-
if (typeof pickedFields["version"] !== "string") {
|
|
8945
|
-
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
8946
|
-
}
|
|
8947
|
-
return pickedFields["version"];
|
|
8948
|
-
}
|
|
8949
|
-
function hasSupportedEslintVersion() {
|
|
8950
|
-
return hasMinDevDependencyVersion("eslint", MIN_ESLINT_VERSION, {
|
|
8951
|
-
exempt: resolvesVersionViaWorkspace
|
|
8952
|
-
});
|
|
8953
|
-
}
|
|
8954
|
-
function hasSupportedStrictLintVersion() {
|
|
8955
|
-
return hasMinDevDependencyVersion("@williamthorsen/strict-lint", MIN_STRICT_LINT_VERSION, {
|
|
8956
|
-
exempt: resolvesVersionViaWorkspace
|
|
8957
|
-
});
|
|
8958
|
-
}
|
|
8959
|
-
function hasViteConfigBeside(cwd, relativePath) {
|
|
8960
|
-
return findFiles([VITE_CONFIG_PATTERN], join(cwd, dirname(relativePath))).length > 0;
|
|
8961
|
-
}
|
|
8962
|
-
function importsSharedExport(content, exportName, moduleSpecifier) {
|
|
8963
|
-
if (content === void 0) return false;
|
|
8964
|
-
const pattern = new RegExp(String.raw`import\s*\{[^}]*\b${exportName}\b[^}]*\}\s*from\s*['"]${moduleSpecifier}['"]`);
|
|
8965
|
-
return pattern.test(content);
|
|
8966
|
-
}
|
|
8967
|
-
function isOwnedByReExportCheck(cwd, relativePath) {
|
|
8968
|
-
return isReExportOnly(readFileIn(cwd, relativePath)) && !hasViteConfigBeside(cwd, relativePath);
|
|
8969
|
-
}
|
|
8970
|
-
function isReExportOnly(content) {
|
|
8971
|
-
if (content === void 0) return false;
|
|
8972
|
-
const statements = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
8973
|
-
return statements.length > 0 && statements.every((line) => RE_EXPORT_LINE_PATTERN.test(line));
|
|
8974
|
-
}
|
|
8975
|
-
function noPnpmFieldInPackageJson(cwd = process.cwd()) {
|
|
8976
|
-
const declaringFiles = findFiles(["**/package.json"], cwd).flatMap((relativePath) => {
|
|
8977
|
-
const keys = readPnpmFieldKeys(readFileIn(cwd, relativePath));
|
|
8978
|
-
if (keys === void 0) return [];
|
|
8979
|
-
return [keys.length > 0 ? `${relativePath} (${keys.join(", ")})` : relativePath];
|
|
8980
|
-
});
|
|
8981
|
-
if (declaringFiles.length === 0) return true;
|
|
8982
|
-
return { ok: false, detail: formatPaths(declaringFiles) };
|
|
8983
|
-
}
|
|
8984
|
-
function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
|
|
8985
|
-
const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path7) => path7.includes("/"));
|
|
8986
|
-
const reExports = nonRootConfigs.filter((path7) => isOwnedByReExportCheck(cwd, path7));
|
|
8987
|
-
if (reExports.length === 0) return true;
|
|
8988
|
-
return { ok: false, detail: formatPaths(reExports) };
|
|
8989
|
-
}
|
|
8990
|
-
function noRedundantRootScripts() {
|
|
8991
|
-
const pkg = readPackageJson();
|
|
8992
|
-
if (!pkg) return true;
|
|
8993
|
-
const scripts = pkg["scripts"];
|
|
8994
|
-
if (!isRecord(scripts)) return true;
|
|
8995
|
-
const builtInNames = Object.keys(getDefaultRootScripts(process.cwd()));
|
|
8996
|
-
const redundantNames = Object.keys(scripts).filter((name) => builtInNames.includes(name));
|
|
8997
|
-
if (redundantNames.length === 0) return true;
|
|
8998
|
-
return {
|
|
8999
|
-
ok: false,
|
|
9000
|
-
detail: `redundant: ${redundantNames.join(", ")}`
|
|
9001
|
-
};
|
|
9002
|
-
}
|
|
9003
|
-
function noRetiredVitestConfigs(cwd = process.cwd()) {
|
|
9004
|
-
return checkNoMatchingFiles(
|
|
9005
|
-
[`**/vitest.standalone.config.${CONFIG_EXTENSIONS}`, `**/vitest.integration.config.${CONFIG_EXTENSIONS}`],
|
|
9006
|
-
cwd
|
|
9007
|
-
);
|
|
9008
|
-
}
|
|
9009
|
-
function noUnguardedLefthookInstall(cwd = process.cwd()) {
|
|
9010
|
-
const scripts = readRootScripts(cwd);
|
|
9011
|
-
const unguardedScripts = INSTALL_LIFECYCLE_SCRIPTS.flatMap((name) => {
|
|
9012
|
-
const command = scripts[name];
|
|
9013
|
-
if (typeof command !== "string") return [];
|
|
9014
|
-
const isUnguarded = LEFTHOOK_INSTALL_PATTERN.test(command) && !LEFTHOOK_CHECK_INSTALL_PATTERN.test(command);
|
|
9015
|
-
return isUnguarded ? [`${name}: ${command}`] : [];
|
|
9016
|
-
});
|
|
9017
|
-
if (unguardedScripts.length === 0) return true;
|
|
9018
|
-
return { ok: false, detail: formatPaths(unguardedScripts) };
|
|
9019
|
-
}
|
|
9020
|
-
function noWorkspaceRunScriptReferences() {
|
|
9021
|
-
const packagesDir = join(process.cwd(), "packages");
|
|
9022
|
-
if (!existsSync3(packagesDir)) return true;
|
|
9023
|
-
const legacyPattern = /run-workspace-script|"pnpm\s+run\s+ws\b/;
|
|
9024
|
-
const entries = readdirSync2(packagesDir, { withFileTypes: true });
|
|
9025
|
-
const matches = [];
|
|
9026
|
-
for (const entry of entries) {
|
|
9027
|
-
if (!entry.isDirectory()) continue;
|
|
9028
|
-
const content = readFile(`packages/${entry.name}/package.json`);
|
|
9029
|
-
if (content && legacyPattern.test(content)) {
|
|
9030
|
-
matches.push(entry.name);
|
|
9031
|
-
}
|
|
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
9032
|
}
|
|
9033
|
-
if (
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
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) };
|
|
9038
9049
|
}
|
|
9039
9050
|
function noWorkspacesFieldInPackageJson(cwd = process.cwd()) {
|
|
9040
9051
|
const declaringFiles = findFiles(["**/package.json"], cwd).filter(
|
|
@@ -9047,28 +9058,11 @@ function declaresWorkspacesField(content) {
|
|
|
9047
9058
|
if (content === void 0) return false;
|
|
9048
9059
|
try {
|
|
9049
9060
|
const parsedManifest = JSON.parse(content);
|
|
9050
|
-
return
|
|
9061
|
+
return isRecord6(parsedManifest) && "workspaces" in parsedManifest;
|
|
9051
9062
|
} catch {
|
|
9052
9063
|
return false;
|
|
9053
9064
|
}
|
|
9054
9065
|
}
|
|
9055
|
-
function readCataloguedNames(cwd) {
|
|
9056
|
-
const content = readFileIn(cwd, "pnpm-workspace.yaml");
|
|
9057
|
-
if (content === void 0) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9058
|
-
let workspaceManifest;
|
|
9059
|
-
try {
|
|
9060
|
-
workspaceManifest = (0, import_yaml2.parse)(content);
|
|
9061
|
-
} catch (error) {
|
|
9062
|
-
return { ok: false, detail: `cannot parse pnpm-workspace.yaml: ${describeError(error)}` };
|
|
9063
|
-
}
|
|
9064
|
-
if (!isRecord(workspaceManifest)) return { ok: true, names: /* @__PURE__ */ new Set() };
|
|
9065
|
-
const namedCatalogs = workspaceManifest["catalogs"];
|
|
9066
|
-
const catalogs = [workspaceManifest["catalog"], ...isRecord(namedCatalogs) ? Object.values(namedCatalogs) : []];
|
|
9067
|
-
return { ok: true, names: new Set(catalogs.filter(isRecord).flatMap((catalog) => Object.keys(catalog))) };
|
|
9068
|
-
}
|
|
9069
|
-
function readFileIn(cwd, relativePath) {
|
|
9070
|
-
return readFile(join(cwd, relativePath));
|
|
9071
|
-
}
|
|
9072
9066
|
function readPnpmFieldKeys(content) {
|
|
9073
9067
|
if (content === void 0) return void 0;
|
|
9074
9068
|
let parsedManifest;
|
|
@@ -9077,36 +9071,187 @@ function readPnpmFieldKeys(content) {
|
|
|
9077
9071
|
} catch {
|
|
9078
9072
|
return void 0;
|
|
9079
9073
|
}
|
|
9080
|
-
if (!
|
|
9074
|
+
if (!isRecord6(parsedManifest)) return void 0;
|
|
9081
9075
|
const pnpm = parsedManifest["pnpm"];
|
|
9082
|
-
return
|
|
9076
|
+
return isRecord6(pnpm) ? Object.keys(pnpm).toSorted() : void 0;
|
|
9083
9077
|
}
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
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
|
+
}
|
|
9092
9174
|
}
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
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.50.0" };
|
|
9188
|
+
if (typeof pickedFields["version"] !== "string") {
|
|
9189
|
+
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
9190
|
+
}
|
|
9191
|
+
return pickedFields["version"];
|
|
9096
9192
|
}
|
|
9097
9193
|
function resolvesVersionViaWorkspace(range) {
|
|
9098
9194
|
return WORKSPACE_VERSION_MARKERS.some((marker) => range.startsWith(marker));
|
|
9099
9195
|
}
|
|
9100
|
-
function
|
|
9101
|
-
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
|
+
);
|
|
9102
9244
|
if (configs.length === 0) {
|
|
9103
|
-
return { ok: false, detail:
|
|
9245
|
+
return { ok: false, detail: describeMissingPrettierConfig(cwd) };
|
|
9104
9246
|
}
|
|
9105
9247
|
const staleConfigs = configs.filter(
|
|
9106
|
-
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "
|
|
9248
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), "definePrettierConfig", SHARED_PRETTIER_MODULE)
|
|
9107
9249
|
);
|
|
9108
9250
|
if (staleConfigs.length === 0) return true;
|
|
9109
|
-
return {
|
|
9251
|
+
return {
|
|
9252
|
+
ok: false,
|
|
9253
|
+
detail: `does not import definePrettierConfig from ${SHARED_PRETTIER_MODULE}: ${staleConfigs.join(", ")}`
|
|
9254
|
+
};
|
|
9110
9255
|
}
|
|
9111
9256
|
function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
|
|
9112
9257
|
const configs = findFiles([`taze.config.${CONFIG_EXTENSIONS}`], cwd);
|
|
@@ -9120,345 +9265,267 @@ function tazeConfigAvoidsClobberedOptions(cwd = process.cwd()) {
|
|
|
9120
9265
|
if (findings.length === 0) return true;
|
|
9121
9266
|
return { ok: false, detail: formatPaths(findings) };
|
|
9122
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
|
+
}
|
|
9123
9287
|
function describeMissingTazeConfig(cwd) {
|
|
9124
9288
|
const inertConfigs = findFiles(INERT_TAZE_CONFIGS, cwd);
|
|
9125
9289
|
if (inertConfigs.length > 0) return `does not contain code that calls the factory: ${inertConfigs.join(", ")}`;
|
|
9126
9290
|
return "taze.config.ts is missing";
|
|
9127
9291
|
}
|
|
9128
|
-
function
|
|
9129
|
-
|
|
9130
|
-
return
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
}
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
return guard !== void 0 && `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
|
+
}
|
|
9138
9301
|
}
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
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) };
|
|
9143
9368
|
}
|
|
9144
|
-
function
|
|
9145
|
-
const
|
|
9146
|
-
if (
|
|
9147
|
-
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) };
|
|
9148
9373
|
}
|
|
9149
|
-
function
|
|
9150
|
-
const cwd = process.cwd();
|
|
9151
|
-
const rootConfigs = findFiles([VITEST_CONFIG_PATTERN], cwd);
|
|
9152
|
-
if (rootConfigs.length === 0) return { ok: false, detail: "vitest.config.ts is missing" };
|
|
9374
|
+
function everyViteConfigHasVitestConfig() {
|
|
9153
9375
|
const workspaceDiscovery = discoverWorkspacesOrFailure((workspace) => !workspace.isRoot);
|
|
9154
9376
|
if (!workspaceDiscovery.ok) return workspaceDiscovery;
|
|
9155
|
-
const
|
|
9156
|
-
|
|
9157
|
-
(
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
}
|
|
9162
|
-
function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9163
|
-
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
9164
|
-
}
|
|
9165
|
-
var SET_CI_RUNS_ON_COMMAND = `gh variable set CI_RUNS_ON --body '"self-hosted"'`;
|
|
9166
|
-
var CODE_QUALITY_WORKFLOW = "williamthorsen/.github/.github/workflows/code-quality-pnpm-workflow.yaml";
|
|
9167
|
-
var MIN_CODE_QUALITY_MAJOR = 8;
|
|
9168
|
-
var GH_TIMEOUT_MS = 15e3;
|
|
9169
|
-
var IMAGE_ABSENT_PROGRAM_PATTERN = /(?:^|[\s;&|(])(corepack|gh|node|npm|npx|pnpm)(?=\s|$)/gm;
|
|
9170
|
-
var NEVER_INSTALLED_PROGRAMS = /* @__PURE__ */ new Set(["corepack", "gh"]);
|
|
9171
|
-
var statusByCwd = /* @__PURE__ */ new Map();
|
|
9172
|
-
async function describeSelfHostedRunnerSkip(cwd = process.cwd(), readFacts) {
|
|
9173
|
-
const status = await getSelfHostedRunnerStatus(cwd, readFacts);
|
|
9174
|
-
return "skip" in status && status.skip;
|
|
9175
|
-
}
|
|
9176
|
-
function findSelfHostedRunnerFailures(facts, cwd) {
|
|
9177
|
-
const failures = [];
|
|
9178
|
-
if (!facts.isPrivate) {
|
|
9179
|
-
failures.push(
|
|
9180
|
-
`${facts.nameWithOwner} is public: A self-hosted runner must never run code from a fork's pull request`
|
|
9181
|
-
);
|
|
9182
|
-
}
|
|
9183
|
-
if (facts.onlineSelfHostedRunners === 0) {
|
|
9184
|
-
failures.push(
|
|
9185
|
-
`${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`
|
|
9186
|
-
);
|
|
9187
|
-
}
|
|
9188
|
-
const workflows = readWorkflows(cwd);
|
|
9189
|
-
failures.push(...findCodeQualityCallerDefects(workflows), ...findImageIncompatibleSteps(workflows));
|
|
9190
|
-
return failures;
|
|
9191
|
-
}
|
|
9192
|
-
async function readGitHubFacts(cwd, runGh = runGhCommand) {
|
|
9193
|
-
const repo = await runGh(["repo", "view", "--json", "nameWithOwner,visibility"], cwd);
|
|
9194
|
-
if (!repo.ok) return { skip: describeGhFailure(repo, "cannot resolve this repo on GitHub") };
|
|
9195
|
-
const repoView = parseJson(repo.stdout);
|
|
9196
|
-
if (!isRecord(repoView) || typeof repoView["nameWithOwner"] !== "string") {
|
|
9197
|
-
return { skip: "gh repo view returned an unexpected shape" };
|
|
9198
|
-
}
|
|
9199
|
-
const nameWithOwner = repoView["nameWithOwner"];
|
|
9200
|
-
const runners = await runGh(["api", `repos/${nameWithOwner}/actions/runners?per_page=100`], cwd);
|
|
9201
|
-
if (!runners.ok) {
|
|
9202
|
-
return {
|
|
9203
|
-
skip: describeGhFailure(runners, `cannot list the runners of ${nameWithOwner}, which needs admin access`)
|
|
9204
|
-
};
|
|
9205
|
-
}
|
|
9206
|
-
const repoVariable = await runGh(["api", `repos/${nameWithOwner}/actions/variables/CI_RUNS_ON`], cwd);
|
|
9207
|
-
if (!repoVariable.ok && !isNotFound(repoVariable)) {
|
|
9208
|
-
return { skip: describeGhFailure(repoVariable, `cannot read the CI_RUNS_ON variable of ${nameWithOwner}`) };
|
|
9209
|
-
}
|
|
9210
|
-
const repoValue = repoVariable.ok ? readVariableValue(parseJson(repoVariable.stdout)) : void 0;
|
|
9211
|
-
return {
|
|
9212
|
-
nameWithOwner,
|
|
9213
|
-
isPrivate: repoView["visibility"] !== "PUBLIC",
|
|
9214
|
-
onlineSelfHostedRunners: countOnlineSelfHostedRunners(parseJson(runners.stdout)),
|
|
9215
|
-
ciRunsOn: repoValue ?? await readOrganizationValue(nameWithOwner, cwd, runGh)
|
|
9216
|
-
};
|
|
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) };
|
|
9217
9384
|
}
|
|
9218
|
-
function
|
|
9219
|
-
|
|
9220
|
-
const
|
|
9221
|
-
if (
|
|
9222
|
-
|
|
9223
|
-
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) };
|
|
9224
9390
|
}
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
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;
|
|
9229
9399
|
return {
|
|
9230
9400
|
ok: false,
|
|
9231
|
-
detail:
|
|
9232
|
-
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}`
|
|
9233
9402
|
};
|
|
9234
9403
|
}
|
|
9235
|
-
|
|
9236
|
-
const
|
|
9237
|
-
|
|
9238
|
-
if (
|
|
9239
|
-
|
|
9240
|
-
if (
|
|
9241
|
-
|
|
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) };
|
|
9242
9416
|
}
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
if ("skip" in status) return status.skip;
|
|
9246
|
-
if (status.selection === "unset") return "CI_RUNS_ON is unset";
|
|
9247
|
-
if (status.selection === "other") return `CI_RUNS_ON selects ${status.facts.ciRunsOn ?? ""}`;
|
|
9248
|
-
return false;
|
|
9417
|
+
function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
|
|
9418
|
+
return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
|
|
9249
9419
|
}
|
|
9250
|
-
|
|
9251
|
-
const
|
|
9252
|
-
if (
|
|
9253
|
-
|
|
9254
|
-
const conditions = status.exempt === void 0 ? status.failures : [`the repo is exempt: ${status.exempt}`, ...status.failures];
|
|
9255
|
-
if (status.selection !== "invalid" && conditions.length === 0) return true;
|
|
9256
|
-
const invalidValue = `CI_RUNS_ON is ${facts.ciRunsOn ?? ""}, which is not JSON, so every workflow that reads it with fromJSON fails`;
|
|
9257
|
-
if (conditions.length === 0) {
|
|
9258
|
-
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` };
|
|
9259
9424
|
}
|
|
9260
|
-
const
|
|
9425
|
+
const staleConfigs = matches.filter(
|
|
9426
|
+
(relativePath) => !importsSharedExport(readFileIn(cwd, relativePath), exportName, SHARED_VITEST_MODULE)
|
|
9427
|
+
);
|
|
9428
|
+
if (staleConfigs.length === 0) return true;
|
|
9261
9429
|
return {
|
|
9262
9430
|
ok: false,
|
|
9263
|
-
detail:
|
|
9264
|
-
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(", ")}`
|
|
9265
9432
|
};
|
|
9266
9433
|
}
|
|
9267
|
-
function
|
|
9268
|
-
|
|
9269
|
-
return
|
|
9270
|
-
(runner) => isRecord(runner) && runner["status"] === "online" && Array.isArray(runner["labels"]) && runner["labels"].some((label) => isRecord(label) && label["name"] === "self-hosted")
|
|
9271
|
-
).length;
|
|
9272
|
-
}
|
|
9273
|
-
function describeGhFailure(result, context) {
|
|
9274
|
-
if (result.isMissing) return "gh is not installed";
|
|
9275
|
-
if (/gh auth login|not logged in/i.test(result.stderr)) return "gh is not authenticated";
|
|
9276
|
-
const firstLine = result.stderr.trim().split("\n", 1)[0] ?? "";
|
|
9277
|
-
return firstLine === "" ? `gh ${context}` : `gh ${context}: ${firstLine}`;
|
|
9278
|
-
}
|
|
9279
|
-
function findCodeQualityCallerDefects(workflows) {
|
|
9280
|
-
const defects = [];
|
|
9281
|
-
for (const { file, jobs } of workflows) {
|
|
9282
|
-
for (const [jobName, job] of jobs) {
|
|
9283
|
-
const uses = job["uses"];
|
|
9284
|
-
if (typeof uses !== "string" || !uses.startsWith(`${CODE_QUALITY_WORKFLOW}@`)) continue;
|
|
9285
|
-
const ref = uses.slice(CODE_QUALITY_WORKFLOW.length + 1);
|
|
9286
|
-
const major = /^v(\d+)(?:\.\d+){0,2}$/.exec(ref)?.[1];
|
|
9287
|
-
if (major === void 0) {
|
|
9288
|
-
defects.push(
|
|
9289
|
-
`${file} job ${jobName} pins the code-quality workflow at ${ref}, whose release cannot be determined`
|
|
9290
|
-
);
|
|
9291
|
-
} else if (Number(major) < MIN_CODE_QUALITY_MAJOR) {
|
|
9292
|
-
defects.push(
|
|
9293
|
-
`${file} job ${jobName} pins the code-quality workflow at ${ref}, which predates CI_RUNS_ON; pin v${MIN_CODE_QUALITY_MAJOR} or later`
|
|
9294
|
-
);
|
|
9295
|
-
}
|
|
9296
|
-
const inputs = job["with"];
|
|
9297
|
-
if (isRecord(inputs) && inputs["runs-on"] !== void 0) {
|
|
9298
|
-
defects.push(`${file} job ${jobName} passes runs-on, which overrides CI_RUNS_ON`);
|
|
9299
|
-
}
|
|
9300
|
-
}
|
|
9301
|
-
}
|
|
9302
|
-
return defects;
|
|
9434
|
+
function describeConventionsGuardSkip() {
|
|
9435
|
+
const guard = findConventionsGuard(process.cwd());
|
|
9436
|
+
return guard !== void 0 && `reported by checkTestFileConventions in ${guard}`;
|
|
9303
9437
|
}
|
|
9304
|
-
function
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
if (!/\bvars\.CI_RUNS_ON\b/.test(JSON.stringify(job["runs-on"] ?? ""))) continue;
|
|
9309
|
-
const steps = job["steps"];
|
|
9310
|
-
if (!Array.isArray(steps)) continue;
|
|
9311
|
-
const installed = /* @__PURE__ */ new Set();
|
|
9312
|
-
for (const [index, step] of steps.entries()) {
|
|
9313
|
-
if (!isRecord(step)) continue;
|
|
9314
|
-
const stepLabel = typeof step["name"] === "string" ? step["name"] : `step ${index + 1}`;
|
|
9315
|
-
const run = step["run"];
|
|
9316
|
-
if (typeof run === "string") {
|
|
9317
|
-
const missing = findUninstalledPrograms(run, installed);
|
|
9318
|
-
if (missing.length > 0) {
|
|
9319
|
-
defects.push(
|
|
9320
|
-
`${file} job ${jobName}, ${stepLabel}: runs ${missing.join(", ")}, which the runner image lacks at that step`
|
|
9321
|
-
);
|
|
9322
|
-
}
|
|
9323
|
-
}
|
|
9324
|
-
for (const program of readInstalledPrograms(step)) installed.add(program);
|
|
9325
|
-
}
|
|
9326
|
-
}
|
|
9327
|
-
}
|
|
9328
|
-
return defects;
|
|
9438
|
+
function findConventionsGuard(cwd) {
|
|
9439
|
+
return findTestFiles(cwd).find(
|
|
9440
|
+
(relativePath) => importsSharedExport(readFileIn(cwd, relativePath), "checkTestFileConventions", SHARED_TESTS_MODULE)
|
|
9441
|
+
);
|
|
9329
9442
|
}
|
|
9330
|
-
function
|
|
9331
|
-
|
|
9332
|
-
for (const match of run.matchAll(IMAGE_ABSENT_PROGRAM_PATTERN)) {
|
|
9333
|
-
const program = match[1];
|
|
9334
|
-
if (program === void 0) continue;
|
|
9335
|
-
const provider = program === "npm" || program === "npx" ? "node" : program;
|
|
9336
|
-
if (NEVER_INSTALLED_PROGRAMS.has(program) || !installed.has(provider)) programs.add(program);
|
|
9337
|
-
}
|
|
9338
|
-
return [...programs].toSorted();
|
|
9443
|
+
function findWorkspaceConfigs(workspace, pattern) {
|
|
9444
|
+
return findFiles([pattern], workspace.absolutePath).map((name) => `${workspace.dir}/${name}`);
|
|
9339
9445
|
}
|
|
9340
|
-
function
|
|
9341
|
-
|
|
9342
|
-
let status = statusByCwd.get(cwd);
|
|
9343
|
-
if (status === void 0) {
|
|
9344
|
-
status = gatherSelfHostedRunnerStatus(cwd, (dir) => readGitHubFacts(dir));
|
|
9345
|
-
statusByCwd.set(cwd, status);
|
|
9346
|
-
}
|
|
9347
|
-
return status;
|
|
9446
|
+
function hasViteConfigBeside(cwd, relativePath) {
|
|
9447
|
+
return findFiles([VITE_CONFIG_PATTERN], join4(cwd, dirname(relativePath))).length > 0;
|
|
9348
9448
|
}
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
if ("skip" in facts) return facts;
|
|
9352
|
-
const failures = findSelfHostedRunnerFailures(facts, cwd);
|
|
9353
|
-
let exempt;
|
|
9354
|
-
try {
|
|
9355
|
-
exempt = (await loadRootConfig(cwd)).selfHostedRunner?.exempt;
|
|
9356
|
-
} catch (error) {
|
|
9357
|
-
failures.push(`the nmr config cannot be loaded: ${describeError(error)}`);
|
|
9358
|
-
}
|
|
9359
|
-
return {
|
|
9360
|
-
facts,
|
|
9361
|
-
selection: readRunnerSelection(facts.ciRunsOn),
|
|
9362
|
-
failures,
|
|
9363
|
-
exempt,
|
|
9364
|
-
toolTestFiles: findTestFiles(cwd).filter(
|
|
9365
|
-
(file) => file.endsWith(".tool.test.ts") || file.endsWith(".tool.test.tsx")
|
|
9366
|
-
)
|
|
9367
|
-
};
|
|
9449
|
+
function isOwnedByReExportCheck(cwd, relativePath) {
|
|
9450
|
+
return isReExportOnly(readFileIn(cwd, relativePath)) && !hasViteConfigBeside(cwd, relativePath);
|
|
9368
9451
|
}
|
|
9369
|
-
function
|
|
9370
|
-
|
|
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));
|
|
9371
9456
|
}
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
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`
|
|
9377
9468
|
}
|
|
9378
|
-
|
|
9379
|
-
function
|
|
9380
|
-
const
|
|
9381
|
-
if (
|
|
9382
|
-
const
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
}
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
if (!result.ok) return void 0;
|
|
9394
|
-
const response = parseJson(result.stdout);
|
|
9395
|
-
if (!isRecord(response) || !Array.isArray(response["variables"])) return void 0;
|
|
9396
|
-
const variable = response["variables"].find(
|
|
9397
|
-
(entry) => isRecord(entry) && entry["name"] === "CI_RUNS_ON"
|
|
9398
|
-
);
|
|
9399
|
-
return readVariableValue(variable);
|
|
9400
|
-
}
|
|
9401
|
-
function readVariableValue(variable) {
|
|
9402
|
-
return isRecord(variable) && typeof variable["value"] === "string" ? variable["value"] : void 0;
|
|
9403
|
-
}
|
|
9404
|
-
function readWorkflows(cwd) {
|
|
9405
|
-
const dir = join(cwd, ".github", "workflows");
|
|
9406
|
-
if (!existsSync3(dir)) return [];
|
|
9407
|
-
const workflows = [];
|
|
9408
|
-
const files = readdirSync2(dir).filter((name) => /\.ya?ml$/.test(name)).toSorted();
|
|
9409
|
-
for (const file of files) {
|
|
9410
|
-
let document;
|
|
9411
|
-
try {
|
|
9412
|
-
document = (0, import_yaml2.parse)(readFile(join(dir, file)) ?? "");
|
|
9413
|
-
} catch {
|
|
9414
|
-
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);
|
|
9415
9484
|
}
|
|
9416
|
-
if (!isRecord(document) || !isRecord(document["jobs"])) continue;
|
|
9417
|
-
const jobs = Object.entries(document["jobs"]).filter(
|
|
9418
|
-
(entry) => isRecord(entry[1])
|
|
9419
|
-
);
|
|
9420
|
-
workflows.push({ file: posix.join(".github/workflows", file), jobs });
|
|
9421
9485
|
}
|
|
9422
|
-
return
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
if (error === null) {
|
|
9428
|
-
resolve({ ok: true, stdout });
|
|
9429
|
-
return;
|
|
9430
|
-
}
|
|
9431
|
-
resolve({ ok: false, isMissing: "code" in error && error.code === "ENOENT", stderr: stderr || error.message });
|
|
9432
|
-
});
|
|
9433
|
-
});
|
|
9486
|
+
if (failingPackages.length === 0) return true;
|
|
9487
|
+
return {
|
|
9488
|
+
ok: false,
|
|
9489
|
+
detail: `missing build override or tsconfig.json + src/: ${failingPackages.join(", ")}`
|
|
9490
|
+
};
|
|
9434
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
|
+
});
|
|
9435
9529
|
export {
|
|
9436
|
-
|
|
9437
|
-
default_default as default,
|
|
9438
|
-
describeReadinessSkip,
|
|
9439
|
-
describeRecommendationSkip,
|
|
9440
|
-
describeSelfHostedRunnerSkip,
|
|
9441
|
-
everyBinTargetIsACommittedWrapper,
|
|
9442
|
-
everyBinWrapperTargetIsCoveredByFiles,
|
|
9443
|
-
everyTestFileNamesItsTier,
|
|
9444
|
-
everyTestFileSitsUnderTestsDir,
|
|
9445
|
-
everyViteConfigHasVitestConfig,
|
|
9446
|
-
findSelfHostedRunnerFailures,
|
|
9447
|
-
hasSupportedEslintVersion,
|
|
9448
|
-
hasSupportedStrictLintVersion,
|
|
9449
|
-
noPnpmFieldInPackageJson,
|
|
9450
|
-
noReExportOnlyVitestConfigs,
|
|
9451
|
-
noRetiredVitestConfigs,
|
|
9452
|
-
noUnguardedLefthookInstall,
|
|
9453
|
-
noWorkspacesFieldInPackageJson,
|
|
9454
|
-
prettierConfigBuildsOnSharedConfig,
|
|
9455
|
-
readGitHubFacts,
|
|
9456
|
-
readRunnerSelection,
|
|
9457
|
-
recommendSelfHostedRunner,
|
|
9458
|
-
selfHostedRunnerCanRunCi,
|
|
9459
|
-
tazeConfigAvoidsClobberedOptions,
|
|
9460
|
-
tazeConfigBuildsOnSharedConfig,
|
|
9461
|
-
testSuiteGatesTestFileConventions,
|
|
9462
|
-
vitestConfigBuildsOnSharedConfig,
|
|
9463
|
-
vitestRootConfigBuildsOnSharedConfig
|
|
9530
|
+
default_default as default
|
|
9464
9531
|
};
|