@williamthorsen/nmr 0.47.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.meta/changelog.json +206 -0
  2. package/.readyup/kits/default.js +1534 -1466
  3. package/.readyup/manifest.json +142 -12
  4. package/CHANGELOG.md +55 -0
  5. package/README.md +44 -6
  6. package/agents/guidance/rulebooks/nmr.md +2 -1
  7. package/dist/esm/bin-path.js +4 -3
  8. package/dist/esm/check-cache-build-output.d.ts +24 -0
  9. package/dist/esm/check-cache-build-output.js +49 -0
  10. package/dist/esm/check-cache-delegation.d.ts +27 -0
  11. package/dist/esm/check-cache-delegation.js +92 -0
  12. package/dist/esm/check-cache-entries.d.ts +109 -0
  13. package/dist/esm/check-cache-entries.js +87 -0
  14. package/dist/esm/check-cache-gate.d.ts +64 -0
  15. package/dist/esm/check-cache-gate.js +226 -0
  16. package/dist/esm/check-cache-key.d.ts +47 -0
  17. package/dist/esm/check-cache-key.js +77 -0
  18. package/dist/esm/check-cache-record.d.ts +69 -0
  19. package/dist/esm/check-cache-record.js +167 -0
  20. package/dist/esm/check-cache.d.ts +8 -176
  21. package/dist/esm/check-cache.js +9 -214
  22. package/dist/esm/child-env.d.ts +57 -0
  23. package/dist/esm/child-env.js +59 -0
  24. package/dist/esm/child-process.d.ts +55 -0
  25. package/dist/esm/child-process.js +112 -0
  26. package/dist/esm/commands/build.d.ts +0 -16
  27. package/dist/esm/commands/build.js +9 -214
  28. package/dist/esm/commands/clean.js +3 -2
  29. package/dist/esm/commands/compiler.d.ts +18 -0
  30. package/dist/esm/commands/compiler.js +49 -0
  31. package/dist/esm/commands/fmt.d.ts +0 -39
  32. package/dist/esm/commands/fmt.js +4 -81
  33. package/dist/esm/commands/format-targets.d.ts +39 -0
  34. package/dist/esm/commands/format-targets.js +83 -0
  35. package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
  36. package/dist/esm/commands/rewrite-specifiers.js +122 -0
  37. package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
  38. package/dist/esm/commands/tsconfig-chain.js +63 -0
  39. package/dist/esm/config-check-cache.d.ts +17 -0
  40. package/dist/esm/config-check-cache.js +63 -0
  41. package/dist/esm/config-validation.d.ts +20 -0
  42. package/dist/esm/config-validation.js +236 -0
  43. package/dist/esm/config.js +4 -286
  44. package/dist/esm/crossing-diagnostics.d.ts +39 -0
  45. package/dist/esm/crossing-diagnostics.js +132 -0
  46. package/dist/esm/default-scripts.js +1 -1
  47. package/dist/esm/delegate-host.d.ts +43 -0
  48. package/dist/esm/delegate-host.js +178 -0
  49. package/dist/esm/delegation-target.d.ts +52 -0
  50. package/dist/esm/delegation-target.js +71 -0
  51. package/dist/esm/fan-out.d.ts +57 -0
  52. package/dist/esm/fan-out.js +146 -0
  53. package/dist/esm/glyphs.d.ts +1 -1
  54. package/dist/esm/help.js +10 -0
  55. package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
  56. package/dist/esm/helpers/deriveExcerpt.js +1 -1
  57. package/dist/esm/helpers/findClosestName.js +4 -3
  58. package/dist/esm/helpers/quoteForShell.d.ts +5 -0
  59. package/dist/esm/helpers/quoteForShell.js +3 -0
  60. package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
  61. package/dist/esm/helpers/renderQuotedList.js +9 -0
  62. package/dist/esm/hooks.d.ts +17 -0
  63. package/dist/esm/hooks.js +23 -0
  64. package/dist/esm/invocation-args.d.ts +40 -0
  65. package/dist/esm/invocation-args.js +178 -0
  66. package/dist/esm/invocation.d.ts +67 -0
  67. package/dist/esm/invocation.js +3 -0
  68. package/dist/esm/machine-lock.d.ts +2 -0
  69. package/dist/esm/machine-lock.js +1 -1
  70. package/dist/esm/output-channel.d.ts +23 -0
  71. package/dist/esm/output-channel.js +14 -0
  72. package/dist/esm/pass-manifest.d.ts +53 -0
  73. package/dist/esm/pass-manifest.js +89 -0
  74. package/dist/esm/presentation.d.ts +47 -0
  75. package/dist/esm/presentation.js +32 -0
  76. package/dist/esm/recording.d.ts +1 -1
  77. package/dist/esm/recording.js +2 -2
  78. package/dist/esm/remote-host-session.d.ts +62 -0
  79. package/dist/esm/remote-host-session.js +277 -0
  80. package/dist/esm/remote-probe-host.d.ts +22 -0
  81. package/dist/esm/remote-probe-host.js +50 -0
  82. package/dist/esm/remote-probe.d.ts +31 -0
  83. package/dist/esm/remote-probe.js +78 -0
  84. package/dist/esm/remote-run.d.ts +91 -0
  85. package/dist/esm/remote-run.js +189 -0
  86. package/dist/esm/remote-session.d.ts +40 -0
  87. package/dist/esm/remote-session.js +151 -0
  88. package/dist/esm/remote-snapshot.d.ts +21 -0
  89. package/dist/esm/remote-snapshot.js +80 -0
  90. package/dist/esm/replay-assembly.d.ts +2 -2
  91. package/dist/esm/replay-assembly.js +1 -1
  92. package/dist/esm/run-chain.d.ts +21 -0
  93. package/dist/esm/run-chain.js +108 -0
  94. package/dist/esm/runCli.d.ts +1 -9
  95. package/dist/esm/runCli.js +41 -895
  96. package/dist/esm/runner.d.ts +1 -54
  97. package/dist/esm/runner.js +2 -125
  98. package/dist/esm/shell-syntax.d.ts +30 -0
  99. package/dist/esm/shell-syntax.js +120 -0
  100. package/dist/esm/steps.d.ts +0 -2
  101. package/dist/esm/steps.js +3 -119
  102. package/dist/esm/types.d.ts +5 -0
  103. package/dist/esm/user-config.d.ts +34 -0
  104. package/dist/esm/user-config.js +93 -0
  105. package/dist/esm/verbosity.d.ts +6 -0
  106. package/dist/esm/verbosity.js +7 -7
  107. package/dist/esm/verdict.d.ts +3 -1
  108. package/dist/esm/verdict.js +4 -3
  109. package/dist/esm/vitest-source-resolution.js +2 -3
  110. package/docs/check-cache.md +30 -14
  111. package/docs/delegate.md +141 -0
  112. package/docs/machine-lock.md +2 -1
  113. package/docs/reporting.md +6 -1
  114. package/docs/scripts.md +2 -1
  115. package/package.json +5 -5
  116. /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
  117. /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
