@williamthorsen/nmr 0.55.0 → 1.0.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 (108) hide show
  1. package/.meta/changelog.json +124 -0
  2. package/.readyup/kits/default.js +667 -167
  3. package/.readyup/manifest.json +77 -46
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +1 -14
  6. package/agents/guidance/rulebooks/nmr.md +1 -1
  7. package/bin/ensure-prepublish-hooks.js +1 -1
  8. package/bin/nmr-actionlint.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-shellcheck.js +1 -1
  15. package/bin/nmr-taze.js +1 -1
  16. package/bin/nmr-vitest.js +1 -1
  17. package/bin/nmr.js +1 -1
  18. package/dist/esm/bins.d.ts +11 -0
  19. package/dist/esm/bins.js +11 -0
  20. package/dist/esm/check-cache-entries.js +5 -5
  21. package/dist/esm/check-cache-gate.js +2 -1
  22. package/dist/esm/child-env.js +2 -1
  23. package/dist/esm/child-process.d.ts +2 -0
  24. package/dist/esm/child-process.js +3 -3
  25. package/dist/esm/cli-actionlint.js +2 -1
  26. package/dist/esm/cli-build.js +2 -1
  27. package/dist/esm/cli-clean.js +2 -1
  28. package/dist/esm/cli-ensure-prepublish-hooks.js +29 -16
  29. package/dist/esm/cli-fmt.js +2 -1
  30. package/dist/esm/cli-report-catalog.js +2 -1
  31. package/dist/esm/cli-report-overrides.js +2 -1
  32. package/dist/esm/cli-shellcheck.js +2 -1
  33. package/dist/esm/cli-taze.js +2 -1
  34. package/dist/esm/cli-vitest.js +2 -1
  35. package/dist/esm/cli.js +2 -1
  36. package/dist/esm/commands/actionlint.d.ts +3 -3
  37. package/dist/esm/commands/actionlint.js +5 -5
  38. package/dist/esm/commands/build.js +6 -6
  39. package/dist/esm/commands/clean.js +3 -1
  40. package/dist/esm/commands/compiler.js +3 -3
  41. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -1
  42. package/dist/esm/commands/ensure-prepublish-hooks.js +4 -5
  43. package/dist/esm/commands/fmt.d.ts +3 -3
  44. package/dist/esm/commands/fmt.js +33 -52
  45. package/dist/esm/commands/format-targets.js +1 -13
  46. package/dist/esm/commands/node-pin.js +3 -4
  47. package/dist/esm/commands/rewrite-specifiers.js +2 -2
  48. package/dist/esm/commands/run-vitest.js +5 -5
  49. package/dist/esm/commands/shellcheck.d.ts +3 -3
  50. package/dist/esm/commands/shellcheck.js +8 -11
  51. package/dist/esm/commands/taze.js +5 -4
  52. package/dist/esm/commands/tsconfig-chain.js +3 -3
  53. package/dist/esm/commands/workflow-tools.d.ts +12 -3
  54. package/dist/esm/commands/workflow-tools.js +29 -14
  55. package/dist/esm/config-validation.js +9 -9
  56. package/dist/esm/config.js +2 -3
  57. package/dist/esm/delegate-host.d.ts +1 -1
  58. package/dist/esm/delegate-host.js +5 -3
  59. package/dist/esm/entry-chain.js +2 -3
  60. package/dist/esm/fan-out.d.ts +3 -3
  61. package/dist/esm/fan-out.js +14 -6
  62. package/dist/esm/git-ignored-paths.js +3 -3
  63. package/dist/esm/help.d.ts +14 -15
  64. package/dist/esm/help.js +29 -54
  65. package/dist/esm/helpers/filter-selection.js +2 -2
  66. package/dist/esm/helpers/package-json.js +8 -9
  67. package/dist/esm/helpers/resolveXdgBaseDir.d.ts +5 -0
  68. package/dist/esm/helpers/resolveXdgBaseDir.js +12 -0
  69. package/dist/esm/helpers/type-guards.d.ts +0 -2
  70. package/dist/esm/helpers/type-guards.js +2 -4
  71. package/dist/esm/invocation-args.d.ts +0 -32
  72. package/dist/esm/invocation-args.js +23 -121
  73. package/dist/esm/invocation.d.ts +3 -1
  74. package/dist/esm/invocation.js +1 -0
  75. package/dist/esm/machine-lock.js +3 -4
  76. package/dist/esm/nmr-flags.d.ts +109 -0
  77. package/dist/esm/nmr-flags.js +120 -0
  78. package/dist/esm/output-style.d.ts +1 -1
  79. package/dist/esm/output-style.js +2 -1
  80. package/dist/esm/pass-manifest.js +2 -2
  81. package/dist/esm/presentation.d.ts +1 -1
  82. package/dist/esm/remote-host-session.d.ts +2 -0
  83. package/dist/esm/remote-host-session.js +1 -0
  84. package/dist/esm/remote-probe.js +2 -2
  85. package/dist/esm/remote-run.d.ts +3 -3
  86. package/dist/esm/remote-run.js +6 -4
  87. package/dist/esm/remote-session.js +4 -4
  88. package/dist/esm/remote-snapshot.js +8 -22
  89. package/dist/esm/resolver.js +3 -4
  90. package/dist/esm/route.d.ts +29 -0
  91. package/dist/esm/route.js +94 -0
  92. package/dist/esm/run-chain.d.ts +1 -1
  93. package/dist/esm/run-chain.js +4 -2
  94. package/dist/esm/runCli.d.ts +2 -0
  95. package/dist/esm/runCli.js +36 -78
  96. package/dist/esm/stated-outcome.js +3 -3
  97. package/dist/esm/steps.js +6 -30
  98. package/dist/esm/user-config.js +7 -14
  99. package/dist/esm/vitest-source-resolution.js +5 -6
  100. package/dist/esm/vitest-temp-isolation.d.ts +2 -0
  101. package/dist/esm/vitest-temp-isolation.js +44 -0
  102. package/dist/esm/vitest.d.ts +9 -0
  103. package/dist/esm/vitest.js +8 -5
  104. package/docs/reporting.md +3 -3
  105. package/docs/scripts.md +1 -1
  106. package/docs/utilities.md +5 -5
  107. package/docs/vitest.md +11 -6
  108. package/package.json +4 -4
