@williamthorsen/nmr 0.44.0 → 0.45.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.
@@ -7421,6 +7421,20 @@ function resolveConfigPath(baseDir) {
7421
7421
  return path.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
7422
7422
  }
7423
7423
 
7424
+ // src/helpers/escapeControlCharacters.ts
7425
+ var NAMED_ESCAPES = /* @__PURE__ */ new Map([
7426
+ ["\n", String.raw`\n`],
7427
+ ["\r", String.raw`\r`],
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);
7434
+ }
7435
+ return escapedText;
7436
+ }
7437
+
7424
7438
  // src/helpers/findClosestName.ts
7425
7439
  function findClosestName(name, candidates) {
7426
7440
  const ceiling = Math.max(1, Math.floor(name.length / 4));
@@ -7478,24 +7492,332 @@ function isStringRecord(value) {
7478
7492
  return isObject(value) && Object.values(value).every((entry) => typeof entry === "string");
7479
7493
  }
7480
7494
 
7481
- // src/resolver.ts
7482
- import { readFileSync as readFileSync2 } from "node:fs";
7483
-
7484
- // ../nmr-core/dist/esm/hasErrnoCode.js
7485
- function hasErrnoCode(error, code) {
7486
- return isError(error) && "code" in error && error.code === code;
7495
+ // src/steps.ts
7496
+ var ENV_ASSIGNMENT = /^\w+=/;
7497
+ var LAUNCHERS = /* @__PURE__ */ new Map([
7498
+ ["bun", /* @__PURE__ */ new Set(["x"])],
7499
+ ["bunx", /* @__PURE__ */ new Set()],
7500
+ ["npm", /* @__PURE__ */ new Set(["exec"])],
7501
+ ["npx", /* @__PURE__ */ new Set()],
7502
+ ["pnpm", /* @__PURE__ */ new Set(["dlx", "exec"])],
7503
+ ["yarn", /* @__PURE__ */ new Set(["dlx", "exec"])]
7504
+ ]);
7505
+ var QUOTES = /* @__PURE__ */ new Set(['"', "'"]);
7506
+ var SEGMENT_SEPARATORS = /* @__PURE__ */ new Set([";", "|", "&", "\n", "\r"]);
7507
+ var SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
7508
+ var TOKEN_SEPARATORS = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
7509
+ function findUnexpressibleToken(element) {
7510
+ return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
7487
7511
  }
7488
-
7489
- // src/helpers/package-json.ts
7490
- import path4 from "node:path";
7491
-
7492
- // src/UserError.ts
7493
- var UserError = class extends Error {
7494
- constructor(message) {
7495
- super(message);
7496
- this.name = "UserError";
7512
+ function quoteToken(token) {
7513
+ if (SHELL_SAFE_TOKEN.test(token)) {
7514
+ return token;
7515
+ }
7516
+ return "'" + token.replaceAll("'", String.raw`'\''`) + "'";
7517
+ }
7518
+ function readSelfReference(options) {
7519
+ const { commandName, scope, script } = options;
7520
+ const segments = splitSegments(script).filter((segment) => segment.trim() !== "");
7521
+ const doesReenter = segments.some((segment) => {
7522
+ const invocation = readNmrTail(segment);
7523
+ const target = invocation === void 0 ? void 0 : readNmrTarget(invocation.tail);
7524
+ if (invocation === void 0 || target === void 0) {
7525
+ return false;
7526
+ }
7527
+ const launcherScope = readLauncherScope(invocation.launcher, invocation.launcherArgs);
7528
+ const effectiveTarget = {
7529
+ command: target.command,
7530
+ isDelegate: target.isDelegate || launcherScope.isDelegate,
7531
+ isRecursive: target.isRecursive || launcherScope.isRecursive,
7532
+ isWorkspaceRoot: target.isWorkspaceRoot || launcherScope.isWorkspaceRoot
7533
+ };
7534
+ return isReentry(effectiveTarget, commandName, scope);
7535
+ });
7536
+ if (!doesReenter) {
7537
+ return void 0;
7538
+ }
7539
+ return segments.length === 1 ? "sole" : "chained";
7540
+ }
7541
+ function readStepListSelfReference(options) {
7542
+ const { commandName, elements, scope } = options;
7543
+ const doesReenter = elements.some((element) => {
7544
+ const target = readNmrTarget(tokenize(element));
7545
+ return target !== void 0 && isReentry(target, commandName, scope);
7546
+ });
7547
+ if (!doesReenter) {
7548
+ return void 0;
7549
+ }
7550
+ return elements.length === 1 ? "sole" : "chained";
7551
+ }
7552
+ function dropLeadingAssignments(tokens) {
7553
+ const start = tokens.findIndex((token) => !ENV_ASSIGNMENT.test(token));
7554
+ return start === -1 ? [] : tokens.slice(start);
7555
+ }
7556
+ function isRedirectionOperator(char, precedingText, nextChar) {
7557
+ if (char !== "&" && char !== "|") {
7558
+ return false;
7559
+ }
7560
+ return char === "&" && nextChar === ">" || /[<>]\s*$/.test(precedingText);
7561
+ }
7562
+ function isReentry(target, commandName, scope) {
7563
+ if (target.command !== commandName) {
7564
+ return false;
7565
+ }
7566
+ switch (scope) {
7567
+ case "root":
7568
+ return !target.isDelegate;
7569
+ case "package":
7570
+ return target.isRecursive || !target.isDelegate && !target.isWorkspaceRoot;
7571
+ case "workspaceRegistry":
7572
+ return target.isDelegate || !target.isWorkspaceRoot;
7573
+ }
7574
+ }
7575
+ function readLauncherScope(launcher, launcherArgs) {
7576
+ const scope = { isDelegate: false, isRecursive: false, isWorkspaceRoot: false };
7577
+ if (launcher !== "pnpm") {
7578
+ return scope;
7579
+ }
7580
+ for (const token of launcherArgs) {
7581
+ if (token === "-r" || token === "--recursive") {
7582
+ scope.isDelegate = true;
7583
+ scope.isRecursive = true;
7584
+ } else if (token === "-F" || token === "--filter" || token.startsWith("--filter=")) {
7585
+ scope.isDelegate = true;
7586
+ } else if (token === "-w" || token === "--workspace-root") {
7587
+ scope.isWorkspaceRoot = true;
7588
+ }
7589
+ }
7590
+ return scope;
7591
+ }
7592
+ function readNmrTail(segment) {
7593
+ const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
7594
+ if (head === void 0) {
7595
+ return void 0;
7596
+ }
7597
+ if (head === "nmr") {
7598
+ return { launcher: void 0, launcherArgs: [], tail: rest };
7599
+ }
7600
+ const subcommands = LAUNCHERS.get(head);
7601
+ if (subcommands === void 0) {
7602
+ return void 0;
7603
+ }
7604
+ const nmrIndex = subcommands.size === 0 ? rest.findIndex((token) => !token.startsWith("-")) : rest.findIndex((token) => subcommands.has(token)) + 1;
7605
+ if (nmrIndex <= 0 && subcommands.size > 0) {
7606
+ return void 0;
7607
+ }
7608
+ return rest[nmrIndex] === "nmr" ? { launcher: head, launcherArgs: rest.slice(0, nmrIndex), tail: rest.slice(nmrIndex + 1) } : void 0;
7609
+ }
7610
+ function readNmrTarget(tokens) {
7611
+ let isDelegate = false;
7612
+ let isRecursive = false;
7613
+ let isWorkspaceRoot = false;
7614
+ let index = 0;
7615
+ while (index < tokens.length) {
7616
+ const token = tokens[index] ?? "";
7617
+ switch (token) {
7618
+ // The pattern is the flag's value, and reading it as a command name would name whichever package it
7619
+ // selects rather than the command that every selected scope runs.
7620
+ case "-F":
7621
+ case "--filter":
7622
+ isDelegate = true;
7623
+ index += 2;
7624
+ break;
7625
+ case "-R":
7626
+ case "--recursive":
7627
+ isDelegate = true;
7628
+ isRecursive = true;
7629
+ index += 1;
7630
+ break;
7631
+ case "-w":
7632
+ case "--workspace-root":
7633
+ isWorkspaceRoot = true;
7634
+ index += 1;
7635
+ break;
7636
+ default:
7637
+ if (!token.startsWith("-")) {
7638
+ return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
7639
+ }
7640
+ index += 1;
7641
+ }
7642
+ }
7643
+ return void 0;
7644
+ }
7645
+ function splitSegments(command) {
7646
+ const segments = [];
7647
+ let currentSegment = "";
7648
+ let quote;
7649
+ let index = 0;
7650
+ while (index < command.length) {
7651
+ const char = command[index] ?? "";
7652
+ if (quote !== void 0) {
7653
+ if (char === "\\" && quote === '"') {
7654
+ currentSegment += char + (command[index + 1] ?? "");
7655
+ index += 2;
7656
+ continue;
7657
+ }
7658
+ currentSegment += char;
7659
+ if (char === quote) quote = void 0;
7660
+ index += 1;
7661
+ continue;
7662
+ }
7663
+ if (char === "\\") {
7664
+ currentSegment += char + (command[index + 1] ?? "");
7665
+ index += 2;
7666
+ continue;
7667
+ }
7668
+ if (QUOTES.has(char)) {
7669
+ quote = char;
7670
+ currentSegment += char;
7671
+ index += 1;
7672
+ continue;
7673
+ }
7674
+ if (SEGMENT_SEPARATORS.has(char)) {
7675
+ if (isRedirectionOperator(char, currentSegment, command[index + 1])) {
7676
+ currentSegment += char;
7677
+ index += 1;
7678
+ continue;
7679
+ }
7680
+ index += command[index + 1] === char ? 2 : 1;
7681
+ segments.push(currentSegment);
7682
+ currentSegment = "";
7683
+ continue;
7684
+ }
7685
+ currentSegment += char;
7686
+ index += 1;
7687
+ }
7688
+ segments.push(currentSegment);
7689
+ return segments;
7690
+ }
7691
+ function tokenize(element) {
7692
+ return element.split(/\s+/).filter((token) => token.length > 0);
7693
+ }
7694
+ function tokenizeSegment(segment) {
7695
+ const tokens = [];
7696
+ let currentToken = "";
7697
+ let quote;
7698
+ let index = 0;
7699
+ while (index < segment.length) {
7700
+ const char = segment[index] ?? "";
7701
+ if (quote !== void 0) {
7702
+ if (char === "\\" && quote === '"') {
7703
+ currentToken += char + (segment[index + 1] ?? "");
7704
+ index += 2;
7705
+ continue;
7706
+ }
7707
+ currentToken += char;
7708
+ if (char === quote) quote = void 0;
7709
+ index += 1;
7710
+ continue;
7711
+ }
7712
+ if (char === "\\") {
7713
+ currentToken += char + (segment[index + 1] ?? "");
7714
+ index += 2;
7715
+ continue;
7716
+ }
7717
+ if (QUOTES.has(char)) {
7718
+ quote = char;
7719
+ currentToken += char;
7720
+ index += 1;
7721
+ continue;
7722
+ }
7723
+ if (TOKEN_SEPARATORS.has(char)) {
7724
+ if (currentToken !== "") {
7725
+ tokens.push(currentToken);
7726
+ }
7727
+ currentToken = "";
7728
+ index += 1;
7729
+ continue;
7730
+ }
7731
+ currentToken += char;
7732
+ index += 1;
7733
+ }
7734
+ if (currentToken !== "") {
7735
+ tokens.push(currentToken);
7497
7736
  }
7737
+ return tokens;
7738
+ }
7739
+
7740
+ // src/default-scripts.ts
7741
+ var GATE_PROJECTS = "--project unit --project tool";
7742
+ var TYPECHECK_STEP = { run: "typecheck", shouldDeclineArguments: true };
7743
+ var ROOT_TYPECHECK_STEP = { run: "root:typecheck", shouldDeclineArguments: true };
7744
+ var LINT_CHECK_STEP = { run: "lint:check", shouldRunInBackground: true };
7745
+ var LINT_STRICT_STEP = { run: "lint:strict", shouldRunInBackground: true };
7746
+ var workspaceScripts = {
7747
+ build: ["compile"],
7748
+ check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7749
+ "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7750
+ clean: "nmr-clean",
7751
+ compile: "nmr-compile",
7752
+ fix: ["lint", "fmt"],
7753
+ "fix:check": ["fmt:check", "lint:check"],
7754
+ fmt: "nmr-fmt --write",
7755
+ "fmt:check": "nmr-fmt --check",
7756
+ lint: "eslint --fix .",
7757
+ "lint:check": "eslint .",
7758
+ "lint:strict": "strict-lint",
7759
+ "report-catalog": "nmr-report-catalog",
7760
+ test: `pnpm exec vitest ${GATE_PROJECTS}`,
7761
+ "test:all": "pnpm exec vitest",
7762
+ "test:coverage": `pnpm exec vitest ${GATE_PROJECTS} --coverage`,
7763
+ "test:tool": "pnpm exec vitest --project tool",
7764
+ "test:unit": "pnpm exec vitest --project unit",
7765
+ "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
7766
+ typecheck: "tsgo --noEmit",
7767
+ // The command is a string because neither half names an nmr command: Both are binaries.
7768
+ upgrade: "nmr-report-catalog && nmr-taze",
7769
+ "view-coverage": "open coverage/index.html"
7498
7770
  };
7771
+ function buildRootScripts(workspaceGlobs) {
7772
+ const ignorePatterns = workspaceGlobs.map((glob) => `--ignore-pattern ${quoteToken(glob)} `).join("");
7773
+ return {
7774
+ audit: ["audit:prod", "audit:dev"],
7775
+ "audit:dev": "pnpm exec v11y --dev",
7776
+ "audit:prod": "pnpm exec v11y --prod",
7777
+ build: ["-R build"],
7778
+ check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7779
+ "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7780
+ // Excludes the audit, which in CI has a workflow of its own. The narrowed check runs against the build, so
7781
+ // the build declines the arguments rather than being narrowed by them.
7782
+ ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
7783
+ clean: "nmr-clean",
7784
+ fix: ["lint", "fmt"],
7785
+ "fix:check": ["fmt:check", "lint:check"],
7786
+ fmt: "nmr-fmt --write",
7787
+ "fmt:check": "nmr-fmt --check",
7788
+ lint: "eslint --fix .",
7789
+ "lint:check": "eslint .",
7790
+ "lint:strict": "strict-lint",
7791
+ // The audit takes seconds and `ci` takes minutes, so the cheap gate fails first. The audit reads the
7792
+ // dependency tree, and an argument narrowing the code under test does not say anything about that tree.
7793
+ prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
7794
+ "report-overrides": "nmr-report-overrides",
7795
+ "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
7796
+ "root:lint": `eslint --fix ${ignorePatterns}.`,
7797
+ "root:lint:check": `eslint ${ignorePatterns}.`,
7798
+ "root:lint:strict": `strict-lint ${ignorePatterns}.`,
7799
+ "root:test": `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
7800
+ "root:test:all": "vitest --config ./vitest.root.config.ts",
7801
+ "root:test:tool": "vitest --config ./vitest.root.config.ts --project tool",
7802
+ "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
7803
+ "root:typecheck": "tsgo --noEmit",
7804
+ // Includes the override report for the same reason `upgrade` does: Both end in the tool that rewrites a
7805
+ // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
7806
+ "root:upgrade": "nmr-report-overrides && nmr-taze",
7807
+ test: ["root:test", "-R test"],
7808
+ "test:all": ["root:test:all", "-R test:all"],
7809
+ "test:coverage": ["root:test", "-R test:coverage"],
7810
+ "test:tool": ["root:test:tool", "-R test:tool"],
7811
+ "test:unit": ["root:test:unit", "-R test:unit"],
7812
+ "test:watch": `vitest ${GATE_PROJECTS} --watch`,
7813
+ // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
7814
+ // default options at the root and searching for it in every package.
7815
+ typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
7816
+ // The command is a string because neither half names an nmr command: Both are binaries, and a composite
7817
+ // element can name only a command.
7818
+ upgrade: "nmr-report-overrides && nmr-taze --recursive"
7819
+ };
7820
+ }
7499
7821
 
7500
7822
  // ../nmr-core/dist/esm/workspace.js
7501
7823
  import { existsSync, readFileSync } from "node:fs";
@@ -7602,6 +7924,14 @@ function readWorkspaceManifest(monorepoRoot) {
7602
7924
  }
7603
7925
  }
7604
7926
 
7927
+ // src/UserError.ts
7928
+ var UserError = class extends Error {
7929
+ constructor(message) {
7930
+ super(message);
7931
+ this.name = "UserError";
7932
+ }
7933
+ };
7934
+
7605
7935
  // src/workspace.ts
7606
7936
  var WORKSPACE_MANIFEST2 = "pnpm-workspace.yaml";
7607
7937
  function getWorkspacePackageDirs(monorepoRoot) {
@@ -7612,7 +7942,32 @@ function getWorkspacePackageDirs(monorepoRoot) {
7612
7942
  return resolution.kind === "packages" ? resolution.packageDirs : [];
7613
7943
  }
7614
7944
 
7945
+ // src/workspace-globs.ts
7946
+ import path4 from "node:path";
7947
+ function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
7948
+ return packageDirs.map((dir) => path4.relative(monorepoRoot, dir).split(path4.sep).join("/")).filter((relativeDir) => relativeDir !== "").map((relativeDir) => `${relativeDir}/**`).toSorted();
7949
+ }
7950
+
7951
+ // src/resolve-scripts.ts
7952
+ function getDefaultWorkspaceScripts() {
7953
+ return { ...workspaceScripts };
7954
+ }
7955
+ function getDefaultRootScripts(monorepoRoot) {
7956
+ const resolution = resolveWorkspace(monorepoRoot);
7957
+ const packageDirs = resolution.kind === "packages" ? resolution.packageDirs : [];
7958
+ return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
7959
+ }
7960
+
7961
+ // src/resolver.ts
7962
+ import { readFileSync as readFileSync2 } from "node:fs";
7963
+
7964
+ // ../nmr-core/dist/esm/hasErrnoCode.js
7965
+ function hasErrnoCode(error, code) {
7966
+ return isError(error) && "code" in error && error.code === code;
7967
+ }
7968
+
7615
7969
  // src/helpers/package-json.ts
7970
+ import path5 from "node:path";
7616
7971
  function parsePackageJson(rawText, file) {
7617
7972
  try {
7618
7973
  return JSON.parse(rawText);
@@ -7631,7 +7986,7 @@ function readScriptRecord(dir, scripts) {
7631
7986
  return result;
7632
7987
  }
7633
7988
  function resolvePackageJsonPath(dir) {
7634
- return path4.join(dir, "package.json");
7989
+ return path5.join(dir, "package.json");
7635
7990
  }
7636
7991
  function formatMalformedScript(dir, key, value) {
7637
7992
  const rejection = `Invalid package.json at ${resolvePackageJsonPath(dir)}: \`scripts.${key}\` must be a string.`;
@@ -7642,120 +7997,20 @@ function formatMalformedScript(dir, key, value) {
7642
7997
  return `${rejection} A step list belongs in \`${CONFIG_RELATIVE_PATH}\` under \`${field}\`.`;
7643
7998
  }
7644
7999
 
7645
- // src/steps.ts
7646
- var SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
7647
- function findUnexpressibleToken(element) {
7648
- return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
8000
+ // src/resolver.ts
8001
+ function describeScript(script) {
8002
+ return typeof script === "string" ? script : `[${script.map(describeElement).join(", ")}]`;
7649
8003
  }
7650
- function quoteToken(token) {
7651
- if (SHELL_SAFE_TOKEN.test(token)) {
7652
- return token;
8004
+ function describeElement(element) {
8005
+ if (typeof element === "string") {
8006
+ return element;
7653
8007
  }
7654
- return "'" + token.replaceAll("'", String.raw`'\''`) + "'";
7655
- }
7656
- function tokenize(element) {
7657
- return element.split(/\s+/).filter((token) => token.length > 0);
7658
- }
7659
-
7660
- // src/default-scripts.ts
7661
- var GATE_PROJECTS = "--project unit --project tool";
7662
- var TYPECHECK_STEP = { run: "typecheck", shouldDeclineArguments: true };
7663
- var ROOT_TYPECHECK_STEP = { run: "root:typecheck", shouldDeclineArguments: true };
7664
- var LINT_CHECK_STEP = { run: "lint:check", shouldRunInBackground: true };
7665
- var LINT_STRICT_STEP = { run: "lint:strict", shouldRunInBackground: true };
7666
- var workspaceScripts = {
7667
- build: ["compile"],
7668
- check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7669
- "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7670
- clean: "nmr-clean",
7671
- compile: "nmr-compile",
7672
- fix: ["lint", "fmt"],
7673
- "fix:check": ["fmt:check", "lint:check"],
7674
- fmt: "nmr-fmt --write",
7675
- "fmt:check": "nmr-fmt --check",
7676
- lint: "eslint --fix .",
7677
- "lint:check": "eslint .",
7678
- "lint:strict": "strict-lint",
7679
- "report-catalog": "nmr-report-catalog",
7680
- test: `pnpm exec vitest ${GATE_PROJECTS}`,
7681
- "test:all": "pnpm exec vitest",
7682
- "test:coverage": `pnpm exec vitest ${GATE_PROJECTS} --coverage`,
7683
- "test:tool": "pnpm exec vitest --project tool",
7684
- "test:unit": "pnpm exec vitest --project unit",
7685
- "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
7686
- typecheck: "tsgo --noEmit",
7687
- // The command is a string because neither half names an nmr command: Both are binaries.
7688
- upgrade: "nmr-report-catalog && nmr-taze",
7689
- "view-coverage": "open coverage/index.html"
7690
- };
7691
- function buildRootScripts(workspaceGlobs) {
7692
- const ignorePatterns = workspaceGlobs.map((glob) => `--ignore-pattern ${quoteToken(glob)} `).join("");
7693
- return {
7694
- audit: ["audit:prod", "audit:dev"],
7695
- "audit:dev": "pnpm exec v11y --dev",
7696
- "audit:prod": "pnpm exec v11y --prod",
7697
- build: ["-R build"],
7698
- check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7699
- "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7700
- // Excludes the audit, which in CI has a workflow of its own. The narrowed check runs against the build, so
7701
- // the build declines the arguments rather than being narrowed by them.
7702
- ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
7703
- clean: "nmr-clean",
7704
- fix: ["lint", "fmt"],
7705
- "fix:check": ["fmt:check", "lint:check"],
7706
- fmt: "nmr-fmt --write",
7707
- "fmt:check": "nmr-fmt --check",
7708
- lint: "eslint --fix .",
7709
- "lint:check": "eslint .",
7710
- "lint:strict": "strict-lint",
7711
- // The audit takes seconds and `ci` takes minutes, so the cheap gate fails first. The audit reads the
7712
- // dependency tree, and an argument narrowing the code under test does not say anything about that tree.
7713
- prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
7714
- "report-overrides": "nmr-report-overrides",
7715
- "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
7716
- "root:lint": `eslint --fix ${ignorePatterns}.`,
7717
- "root:lint:check": `eslint ${ignorePatterns}.`,
7718
- "root:lint:strict": `strict-lint ${ignorePatterns}.`,
7719
- "root:test": `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
7720
- "root:test:all": "vitest --config ./vitest.root.config.ts",
7721
- "root:test:tool": "vitest --config ./vitest.root.config.ts --project tool",
7722
- "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
7723
- "root:typecheck": "tsgo --noEmit",
7724
- // Includes the override report for the same reason `upgrade` does: Both end in the tool that rewrites a
7725
- // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
7726
- "root:upgrade": "nmr-report-overrides && nmr-taze",
7727
- test: ["root:test", "-R test"],
7728
- "test:all": ["root:test:all", "-R test:all"],
7729
- "test:coverage": ["root:test", "-R test:coverage"],
7730
- "test:tool": ["root:test:tool", "-R test:tool"],
7731
- "test:unit": ["root:test:unit", "-R test:unit"],
7732
- "test:watch": `vitest ${GATE_PROJECTS} --watch`,
7733
- // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
7734
- // default options at the root and searching for it in every package.
7735
- typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
7736
- // The command is a string because neither half names an nmr command: Both are binaries, and a composite
7737
- // element can name only a command.
7738
- upgrade: "nmr-report-overrides && nmr-taze --recursive"
7739
- };
7740
- }
7741
-
7742
- // src/workspace-globs.ts
7743
- import path5 from "node:path";
7744
- function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
7745
- return packageDirs.map((dir) => path5.relative(monorepoRoot, dir).split(path5.sep).join("/")).filter((relativeDir) => relativeDir !== "").map((relativeDir) => `${relativeDir}/**`).toSorted();
8008
+ const notes = [
8009
+ ...element.shouldRunInBackground === true ? ["background"] : [],
8010
+ ...element.shouldDeclineArguments === true ? ["no args"] : []
8011
+ ];
8012
+ return notes.length === 0 ? element.run : `${element.run} (${notes.join(", ")})`;
7746
8013
  }
7747
-
7748
- // src/resolve-scripts.ts
7749
- function getDefaultWorkspaceScripts() {
7750
- return { ...workspaceScripts };
7751
- }
7752
- function getDefaultRootScripts(monorepoRoot) {
7753
- const resolution = resolveWorkspace(monorepoRoot);
7754
- const packageDirs = resolution.kind === "packages" ? resolution.packageDirs : [];
7755
- return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
7756
- }
7757
-
7758
- // src/resolver.ts
7759
8014
  function readPackageJsonScripts(packageDir) {
7760
8015
  const file = resolvePackageJsonPath(packageDir);
7761
8016
  let rawText;
@@ -7815,6 +8070,10 @@ var RECOGNIZED_CHECK_CACHE_KEYS = ["enabled", "excludeCommands", "extraCommands"
7815
8070
  var RECOGNIZED_OUTPUT_KEYS = ["commandVerbosity", "extraAgentEnvVars"];
7816
8071
  var RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ["exempt"];
7817
8072
  var RECOGNIZED_STEP_KEYS = ["run", "shouldDeclineArguments", "shouldRunInBackground"];
8073
+ var SCRIPT_FIELDS = [
8074
+ { field: "rootScripts", scope: "root" },
8075
+ { field: "workspaceScripts", scope: "workspaceRegistry" }
8076
+ ];
7818
8077
  var RETIRED_STEP_KEYS = /* @__PURE__ */ new Map([["declinesArgs", "shouldDeclineArguments"]]);
7819
8078
  function isScriptRecord(value) {
7820
8079
  if (!isObject(value)) return false;
@@ -8056,7 +8315,9 @@ async function loadConfig(baseDir) {
8056
8315
  }
8057
8316
  async function loadRootConfig(monorepoRoot) {
8058
8317
  const config = await loadConfig(monorepoRoot);
8059
- assertTierKeys(config, CONFIG_TIERS.root, resolveConfigPath(monorepoRoot));
8318
+ const configPath = resolveConfigPath(monorepoRoot);
8319
+ assertTierKeys(config, CONFIG_TIERS.root, configPath);
8320
+ assertNoSelfReferences(config, configPath, monorepoRoot);
8060
8321
  return config;
8061
8322
  }
8062
8323
  function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
@@ -8068,6 +8329,28 @@ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
8068
8329
  );
8069
8330
  }
8070
8331
  }
8332
+ function assertNoSelfReferences(config, configPath, monorepoRoot) {
8333
+ for (const { field, scope } of SCRIPT_FIELDS) {
8334
+ const scripts = config[field] ?? {};
8335
+ for (const [key, script] of Object.entries(scripts)) {
8336
+ const selfReference = typeof script === "string" ? readSelfReference({ commandName: key, scope, script }) : readStepListSelfReference({
8337
+ commandName: key,
8338
+ elements: script.map((element) => typeof element === "string" ? element : element.run),
8339
+ scope
8340
+ });
8341
+ if (selfReference === void 0) continue;
8342
+ const defaults = field === "rootScripts" ? getDefaultRootScripts(monorepoRoot) : getDefaultWorkspaceScripts();
8343
+ const remedy = formatSelfReferenceRemedy({
8344
+ isDefault: !isHookName(key) && Object.hasOwn(defaults, key),
8345
+ key,
8346
+ selfReference
8347
+ });
8348
+ throw new UserError(
8349
+ `Invalid nmr config at ${configPath}: \`${field}.${key}\` re-invokes \`${key}\` (\`${escapeControlCharacters(describeScript(script))}\`), which would re-enter nmr without bound. ${remedy}`
8350
+ );
8351
+ }
8352
+ }
8353
+ }
8071
8354
  function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = "") {
8072
8355
  const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
8073
8356
  if (unrecognizedKeys.length === 0) {
@@ -8089,6 +8372,13 @@ function assertTierKeys(config, tier, configPath) {
8089
8372
  function formatKeyList(keys, prefix) {
8090
8373
  return keys.toSorted().map((key) => `\`${prefix}${key}\``).join(", ");
8091
8374
  }
8375
+ function formatSelfReferenceRemedy(options) {
8376
+ const { isDefault, key, selfReference } = options;
8377
+ if (isDefault) {
8378
+ return selfReference === "sole" ? `Delete the entry: nmr's own \`${key}\` then runs.` : `Delete the entry and move the steps that it adds to a \`${key}:pre\` or \`${key}:post\` script.`;
8379
+ }
8380
+ return selfReference === "sole" ? "Delete the entry." : `Delete the re-invocation: \`${key}\` runs the steps beside it.`;
8381
+ }
8092
8382
 
8093
8383
  // src/tiers.ts
8094
8384
  import { readdirSync } from "node:fs";
@@ -8649,7 +8939,7 @@ function formatPaths(paths) {
8649
8939
  ${paths.map((path7) => ` ${path7}`).join("\n")}`;
8650
8940
  }
8651
8941
  function getMinVersion() {
8652
- const pickedFields = { "version": "0.44.0" };
8942
+ const pickedFields = { "version": "0.45.0" };
8653
8943
  if (typeof pickedFields["version"] !== "string") {
8654
8944
  throw new TypeError("nmr/package.json: 'version' must be a string");
8655
8945
  }