@@ -0,0 +1,57 @@
1
+ import type { StreamStyles } from '@williamthorsen/nmr-core';
2
+ import { type TreeSnapshot } from './check-cache.js';
3
+ import type { Invocation } from './invocation.js';
4
+ import { type MachineLock } from './machine-lock.js';
5
+ import { type ReportFormat } from './report-format.js';
6
+ import { type CommandVerbosity } from './verbosity.js';
7
+ /**
8
+ * Returns the environment that tells the processes below this one what the lock came to: They run under its hold,
9
+ * or, when the chain runs unlocked, they skip the lock.
10
+ */
11
+ export declare function addLockEnv(env: NodeJS.ProcessEnv, lock: MachineLock | undefined): NodeJS.ProcessEnv;
12
+ /**
13
+ * Builds the environment that every process below this one inherits. The snapshot travels down to make a chain
14
+ * of nmr invocations gate on one observation of the tree; a bypass, to cover the whole chain rather than only the
15
+ * command next to which it was typed; the verbosity, to make each process suppress the output of the command
16
+ * that it runs rather than of the subtree beneath it; and the run's identity, to mark the excerpts that a run
17
+ * records at every scope as one run's.
18
+ *
19
+ * The verbosity is written in both modes, so that a chain's loudness is decided once at the top rather than
20
+ * re-derived at every link from an environment that a caller may have set.
21
+ *
22
+ * Trailing arguments bypass the cache for the whole chain, as `--no-cache` does. `openGate` disables this
23
+ * invocation's own gate for them, but the steps below it are separate nmr invocations that do not receive any
24
+ * arguments of their own, and without the bypass a narrowed command would serve part of its work from a recorded
25
+ * pass.
26
+ *
27
+ * `NMR_DELEGATE` is set empty: a command that runs here runs its whole chain here, so that a step or a `-F`/`-R`
28
+ * child of a local run passes itself on neither to the host that the variable names nor to the user config's.
29
+ *
30
+ * The style written down is the resolved one and never `auto`: a child on a pipe renders as this process does
31
+ * rather than detecting plain on its own descriptor. It is the style to which stdout resolved: the verdicts and
32
+ * nearly every other line that a child prints go there.
33
+ */
34
+ export declare function buildChildEnv(options: {
35
+ env: NodeJS.ProcessEnv;
36
+ format: ReportFormat;
37
+ passthrough: readonly string[];
38
+ runId: string;
39
+ shouldBypassCache: boolean;
40
+ shouldBypassLock: boolean;
41
+ snapshot: TreeSnapshot | undefined;
42
+ styles: StreamStyles;
43
+ verbosity: CommandVerbosity;
44
+ }): NodeJS.ProcessEnv;
45
+ /**
46
+ * Returns the variables of a child environment that the host's nmr receives: those that decide how it reports, whether
47
+ * it bypasses its cache, whether it writes debug notes, and which run it belongs to. The rest describe this machine,
48
+ * such as the tree snapshot and the lock token, or would disable the host's lock on the strength of a local
49
+ * workaround, as an inherited `NMR_NO_LOCK` would; the `--no-lock` flag still reaches the host as an argument.
50
+ */
51
+ export declare function buildRemoteEnv(childEnv: NodeJS.ProcessEnv): Record<string, string>;
52
+ /**
53
+ * Takes the machine lock for a heavy command that runs, or returns `undefined` for any other invocation. A delegated
54
+ * `--log` reads recordings and runs nothing. A `--no-lock` written after the command name is an argument to the
55
+ * command, so it is reported rather than read.
56
+ */
57
+ export declare function takeMachineLock(invocation: Invocation): Promise<MachineLock | undefined>;
@@ -0,0 +1,59 @@
1
+ import { DEBUG_ENV_VAR, encodeTreeSnapshot, NO_CACHE_ENV_VAR, RUN_ID_ENV_VAR, TREE_SNAPSHOT_ENV_VAR, } from "./check-cache.js";
2
+ import { acquireMachineLock, formatMisplacedNoLockWarning, HEAVY_COMMANDS, LOCK_TOKEN_ENV_VAR, NO_LOCK_ENV_VAR, } from "./machine-lock.js";
3
+ import { OUTPUT_STYLE_ENV_VAR } from "./output-style.js";
4
+ import { DELEGATE_ENV_VAR } from "./remote-run.js";
5
+ import { REPORT_FORMAT_ENV_VAR } from "./report-format.js";
6
+ import { COMMAND_VERBOSITY_ENV_VAR } from "./verbosity.js";
7
+ export function addLockEnv(env, lock) {
8
+ if (lock?.token !== undefined) {
9
+ return { ...env, [LOCK_TOKEN_ENV_VAR]: lock.token };
10
+ }
11
+ if (lock?.isChainUnlocked === true) {
12
+ return { ...env, [NO_LOCK_ENV_VAR]: '1' };
13
+ }
14
+ return env;
15
+ }
16
+ export function buildChildEnv(options) {
17
+ const { env, format, passthrough, runId, shouldBypassCache, shouldBypassLock, snapshot, styles, verbosity } = options;
18
+ const bypassesCache = shouldBypassCache || passthrough.length > 0;
19
+ return {
20
+ ...env,
21
+ [DELEGATE_ENV_VAR]: '',
22
+ ...(snapshot !== undefined && { [TREE_SNAPSHOT_ENV_VAR]: encodeTreeSnapshot(snapshot) }),
23
+ ...(bypassesCache && { [NO_CACHE_ENV_VAR]: '1' }),
24
+ ...(shouldBypassLock && { [NO_LOCK_ENV_VAR]: '1' }),
25
+ [COMMAND_VERBOSITY_ENV_VAR]: verbosity,
26
+ [OUTPUT_STYLE_ENV_VAR]: styles.stdout,
27
+ [REPORT_FORMAT_ENV_VAR]: format,
28
+ [RUN_ID_ENV_VAR]: runId,
29
+ };
30
+ }
31
+ export function buildRemoteEnv(childEnv) {
32
+ return Object.fromEntries(REMOTE_ENV_VARS.flatMap((name) => {
33
+ const value = childEnv[name];
34
+ return value === undefined ? [] : [[name, value]];
35
+ }));
36
+ }
37
+ export async function takeMachineLock(invocation) {
38
+ const { anchorDir, command, env, parsedArgs, stderr, styles } = invocation;
39
+ if (!HEAVY_COMMANDS.has(command) || parsedArgs.shouldPrintLog) {
40
+ return undefined;
41
+ }
42
+ if (parsedArgs.passthrough.includes('--no-lock')) {
43
+ stderr.write(`${formatMisplacedNoLockWarning(command, styles.stderr)}\n`);
44
+ }
45
+ return acquireMachineLock({
46
+ command,
47
+ dir: anchorDir,
48
+ env: parsedArgs.shouldBypassLock ? { ...env, [NO_LOCK_ENV_VAR]: '1' } : env,
49
+ stderr,
50
+ });
51
+ }
52
+ const REMOTE_ENV_VARS = [
53
+ COMMAND_VERBOSITY_ENV_VAR,
54
+ DEBUG_ENV_VAR,
55
+ NO_CACHE_ENV_VAR,
56
+ OUTPUT_STYLE_ENV_VAR,
57
+ REPORT_FORMAT_ENV_VAR,
58
+ RUN_ID_ENV_VAR,
59
+ ];
@@ -0,0 +1,55 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import type { Writable } from 'node:stream';
3
+ import type { OutputChannels } from './output-channel.js';
4
+ export interface RunCommandOptions {
5
+ /** The channel on which each of the child's output streams runs. */
6
+ channels: OutputChannels;
7
+ /** When true, suppresses output on success and writes captured output to stderr on failure. */
8
+ quiet?: boolean;
9
+ /** Stream that subprocess stdout flows to in non-quiet mode. Defaults to `process.stdout`. */
10
+ stdout?: Writable;
11
+ /** Stream that subprocess stderr flows to (and that quiet-mode failure output is written to). Defaults to `process.stderr`. */
12
+ stderr?: Writable;
13
+ /** Environment for the subprocess. Defaults to `process.env`. */
14
+ env?: NodeJS.ProcessEnv;
15
+ }
16
+ export type RunCommandResult = CommandCompletion & {
17
+ /**
18
+ * Output retained while the command ran, elided in the middle when it overran the bound.
19
+ * `undefined` when the stream was handed to the child directly, or when capture stopped early
20
+ * and the retained copy would understate what the command produced.
21
+ */
22
+ stdout: Buffer | undefined;
23
+ stderr: Buffer | undefined;
24
+ };
25
+ /** What to hand `spawn`: a whole command line for a shell to parse, or a file and the arguments that it receives. */
26
+ export interface SpawnSpec {
27
+ args: readonly string[];
28
+ file: string;
29
+ /** Whether the child reads nmr's own stdin or the null device. */
30
+ input: 'ignore' | 'inherit';
31
+ shouldUseShell: boolean;
32
+ }
33
+ /** A spawned child and a promise of how it ended. */
34
+ export interface StartedCommand {
35
+ child: ChildProcess;
36
+ result: Promise<RunCommandResult>;
37
+ }
38
+ /** Returns the exit code that a shell reports for a death by the signal. */
39
+ export declare function describeSignalExitCode(signal: NodeJS.Signals): number;
40
+ /** Spawns one child on the caller's channels, returning the child and a promise of how it ended. */
41
+ export declare function startSpawned({ args, file, input, shouldUseShell }: SpawnSpec, cwd: string | undefined, options: RunCommandOptions): StartedCommand;
42
+ /** How a command ended, and the exit code that reports that ending to nmr's own caller. */
43
+ type CommandCompletion = {
44
+ outcome: 'exited';
45
+ exitCode: number;
46
+ } | {
47
+ outcome: 'signaled';
48
+ exitCode: number;
49
+ signal: NodeJS.Signals;
50
+ } | {
51
+ outcome: 'spawn-failed';
52
+ exitCode: number;
53
+ error: Error;
54
+ };
55
+ export {};
@@ -0,0 +1,112 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import process from 'node:process';
4
+ import { reportError } from '@williamthorsen/nmr-core';
5
+ import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
6
+ const SIGNAL_EXIT_BASE = 128;
7
+ const PIPE_DRAIN_GRACE_MS = 2_000;
8
+ export function describeSignalExitCode(signal) {
9
+ return SIGNAL_EXIT_BASE + os.constants.signals[signal];
10
+ }
11
+ export function startSpawned({ args, file, input, shouldUseShell }, cwd, options) {
12
+ const quiet = options.quiet === true;
13
+ const stdout = options.stdout ?? process.stdout;
14
+ const stderr = options.stderr ?? process.stderr;
15
+ const env = options.env ?? process.env;
16
+ const child = spawn(file, args, {
17
+ shell: shouldUseShell,
18
+ stdio: [input, options.channels.stdout, options.channels.stderr],
19
+ cwd,
20
+ env,
21
+ });
22
+ return { child, result: collectResult(child, { quiet, stderr, stdout }) };
23
+ }
24
+ function awaitCompletion(child, stopReading) {
25
+ return new Promise((resolve) => {
26
+ let exitCompletion;
27
+ let graceTimer;
28
+ let isSettled = false;
29
+ function settle(completion) {
30
+ if (isSettled)
31
+ return;
32
+ isSettled = true;
33
+ if (graceTimer !== undefined)
34
+ clearTimeout(graceTimer);
35
+ resolve(completion);
36
+ }
37
+ child.on('error', (error) => {
38
+ settle({ outcome: 'spawn-failed', exitCode: 1, error });
39
+ });
40
+ child.on('exit', (code, signal) => {
41
+ const exitDescription = describeExit(code, signal);
42
+ exitCompletion = exitDescription;
43
+ graceTimer = setTimeout(() => {
44
+ stopReading();
45
+ settle(exitDescription);
46
+ }, PIPE_DRAIN_GRACE_MS);
47
+ });
48
+ child.on('close', () => {
49
+ settle(exitCompletion ?? { outcome: 'exited', exitCode: 1 });
50
+ });
51
+ });
52
+ }
53
+ function captureStream(source, destination) {
54
+ const retainedCopy = createBoundedBuffer();
55
+ let isComplete = true;
56
+ function stopReading() {
57
+ if (destination !== undefined)
58
+ source.unpipe(destination);
59
+ source.destroy();
60
+ }
61
+ function abandon() {
62
+ isComplete = false;
63
+ stopReading();
64
+ }
65
+ source.on('error', abandon);
66
+ if (destination !== undefined) {
67
+ destination.on('error', abandon);
68
+ source.pipe(destination, { end: false });
69
+ }
70
+ source.on('data', (chunk) => retainedCopy.append(chunk));
71
+ return {
72
+ readBuffer: () => (isComplete ? retainedCopy.readBuffer() : undefined),
73
+ detach: () => {
74
+ destination?.off('error', abandon);
75
+ },
76
+ stopReading,
77
+ };
78
+ }
79
+ async function collectResult(child, { quiet, stderr, stdout }) {
80
+ const stdoutCapture = child.stdout === null ? undefined : captureStream(child.stdout, quiet ? undefined : stdout);
81
+ const stderrCapture = child.stderr === null ? undefined : captureStream(child.stderr, quiet ? undefined : stderr);
82
+ const commandCompletion = await awaitCompletion(child, () => {
83
+ stdoutCapture?.stopReading();
84
+ stderrCapture?.stopReading();
85
+ });
86
+ stdoutCapture?.detach();
87
+ stderrCapture?.detach();
88
+ const result = {
89
+ ...commandCompletion,
90
+ stdout: stdoutCapture?.readBuffer(),
91
+ stderr: stderrCapture?.readBuffer(),
92
+ };
93
+ if (result.outcome === 'spawn-failed') {
94
+ reportError(result.error.message, stderr);
95
+ return result;
96
+ }
97
+ if (quiet && result.exitCode !== 0) {
98
+ writeBuffer(result.stdout, stderr);
99
+ writeBuffer(result.stderr, stderr);
100
+ }
101
+ return result;
102
+ }
103
+ function describeExit(code, signal) {
104
+ if (signal !== null) {
105
+ return { outcome: 'signaled', exitCode: describeSignalExitCode(signal), signal };
106
+ }
107
+ return { outcome: 'exited', exitCode: code ?? 1 };
108
+ }
109
+ function writeBuffer(buffer, destination) {
110
+ if (buffer !== undefined && buffer.length > 0)
111
+ destination.write(buffer);
112
+ }
@@ -37,19 +37,3 @@ export declare function buildPackage(packageDir: string, options: BuildPackageOp
37
37
  * detected.
38
38
  */
39
39
  export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, toolchain: BuildToolchain): Promise<string>;
