@williamthorsen/nmr 2.0.2 → 2.1.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.
Files changed (73) hide show
  1. package/.meta/changelog.json +72 -0
  2. package/.readyup/kits/default.js +66 -35
  3. package/.readyup/manifest.json +38 -39
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +7 -9
  6. package/dist/esm/assertResolvableCommandLists.d.ts +18 -0
  7. package/dist/esm/{check-cache/config-check-cache.js → assertResolvableCommandLists.js} +20 -12
  8. package/dist/esm/check-cache/check-cache-entries.js +1 -1
  9. package/dist/esm/check-cache/check-cache-gate.js +1 -1
  10. package/dist/esm/check-cache/check-cache-record.js +1 -1
  11. package/dist/esm/check-cache/check-cache.js +1 -1
  12. package/dist/esm/commands/build-output.d.ts +2 -1
  13. package/dist/esm/commands/build.js +1 -1
  14. package/dist/esm/commands/fmt.js +1 -1
  15. package/dist/esm/commands/taze.js +2 -2
  16. package/dist/esm/config-validation.js +25 -3
  17. package/dist/esm/config.js +1 -1
  18. package/dist/esm/crossing-diagnostics.js +1 -1
  19. package/dist/esm/defineConfig.d.ts +1 -1
  20. package/dist/esm/delegation/delegate-host.js +2 -2
  21. package/dist/esm/delegation/remote-probe.js +1 -1
  22. package/dist/esm/delegation/user-config.d.ts +2 -1
  23. package/dist/esm/delegation/user-config.js +1 -1
  24. package/dist/esm/{help.js → describeHelp.js} +1 -1
  25. package/dist/esm/fan-out.d.ts +7 -0
  26. package/dist/esm/fan-out.js +7 -2
  27. package/dist/esm/invocation.d.ts +1 -1
  28. package/dist/esm/machine-lock.d.ts +0 -4
  29. package/dist/esm/machine-lock.js +2 -2
  30. package/dist/esm/presentation.js +1 -1
  31. package/dist/esm/report-format.d.ts +2 -8
  32. package/dist/esm/report-format.js +8 -8
  33. package/dist/esm/resolve-scripts.js +1 -1
  34. package/dist/esm/route.js +1 -1
  35. package/dist/esm/run-chain.js +1 -1
  36. package/dist/esm/runCli.js +2 -2
  37. package/dist/esm/tiers.js +1 -1
  38. package/dist/esm/types.d.ts +12 -0
  39. package/dist/esm/verbosity.d.ts +2 -1
  40. package/dist/esm/verbosity.js +1 -1
  41. package/dist/esm/vitest.js +2 -2
  42. package/docs/scripts.md +12 -0
  43. package/package.json +8 -8
  44. package/dist/esm/check-cache/config-check-cache.d.ts +0 -17
  45. /package/dist/esm/check-cache/{replay-assembly.d.ts → assembleReplay.d.ts} +0 -0
  46. /package/dist/esm/check-cache/{replay-assembly.js → assembleReplay.js} +0 -0
  47. /package/dist/esm/check-cache/{check-cache-delegation.d.ts → recordDelegatedPass.d.ts} +0 -0
  48. /package/dist/esm/check-cache/{check-cache-delegation.js → recordDelegatedPass.js} +0 -0
  49. /package/dist/esm/commands/{load-package-maturity-period.d.ts → loadPackageMaturityPeriod.d.ts} +0 -0
  50. /package/dist/esm/commands/{load-package-maturity-period.js → loadPackageMaturityPeriod.js} +0 -0
  51. /package/dist/esm/commands/{taze-passes.d.ts → planTazePasses.d.ts} +0 -0
  52. /package/dist/esm/commands/{taze-passes.js → planTazePasses.js} +0 -0
  53. /package/dist/esm/commands/{format-targets.d.ts → resolveFormatTargets.d.ts} +0 -0
  54. /package/dist/esm/commands/{format-targets.js → resolveFormatTargets.js} +0 -0
  55. /package/dist/esm/commands/{tsconfig-chain.d.ts → resolveTsconfigChain.d.ts} +0 -0
  56. /package/dist/esm/commands/{tsconfig-chain.js → resolveTsconfigChain.js} +0 -0
  57. /package/dist/esm/delegation/{remote-snapshot.d.ts → createSnapshot.d.ts} +0 -0
  58. /package/dist/esm/delegation/{remote-snapshot.js → createSnapshot.js} +0 -0
  59. /package/dist/esm/delegation/{remote-probe-host.d.ts → probeHostLocks.d.ts} +0 -0
  60. /package/dist/esm/delegation/{remote-probe-host.js → probeHostLocks.js} +0 -0
  61. /package/dist/esm/delegation/{delegation-target.d.ts → resolveDelegationTarget.d.ts} +0 -0
  62. /package/dist/esm/delegation/{delegation-target.js → resolveDelegationTarget.js} +0 -0
  63. /package/dist/esm/{help.d.ts → describeHelp.d.ts} +0 -0
  64. /package/dist/esm/helpers/{hook-name.d.ts → isHookName.d.ts} +0 -0
  65. /package/dist/esm/helpers/{hook-name.js → isHookName.js} +0 -0
  66. /package/dist/esm/helpers/{type-guards.d.ts → isStringRecord.d.ts} +0 -0
  67. /package/dist/esm/helpers/{type-guards.js → isStringRecord.js} +0 -0
  68. /package/dist/esm/{git-ignored-paths.d.ts → listGitIgnoredPaths.d.ts} +0 -0
  69. /package/dist/esm/{git-ignored-paths.js → listGitIgnoredPaths.js} +0 -0
  70. /package/dist/esm/{bin-path.d.ts → prependBinDirs.d.ts} +0 -0
  71. /package/dist/esm/{bin-path.js → prependBinDirs.js} +0 -0
  72. /package/dist/esm/{workspace-globs.d.ts → toWorkspacePackageGlobs.d.ts} +0 -0
  73. /package/dist/esm/{workspace-globs.js → toWorkspacePackageGlobs.js} +0 -0