@@ -1,24 +1,37 @@
1
- import { parseArgsOrExit, reportFailure } from '@williamthorsen/nmr-core';
1
+ import process from 'node:process';
2
+ import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { defineCommand, runCli } from '@williamthorsen/nmr-core/cli';
4
+ import { ENSURE_PREPUBLISH_HOOKS_BIN } from "./bins.js";
2
5
  import { DEFAULT_HOOK, ensurePrepublishHooks, reportPrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
3
6
  import { resolveBinStyles } from "./resolveBinStyles.js";
4
7
  import { findMonorepoRoot } from "./workspace.js";
5
- const flagSchema = {
6
- shouldFix: { long: '--fix', type: 'boolean' },
7
- isDryRun: { long: '--dry-run', type: 'boolean' },
8
- command: { long: '--command', type: 'string' },
9
- };
10
- const { shouldFix, isDryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
8
+ const command = defineCommand({
9
+ summary: 'Check that every publishable workspace package has a prepublishOnly script',
10
+ flags: {
11
+ fix: { type: 'boolean', description: 'Add the script to each publishable package that lacks it' },
12
+ dryRun: { type: 'boolean', description: 'With --fix, report the packages that it would change without writing' },
13
+ command: {
14
+ type: 'string',
15
+ description: `The command that the script runs (default: ${DEFAULT_HOOK})`,
16
+ valueHint: 'command',
17
+ },
18
+ },
19
+ run: ({ flags }) => {
20
+ const styles = resolveBinStyles();
21
+ const options = { shouldFix: flags.fix, isDryRun: flags.dryRun, ...(flags.command && { command: flags.command }) };
22
+ const result = ensurePrepublishHooks(findMonorepoRoot(), options);
23
+ reportPrepublishHooks(result, flags.command ?? DEFAULT_HOOK, styles.stdout);
24
+ return result.hasFailures ? 1 : 0;
25
+ },
26
+ });
11
27
  try {
12
- const { stdout } = resolveBinStyles();
13
- const monorepoRoot = findMonorepoRoot();
14
- const options = command ? { shouldFix, isDryRun, command } : { shouldFix, isDryRun };
15
- const result = ensurePrepublishHooks(monorepoRoot, options);
16
- reportPrepublishHooks(result, command ?? DEFAULT_HOOK, stdout);
17
- if (result.hasFailures) {
18
- process.exitCode = 1;
19
- }
28
+ process.exitCode = await runCli(process.argv.slice(2), command, {
29
+ name: ENSURE_PREPUBLISH_HOOKS_BIN,
30
+ stdout: process.stdout,
31
+ stderr: process.stderr,
32
+ });
20
33
  }
21
34
  catch (error) {
22
- reportFailure('ensure-prepublish-hooks', error);
35
+ reportFailure(ENSURE_PREPUBLISH_HOOKS_BIN, error);
23
36
  process.exitCode = 1;
24
37
  }
@@ -1,10 +1,11 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_FMT_BIN } from "./bins.js";
3
4
  import { runFmt } from "./commands/fmt.js";
4
5
  try {
5
6
  process.exitCode = await runFmt(process.argv.slice(2), process.cwd());
6
7
  }
7
8
  catch (error) {
8
- reportFailure('nmr-fmt', error);
9
+ reportFailure(NMR_FMT_BIN, error);
9
10
  process.exitCode = 1;
10
11
  }
@@ -1,11 +1,12 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_REPORT_CATALOG_BIN } from "./bins.js";
3
4
  import { reportCatalog } from "./commands/report-catalog.js";
4
5
  import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
6
7
  reportCatalog(process.cwd(), resolveBinStyles().stderr);
7
8
  }