40
- /**
41
- * Loads the compiler API on which every other function in this module relies, reporting a missing `typescript` peer
42
- * with the version that `nmr-compile` requires. `buildPackage` calls it first; a caller of another export calls it
43
- * beforehand.
44
- *
45
- * @internal
46
- */
47
- export declare function loadCompiler(loadTypeScript?: () => Promise<typeof TypeScript>): Promise<void>;
48
- /**
49
- * Resolves a package's full tsconfig `extends` chain, returning every config file in it (the leaf `tsconfig.json` and
50
- * each base that it transitively extends, up to the repo root) as paths relative to `packageDir`.
51
- * Emit is driven by the fully-resolved compiler options, so the base configs must be in the cache's hashed input
52
- * set; otherwise a change to a base config would not bust the cache and stale output could be released. The
53
- * relative paths give `computeBuildHash` a location-independent path string to fold into the digest.
54
- */
55
- export declare function resolveTsconfigChain(packageDir: string, configFileName?: string): string[];
@@ -2,20 +2,15 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { readFile, rename, rm } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { formatGlyphLine, hasErrnoCode, readCacheEntry, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
5
+ import { formatGlyphLine, readCacheEntry, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
6
  import { glob } from 'glob';
7
7
  import { NMR_GLYPHS } from "../glyphs.js";
8
8
  import { resolveConfigPath } from "../helpers/config-path.js";
9
9
  import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, resolveScratchDirs, resolveToolchainFingerprint, } from "./build-output.js";
10
+ import { assertSupportedTypeScript, formatDiagnostics, getCompiler, loadCompiler } from "./compiler.js";
11
+ import { isWithin, rewriteSpecifiers } from "./rewrite-specifiers.js";
12
+ import { resolveTsconfigChain } from "./tsconfig-chain.js";
10
13
  const SOURCE_ROOT = 'src';
11
- const MINIMUM_TYPESCRIPT_MAJOR = 5;
12
- const MINIMUM_TYPESCRIPT_MINOR = 7;
13
- const UNSUPPORTED_TYPESCRIPT_MAJOR = 7;
14
- const SUPPORTED_TYPESCRIPT_RANGE = `>=${MINIMUM_TYPESCRIPT_MAJOR}.${MINIMUM_TYPESCRIPT_MINOR} <${UNSUPPORTED_TYPESCRIPT_MAJOR}`;
15
- const INSTALL_TYPESCRIPT_GUIDANCE = "Install a version in that range as the 'typescript' peer dependency.";
16
- const TS_EXTENSIONS = ['.ts', '.tsx'];
17
- const JS_EXTENSION = '.js';
18
- let ts;
19
14
  export async function buildPackage(packageDir, options) {
20
15
  await loadCompiler(options.loadTypeScript);
21
16
  assertSupportedTypeScript();
@@ -34,7 +29,7 @@ export async function buildPackage(packageDir, options) {
34
29
  ...resolveTsconfigChain(packageDir),
35
30
  ];
36
31
  const toolchain = {
37
- compilerVersion: ts.version,
32
+ compilerVersion: getCompiler().version,
38
33
  fingerprint: await resolveToolchainFingerprint(packageDir),
39
34
  };
40
35
  const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints), options.style);