@@ -1,18 +1,16 @@
1
1
  import { UserError } from '@williamthorsen/nmr-core';
2
- import { findClosestName } from "../helpers/findClosestName.js";
3
- import { isHookName } from "../helpers/hook-name.js";
4
- import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "../resolver.js";
5
- import { getWorkspacePackageDirs, isMonorepoRoot } from "../workspace.js";
6
- export function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
7
- const { checkCache } = config;
8
- if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
2
+ import { findClosestName } from "./helpers/findClosestName.js";
3
+ import { isHookName } from "./helpers/isHookName.js";
4
+ import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "./resolver.js";
5
+ import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
6
+ export function assertResolvableCommandLists(config, configPath, baseDir) {
7
+ const entries = listCommandEntries(config);
8
+ if (entries.length === 0 || !isMonorepoRoot(baseDir)) {
9
9
  return;
10
10
  }
11
- const fields = ['excludeCommands', 'extraCommands'];
12
- const entries = fields.flatMap((field) => (checkCache[field] ?? []).map((command) => ({ command, field })));
13
- const hook = entries.find(({ command }) => isHookName(command));
11
+ const hook = entries.find(({ command, fieldPath }) => fieldPath.startsWith('checkCache.') && isHookName(command));
14
12
  if (hook !== undefined) {
15
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
13
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${hook.fieldPath}\` names the hook \`${hook.command}\`, ` +
16
14
  'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
17
15
  }
18
16
  const registeredNames = new Set([
@@ -29,9 +27,19 @@ export function assertResolvableCheckCacheCommands(config, configPath, baseDir)
29
27
  return;
30
28
  }
31
29
  const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
32
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
30
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${unresolvableEntry.fieldPath}\` does not name any command: ` +
33
31
  `\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
34
32
  }
33
+ function listCommandEntries(config) {
34
+ const checkCacheFields = ['excludeCommands', 'extraCommands'];
35
+ return [
36
+ ...checkCacheFields.flatMap((field) => (config.checkCache?.[field] ?? []).map((command) => ({ command, fieldPath: `checkCache.${field}` }))),
37
+ ...(config.fanOut?.extraSortedCommands ?? []).map((command) => ({
38
+ command,
39
+ fieldPath: 'fanOut.extraSortedCommands',
40
+ })),
41
+ ];
42
+ }
35
43
  function readDeclaredScriptNames(monorepoRoot) {
36
44
  let packageDirs;
37
45
  try {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { isRecord, readCacheEntry, readJsonCacheEntry, removeCacheDir, removeCacheEntry, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
- import { isStringRecord } from "../helpers/type-guards.js";
4
+ import { isStringRecord } from "../helpers/isStringRecord.js";
5
5
  import { writeDebugNote } from "./check-cache.js";
6
6
  const CACHE_TOOL = 'nmr-check';
7
7
  const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
@@ -7,11 +7,11 @@ import { resolveChannel } from "../output-channel.js";
7
7
  import { splitSegments } from "../shell-syntax.js";
8
8
  import { formatMisplacedNoCacheWarning, isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
9
9
  import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
10
- import { recordDelegatedPass } from "./check-cache-delegation.js";
11
10
  import { certifyRetention, readCheckCacheEntry } from "./check-cache-entries.js";
12
11
  import { computeCacheKey, computeRetentionKey } from "./check-cache-key.js";
13
12
  import { recordPass } from "./check-cache-record.js";
14
13
  import { writePassManifestEntry } from "./pass-manifest.js";
14
+ import { recordDelegatedPass } from "./recordDelegatedPass.js";
15
15
  export function openGate(options) {
16
16
  const { command, config, env, monorepoRoot, passthrough, readTree, stderr, style } = options;
17
17
  if (!isCacheableCommand(config.checkCache, command)) {
@@ -4,13 +4,13 @@ import { isEnvSwitchOn } from "../env-switch.js";
4
4
  import { deriveExcerpt } from "../helpers/deriveExcerpt.js";
5
5
  import { composeTranscript } from "../helpers/transcript.js";
6
6
  import { RUN_IF_PRESENT_ENV_VAR, VERSION } from "../invocation.js";
7
+ import { assembleReplay } from "./assembleReplay.js";
7
8
  import { isCacheableCommand, resolveTreeSnapshot, writeDebugNote, } from "./check-cache.js";
8
9
  import { findStaleBuildOutput, readBuildOutputState } from "./check-cache-build-output.js";
9
10
  import { readCheckCacheEntry, recordTranscript, writeCheckCacheEntry, } from "./check-cache-entries.js";
10
11
  import { CURRENT_RUNTIME } from "./check-cache-key.js";
11
12
  import { writePassManifestEntry } from "./pass-manifest.js";
12
13
  import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
13
- import { assembleReplay } from "./replay-assembly.js";
14
14
  export async function recordHostPass(options) {
15
15
  const { env, host, key, ref, retention, snapshot, stderr } = options;
16
16
  const recorded = await readCheckCacheEntry(ref);
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { hashWorkingTree, readHeadSha, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
4
4
  import { isEnvSwitchOn } from "../env-switch.js";
5
- import { isHookName } from "../helpers/hook-name.js";
5
+ import { isHookName } from "../helpers/isHookName.js";
6
6
  export const GIT_TREE_READER = { hashWorkingTree, readHeadSha };
7
7
  export const DEBUG_ENV_VAR = 'NMR_DEBUG';
8
8
  export const DEFAULT_CACHEABLE_COMMANDS = [
@@ -63,7 +63,8 @@ export declare function resolveEntryPoints(packageDir: string, options?: BuildOp
63
63
  * rebuild on every invocation and change the fingerprint that every other package reads.
64
64
  *
65
65
  * Because the fingerprint names the nmr that produced the output, not the sources in `src`, a package compiled by
66
- * the pre-rebuild binary keeps the old fingerprint and rebuilds on the next run.
66
+ * the pre-rebuild binary keeps the old fingerprint and rebuilds on the next run; a workspace that orders nmr's
67
+ * build ahead of the packages that it compiles avoids that rebuild.
67
68
  */
68
69
  export declare function resolveToolchainFingerprint(packageDir: string, selfDir?: string): Promise<string>;
69
70
  /** Records how long the emit took for the build whose digest is `digest`. */
@@ -8,8 +8,8 @@ import { writeStatedOutcome } from "../stated-outcome.js";
8
8
  import { DEFAULT_OUTDIR, hasExpectedBuildOutput, readEmitDuration, resolveBuildCachePath, resolveEntryPoints, resolveToolchainFingerprint, writeEmitDuration, } from "./build-output.js";
9
9
  import { assertSupportedTypeScript, formatDiagnostics, getCompiler, loadCompiler } from "./compiler.js";
10
10
  import { discardScratchDirs, publishOutput, removeScratchDirs } from "./publish-output.js";
11
+ import { resolveTsconfigChain } from "./resolveTsconfigChain.js";
11
12
  import { rewriteSpecifiers } from "./rewrite-specifiers.js";
12
- import { resolveTsconfigChain } from "./tsconfig-chain.js";
13
13
  const SOURCE_ROOT = 'src';
14
14
  export async function buildPackage(packageDir, options = {}) {
15
15
  await loadCompiler(options.loadTypeScript);
@@ -10,8 +10,8 @@ import { NMR_FMT_BIN } from "../bins.js";
10
10
  import { NO_CACHE_ENV_VAR } from "../check-cache/check-cache.js";
11
11
  import { readEnvSwitch } from "../env-switch.js";
12
12
  import { prepareFmtCache } from "./fmt-cache.js";
13
- import { resolveFormatTargets } from "./format-targets.js";
14
13
  import { BROKEN_MANIFEST_FIX, findPackageManifest, resolveDeclaredCli, } from "./package-manifest.js";
14
+ import { resolveFormatTargets } from "./resolveFormatTargets.js";
15
15
  import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
16
16
  const PRETTIER_PACKAGE = 'prettier';
17
17
  const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default.config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
@@ -4,11 +4,11 @@ import { fileURLToPath } from 'node:url';
4
4
  import { reportError } from '@williamthorsen/nmr-core';
5
5
  import { describeError } from '@williamthorsen/toolbelt.errors';
6
6
  import { NMR_TAZE_BIN } from "../bins.js";
7
- import { loadPackageMaturityPeriod } from "./load-package-maturity-period.js";
7
+ import { loadPackageMaturityPeriod } from "./loadPackageMaturityPeriod.js";
8
8
  import { checkNodePin as checkNodePinDefault } from "./node-pin.js";
9
+ import { planTazePasses } from "./planTazePasses.js";
9
10
  import { resolveInheritedExitCode, spawnInherited } from "./spawn-inherited.js";
10
11
  import { isSilent, readFlag } from "./taze-args.js";
11
- import { planTazePasses } from "./taze-passes.js";
12
12
  const TAZE_CLI_SPECIFIER = 'taze/cli';
13
13
  const REQUEST_TIMEOUT_FLAG = '--request-timeout';
14
14
  const REQUEST_TIMEOUT_FLAGS = [REQUEST_TIMEOUT_FLAG, '--requestTimeout'];
@@ -1,11 +1,11 @@
1
1
  import { isRecord, UserError } from '@williamthorsen/nmr-core';
2
- import { assertResolvableCheckCacheCommands } from "./check-cache/config-check-cache.js";
2
+ import { assertResolvableCommandLists } from "./assertResolvableCommandLists.js";
3
3
  import { findUnexpressibleToken } from "./steps.js";
4
4
  import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
5
5
  export const CONFIG_TIERS = {
6
6
  root: {
7
7
  label: 'a monorepo-root config',
8
- honoredKeys: ['checkCache', 'delegate', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
8
+ honoredKeys: ['checkCache', 'delegate', 'fanOut', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
9
9
  elsewhere: "the package's own config",
10
10
  },
11
11
  workspace: {
@@ -17,6 +17,7 @@ export const CONFIG_TIERS = {
17
17
  const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
18
18
  const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
19
19
  const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
20
+ const RECOGNIZED_FAN_OUT_KEYS = ['extraSortedCommands'];
20
21
  const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
21
22
  const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
22
23
  const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
@@ -43,6 +44,9 @@ export function validateConfig(value, configPath, baseDir) {
43
44
  config.checkCache = checkCache;
44
45
  if (value['delegate'] !== undefined)
45
46
  config.delegate = validateDelegateField(value['delegate'], configPath);
47
+ const fanOut = validateFanOutField(value, configPath);
48
+ if (fanOut)
49
+ config.fanOut = fanOut;
46
50
  const output = validateOutputField(value, configPath);
47
51
  if (output)
48
52
  config.output = output;
@@ -55,7 +59,7 @@ export function validateConfig(value, configPath, baseDir) {
55
59
  const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
56
60
  if (selfHostedRunner)
57
61
  config.selfHostedRunner = selfHostedRunner;
58
- assertResolvableCheckCacheCommands(config, configPath, baseDir);
62
+ assertResolvableCommandLists(config, configPath, baseDir);
59
63
  return config;
60
64
  }
61
65
  function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
@@ -166,6 +170,24 @@ function validateDelegateField(delegate, configPath) {
166
170
  throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` accepts only \`false\`, which keeps the repo's checks on the ` +
167
171
  'machine that has the tree. Hosts and `when` belong in the user config, `~/.config/nmr/config.ts`.');
168
172
  }