8
9
  catch (error) {
9
- reportFailure('nmr-report-catalog', error);
10
+ reportFailure(NMR_REPORT_CATALOG_BIN, error);
10
11
  process.exitCode = 1;
11
12
  }
@@ -1,4 +1,5 @@
1
1
  import { reportFailure } from '@williamthorsen/nmr-core';
2
+ import { NMR_REPORT_OVERRIDES_BIN } from "./bins.js";
2
3
  import { reportOverrides } from "./commands/report-overrides.js";
3
4
  import { resolveBinStyles } from "./resolveBinStyles.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
@@ -7,6 +8,6 @@ try {
7
8
  reportOverrides(monorepoRoot, resolveBinStyles().stderr);
8
9
  }
9
10
  catch (error) {
10
- reportFailure('nmr-report-overrides', error);
11
+ reportFailure(NMR_REPORT_OVERRIDES_BIN, error);
11
12
  process.exitCode = 1;
12
13
  }
@@ -1,11 +1,12 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_SHELLCHECK_BIN } from "./bins.js";
3
4
  import { runShellcheck } from "./commands/shellcheck.js";
4
5
  import { findMonorepoRoot } from "./workspace.js";
5
6
  try {
6
7
  process.exitCode = await runShellcheck(findMonorepoRoot(), process.argv.slice(2));
7
8
  }
8
9
  catch (error) {
9
- reportFailure('nmr-shellcheck', error);
10
+ reportFailure(NMR_SHELLCHECK_BIN, error);
10
11
  process.exitCode = 1;
11
12
  }
@@ -1,11 +1,12 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_TAZE_BIN } from "./bins.js";
3
4
  import { runTaze } from "./commands/taze.js";
4
5
  import { resolveBinStyles } from "./resolveBinStyles.js";
5
6
  try {
6
7
  process.exitCode = await runTaze(process.argv.slice(2), { stderrStyle: resolveBinStyles().stderr });
7
8
  }
8
9
  catch (error) {
9
- reportFailure('nmr-taze', error);
10
+ reportFailure(NMR_TAZE_BIN, error);
10
11
  process.exitCode = 1;
11
12
  }
@@ -1,10 +1,11 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_VITEST_BIN } from "./bins.js";
3
4
  import { runNmrVitest } from "./commands/run-vitest.js";
4
5
  try {
5
6
  process.exitCode = runNmrVitest(process.argv.slice(2), process.cwd());
6
7
  }
7
8
  catch (error) {
8
- reportFailure('nmr-vitest', error);
9
+ reportFailure(NMR_VITEST_BIN, error);
9
10
  process.exitCode = 1;
10
11
  }
package/dist/esm/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { reportFailure } from '@williamthorsen/nmr-core';
3
+ import { NMR_BIN } from "./bins.js";
3
4
  import { runCli } from "./runCli.js";