@@ -60,40 +55,8 @@ export async function computeBuildHash(packageDir, files, emitConfig, toolchain)
60
55
  hash.update(toolchain.fingerprint);
61
56
  return hash.digest('hex');
62
57
  }
63
- export async function loadCompiler(loadTypeScript = importTypeScript) {
64
- try {
65
- ts = await loadTypeScript();
66
- }
67
- catch (error) {
68
- if (hasErrnoCode(error, 'ERR_MODULE_NOT_FOUND') && error.message.includes("'typescript'")) {
69
- throw new Error(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but 'typescript' could not be resolved. ` +
70
- INSTALL_TYPESCRIPT_GUIDANCE, { cause: error });
71
- }
72
- throw error;
73
- }
74
- }
75
- export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json') {
76
- const resolvedChain = [];
77
- const seen = new Set();
78
- function walk(configPath) {
79
- const normalizedPath = path.resolve(configPath);
80
- if (seen.has(normalizedPath)) {
81
- return;
82
- }
83
- seen.add(normalizedPath);
84
- resolvedChain.push(normalizedPath);
85
- const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
86
- if (configFile.error) {
87
- throw new Error(`nmr-compile: failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
88
- }
89
- for (const entry of normalizeExtendsField(configFile.config)) {
90
- walk(resolveExtendsTarget(entry, normalizedPath));
91
- }
92
- }
93
- walk(path.resolve(packageDir, configFileName));
94
- return resolvedChain.map((absolute) => path.relative(packageDir, absolute));
95
- }
96
58
  async function emitPackage(packageDir, entryPoints, outdir) {
59
+ const ts = getCompiler();
97
60
  const compilerOptions = synthesizeCompilerOptions(packageDir, outdir);
98
61
  const rootNames = entryPoints.map((entry) => path.resolve(packageDir, entry));
99
62
  const sourceRoot = path.resolve(packageDir, SOURCE_ROOT);
@@ -127,6 +90,7 @@ function assertEmitSucceeded(result) {
127
90
  }
128
91
  }
129
92
  function createCachingCompilerHost(compilerOptions) {
93
+ const ts = getCompiler();
130
94
  const host = ts.createCompilerHost(compilerOptions);
131
95
  const parsedFiles = new Map();
132
96
  const readSourceFile = host.getSourceFile.bind(host);
@@ -179,6 +143,7 @@ async function swapIntoPlace(emitDir, scratchDirs) {
179
143
  await discardScratchDirs(scratchDirs);
180
144
  }
181
145
  function synthesizeCompilerOptions(packageDir, outdir) {
146
+ const ts = getCompiler();
182
147
  const configPath = path.join(packageDir, 'tsconfig.json');
183
148
  const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
184
149
  if (configFile.error) {
@@ -204,6 +169,7 @@ function synthesizeCompilerOptions(packageDir, outdir) {
204
169
  };
205
170
  }
206
171
  function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
172
+ const ts = getCompiler();
207
173
  for (const [fileName, file] of stagedFiles) {
208
174
  if (!isWithin(emitDir, fileName)) {
209
175
  throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
@@ -212,86 +178,6 @@ function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
212
178
  ts.sys.writeFile(path.join(stagingDir, path.relative(emitDir, fileName)), file.text, file.writeByteOrderMark);
213
179
  }
214
180
  }
215
- function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
216
- if (!isRewritableOutput(outputFile)) {
217
- return text;
218
- }
219
- const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, resolveScriptKind(outputFile));
220
- const aliasPrefixes = collectAliasPrefixes(compilerOptions);
221
- const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
222
- const edits = [];
223
- visitModuleSpecifiers(sourceFile, (literal) => {
224
- const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
225
- if (replacement === undefined) {
226
- return;
227
- }
228
- const startOffset = literal.getStart(sourceFile);
229
- const quote = text[startOffset] ?? '"';
230
- edits.push({ startOffset, endOffset: literal.getEnd(), text: `${quote}${replacement}${quote}` });
231
- });
232
- if (edits.length === 0) {
233
- return text;
234
- }
235
- const orderedEdits = edits.toSorted((a, b) => b.startOffset - a.startOffset);
236
- let updatedText = text;
237
- for (const edit of orderedEdits) {
238
- updatedText = updatedText.slice(0, edit.startOffset) + edit.text + updatedText.slice(edit.endOffset);
239
- }
240
- return updatedText;
241
- }
242
- function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
243
- if (isRelativeSpecifier(specifier)) {
244
- const rewrittenSpecifier = swapTypeScriptExtension(specifier);
245
- return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
246
- }
247
- if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
248
- return undefined;
249
- }
250
- const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
251
- if (!resolvedModule) {
252
- throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
253
- `Verify the tsconfig 'paths' mapping and that the target file exists.`);
254
- }
255
- if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
256
- const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
257
- const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
258
- if (bareResolvedModule) {
259
- return undefined;
260
- }
261
- throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
262
- `${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
263
- `the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
264
- `emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
265
- }
266
- const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
267
- return swapTypeScriptExtension(relativeSpecifier);
268
- }
269
- function visitModuleSpecifiers(sourceFile, visit) {
270
- function walk(node) {
271
- const specifier = getModuleSpecifier(node);
272
- if (specifier !== undefined) {
273
- visit(specifier);
274
- }
275
- ts.forEachChild(node, walk);
276
- }
277
- walk(sourceFile);
278
- }
279
- function getModuleSpecifier(node) {
280
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier !== undefined) {
281
- return ts.isStringLiteralLike(node.moduleSpecifier) ? node.moduleSpecifier : undefined;
282
- }
283
- if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
284
- return ts.isStringLiteralLike(node.moduleReference.expression) ? node.moduleReference.expression : undefined;
285
- }
286
- if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
287
- return ts.isStringLiteralLike(node.argument.literal) ? node.argument.literal : undefined;
288
- }
289
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
290
- const [first] = node.arguments;
291
- return first !== undefined && ts.isStringLiteralLike(first) ? first : undefined;
292
- }
293
- return undefined;
294
- }
295
181
  async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
296
182
  const packageName = path.basename(packageDir);
297
183
  const previousHash = await readCacheEntry(cachePath);
@@ -307,100 +193,9 @@ async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cach
307
193
  console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Changes detected.`));