173
+ function validateFanOutField(value, configPath) {
174
+ const fanOut = value['fanOut'];
175
+ if (fanOut === undefined) {
176
+ return undefined;
177
+ }
178
+ if (!isRecord(fanOut)) {
179
+ throw new UserError(`Invalid nmr config at ${configPath}: \`fanOut\` must be an object`);
180
+ }
181
+ assertRecognizedKeys(fanOut, RECOGNIZED_FAN_OUT_KEYS, configPath, 'fanOut.');
182
+ const extraSortedCommands = fanOut['extraSortedCommands'];
183
+ if (extraSortedCommands === undefined) {
184
+ return {};
185
+ }
186
+ if (!isStringArray(extraSortedCommands)) {
187
+ throw new UserError(`Invalid nmr config at ${configPath}: \`fanOut.extraSortedCommands\` must be a string[]`);
188
+ }
189
+ return { extraSortedCommands };
190
+ }
169
191
  function validateOutputField(value, configPath) {
170
192
  const output = value['output'];
171
193
  if (output === undefined) {
@@ -3,8 +3,8 @@ import { UserError } from '@williamthorsen/nmr-core';
3
3
  import { CONFIG_TIERS, validateConfig } from "./config-validation.js";
4
4
  import { resolveConfigPath } from "./helpers/config-path.js";
5
5
  import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
6
- import { isHookName } from "./helpers/hook-name.js";
7
6
  import { importConfigDefault } from "./helpers/importConfigDefault.js";
7
+ import { isHookName } from "./helpers/isHookName.js";
8
8
  import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
9
9
  import { describeScript } from "./resolver.js";
10
10
  import { readSelfReference, readStepListSelfReference } from "./steps.js";
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { STATUS_GLYPHS, UserError } from '@williamthorsen/nmr-core';
3
3
  import { resolveConfigPath } from "./helpers/config-path.js";
4
4
  import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
5
- import { isHookName } from "./helpers/hook-name.js";
5
+ import { isHookName } from "./helpers/isHookName.js";
6
6
  import { resolvePackageJsonPath } from "./helpers/package-json.js";
7
7
  import { hasRunnableHook } from "./hooks.js";
8
8
  import { expandScript, findChainedSelfReference, readPackageJsonScripts, } from "./resolver.js";
@@ -1,5 +1,5 @@
1
1
  import type { NmrConfig } from './types.js';
2
- export type { BuildConfig, CheckCacheConfig, CommandVerbosity, NmrConfig, OutputConfig, ScriptValue, SelfHostedRunnerConfig, StepSpec, } from './types.js';
2
+ export type { BuildConfig, CheckCacheConfig, CommandVerbosity, FanOutConfig, NmrConfig, OutputConfig, ScriptValue, SelfHostedRunnerConfig, StepSpec, } from './types.js';
3
3
  /**
4
4
  * Returns `config` unchanged, typing a configuration file's default export as `NmrConfig`.
5
5
  *
@@ -9,10 +9,10 @@ import { createBoundedBuffer } from "../helpers/createBoundedBuffer.js";
9
9
  import { USAGE_EXIT_CODE, } from "../invocation.js";
10
10
  import { isDelegationFlag } from "../invocation-args.js";
11
11
  import { reportVerdict } from "../presentation.js";
12
- import { resolveDelegationTarget } from "./delegation-target.js";
12
+ import { createSnapshot } from "./createSnapshot.js";
13
13
  import { chooseHost, probeHost } from "./remote-probe.js";
14
14
  import { DELEGATE_ENV_VAR, DELEGATE_FLAG, DELEGATE_TO_FLAG, DELEGATION_FAILURE_EXIT_CODE, deriveCheckoutPath, formatMisplacedDelegationWarning, readLastLine, runRemotely, } from "./remote-run.js";
15
- import { createSnapshot } from "./remote-snapshot.js";
15
+ import { resolveDelegationTarget } from "./resolveDelegationTarget.js";
16
16
  import { formatDisplayPath, loadUserConfig } from "./user-config.js";
17
17
  export async function resolveExecutionTarget(invocation) {
18
18
  const { command, context, env, parsedArgs, stderr, styles } = invocation;
@@ -2,8 +2,8 @@ import path from 'node:path';
2
2
  import { isRecord } from '@williamthorsen/nmr-core';
3
3
  import { quoteForShell } from "../helpers/quoteForShell.js";
4
4
  import { LOCK_DIR_ENV_VAR, LOCK_FILE, STALE_AFTER_MS } from "../machine-lock.js";
5
+ import { probeHostLocks } from "./probeHostLocks.js";
5
6
  import { CHECKOUT_LOCK_PATH } from "./remote-host-session.js";
6
- import { probeHostLocks } from "./remote-probe-host.js";
7
7
  import { DELEGATION_FAILURE_EXIT_CODE, readLastLine, runSsh } from "./remote-run.js";
8
8
  export function chooseHost(options) {
9
9
  const { hosts, onSettled, probe } = options;
@@ -1,6 +1,6 @@
1
1
  import type { HostList } from './remote-run.js';
2
2
  /** The values that `delegate.when` accepts, each naming the runs that delegate without being asked. */
3
- export declare const DELEGATE_WHEN_VALUES: readonly ["agent", "always"];
3
+ declare const DELEGATE_WHEN_VALUES: readonly ["agent", "always"];
4
4
  export type DelegateWhen = (typeof DELEGATE_WHEN_VALUES)[number];
5
5
  /**
6
6
  * Renders a path for a message, with `~` standing for the home directory that `env` names when the path lies under
@@ -32,3 +32,4 @@ export interface UserDelegateConfig {
32
32
  hosts: HostList;
33
33
  when?: DelegateWhen;
34
34
  }
35
+ export {};
@@ -4,7 +4,7 @@ import { isRecord, UserError } from '@williamthorsen/nmr-core';
4
4
  import { assertRecognizedKeys } from "../config-validation.js";
5
5
  import { importConfigDefault } from "../helpers/importConfigDefault.js";
6
6
  import { resolveXdgBaseDir } from "../helpers/resolveXdgBaseDir.js";
7
- export const DELEGATE_WHEN_VALUES = ['agent', 'always'];
7
+ const DELEGATE_WHEN_VALUES = ['agent', 'always'];
8
8
  export function formatDisplayPath(filePath, env) {
9
9
  const home = env['HOME'];
10
10
  if (home === undefined || !path.isAbsolute(home)) {
@@ -1,5 +1,5 @@
1
1
  import { DELEGATE_ENV_VAR } from "./delegation/remote-run.js";
2
- import { isHookName } from "./helpers/hook-name.js";
2
+ import { isHookName } from "./helpers/isHookName.js";
3
3
  import { OUTPUT_STYLE_ENV_VAR } from "./output-style.js";
4
4
  import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
5
5
  const LIFECYCLE_SCRIPT_NAMES = new Set([
@@ -5,6 +5,7 @@ import { type RunCliResult } from './invocation.js';
5
5
  import type { ParsedArgs } from './nmr-flags.js';
6
6
  import type { RunSteps, RunStepsOptions } from './runner.js';
7
7
  import type { Step } from './steps.js';
8
+ import type { NmrConfig } from './types.js';
8
9
  /**
9
10
  * Composes the delegation that an `-F` or `-R` calls for, or `undefined` when the invocation runs here.
10
11
  *
@@ -15,10 +16,16 @@ import type { Step } from './steps.js';
15
16
  *
16
17
  * A `--log` fan-out surveys the scopes selected, so a scope with nothing to show is a gap in the survey rather
17
18
  * than a failure of it, however that scope was selected.
19
+ *
20
+ * pnpm runs the selected packages in topological order unless told otherwise, which holds each package's run until
21
+ * its workspace dependencies have finished theirs. Only a command that reads what its dependencies' runs produce
22
+ * needs that, so the order is kept for `build` and for the commands in `fanOut.extraSortedCommands`, and every
23
+ * other fan-out runs with `--no-sort`.
18
24
  */
19
25
  export declare function composeDelegation(options: {
20
26
  childEnv: NodeJS.ProcessEnv;
21
27
  command: string;
28
+ config: NmrConfig;
22
29
  parsedArgs: ParsedArgs;
23
30
  }): {
24
31
  env: NodeJS.ProcessEnv;
@@ -6,8 +6,9 @@ import { RUN_IF_PRESENT_ENV_VAR } from "./invocation.js";
6
6
  import { buildRootRegistry, buildWorkspaceRegistry, readPackageJsonScripts } from "./resolver.js";
7
7
  import { readWorkspacePackageNames, resolveWorkspace } from "./workspace.js";
8
8
  const RECURSIVE_REJECTION = '-R/--recursive did not match any workspace:';
9
+ const DEFAULT_SORTED_COMMANDS = new Set(['build']);
9
10
  export function composeDelegation(options) {
10
- const { childEnv, command, parsedArgs } = options;
11
+ const { childEnv, command, config, parsedArgs } = options;
11
12
  let scope;
12
13
  let shouldRunIfPresent = parsedArgs.shouldPrintLog;
13
14
  if (parsedArgs.filter) {
@@ -20,12 +21,13 @@ export function composeDelegation(options) {
20
21
  if (scope === undefined) {
21
22
  return undefined;
22
23
  }
24
+ const sortFlags = isSortedCommand(command, config) ? [] : ['--no-sort'];
23
25
  const flags = parsedArgs.shouldPrintLog ? ['--log'] : [];
24
26
  return {
25
27
  env: shouldRunIfPresent ? { ...childEnv, [RUN_IF_PRESENT_ENV_VAR]: '1' } : childEnv,
26
28
  step: {
27
29
  kind: 'structural',
28
- argv: ['pnpm', ...scope, 'exec', 'nmr', ...flags, command, ...parsedArgs.passthrough],
30
+ argv: ['pnpm', ...scope, ...sortFlags, 'exec', 'nmr', ...flags, command, ...parsedArgs.passthrough],
29
31
  },
30
32
  };
31
33
  }
@@ -136,6 +138,9 @@ function formatEmptyFilterError(pattern, names) {
136
138
  function formatFilterRejection(pattern) {
137
139
  return `-F/--filter did not match any workspace: \`${pattern}\`.`;
138
140
  }
141
+ function isSortedCommand(command, config) {
142
+ return DEFAULT_SORTED_COMMANDS.has(command) || (config.fanOut?.extraSortedCommands ?? []).includes(command);
143
+ }
139
144
  function suggestWorkspaceNames(pattern, names) {
140
145
  if (names.length === 0) {
141
146
  return '';
@@ -2,7 +2,7 @@ import type { Writable } from 'node:stream';
2
2
  import { type StreamStyles } from '@williamthorsen/nmr-core';
3
3
  import type { TreeReader, TreeSnapshot } from './check-cache/check-cache.js';
4
4
  import type { ResolvedContext } from './context.js';
5
- import type { DelegationTarget } from './delegation/delegation-target.js';
5
+ import type { DelegationTarget } from './delegation/resolveDelegationTarget.js';
6
6
  import type { ReadFilterSelection } from './helpers/filter-selection.js';
7
7
  import type { ParsedArgs } from './nmr-flags.js';
8
8
  import type { ReportFormat } from './report-format.js';
@@ -1,7 +1,5 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import { type OutputStyle } from '@williamthorsen/nmr-core';
3
- /** How often a holder refreshes the lock file's mtime. */
4
- export declare const HEARTBEAT_INTERVAL_MS = 10000;
5
3
  /**
6
4
  * The commands that take the machine lock: the ones whose memory peak makes two concurrent runs slower than two in
7
5
  * sequence. `test:watch` is absent because it runs until it is stopped, and holding the lock would block every other
@@ -16,8 +14,6 @@ export declare const LOCK_FILE = "heavy.lock";
16
14
  export declare const LOCK_TOKEN_ENV_VAR = "NMR_MACHINE_LOCK_TOKEN";
17
15
  /** Set to `1` to run without the lock, for the whole chain. */
18
16
  export declare const NO_LOCK_ENV_VAR = "NMR_NO_LOCK";
19
- /** How often a waiter checks the lock again. */
20
- export declare const POLL_INTERVAL_MS = 1000;
21
17
  /**
22
18
  * Age at which an unrefreshed lock file is abandoned. It covers a holder whose pid was reused by another process,
23
19
  * and a file left half-written, neither of which a liveness check detects.
@@ -8,7 +8,7 @@ import { isRecord, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
8
8
  import { isEnvSwitchOn } from "./env-switch.js";
9
9
  const RECLAIM_GUARD = 'heavy.lock.reclaim';
10
10
  const RECLAIM_GUARD_STALE_AFTER_MS = 10_000;
11
- export const HEARTBEAT_INTERVAL_MS = 10_000;
11
+ const HEARTBEAT_INTERVAL_MS = 10_000;
12
12
  export const HEAVY_COMMANDS = new Set([
13
13
  'check',
14
14
  'check:strict',
@@ -28,7 +28,7 @@ export const LOCK_DIR_ENV_VAR = 'NMR_LOCK_DIR';
28
28
  export const LOCK_FILE = 'heavy.lock';
29
29
  export const LOCK_TOKEN_ENV_VAR = 'NMR_MACHINE_LOCK_TOKEN';
30
30
  export const NO_LOCK_ENV_VAR = 'NMR_NO_LOCK';
31
- export const POLL_INTERVAL_MS = 1000;
31
+ const POLL_INTERVAL_MS = 1000;
32
32
  export const STALE_AFTER_MS = 60_000;
33
33
  export async function acquireMachineLock(options) {
34
34
  const { command, dir, env, stderr } = options;
@@ -1,5 +1,5 @@
1
1
  import { describeInvalidOutputStyle } from '@williamthorsen/nmr-core';
2
- import { isHookName } from "./helpers/hook-name.js";
2
+ import { isHookName } from "./helpers/isHookName.js";
3
3
  import { resolveOutputStyles } from "./output-style.js";
4
4
  import { readReportFormatEnv } from "./report-format.js";
5
5
  import { readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
@@ -1,10 +1,3 @@
1
- /**
2
- * Renders the rejection of a value off the format ladder, naming where it was written. Every source that
3
- * accepts a format renders through this, so two of them can never come to name different ladders.
4
- */
5
- export declare function formatReportFormatRejection(source: string, value: string): string;
6
- /** Narrows a raw value to a point on the format ladder. */
7
- export declare function isReportFormat(value: string): value is ReportFormat;
8
1
  /**
9
2
  * Reads the format that the environment names. An unrecognized value is returned as an error, which keeps a
10
3
  * misspelling from going unnoticed for the life of the shell.
@@ -22,7 +15,7 @@ export declare const REPORT_FORMAT_ENV_VAR = "NMR_REPORT_FORMAT";
22
15
  * The points on the format ladder. `text` is a named point, so that one invocation can override a format
23
16
  * exported for the shell in either direction.
24
17
  */
25
- export declare const REPORT_FORMATS: readonly ["text", "json"];
18
+ declare const REPORT_FORMATS: readonly ["text", "json"];
26
19
  /** How nmr renders its own verdicts, which is never the output of the commands that it runs. */
27
20
  export type ReportFormat = (typeof REPORT_FORMATS)[number];
28
21
  /** The format that the environment names, if any, or the message naming why its value could not be read. */
@@ -44,3 +37,4 @@ export interface ResolveReportFormatOptions {
44
37
  envFormat: ReportFormat | undefined;
45
38
  hasJsonFlag: boolean;
46
39
  }
40
+ export {};
@@ -1,10 +1,3 @@
1
- export function formatReportFormatRejection(source, value) {
2
- return `${source} is \`${value}\`, which is not one of: ${REPORT_FORMATS.join(', ')}`;
3
- }
4
- export function isReportFormat(value) {
5
- const names = REPORT_FORMATS;
6
- return names.includes(value);
7
- }
8
1
  export function readReportFormatEnv(env) {
9
2
  const rawValue = env[REPORT_FORMAT_ENV_VAR];
10
3
  if (rawValue === undefined || rawValue === '') {
@@ -16,10 +9,17 @@ export function readReportFormatEnv(env) {
16
9
  return { ok: true, format: rawValue };
17
10
  }
18
11
  export const REPORT_FORMAT_ENV_VAR = 'NMR_REPORT_FORMAT';
19
- export const REPORT_FORMATS = ['text', 'json'];
12
+ const REPORT_FORMATS = ['text', 'json'];
20
13
  export function resolveReportFormat(options) {
21
14
  const { envFormat, hasJsonFlag } = options;
22
15
  if (hasJsonFlag)
23
16
  return 'json';
24
17
  return envFormat ?? 'text';
25
18
  }
19
+ function formatReportFormatRejection(source, value) {
20
+ return `${source} is \`${value}\`, which is not one of: ${REPORT_FORMATS.join(', ')}`;
21
+ }
22
+ function isReportFormat(value) {
23
+ const names = REPORT_FORMATS;
24
+ return names.includes(value);
25
+ }
@@ -1,6 +1,6 @@
1
1
  import { buildRootScripts, workspaceScripts } from "./default-scripts.js";
2
+ import { toWorkspacePackageGlobs } from "./toWorkspacePackageGlobs.js";
2
3
  import { resolveWorkspace } from "./workspace.js";
3
- import { toWorkspacePackageGlobs } from "./workspace-globs.js";
4
4
  export function getDefaultWorkspaceScripts() {
5
5
  return { ...workspaceScripts };
6
6
  }
package/dist/esm/route.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { createCli, UsageError } from '@williamthorsen/nmr-core/cli';
3
3
  import { DEBUG_ENV_VAR, NO_CACHE_ENV_VAR } from "./check-cache/check-cache.js";
4
+ import { describeHelp } from "./describeHelp.js";
4
5
  import { readEnvSwitch, readEnvSwitches } from "./env-switch.js";
5
6
  import { listDefinedCommands } from "./fan-out.js";
6
- import { describeHelp } from "./help.js";
7
7
  import { RUN_IF_PRESENT_ENV_VAR } from "./invocation.js";
8
8
  import { NO_LOCK_ENV_VAR } from "./machine-lock.js";
9
9
  import { NMR_FLAGS, toParsedArgs } from "./nmr-flags.js";
@@ -4,7 +4,7 @@ import { NMR_BIN } from "./bins.js";
4
4
  import { resolveCacheKey } from "./check-cache/check-cache-gate.js";
5
5
  import { reportNmrCrossing } from "./crossing-diagnostics.js";
6
6
  import { NMR_GLYPHS } from "./glyphs.js";
7
- import { isHookName } from "./helpers/hook-name.js";
7
+ import { isHookName } from "./helpers/isHookName.js";
8
8
  import { quoteForShell } from "./helpers/quoteForShell.js";
9
9
  import { wrapWithHooks } from "./hooks.js";
10
10
  import { USAGE_EXIT_CODE } from "./invocation.js";
@@ -1,6 +1,5 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
2
  import { runCli as runRouter } from '@williamthorsen/nmr-core/cli';
3
- import { prependBinDirs } from "./bin-path.js";
4
3
  import { NMR_BIN } from "./bins.js";
5
4
  import { GIT_TREE_READER, NO_CACHE_ENV_VAR, resolveRunId, RUN_ID_ENV_VAR, } from "./check-cache/check-cache.js";
6
5
  import { openGate, runGated } from "./check-cache/check-cache-gate.js";
@@ -17,6 +16,7 @@ import { readFilterSelection as readFilterSelectionDefault, } from "./helpers/fi
17
16
  import { RUN_IF_PRESENT_ENV_VAR, USAGE_EXIT_CODE, VERSION, } from "./invocation.js";
18
17
  import { formatMisplacedFlagHint } from "./invocation-args.js";
19
18
  import { readInvocationFlags } from "./nmr-flags.js";
19
+ import { prependBinDirs } from "./prependBinDirs.js";
20
20
  import { reportVerdict } from "./presentation.js";
21
21
  import { buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
22
22
  import { buildRoot, buildVersionRoot } from "./route.js";
@@ -104,7 +104,7 @@ async function planRun(invocation, target) {
104
104
  stderr,
105
105
  env: childEnv,
106
106
  };
107
- const delegation = composeDelegation({ childEnv, command, parsedArgs });
107
+ const delegation = composeDelegation({ childEnv, command, config: context.config, parsedArgs });
108
108
  if (delegation !== undefined) {
109
109
  const lock = await takeMachineLock(invocation);
110
110
  let result;
package/dist/esm/tiers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { listGitIgnoredPaths } from "./git-ignored-paths.js";
3
+ import { listGitIgnoredPaths } from "./listGitIgnoredPaths.js";
4
4
  const TEST_DIR = '__tests__';
5
5
  const TEST_EXTENSIONS = '{ts,tsx}';
6
6
  const TEST_GLOB_PREFIX = `**/${TEST_DIR}/**`;
@@ -28,6 +28,17 @@ export interface CheckCacheConfig {
28
28
  */
29
29
  extraCommands?: string[];
30
30
  }
31
+ /** Fan-out settings honored at the monorepo root. */
32
+ export interface FanOutConfig {
33
+ /**
34
+ * Command names added to the set whose `-R` and `-F` fan-outs run packages in topological order, which holds
35
+ * `build` by default; every other fan-out starts each package without waiting for its workspace dependencies.
36
+ * Extends rather than replaces, so that declaring one command cannot drop `build`. List a command whose run in a
37
+ * package reads what the same command produces in that package's workspace dependencies. Every name has to
38
+ * resolve to a command, because a misspelt entry would otherwise be inert.
39
+ */
40
+ extraSortedCommands?: string[];
41
+ }
31
42
  /** Self-hosted runner settings honored at the monorepo root, read by nmr's readyup kit. */
32
43
  export interface SelfHostedRunnerConfig {
33
44
  /**
@@ -59,6 +70,7 @@ export interface NmrConfig {
59
70
  * config says. A delegation flag still sends a run elsewhere.
60
71
  */
61
72
  delegate?: false;
73
+ fanOut?: FanOutConfig;
62
74
  output?: OutputConfig;
63
75
  workspaceScripts?: Record<string, ScriptValue>;
64
76
  rootScripts?: Record<string, ScriptValue>;
@@ -9,7 +9,7 @@ export declare const AGENT_ENV_VARS: readonly ["CLAUDECODE", "ROVODEV_CLI", "ROV
9
9
  * The points on the loudness ladder. Both are spelled out rather than leaving `full` implicit in the variable's
10
10
  * absence, so that a repo-level default is overridable from the environment in either direction.
11
11
  */
12
- export declare const COMMAND_VERBOSITIES: readonly ["full", "quiet"];
12
+ declare const COMMAND_VERBOSITIES: readonly ["full", "quiet"];
13
13
  /**
14
14
  * Passes the resolved verbosity down the chain of spawned processes, so that `-q` applies to every process rather
15
15
  * than only the first. The pass key does not include it: It changes what a run prints and never what a command
@@ -65,3 +65,4 @@ export type VerbosityRead = {
65
65
  ok: false;
66
66
  error: string;
67
67
  };
68
+ export {};
@@ -1,5 +1,5 @@
1
1
  export const AGENT_ENV_VARS = ['CLAUDECODE', 'ROVODEV_CLI', 'ROVO_CLI'];
2
- export const COMMAND_VERBOSITIES = ['full', 'quiet'];
2
+ const COMMAND_VERBOSITIES = ['full', 'quiet'];
3
3
  export const COMMAND_VERBOSITY_ENV_VAR = 'NMR_COMMAND_VERBOSITY';
4
4
  export function formatVerbosityRejection(source, value) {
5
5
  return `${source} is \`${value}\`, which is not one of: ${COMMAND_VERBOSITIES.join(', ')}`;
@@ -3,12 +3,12 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { isRecord } from '@williamthorsen/nmr-core';
5
5
  import { defaultExclude, mergeConfig } from 'vitest/config';
6
- import { listGitIgnoredPaths } from "./git-ignored-paths.js";
6
+ import { listGitIgnoredPaths } from "./listGitIgnoredPaths.js";
7
7
  import { ALL_TEST_PATTERNS, buildTierPatterns, TEST_COLLECTION_EXCLUDE, TIER_NAMES } from "./tiers.js";
8
+ import { toWorkspacePackageGlobs } from "./toWorkspacePackageGlobs.js";
8
9
  import { deriveDefaultMaxWorkers, readMaxWorkersFlag } from "./vitest-max-workers.js";
9
10
  import { createSourceResolutionPlugin } from "./vitest-source-resolution.js";
10
11
  import { getWorkspacePackageDirs } from "./workspace.js";
11
- import { toWorkspacePackageGlobs } from "./workspace-globs.js";
12
12
  const [RESIDUAL_TIER, ...NAMED_TIERS] = TIER_NAMES;
13
13
  const TIERED_PATTERNS = NAMED_TIERS.flatMap(buildTierPatterns);
14
14
  const TIER_TIMEOUT_MS = 30_000;