UNPKG

ttsc

Version:

General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.

781 lines 33.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PendingResidentCheckWatchChanges = void 0; exports.runTtsc = runTtsc; const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importDefault(require("node:path")); const TtscCompiler_1 = require("../../TtscCompiler"); const readProjectConfig_1 = require("../../compiler/internal/project/readProjectConfig"); const resolveProjectConfig_1 = require("../../compiler/internal/project/resolveProjectConfig"); const runBuild_1 = require("../../compiler/internal/runBuild"); const runSingleFileEmit_1 = require("../../compiler/internal/runSingleFileEmit"); const parser_1 = require("../../flags/parser"); const schema_1 = require("../../flags/schema"); const assertSafeExplicitCacheDirectory_1 = require("../../internal/assertSafeExplicitCacheDirectory"); const buildSourcePlugin_1 = require("../../plugin/internal/buildSourcePlugin"); const getCompilerVersionText_1 = require("./getCompilerVersionText"); const resolveCacheDir_1 = require("./resolveCacheDir"); const singleFileOutput_1 = require("./singleFileOutput"); const watchTopology_1 = require("./watchTopology"); /** * CLI entry point for `ttsc`. Dispatches argv to the appropriate build lane * (build, check, fix, format, prepare, clean, or native-delegate) and returns * an exit code. Errors thrown by any lane are caught here and written to stderr * so the process can exit cleanly. * * @param argv - Command-line arguments (defaults to `process.argv.slice(2)`). * @returns The exit code: `0` on success, `1` on binary-not-found, `2` on user * error or build failure. */ function runTtsc(argv = process.argv.slice(2)) { try { if (argv.length === 0) { return runCompatibleBuild([], "build"); } const [command, ...rest] = argv; // A leading flag is dispatched by its schema identity, so every spelling the // compiler accepts (`--HELP`, `-Version`) reaches the same branch instead of // falling through to the build lane where nothing reads it back out. // `resolveFlagSpec` ignores dash-less tokens, so the bare `help` / `version` // subcommand words below keep their own meaning. const leadingFlag = (0, schema_1.resolveFlagSpec)(command)?.name; switch (leadingFlag ?? command) { case "--help": case "help": printHelp(); return 0; case "--version": case "version": process.stdout.write(`${(0, getCompilerVersionText_1.getCompilerVersionText)()}\n`); return 0; case "build": return runCompatibleBuild(rest, "build"); case "check": return runCompatibleBuild(rest, "check"); case "fix": return runCompatibleBuild(rest, "fix"); case "format": return runCompatibleBuild(rest, "format"); case "cache": return runCache(rest); case "clean": return runClean(rest); case "prepare": return runPrepare(rest); case "--tsconfig": return runCompatibleBuild(argv, "build"); default: if (isBuildAlias(command)) { return runCompatibleBuild(argv, "build"); } process.stderr.write(`ttsc: unknown command ${JSON.stringify(command)}\n`); process.stderr.write(`ttsc: run "ttsc --help" to see supported commands\n`); return 2; } } catch (error) { process.stderr.write(`${formatError(error)}\n`); return 2; } } /** * Return `true` when `command` looks like a build argument rather than a * subcommand name — a flag (`-p`, `--watch`, …) or a TypeScript/config file * path. These are forwarded to the build lane unchanged so users can write * `ttsc -p tsconfig.json` without an explicit `build` subcommand. */ function isBuildAlias(command) { if (command.startsWith("-")) return true; return [".json", ".ts", ".tsx", ".mts", ".cts"].some((ext) => command.endsWith(ext)); } function runCompatibleBuild(argv, mode) { const checkOnly = mode !== "build"; const options = normalizeBuildOptions(parseBuildArgs(argv)); if (mode === "fix") { if (options.emit === true) { throw new Error("ttsc: fix and --emit are mutually exclusive"); } options.fix = true; options.emit = false; } if (mode === "format") { if (options.emit === true) { throw new Error("ttsc: format and --emit are mutually exclusive"); } options.format = true; options.emit = false; } if (options.watch) { if (mode === "fix") { throw new Error("ttsc: fix does not support watch mode; use ttsc --noEmit --watch for incremental checks"); } if (mode === "format") { throw new Error("ttsc: format does not support watch mode; use ttsc --noEmit --watch for incremental checks"); } return runWatch(options, checkOnly); } const buildOptions = checkOnly ? { ...options, emit: false } : options; if (buildOptions.files.length !== 0) { if (mode === "fix") { throw new Error("ttsc: fix requires a project, not single-file mode"); } if (mode === "format") { throw new Error("ttsc: format requires a project, not single-file mode"); } return runSingleFile(buildOptions); } const result = (0, runBuild_1.runBuild)(buildOptions); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); return result.status; } function normalizeBuildOptions(options) { const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); return { ...options, cacheDir: (0, resolveCacheDir_1.resolveCacheDir)(cwd, options.cacheDir), cwd, }; } function runPrepare(argv) { const options = parseProjectArgs(argv); const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); const compiler = new TtscCompiler_1.TtscCompiler({ cacheDir: options.cacheDir, cwd, tsconfig: options.tsconfig, }); const prepared = compiler.prepare(); if (prepared.length === 0) { const projectRoot = node_path_1.default.dirname((0, resolveProjectConfig_1.resolveProjectConfig)({ cwd, tsconfig: options.tsconfig, })); process.stdout.write(`ttsc: no source plugins found under ${formatProjectPath(cwd, projectRoot)}\n`); return 0; } for (const target of prepared) { process.stdout.write(`ttsc: prepared ${formatProjectPath(cwd, target)}\n`); } return 0; } function runClean(argv) { const options = parseProjectArgs(argv); const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); const projectRoot = resolveCleanProjectRoot(cwd, options.tsconfig); const explicitCacheDir = options.cacheDir ? node_path_1.default.resolve(cwd, options.cacheDir) : undefined; if (explicitCacheDir !== undefined) { (0, assertSafeExplicitCacheDirectory_1.assertSafeExplicitCacheDirectory)(projectRoot, explicitCacheDir); } const targets = explicitCacheDir ? // Explicit `ttsc clean --cache-dir X`: the user names X as the cache to // remove for this command, so remove it wholesale plus the legacy // project-local caches. [ explicitCacheDir, node_path_1.default.join(projectRoot, "node_modules", ".ttsc"), node_path_1.default.join(projectRoot, ".ttsc"), ] : // Default / TTSC_CACHE_DIR: remove only ttsc-owned subdirectories (a // possibly-shared root is never deleted) plus the pre-0.17 machine-global // cache so upgraders reclaim that disk. [...(0, buildSourcePlugin_1.resolveCleanTargets)(projectRoot), ...(0, buildSourcePlugin_1.legacyGlobalCacheTargets)()]; const removed = []; for (const target of targets) { if (!node_fs_1.default.existsSync(target)) continue; node_fs_1.default.rmSync(target, { recursive: true, force: true }); removed.push(target); } if (removed.length === 0) { process.stdout.write(`ttsc: no cache directories found under ${projectRoot}\n`); return 0; } for (const target of removed) { process.stdout.write(`ttsc: removed ${formatProjectPath(cwd, target)}\n`); } return 0; } function runCache(argv) { const [command, ...rest] = argv; switch (command) { case "paths": return runCachePaths(rest); case "-h": case "--help": case "help": case undefined: printCacheHelp(); return 0; default: process.stderr.write(`ttsc: unknown cache command ${JSON.stringify(command)}\n`); process.stderr.write(`ttsc: run "ttsc cache --help" to see supported cache commands\n`); return 2; } } function runCachePaths(argv) { const options = parseCachePathsArgs(argv); const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); const cacheDir = (0, resolveCacheDir_1.resolveCacheDir)(cwd, options.cacheDir); const projectRoot = resolveCleanProjectRoot(cwd, options.tsconfig); const paths = (0, buildSourcePlugin_1.resolveSourceBuildCachePaths)(projectRoot, cacheDir); // The minimal directory set a CI cache step must persist. The Go build cache // is folded in only when it lives outside the ttsc cache root (an explicit // GOCACHE / TTSC_GO_CACHE_DIR); by default it nests under `root`. const cacheableRoots = [ paths.root, ...((0, buildSourcePlugin_1.isPathWithin)(paths.goBuildRoot, paths.root) ? [] : [paths.goBuildRoot]), ]; if (options.json) { process.stdout.write(`${JSON.stringify({ cacheRoot: paths.root, cacheableRoots, cwd, goBuildCacheRoot: paths.goBuildRoot, goBuildCacheSource: paths.goBuildRootSource, pluginCacheRoot: paths.pluginRoot, projectRoot, }, null, 2)}\n`); return 0; } process.stdout.write([ "ttsc source-plugin cache paths:", ` cache root ${paths.root}`, ` plugin binaries ${paths.pluginRoot}`, ` go build cache ${paths.goBuildRoot} (${paths.goBuildRootSource})`, "", "Persist these between CI jobs to skip cold plugin rebuilds:", ...cacheableRoots.map((root) => ` ${root}`), "", ].join("\n")); return 0; } function parseCachePathsArgs(argv) { const out = { json: false }; const rest = [...argv]; while (rest.length !== 0) { const token = rest.shift(); const [rawFlag, inlineValue] = splitInlineFlag(token); const flag = (0, schema_1.resolveFlagSpec)(rawFlag); if (flag?.subcommands.includes("cache") !== true) { throw new Error(`ttsc: cache paths does not support ${JSON.stringify(token)}`); } switch (flag.name) { case "--json": if (inlineValue !== undefined) { throw new Error("ttsc: --json does not take a value"); } out.json = true; break; case "--cache-dir": out.cacheDir = readCachePathsValue(flag.name, inlineValue, rest); break; case "--cwd": out.cwd = readCachePathsValue(flag.name, inlineValue, rest); break; case "--tsconfig": out.tsconfig = readCachePathsValue(flag.name, inlineValue, rest); break; default: throw new Error(`ttsc: cache paths does not support ${JSON.stringify(token)}`); } } return out; } function splitInlineFlag(token) { if (!token.startsWith("-")) { return [token, undefined]; } const equals = token.indexOf("="); return equals === -1 ? [token, undefined] : [token.slice(0, equals), token.slice(equals + 1)]; } function readCachePathsValue(flag, inlineValue, rest) { if (inlineValue !== undefined) { return inlineValue; } const value = rest.shift(); if (value === undefined) { throw new Error(`ttsc: ${flag} requires a value`); } if (value.startsWith("-")) { rest.unshift(value); throw new Error(`ttsc: ${flag} requires a value (next token ${JSON.stringify(value)} starts with "-")`); } return value; } function resolveCleanProjectRoot(cwd, tsconfig) { try { return node_path_1.default.dirname((0, resolveProjectConfig_1.resolveProjectConfig)({ cwd, tsconfig })); } catch (error) { if (tsconfig) throw error; return cwd; } } function formatProjectPath(cwd, target) { const relative = node_path_1.default.relative(cwd, target); if (!relative || isOutsideRelativePath(relative)) { return target; } return relative; } function isOutsideRelativePath(relative) { return (relative === ".." || relative.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relative)); } function parseProjectArgs(argv) { // `prepare` and `clean` are project-shaped commands. They share the same // schema as the build lane; the engine forwards unknown flags as well as // build-only flags (e.g. `--strict`) to the launcher's passthrough list // so the legacy "unknown option" behaviour is no longer a separate trap // (RC-3 + RC-4 prevention; see issue #125 §5 in the RCA). const result = (0, parser_1.parseFlags)({ argv, errorPrefix: "ttsc:", subcommand: "prepare", }); return { cacheDir: (0, parser_1.getString)(result, "--cache-dir"), cwd: (0, parser_1.getString)(result, "--cwd"), tsconfig: (0, parser_1.getString)(result, "--tsconfig"), }; } function parseBuildArgs(argv) { const result = (0, parser_1.parseFlags)({ argv, errorPrefix: "ttsc:", // A bare token is a single-file input only when it carries a TypeScript // source extension; any other bare token is the space-separated value of a // preceding forwarded flag (e.g. the `es2020` in `--target es2020`). The // parser routes those values into `passthrough` in place, so the forwarded // flag/value pairs reach tsgo in their original order. isPositional: looksLikeInputFile, subcommand: "build", }); // Defaults: pinned by the previous hand-parser. `quiet` defaults true, // `--verbose` flips it to false; `emit` defaults `undefined` so the resolved // project controls ordinary build mode. `runCompatibleBuild` applies the // check/fix/format no-emit decision before either execution lane runs. const verbose = (0, parser_1.getBoolean)(result, "--verbose"); const quietFlag = (0, parser_1.getBoolean)(result, "--quiet"); const quiet = verbose === true ? false : (quietFlag ?? true); const explicitEmit = (0, parser_1.getBoolean)(result, "--emit"); const explicitNoEmit = (0, parser_1.getBoolean)(result, "--noEmit"); const emit = resolveExplicitEmit(explicitEmit, explicitNoEmit); // `isPositional: looksLikeInputFile` guarantees every `result.positional` // token is a TypeScript input file; forwarded flag values already live in // `result.passthrough` in their original order, so no reconstruction is // needed here (the previous `[...passthrough, ...trailingValues]` concat // reordered every flag ahead of every value). const files = [...result.positional]; const passthrough = [...result.passthrough]; return { binary: (0, parser_1.getString)(result, "--binary"), cacheDir: (0, parser_1.getString)(result, "--cache-dir"), checkers: (0, parser_1.getNumber)(result, "--checkers"), cwd: (0, parser_1.getString)(result, "--cwd"), emit, files, fix: false, format: false, outDir: (0, parser_1.getString)(result, "--outDir"), passthrough, preserveWatchOutput: (0, parser_1.getBoolean)(result, "--preserveWatchOutput") === true, quiet, singleThreaded: (0, parser_1.getBoolean)(result, "--singleThreaded") === true, tsconfig: (0, parser_1.getString)(result, "--tsconfig"), watch: (0, parser_1.getBoolean)(result, "--watch") === true, }; } /** * Collapse the two launcher-owned emit switches into the tri-state consumed by * `runBuild` and the single-file lane. A specified boolean is significant even * when it is `false`: `--emit=false` is analysis-only and `--noEmit=false` * explicitly overrides a project's `noEmit`. `--emit` retains precedence when * callers supply both switches, matching the legacy true-only resolution. */ function resolveExplicitEmit(explicitEmit, explicitNoEmit) { if (explicitEmit !== undefined) return explicitEmit; return explicitNoEmit === undefined ? undefined : !explicitNoEmit; } /** * Report whether a bare CLI token is a TypeScript source file ttsc should * compile in single-file mode. Anything without a TypeScript source extension * is treated as a forwarded flag value rather than an input file. */ function looksLikeInputFile(token) { return [".ts", ".tsx", ".mts", ".cts"].some((ext) => token.endsWith(ext)); } function printHelp() { process.stdout.write([ "ttsc — standalone compiler adapter and plugin host for tsgo.", "", "Usage:", " ttsc", " ttsc -p tsconfig.json", " ttsc --watch", " ttsc --noEmit", " ttsc fix", " ttsc format", " ttsc cache paths --json [options]", " ttsc prepare [options]", " ttsc clean [options]", " ttsc version", " ttsc --help", "", "Options:", " -p, --project <file> Resolve project settings from this tsconfig", " --tsconfig <file> Resolve project settings from this tsconfig", " --cwd <dir> Resolve project-relative paths from this directory", " --emit Force emitted files during build", " --noEmit Force analysis-only build with no file writes", " -w, --watch Rebuild when project files change", " --preserveWatchOutput Do not clear the screen between watch rebuilds", " --outDir <dir> Override compilerOptions.outDir for this invocation", " --quiet Keep build output quiet (default)", " --verbose Print the build summary and emitted files", " --binary <path> Use an explicit tsgo binary", " --cache-dir <dir> Use this cache root for source-plugin builds", " --singleThreaded Run TypeScript-Go single-threaded (one checker)", " --checkers <n> Type-checker pool size (default: TypeScript-Go's)", "", " Any other flag is forwarded to tsgo as-is, so tsgo compiler options", " such as --strict or --target work directly (e.g. ttsc --strict file.ts).", "", "Plugin contract:", " ttsc reads compilerOptions.plugins from tsconfig.json.", " Plugin modules are descriptors for ordered native transformer backends.", " JS transformOutput/transformSource functions are not part of the public contract.", "", "Subcommands:", " ttsc build [options] Same project build lane as `ttsc [options]`.", " ttsc check [options] Same as `ttsc --noEmit [options]`.", " ttsc fix [options] Apply check-plugin lint + format edits, then run `ttsc check`.", " ttsc format [options] Apply check-plugin format-class edits only (write-only, no type check).", " ttsc cache paths --json Print source-plugin and Go build cache paths for CI.", " ttsc prepare [options] Build configured source-plugin binaries into cache.", " ttsc clean [options] Delete source-plugin cache directories.", ].join("\n")); process.stdout.write("\n"); } function printCacheHelp() { process.stdout.write([ "ttsc cache commands.", "", "Usage:", " ttsc cache paths --json [options]", "", "Options:", " --json Print paths as JSON", " -p, --project <file> Resolve project settings from this tsconfig", " --tsconfig <file> Resolve project settings from this tsconfig", " --cwd <dir> Resolve project-relative paths from this directory", " --cache-dir <dir> Use this cache root for source-plugin builds", ].join("\n")); process.stdout.write("\n"); } function runSingleFile(options) { if (options.files.length !== 1) { throw new Error("ttsc: single-file mode currently accepts exactly one input file"); } const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); const file = node_path_1.default.resolve(cwd, options.files[0]); const emit = singleFileShouldEmit(options, cwd, file); const out = emit ? (0, singleFileOutput_1.resolveSingleFileOutput)({ cliOutDir: options.outDir, cwd, file, passthrough: options.passthrough, tsconfig: options.tsconfig, }) : undefined; const text = (0, runSingleFileEmit_1.runSingleFileEmit)({ binary: options.binary, checkers: options.checkers, cwd, file, onProjectInputs: options.onProjectInputs, onWatchInputs: options.onWatchInputs, out, passthrough: options.passthrough, singleThreaded: options.singleThreaded, tsconfig: options.tsconfig, }); if (out !== undefined && !node_fs_1.default.existsSync(out)) { node_fs_1.default.mkdirSync(node_path_1.default.dirname(out), { recursive: true }); node_fs_1.default.writeFileSync(out, text, "utf8"); } if (out !== undefined) { process.stdout.write(`${node_path_1.default.relative(cwd, out) || node_path_1.default.basename(out)}\n`); } return 0; } /** * Resolve the same effective emit decision used by the project lane before * entering the single-file compatibility path. The compatibility path still * emits into a private temporary directory to obtain transformed text and * diagnostics, but only this boundary is allowed to write into the user's * tree. */ function singleFileShouldEmit(options, cwd, file) { if (options.emit !== undefined) return options.emit; const project = (0, readProjectConfig_1.readProjectConfig)({ cwd, file, tsconfig: options.tsconfig, }); return project.compilerOptions.noEmit !== true; } function runWatch(options, checkOnly) { const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd()); let topology; const invocation = { ...(checkOnly ? { ...options, emit: false } : options), cwd, onWatchInputs: (inputs) => { topology?.setExtraInputs(inputs); }, onProjectInputs: (inputs) => { topology?.setProjectInputs(inputs); }, quiet: true, }; const root = node_path_1.default.dirname((0, resolveProjectConfig_1.resolveProjectConfig)({ cwd, tsconfig: options.tsconfig, })); let running = false; let rerun = false; let timer = null; const resident = invocation.files.length === 0 ? new runBuild_1.ResidentCheckWatchSession() : undefined; const pendingChanges = new PendingResidentCheckWatchChanges(); // Tracks the most recent build's exit code so the watch session can exit // non-zero when its latest rebuild failed, instead of always reporting 0. let lastStatus = 0; const runOnce = async () => { running = true; const change = pendingChanges.take(); let completed = false; try { if (!options.preserveWatchOutput) { process.stdout.write("\x1bc"); } process.stdout.write(`[ttsc] rebuilding at ${new Date().toLocaleTimeString()}\n`); // The debounced rebuild fires from setTimeout, outside runTtsc's // top-level try/catch. runBuild/runSingleFile/loadProjectPlugins/ // readProjectConfig can throw (plugin go-build failure, tsconfig edited // to invalid JSON, single-file invariant). Catch here so a throwing // rebuild reports a clear failure and the watcher keeps running instead // of crashing the process with an uncaught exception. const status = await (invocation.files.length !== 0 ? Promise.resolve(runSingleFile(invocation)) : (async () => { const buildOptions = checkOnly ? { ...invocation, emit: false } : invocation; const result = resident === undefined ? (0, runBuild_1.runBuild)(buildOptions) : await resident.run(buildOptions, change); // Watch output is one ordered stream. The diagnostics a build // produced and the "[ttsc] watch build …" marker that concludes // them reach a single merged reader, and on two pipes the OS may // hand it the marker first — a reader that counts the build on the // marker then snapshots before the diagnostics land and sees an // empty build. The rebuilding banner, the screen clear, and the // marker are already on stdout, so the diagnostics join them there, // the way tsc --watch prints everything to one place. if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stdout.write(result.stderr); return result.status; })()); lastStatus = status; completed = true; process.stdout.write(`[ttsc] ${status === 0 ? "watch build complete" : "watch build failed"}\n`); } catch (error) { // Same ordered-stream rule as the build path: the failure text goes on // the stream the marker below uses, so a merged reader sees them in order // rather than racing two pipes. process.stdout.write(`${formatError(error)}\n`); lastStatus = lastStatus === 0 ? 2 : lastStatus; process.stdout.write(`[ttsc] watch build failed\n`); } finally { running = false; if (completed) { try { // A filesystem event can arrive after the build reports completion // but before this synchronous cleanup reaches its re-resolution. // Notify for a membership change here as well, otherwise that new // input is silently absorbed into the watch set without a rebuild. topology?.refresh(true); } catch (error) { process.stderr.write(`[ttsc] watch error on ${node_path_1.default.relative(cwd, root) || "."}: ${formatError(error)}\n`); } } } if (rerun) { rerun = false; trigger(); } }; const trigger = (change, reload = false) => { pendingChanges.push(change, reload); if (running) { rerun = true; return; } if (timer) clearTimeout(timer); timer = setTimeout(() => void runOnce(), 60); }; topology = new watchTopology_1.WatchTopology(invocation, { onError: (location, error) => { process.stderr.write(`[ttsc] watch error on ${node_path_1.default.relative(cwd, location) || "."}: ${formatError(error)}\n`); }, onProjectInputWatchUnavailable: (roots) => { for (const root of roots) { process.stderr.write(`[ttsc] project-input watch unavailable on ${node_path_1.default.relative(cwd, root) || "."}; changes under this root are not being observed\n`); } }, onInputChange: (change) => { debugWatchInputs(`change ${change.kind}${change.invalidate === true ? " invalidate" : ""} ${change.path === undefined ? "(unnamed)" : node_path_1.default.relative(cwd, change.path)}`); trigger(change); }, onProjectInputWatchRoots: (roots) => { debugWatchInputs(`roots ${JSON.stringify(roots.map((root) => node_path_1.default.relative(cwd, root) || "."))}`); }, onTopologyChange: () => trigger(undefined, true), }); topology.refresh(false); const close = () => { if (timer) clearTimeout(timer); topology?.close(); resident?.dispose(); }; process.on("SIGINT", () => { close(); process.exit(toExitCode(lastStatus)); }); process.on("SIGTERM", () => { close(); process.exit(toExitCode(lastStatus)); }); process.stdout.write(`[ttsc] watching ${node_path_1.default.relative(cwd, root) || "."}\n`); try { void runOnce(); } catch (error) { // runOnce already swallows build throws, but guard against any unforeseen // throw escaping the first pass: tear down the persistent watchers so the // event loop drains and the process exits cleanly with a non-zero code // instead of hanging on live fs.watch handles. close(); process.stderr.write(`${formatError(error)}\n`); return toExitCode(lastStatus === 0 ? 2 : lastStatus); } return toExitCode(lastStatus); } /** * Coalesces filesystem events until the next resident check-watch cycle. * * A full reload dominates every narrower signal. Program invalidation remains * distinct so a project-input module creation/deletion can cold-load the * Program without discarding the selected execution or restarting the sidecar. */ class PendingResidentCheckWatchChanges { changed = new Set(); external = new Set(); invalidate = false; reload = false; push(change, reload = false) { if (reload || change?.kind === "config" || change?.kind === "plugin") { this.reload = true; this.invalidate = false; this.changed.clear(); this.external.clear(); return; } if (this.reload) return; if (change?.invalidate === true) this.invalidate = true; if (change?.path === undefined) { if (change?.kind === "compiler") { this.reload = true; this.invalidate = false; this.changed.clear(); this.external.clear(); } return; } this.changed.add(change.path); if (change.kind === "project") this.external.add(change.path); } take() { const change = { ...(this.reload ? { reload: true } : {}), ...(this.invalidate ? { invalidate: true } : {}), ...(this.changed.size === 0 ? {} : { changed: [...this.changed].sort() }), ...(this.external.size === 0 ? {} : { external: [...this.external].sort() }), }; this.reload = false; this.invalidate = false; this.changed.clear(); this.external.clear(); return change; } } exports.PendingResidentCheckWatchChanges = PendingResidentCheckWatchChanges; // Coerces a build status into a valid process exit code: 0 stays 0, any // non-zero (or non-finite) status collapses to 1 so the session signals failure. function toExitCode(status) { return Number.isInteger(status) && status >= 0 && status <= 255 ? status : 1; } /** * Opt-in report of what the project-input layer is watching and announcing. * * A watch that goes quiet is indistinguishable from a watch with nothing to * say: the build simply never runs again, and the transcript that survives says * only that. This separates the two — the roots line states what is covered, * and a change line states what was announced — so a missing rebuild can be * attributed to the watch that never saw the file or to the decision that * declined to report it. Silent unless asked, and on the same ordered stream as * the rest of the watch output so it interleaves with the builds it explains. */ function debugWatchInputs(message) { if (!process.env.TTSC_WATCH_DEBUG_INPUTS) return; process.stdout.write(`[ttsc:debug] ${message}\n`); } function formatError(error) { if (error instanceof Error) { return error.message; } return String(error); } //# sourceMappingURL=runTtsc.js.map