308
194
  return { changed: true, currentHash };
309
195
  }
310
- async function importTypeScript() {
311
- return await import('typescript');
312
- }
313
- function assertSupportedTypeScript() {
314
- const versionMajorMinor = ts.versionMajorMinor;
315
- const version = ts.version;
316
- const [majorPart, minorPart] = typeof versionMajorMinor === 'string' ? versionMajorMinor.split('.', 2) : [];
317
- const major = majorPart === undefined ? 0 : Number(majorPart);
318
- const minor = minorPart === undefined ? 0 : Number(minorPart);
319
- const tooOld = major < MINIMUM_TYPESCRIPT_MAJOR || (major === MINIMUM_TYPESCRIPT_MAJOR && minor < MINIMUM_TYPESCRIPT_MINOR);
320
- if (tooOld || major >= UNSUPPORTED_TYPESCRIPT_MAJOR) {
321
- throw new Error(`nmr-compile requires TypeScript ${SUPPORTED_TYPESCRIPT_RANGE}, but found ` +
322
- `${typeof version === 'string' ? version : 'a version without the compiler API'}. ${INSTALL_TYPESCRIPT_GUIDANCE}`);
323
- }
324
- }
325
- function collectAliasPrefixes(compilerOptions) {
326
- if (compilerOptions.paths === undefined) {
327
- return [];
328
- }
329
- return Object.keys(compilerOptions.paths).map((pattern) => pattern.replace(/\*$/, ''));
330
- }
331
196
  function describeEmit(fileCount, outdir) {
332
197
  if (fileCount === 0) {
333
198
  return 'Did not emit any output.';
334
199
  }
335
200
  return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
336
201
  }