4
5
  try {
5
6
  const { exitCode } = await runCli({
@@ -12,6 +13,6 @@ try {
12
13
  process.exitCode = exitCode;
13
14
  }
14
15
  catch (error) {
15
- reportFailure('nmr', error);
16
+ reportFailure(NMR_BIN, error);
16
17
  process.exitCode = 1;
17
18
  }
@@ -6,9 +6,9 @@ export interface ActionlintOutcome {
6
6
  status: number | null;
7
7
  }
8
8
  export interface RunActionlintOptions {
9
- /** Directory under which the pinned tools are cached; defaults to `node_modules/.cache/nmr-tools` at the root. */
10
- cacheRoot?: string;
11
- ensure?: (tool: WorkflowTool, cacheRoot: string) => Promise<string>;
9
+ /** Directories under which the pinned tools are cached, in lookup order; defaults to `resolveToolCacheRoots`. */
10
+ cacheRoots?: readonly [string, ...string[]];
11
+ ensure?: (tool: WorkflowTool, cacheRoots: readonly [string, ...string[]]) => Promise<string>;
12
12
  spawn?: (file: string, args: string[], cwd: string) => ActionlintOutcome;
13
13
  }
14
14
  /**
@@ -3,17 +3,17 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { UserError } from '@williamthorsen/nmr-core';
5
5
  import { describeSignalExitCode } from "../child-process.js";
6
- import { ensureTool } from "./workflow-tools.js";
6
+ import { ensureTool, resolveToolCacheRoots } from "./workflow-tools.js";
7
7
  const WORKFLOWS_DIR = path.join('.github', 'workflows');
8
8
  export async function runActionlint(monorepoRoot, args, options = {}) {
9
9
  if (!fs.existsSync(path.join(monorepoRoot, WORKFLOWS_DIR))) {
10
10
  console.info(`No workflows to lint: ${WORKFLOWS_DIR}/ does not exist.`);
11
11
  return 0;
12
12
  }
13
- const cacheRoot = options.cacheRoot ?? path.join(monorepoRoot, 'node_modules', '.cache', 'nmr-tools');
14
- const ensure = options.ensure ?? ((tool, root) => ensureTool(tool, { cacheRoot: root }));
15
- const actionlintPath = await ensure('actionlint', cacheRoot);
16
- const shellcheckPath = await ensure('shellcheck', cacheRoot);
13
+ const cacheRoots = options.cacheRoots ?? resolveToolCacheRoots(monorepoRoot);
14
+ const ensure = options.ensure ?? ((tool, roots) => ensureTool(tool, { cacheRoots: roots }));
15
+ const actionlintPath = await ensure('actionlint', cacheRoots);
16
+ const shellcheckPath = await ensure('shellcheck', cacheRoots);
17
17
  const outcome = (options.spawn ?? spawnInherited)(actionlintPath, buildActionlintArgs(shellcheckPath, args), monorepoRoot);
18
18
  if (outcome.error !== undefined) {
19
19
  throw new UserError(`Cannot run actionlint: ${outcome.error.message}`, { cause: outcome.error });
@@ -92,7 +92,7 @@ async function emitPackage(packageDir, entryPoints, outdir) {
92
92
  }
93
93
  function assertEmitSucceeded(result) {
94
94
  if (result.emitSkipped) {
95
- throw new UserError(`nmr-compile: emit failed.\n${formatDiagnostics(result.diagnostics)}`);
95
+ throw new UserError(`emit failed.\n${formatDiagnostics(result.diagnostics)}`);
96
96
  }
97
97
  }
98
98
  function createCachingCompilerHost(compilerOptions) {
@@ -127,7 +127,7 @@ function resolveEmitDir(packageDir, outdir) {
127
127
  if (relativePathFromPackage === '' ||
128
128
  relativePathFromPackage.startsWith('..') ||
129
129
  path.isAbsolute(relativePathFromPackage)) {
130
- throw new UserError(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
130
+ throw new UserError(`refusing to build into '${outdir}', which does not resolve inside the package. ` +
131
131
  'The build replaces its output directory on each emit, so the directory must be below the package root.');
132
132
  }
133
133
  return resolvedDir;
@@ -144,7 +144,7 @@ async function swapIntoPlace(emitDir, scratchDirs) {
144
144
  if (hadPreviousOutput && !existsSync(emitDir)) {
145
145
  await rename(scratchDirs.previousDir, emitDir);
146
146
  }
147
- throw new UserError(`nmr-compile: failed to move the built output into ${emitDir}: ${describeError(error)}`, {
147
+ throw new UserError(`failed to move the built output into ${emitDir}: ${describeError(error)}`, {
148
148
  cause: error,
149
149
  });
150
150
  }
@@ -155,11 +155,11 @@ function synthesizeCompilerOptions(packageDir, outdir) {
155
155
  const configPath = path.join(packageDir, 'tsconfig.json');
156
156
  const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
157
157
  if (configFile.error) {
158
- throw new UserError(`nmr-compile: failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
158
+ throw new UserError(`failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
159
159
  }
160
160
  const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
161
161
  if (parsedConfig.errors.length > 0) {
162
- throw new UserError(`nmr-compile: failed to parse ${configPath}.\n${formatDiagnostics(parsedConfig.errors)}`);
162
+ throw new UserError(`failed to parse ${configPath}.\n${formatDiagnostics(parsedConfig.errors)}`);
163
163
  }
164
164
  const resolvedOutDir = path.resolve(packageDir, outdir);
165
165
  return {
@@ -180,7 +180,7 @@ function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
180
180
  const ts = getCompiler();
181
181
  for (const [fileName, file] of stagedFiles) {
182
182
  if (!isWithin(emitDir, fileName)) {
183
- throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
183
+ throw new Error(`the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
184
184
  "Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
185
185
  }
186
186
  ts.sys.writeFile(path.join(stagingDir, path.relative(emitDir, fileName)), file.text, file.writeByteOrderMark);
@@ -1,6 +1,7 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { formatGlyphLine, UserError } from '@williamthorsen/nmr-core';
4
+ import { NMR_CLEAN_BIN } from "../bins.js";
4
5
  import { removeCheckCache } from "../check-cache-entries.js";
5
6
  import { loadRootConfig } from "../config.js";
6
7
  import { findContainingPackageDir } from "../context.js";
@@ -68,10 +69,11 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
68
69
  }
69
70
  const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
70
71
  const { exitCode } = await runCommand(command, packageDir, {
72
+ bin: NMR_CLEAN_BIN,
71
73
  channels: { stderr: resolveChannel(process.stderr, false), stdout: resolveChannel(process.stdout, false) },
72
74
  });
73
75
  if (exitCode !== 0) {
74
- throw new UserError(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
76
+ throw new UserError(`\`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
75
77
  }
76
78
  cleanedCount++;
77
79
  }
@@ -14,7 +14,7 @@ export function assertSupportedTypeScript() {
14
14
  const minor = minorPart === undefined ? 0 : Number(minorPart);
15
15
  const tooOld = major < MINIMUM_TYPESCRIPT_MAJOR || (major === MINIMUM_TYPESCRIPT_MAJOR && minor < MINIMUM_TYPESCRIPT_MINOR);
16
16
  if (tooOld || major >= UNSUPPORTED_TYPESCRIPT_MAJOR) {
17
- throw new UserError(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but found ` +
17
+ throw new UserError(`requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but found ` +
18
18
  `${typeof version === 'string' ? version : 'a version without the compiler API'}. ${INSTALL_TYPESCRIPT_GUIDANCE}`);
19
19
  }
20
20
  }
@@ -28,7 +28,7 @@ export function formatDiagnostics(diagnostics) {
28
28
  }
29
29
  export function getCompiler() {
30
30
  if (ts === undefined) {
31
- throw new Error('nmr-compile: the compiler is not loaded; call loadCompiler first.');
31
+ throw new Error('the compiler is not loaded; call loadCompiler first.');
32
32
  }
33
33
  return ts;
34
34
  }
@@ -38,7 +38,7 @@ export async function loadCompiler(loadTypeScript = importTypeScript) {
38
38
  }
39
39
  catch (error) {
40
40
  if (hasErrnoCode(error, 'ERR_MODULE_NOT_FOUND') && error.message.includes("'typescript'")) {
41
- throw new UserError(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but 'typescript' could not be resolved. ` +
41
+ throw new UserError(`requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but 'typescript' could not be resolved. ` +
42
42
  INSTALL_TYPESCRIPT_GUIDANCE, { cause: error });
43
43
  }
44
44
  throw error;
@@ -10,7 +10,7 @@ export interface EnsurePrepublishHooksResult {
10
10
  packages: PackageHookStatus[];
11
11
  hasFailures: boolean;
12
12
  }
13
- export declare const DEFAULT_HOOK = "npm run build";
13
+ export declare const DEFAULT_HOOK = "nmr build";
14
14
  /**
15
15
  * Checks (and optionally fixes) whether all publishable workspace packages
16
16
  * have a `prepublishOnly` script.
@@ -1,12 +1,11 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { STATUS_GLYPHS, UserError } from '@williamthorsen/nmr-core';
3
+ import { isRecord, STATUS_GLYPHS, UserError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { readPackageJson } from "../helpers/package-json.js";
6
6
  import { reportClosing } from "../helpers/reportClosing.js";
7
- import { isObject } from "../helpers/type-guards.js";
8
7
  import { getWorkspacePackageDirs } from "../workspace.js";
9
- export const DEFAULT_HOOK = 'npm run build';
8
+ export const DEFAULT_HOOK = 'nmr build';
10
9
  export function ensurePrepublishHooks(monorepoRoot, options) {
11
10
  const hookCommand = options.command ?? DEFAULT_HOOK;
12
11
  const packageDirs = getWorkspacePackageDirs(monorepoRoot);
@@ -83,10 +82,10 @@ function addPrepublishOnly(packageDir, command) {
83
82
  catch (error) {
84
83
  throw new UserError(`Invalid package.json in ${packageDir}: ${describeError(error)}`, { cause: error });
85
84
  }
86
- if (!isObject(parsedManifest)) {
85
+ if (!isRecord(parsedManifest)) {
87
86
  throw new UserError(`Invalid package.json in ${packageDir}: expected an object`);
88
87
  }
89
- const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
88
+ const scripts = isRecord(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
90
89
  scripts['prepublishOnly'] = command;
91
90
  parsedManifest['scripts'] = scripts;
92
91
  writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
@@ -1,8 +1,8 @@
1
1
  export type FormatMode = 'check' | 'write';
2
2
  /**
3
- * Formats or checks the files that git reports for `cwd`, and returns the exit code to report. A repository
4
- * without a Prettier config of its own is formatted with the house config. The run uses Prettier's content-keyed
5
- * cache unless `--no-cache` or `NMR_NO_CACHE=1` bypasses it.
3
+ * Runs `nmr-fmt` against `argv` and returns the exit code to report: It formats or checks the files that git
4
+ * reports for `cwd`. A repository without a Prettier config of its own is formatted with the house config. The run
5
+ * uses Prettier's content-keyed cache unless `--no-cache` or `NMR_NO_CACHE=1` bypasses it.
6
6
  *
7
7
  * Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
8
8
  * than passed along, since git would read it as a pathspec matching nothing and the run would report
@@ -5,29 +5,41 @@ import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import process from 'node:process';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { reportError } from '@williamthorsen/nmr-core';
8
+ import { isRecord, reportError } from '@williamthorsen/nmr-core';
9
+ import { defineCommand, runCli, UsageError } from '@williamthorsen/nmr-core/cli';
9
10
  import { describeError } from '@williamthorsen/toolbelt.errors';
10
- import { isObject } from "../helpers/type-guards.js";
11
+ import { NMR_FMT_BIN } from "../bins.js";
11
12
  import { prepareFmtCache } from "./fmt-cache.js";
12
13
  import { resolveFormatTargets } from "./format-targets.js";
13
14
  const PRETTIER_PACKAGE = 'prettier';
14
15
  const DEFAULT_CONFIG_PATH = fileURLToPath(new URL(`../prettier-default.config${path.extname(fileURLToPath(import.meta.url))}`, import.meta.url));
15
16
  const ARGUMENT_BUDGET_BYTES = 100_000;
16
- const USAGE = 'Usage: nmr-fmt (--check | --write) [--no-cache] [--] [pathspec...]';
17
17
  const MODE_ARGS = {
18
18
  check: ['--check'],
19
19
  write: ['--list-different', '--write'],
20
20
  };
21
21
  export async function runFmt(argv, cwd = process.cwd()) {
22
- const parsedArgs = parseFmtArgs(argv);
23
- if (!parsedArgs.ok) {
24
- reportError(`nmr-fmt: ${parsedArgs.error}`);
25
- process.stderr.write(`${USAGE}\n`);
26
- return 1;
27
- }
28
- const resolvedTargets = resolveFormatTargets(cwd, parsedArgs.pathspecs);
22
+ const command = defineCommand({
23
+ summary: 'Format or check the files that git lists',
24
+ flags: {
25
+ check: { type: 'boolean', description: 'Report the files that are not formatted, without changing them' },
26
+ write: { type: 'boolean', description: 'Format the files in place, naming each one that changes' },
27
+ noCache: { type: 'boolean', description: "Ignore Prettier's cache of files that already passed" },
28
+ },
29
+ operands: [{ name: 'pathspec', description: 'Git pathspecs that limit the files', optional: true, variadic: true }],
30
+ run: ({ flags, operands }) => {
31
+ if (flags.check === flags.write) {
32
+ throw new UsageError(flags.check ? 'Pass --check or --write, not both.' : 'Pass --check or --write.');
33
+ }
34
+ return formatFiles({ mode: flags.check ? 'check' : 'write', pathspecs: operands.pathspec, shouldBypassCache: flags.noCache }, cwd);
35
+ },
36
+ });
37
+ return runCli(argv, command, { name: NMR_FMT_BIN, stdout: process.stdout, stderr: process.stderr, baseDir: cwd });
38
+ }
39
+ async function formatFiles(request, cwd) {
40
+ const resolvedTargets = resolveFormatTargets(cwd, request.pathspecs);
29
41
  if (!resolvedTargets.ok) {
30
- reportError(`nmr-fmt: ${resolvedTargets.error}`);
42
+ reportError(NMR_FMT_BIN, resolvedTargets.error);
31
43
  return 1;
32
44
  }
33
45
  const { excludedFiles, files, ignorePaths, repositoryRoot, unreadableFiles } = resolvedTargets.targets;
@@ -35,10 +47,10 @@ export async function runFmt(argv, cwd = process.cwd()) {
35
47
  process.stderr.write(`nmr-fmt: skipped ${file}, which cannot be read (${code})\n`);
36
48
  }
37
49
  if (files.length === 0) {
38
- if (parsedArgs.pathspecs.length === 0)
50
+ if (request.pathspecs.length === 0)
39
51
  return 0;
40
52
  if (excludedFiles.length === 0 && unreadableFiles.length === 0) {
41
- reportError(`nmr-fmt: the pathspecs did not match any file that git lists: ${parsedArgs.pathspecs.join(', ')}`);
53
+ reportError(NMR_FMT_BIN, `the pathspecs did not match any file that git lists: ${request.pathspecs.join(', ')}`);
42
54
  return 1;
43
55
  }
44
56
  if (excludedFiles.length > 0) {
@@ -48,19 +60,19 @@ export async function runFmt(argv, cwd = process.cwd()) {
48
60
  }
49
61
  const cli = resolvePrettierCli(cwd);
50
62
  if (!cli.ok) {
51
- reportError(`nmr-fmt: ${cli.error}`);
63
+ reportError(NMR_FMT_BIN, cli.error);
52
64
  return 1;
53
65
  }
54
66
  const repositoryConfig = await findRepositoryConfig(cli.manifestPath, cwd);
55
67
  if (!repositoryConfig.ok) {
56
- reportError(`nmr-fmt: ${repositoryConfig.error}`);
68
+ reportError(NMR_FMT_BIN, repositoryConfig.error);
57
69
  return 1;
58
70
  }
59
71
  const cache = prepareFmtCache({
60
72
  cwd,
61
73
  env: process.env,
62
74
  repositoryRoot,
63
- shouldBypassCache: parsedArgs.shouldBypassCache,
75
+ shouldBypassCache: request.shouldBypassCache,
64
76
  });
65
77
  if (cache.kind === 'failed') {
66
78
  process.stderr.write(`nmr-fmt: running without the Prettier cache: ${cache.reason}\n`);
@@ -69,43 +81,12 @@ export async function runFmt(argv, cwd = process.cwd()) {
69
81
  cacheLocation: cache.kind === 'cached' ? cache.location : undefined,
70
82
  cliPath: cli.cliPath,
71
83
  configPath: repositoryConfig.found ? undefined : DEFAULT_CONFIG_PATH,
72
- mode: parsedArgs.mode,
84
+ mode: request.mode,
73
85
  files,
74
86
  ignorePaths,
75
87
  cwd,
76
88
  });
77
89
  }
78
- function parseFmtArgs(argv) {
79
- let mode;
80
- let shouldBypassCache = false;
81
- const pathspecs = [];
82
- for (const [index, arg] of argv.entries()) {
83
- if (arg === '--') {
84
- pathspecs.push(...argv.slice(index + 1));
85
- break;
86
- }
87
- if (arg === '--no-cache') {
88
- shouldBypassCache = true;
89
- continue;
90
- }
91
- if (arg === '--check' || arg === '--write') {
92
- const requestedMode = arg === '--check' ? 'check' : 'write';
93
- if (mode !== undefined && mode !== requestedMode) {
94
- return { ok: false, error: 'Pass --check or --write, not both.' };
95
- }
96
- mode = requestedMode;
97
- continue;
98
- }
99
- if (arg.startsWith('-')) {
100
- return { ok: false, error: `Unknown option: ${arg}` };
101
- }
102
- pathspecs.push(arg);
103
- }
104
- if (mode === undefined) {
105
- return { ok: false, error: 'Pass --check or --write.' };
106
- }
107
- return { ok: true, mode, pathspecs, shouldBypassCache };
108
- }
109
90
  function resolvePrettierCli(cwd) {
110
91
  const missingResult = {
111
92
  ok: false,
@@ -116,9 +97,9 @@ function resolvePrettierCli(cwd) {
116
97
  if (manifestPath === undefined)
117
98
  return missingResult;
118
99
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
119
- if (!isObject(manifest))
100
+ if (!isRecord(manifest))
120
101
  return missingResult;
121
- const bin = isObject(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
102
+ const bin = isRecord(manifest['bin']) ? manifest['bin'][PRETTIER_PACKAGE] : manifest['bin'];
122
103
  if (typeof bin !== 'string')
123
104
  return missingResult;
124
105
  return { ok: true, cliPath: path.resolve(path.dirname(manifestPath), bin), manifestPath };
@@ -126,7 +107,7 @@ function resolvePrettierCli(cwd) {
126
107
  async function findRepositoryConfig(manifestPath, cwd) {
127
108
  try {
128
109
  const prettier = createRequire(manifestPath)(PRETTIER_PACKAGE);
129
- const resolveConfigFile = isObject(prettier) ? prettier['resolveConfigFile'] : undefined;
110
+ const resolveConfigFile = isRecord(prettier) ? prettier['resolveConfigFile'] : undefined;
130
111
  if (typeof resolveConfigFile !== 'function') {
131
112
  return {
132
113
  ok: false,
@@ -157,7 +138,7 @@ export function runPrettier(options) {
157
138
  for (const batch of batchWithinBudget(files, budgetBytes)) {
158
139
  const result = spawnSync(process.execPath, [...prettierArgs, ...batch], { cwd, stdio: 'inherit' });
159
140
  if (result.error) {
160
- reportError(`nmr-fmt: could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
141
+ reportError(NMR_FMT_BIN, `could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
161
142
  return 1;
162
143
  }
163
144
  const status = result.status ?? 1;
@@ -1,7 +1,6 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import { lstatSync } from 'node:fs';
3
2
  import path from 'node:path';
4
- import { hasErrnoCode } from '@williamthorsen/nmr-core';
3
+ import { hasErrnoCode, runGit } from '@williamthorsen/nmr-core';
5
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
6
5
  const IGNORE_FILENAME = '.prettierignore';
7
6
  const LIST_FILES_ARGS = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
@@ -64,17 +63,6 @@ function classifyListedFile(cwd, file) {
64
63
  return { kind: 'failed', error: `could not stat ${file}: ${describeError(error)}` };
65
64
  }
66
65
  }
67
- function runGit(args, cwd) {
68
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
69
- if (result.error) {
70
- return { ok: false, error: result.error.message };
71
- }
72
- if (result.status !== 0) {
73
- const stderr = result.stderr.trim();
74
- return { ok: false, error: stderr || `\`git ${args.join(' ')}\` failed with exit code ${result.status}.` };
75
- }
76
- return { ok: true, stdout: result.stdout };
77
- }
78
66
  function splitNulSeparated(output) {
79
67
  return output.split('\0').filter((entry) => entry !== '');
80
68
  }
@@ -1,10 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import process from 'node:process';
4
- import { formatGlyphLine } from '@williamthorsen/nmr-core';
4
+ import { formatGlyphLine, isRecord } from '@williamthorsen/nmr-core';
5
5
  import { describeError } from '@williamthorsen/toolbelt.errors';
6
6
  import { NMR_GLYPHS } from "../glyphs.js";
7
- import { isObject } from "../helpers/type-guards.js";
8
7
  export const NODE_RELEASE_INDEX_URL = 'https://nodejs.org/dist/index.json';
9
8
  const TOOL_VERSIONS_FILE = '.tool-versions';
10
9
  const SOAK_MS = 2 * 24 * 60 * 60 * 1000;
@@ -77,7 +76,7 @@ export function selectNodeUpgrade(pin, releases, now) {
77
76
  let best;
78
77
  let bestVersion;
79
78
  for (const release of releases) {
80
- if (!isObject(release))
79
+ if (!isRecord(release))
81
80
  continue;
82
81
  const { date, version } = release;
83
82
  if (typeof version !== 'string' || typeof date !== 'string' || !RELEASE_DATE_PATTERN.test(date))
@@ -120,7 +119,7 @@ function findNodePin(content) {
120
119
  return { start: match.index + match[0].length - version.length, version };
121
120
  }
122
121
  function isMissingFileError(error) {
123
- return isObject(error) && error['code'] === 'ENOENT';
122
+ return isRecord(error) && error['code'] === 'ENOENT';
124
123
  }
125
124
  function parseVersion(version) {
126
125
  return version.split('.').map(Number);
@@ -89,7 +89,7 @@ function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOp
89
89
  const ts = getCompiler();
90
90
  const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
91
91
  if (!resolvedModule) {
92
- throw new UserError(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
92
+ throw new UserError(`could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
93
93
  `Verify the tsconfig 'paths' mapping and that the target file exists.`);
94
94
  }
95
95
  if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
@@ -98,7 +98,7 @@ function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOp
98
98
  if (bareResolvedModule) {
99
99
  return undefined;
100
100
  }
101
- throw new UserError(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
101
+ throw new UserError(`aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
102
102
  `${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
103
103
  `the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
104
104
  `emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
@@ -3,10 +3,10 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
6
- import { reportError } from '@williamthorsen/nmr-core';
6
+ import { isRecord, reportError } from '@williamthorsen/nmr-core';
7
7
  import { findMonorepoRoot as findMonorepoRootOrNothing } from '@williamthorsen/nmr-core/workspace';
8
+ import { NMR_VITEST_BIN } from "../bins.js";
8
9
  import { describeSignalExitCode } from "../child-process.js";
9
- import { isObject } from "../helpers/type-guards.js";
10
10
  import { omitOutcomeFile } from "../stated-outcome.js";
11
11
  const VITEST_PACKAGE = 'vitest';
12
12
  const SUPPORTED_VITEST_RANGE = '>=4.0.0 <6';
@@ -14,7 +14,7 @@ const CONFIG_EXTENSIONS = ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs'];
14
14
  export function runNmrVitest(argv, cwd = process.cwd()) {
15
15
  const cli = resolveVitestCli(cwd);
16
16
  if (!cli.ok) {
17
- reportError(`nmr-vitest: ${cli.error}`);
17
+ reportError(NMR_VITEST_BIN, cli.error);
18
18
  return 1;
19
19
  }
20
20
  const result = spawnSync(process.execPath, [cli.cliPath, ...chooseVitestConfigArgs(argv, cwd)], {
@@ -23,7 +23,7 @@ export function runNmrVitest(argv, cwd = process.cwd()) {
23
23
  stdio: 'inherit',
24
24
  });
25
25
  if (result.error) {
26
- reportError(`nmr-vitest: could not run \`${VITEST_PACKAGE}\` (${result.error.message}).`);
26
+ reportError(NMR_VITEST_BIN, `could not run \`${VITEST_PACKAGE}\` (${result.error.message}).`);
27
27
  return 1;
28
28
  }
29
29
  if (result.signal !== null)
@@ -73,7 +73,7 @@ function resolveVitestCli(cwd) {
73
73
  return missingResult;
74
74
  }
75
75
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
76
- const bin = isObject(manifest) && isObject(manifest['bin']) ? manifest['bin'][VITEST_PACKAGE] : undefined;
76
+ const bin = isRecord(manifest) && isRecord(manifest['bin']) ? manifest['bin'][VITEST_PACKAGE] : undefined;
77
77
  if (typeof bin !== 'string') {
78
78
  return { ok: false, error: `\`${VITEST_PACKAGE}\` at ${path.dirname(manifestPath)} does not declare a CLI.` };
79
79
  }
@@ -6,9 +6,9 @@ export interface ShellcheckOutcome {
6
6
  status: number | null;
7
7
  }
8
8
  export interface RunShellcheckOptions {
9
- /** Directory under which the pinned tools are cached; defaults to `node_modules/.cache/nmr-tools` at the root. */
10
- cacheRoot?: string;
11
- ensure?: (tool: WorkflowTool, cacheRoot: string) => Promise<string>;
9
+ /** Directories under which the pinned tools are cached, in lookup order; defaults to `resolveToolCacheRoots`. */
10
+ cacheRoots?: readonly [string, ...string[]];
11
+ ensure?: (tool: WorkflowTool, cacheRoots: readonly [string, ...string[]]) => Promise<string>;
12
12
  /** Lists the tracked `*.sh` files under the root, relative to it. */
13
13
  listFiles?: (monorepoRoot: string) => string[];
14
14
  spawn?: (file: string, args: string[], cwd: string) => ShellcheckOutcome;