337
- function formatDiagnostics(diagnostics) {
338
- return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
339
- getCurrentDirectory: () => process.cwd(),
340
- getCanonicalFileName: (fileName) => fileName,
341
- getNewLine: () => ts.sys.newLine,
342
- });
343
- }
344
- function isRewritableOutput(file) {
345
- return file.endsWith('.d.ts') || file.endsWith('.js');
346
- }
347
- function normalizeExtendsField(config) {
348
- if (config === null || typeof config !== 'object' || !('extends' in config)) {
349
- return [];
350
- }
351
- const extendsField = config.extends;
352
- if (typeof extendsField === 'string') {
353
- return [extendsField];
354
- }
355
- if (Array.isArray(extendsField)) {
356
- return extendsField.filter((entry) => typeof entry === 'string');
357
- }
358
- return [];
359
- }
360
- function resolveExtendsTarget(extendsEntry, fromConfigPath) {
361
- if (isRelativeSpecifier(extendsEntry) || path.isAbsolute(extendsEntry)) {
362
- const base = path.resolve(path.dirname(fromConfigPath), extendsEntry);
363
- if (ts.sys.fileExists(base)) {
364
- return base;
365
- }
366
- const withJsonExtension = `${base}.json`;
367
- if (ts.sys.fileExists(withJsonExtension)) {
368
- return withJsonExtension;
369
- }
370
- throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
371
- }
372
- const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
373
- resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
374
- if (resolvedPath === undefined) {
375
- throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
376
- }
377
- return resolvedPath;
378
- }
379
- function resolvePackageSpecifier(specifier, fromConfigPath) {
380
- const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
381
- return resolvedModule?.resolvedFileName;
382
- }
383
- function isRelativeSpecifier(specifier) {
384
- return specifier.startsWith('./') || specifier.startsWith('../');
385
- }
386
- function isWithin(parent, child) {
387
- const relativePath = path.relative(parent, child);
388
- return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
389
- }
390
- function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
391
- const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
392
- const relativeFromOut = path.relative(outDir, outputFile);
393
- const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
394
- return path.join(sourceRoot, `${withoutExtension}.ts`);
395
- }
396
- function buildRelativeSpecifier(fromDir, targetFile) {
397
- const relativePath = path.relative(fromDir, targetFile).split(path.sep).join('/');
398
- return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
399
- }
400
- function resolveScriptKind(file) {
401
- return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
402
- }
403
- function swapTypeScriptExtension(specifier) {
404
- const extension = TS_EXTENSIONS.find((candidate) => specifier.endsWith(candidate));
405
- return extension === undefined ? specifier : `${specifier.slice(0, -extension.length)}${JS_EXTENSION}`;
406
- }
@@ -1,13 +1,14 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { formatGlyphLine } from '@williamthorsen/nmr-core';
4
- import { removeCheckCache } from "../check-cache.js";
4
+ import { removeCheckCache } from "../check-cache-entries.js";
5
5
  import { loadRootConfig } from "../config.js";
6
6
  import { findContainingPackageDir } from "../context.js";
7
7
  import { NMR_GLYPHS } from "../glyphs.js";
8
8
  import { reportClosing } from "../helpers/reportClosing.js";
9
+ import { resolveChannel } from "../output-channel.js";
9
10
  import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
10
- import { resolveChannel, runCommand } from "../runner.js";
11
+ import { runCommand } from "../runner.js";
11
12
  import { renderChain } from "../steps.js";
12
13
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
13
14
  import { resolveBuildCachePath } from "./build-output.js";
@@ -0,0 +1,18 @@
1
+ import type * as TypeScript from 'typescript';
2
+ /**
3
+ * Asserts the resolved `typescript` peer is new enough for `rewriteRelativeImportExtensions` and old enough to
4
+ * include the compiler API.
5
+ */
6
+ export declare function assertSupportedTypeScript(): void;
7
+ /** Renders compiler diagnostics as colored text with source context. */
8
+ export declare function formatDiagnostics(diagnostics: readonly TypeScript.Diagnostic[]): string;
9
+ /** Returns the compiler API that `loadCompiler` loaded, throwing when it has not run. */
10
+ export declare function getCompiler(): typeof TypeScript;
11
+ /**
12
+ * Loads the compiler API on which every `nmr-compile` module relies, reporting a missing `typescript` peer with the
13
+ * version that `nmr-compile` requires. `buildPackage` calls it first; a caller of another compiler-dependent export
14
+ * calls it beforehand.
15
+ *
16
+ * @internal
17
+ */
18
+ export declare function loadCompiler(loadTypeScript?: () => Promise<typeof TypeScript>): Promise<void>;