import fs from "node:fs"; import type { ITtscCompilerTransformation } from "ttsc"; import { type FilesystemPathIdentityContext, type FilesystemPathIdentityOperations } from "ttsc/path-identity"; import type { TransformResult } from "unplugin"; import type { ResolvedTtscUnpluginOptions } from "./options.mjs"; import { type ITtscProjectMembershipPolicy } from "./tsconfigPaths.mjs"; /** * The normalised transform result type that this module produces. * * Excludes the shorthand `string`, `null`, and `undefined` variants of * unplugin's `TransformResult` so callers always receive an object or * `undefined`. */ export type TtscTransformResult = Exclude; /** One directory's project-membership identity at generation time. */ interface TtscProjectDirectorySnapshot { /** Absolute directory spelling used by the project walk. */ path: string; /** * Whether this directory's subtree can hold a program input. * * A directory that cannot is still walked and still watched, so a source * appearing in it later is noticed, but it takes no part in the membership * comparison. That is what lets a bundler create its output directory and * fill it without voiding a generation no compiler input touched, for any * output directory rather than for fifteen names (samchon/ttsc#1307). */ relevant: boolean; /** * Digest of the entries the walk itself considers: every immediate child the * ignore list does not drop, with its kind. * * Deliberately not the directory's own metadata. A directory's stamp moves * whenever _any_ entry is added or removed, including the ones the walk * exists to ignore, so a bundler emitting into `dist/` — or merely creating * that directory for the first time — moved the project root's stamp and * voided a generation that no compiler input had touched. The ignore list * only protects the generation if the membership proof honours it too. */ signature: string; } /** Generation-scoped directory watchers used to detect membership changes. */ interface TtscProjectMutationTracker { /** Absolute paths named by generation-time mutation events. */ changes: Set; /** Whether additional event paths were discarded after the witness bound. */ changesOmitted: boolean; close: () => void; /** * Absolute spellings whose creation, change or removal this tracker would * report, when it watches exact names rather than whole directories. * * A validation that finds an input here needs no filesystem call of its own: * the tracker is the evidence, and every path that leaves this set falls back * to being proven by hand. Empty for a tracker that watches directories as a * whole, which cannot answer for one name. */ covered?: ReadonlySet; /** Whether this is the repository-owned backend with content-event coverage. */ contentAuthoritative?: boolean; /** * Wait until every event this tracker's watcher has already dispatched has * been applied to it. * * An in-process watcher drains on the next macrotask turn, because its * callbacks are already queued on this loop. A watcher living in the Windows * broker drains by round-trip instead: the child replies after its own turn, * and IPC preserves order, so the reply cannot overtake an event the child * had already sent (samchon/ttsc#1272). */ drain?: () => Promise; failed: boolean; membershipChanged: boolean; /** Compare event and input paths through this tracker's filesystem identity. */ overlaps?: (input: string, changed: string) => boolean; settle?: Promise; } /** * A single entry in the project transform cache. * * Stores the full compiler result together with SHA-256 hashes of every project * input file. In a cache with an explicit build lifecycle, the first delivery * of each compiled module compares its supplied source with the generation * snapshot in constant time. Later graph-bearing deliveries validate only the * requested file's derived inputs plus exact host descriptor/config inputs; * graph-free envelopes retain complete-snapshot validation. */ export interface TtscCachedProjectTransform { /** Predicate-preserving compiler proofs for external candidate spellings. */ externalInputObservations?: Record; /** * SHA-256 hash of every input the compiler reported outside the project walk * (keyed by filesystem identity), captured at the time of the transform. * * The project walk cannot see files outside the project root or under ignored * directories (`node_modules` declarations, monorepo sibling sources, * out-of-root tsconfig `extends` ancestry), yet the host-owned reference * graph proves they are transform inputs. Long-lived hosts that never clear * the cache between builds (Metro workers and the Turbopack loader) would * otherwise replay a project transform computed against a stale out-of-walk * input for the whole process lifetime; per-build hosts clear the cache on * `buildStart` and never replay across edits. */ externalInputHashes?: Record; /** * Compiler-time physical identities for graph-owned entries in * {@link externalInputHashes}. Dependency-only paths have no generation * realpath protocol and therefore omit this evidence. */ externalInputRealpaths?: Record; /** * Original absolute spellings of {@link externalInputHashes} inputs. These * stay separate from their identity keys so validation reads the paths the * compiler reported rather than a normalized replacement spelling. */ externalInputPaths?: string[]; /** * Metadata signature of each out-of-walk input, captured around the read that * proved its {@link externalInputHashes} entry and recorded only once the * observed filesystem's clock provably left the stamp's tick * ({@link stampSeparable}). An input whose signature still holds carries the * recorded content, so revalidation may skip the read. * * Keyed by lexical spelling rather than by physical identity, for the reason * {@link TtscHostInputValidation} states: a symlink or junction spelling and * its selected target deliberately share one identity but have different * metadata, so an identity key would let the two overwrite each other's * signature and force both to be re-read on every delivery. */ externalInputSignatures?: Record; /** * SHA-256 hash of each project-relative input path at the time of the * transform. */ inputHashes: Record; /** * What the resolved configuration admitted into this generation's program. * * Recorded per generation rather than read per validation because it is a * property of the configuration the compile ran under, so a later delivery * must judge membership by the same rule the compile did. A tsconfig edit * that changes the rule also changes a declared input, which replaces the * generation and its policy together. */ membershipPolicy: ITtscProjectMembershipPolicy; /** * Files already reported as absent from the program, and the pass that * reporting belongs to, so the notice is one per file per pass rather than * one per delivery. */ missingOutputReported?: Set; missingOutputEpoch?: number; /** * The project config this generation compiled, so a module the program does * not contain can be told which program that was. */ tsconfig: string; /** * Metadata signature of each {@link inputHashes} entry whose hash was proven * against an unracing read of the file on disk, in a tick the observed * filesystem's clock had provably left ({@link stampSeparable}). * * The generation's own current file is absent at capture: its recorded hash * comes from the bundler's in-memory source, so the walk that produced it * compared nothing. A later delivery of a sibling does compare that file's * disk bytes against the recorded hash, and may record a signature then. */ inputSignatures?: Record; /** * Raw source hash of every readable key in the transform output, keyed by * filesystem identity. Unlike {@link inputHashes}, this includes source * outputs outside the project walk without adding arbitrary output keys to * the complete project snapshot. */ sourceHashes?: Record; /** Metadata snapshot of every directory in the stable generation walk. */ projectDirectories?: TtscProjectDirectorySnapshot[]; /** Live notification state for universal host-input changes. */ hostInputMutationTracker?: TtscProjectMutationTracker; /** * Live notification state for the generation's absent resolution candidates * and the directories that carry them. * * Separate from the universal-input tracker because it listens for a * different thing. Every event that can make an absent candidate present is a * rename — the file appearing, a component of the path being created, * replaced, or retargeted — so a change event on one of these names is never * evidence this tracker exists to collect. What it is, on a backend that * reports a write below a directory as a change to that directory's own entry * (Windows does), is a dev server's steady traffic: listening for every event * would replace the generation each time a bundler wrote inside * `node_modules`. The filter therefore drops noise without dropping proof. * The one appearance it cannot see is a Windows junction retargeted in place * through `FSCTL_SET_REPARSE_POINT`, which no mainstream tool does; every * package manager replaces the entry instead, which is a rename. */ candidateMutationTracker?: TtscProjectMutationTracker; /** * Universal descriptor/config inputs proven once at generation time, then by * metadata. * * Recorded state of the generation, like the input hashes and the directory * snapshot beside it, rather than state derived from the envelope: an entry * carries the manifest that proved it, so nothing can present one * generation's recorded inputs under another envelope's proof. */ hostInputValidation?: TtscHostInputValidation; /** Live notification state for file/directory creation, deletion, and rename. */ projectMutationTracker?: TtscProjectMutationTracker; /** Whether a generated wrapper and its source config graph stayed coherent. */ configStateComplete?: boolean; /** * Whether the generation-time project walk observed every directory and file * it attempted to snapshot. An incomplete walk may never authorize narrow * validation; a later complete walk must be allowed to replace it. */ projectSnapshotComplete?: boolean; /** Absolute path to the directory that owns the tsconfig. */ projectRoot: string; /** Raw compiler output returned by {@link TtscCompiler.transform}. */ result: ITtscCompilerTransformation; /** * The delivery epoch this generation is currently settled against, or * `undefined` for a generation no epoch has proven. * * Set when the generation is compiled, and again whenever a later epoch's * first delivery proves the whole generation still matches the filesystem. * While it equals the cache's current epoch, each module's first delivery is * settled by the supplied source alone, exactly as it was when every pass * compiled its own generation (samchon/ttsc#1300). */ deliveryEpoch?: number; /** * Whether this generation's non-error diagnostics have been surfaced at all, * and the epoch they were last surfaced in. * * The diagnostics describe one compile of one program, so they belong to the * generation rather than to a delivery; a pass that reuses a retained * generation still surfaces them once, because a build's warnings are part of * what that build reports (samchon/ttsc#1304). The two fields are separate so * a persistent host, whose epoch is `undefined`, still reports the first * time. */ diagnosticsReported?: boolean; diagnosticsEpoch?: number; /** * Files already delivered from this generation, keyed by filesystem identity. * A cache with a delivery epoch uses this to skip persistent validation only * for a module's first delivery inside the current pass; the set is cleared * whenever a new epoch's gate re-proves the generation. */ servedFiles?: Set; /** * Absolute path of the adapter-owned scratch directory used for this * generation. It is disposed after compilation, so none of its compiler, * resolver, or plugin artifacts can be a persistent cache or watch input. */ scratchDirectory?: string; /** * Absolute path of the generated temp-dir tsconfig this compile ran against, * when an alias/compiler-options overlay required one. The compiler reports * it in the envelope's `graph.configs` chain, but it is disposed right after * the compile, so registering it as a watch input would invalidate every * bundler cache snapshot on the next build; watch derivation must skip this * path. {@link scratchDirectory} owns the wider disposable-input bound. */ temporaryTsconfig?: string; } /** * Keyed by a stable JSON string that encodes the tsconfig path, compiler * options overlay, plugin list, and alias paths. The value is a `Promise` so * concurrent transforms for the same project share a single in-flight * compilation rather than spawning multiple `TtscCompiler` instances. */ export type TtscTransformCache = Map>; /** Cache-owned synchronous filesystem reads used by transform validation. */ export interface TtscTransformFilesystemOperations { /** Override the case policy when the observed filesystem is not the host. */ caseSensitive?: FilesystemPathIdentityOperations["caseSensitive"]; /** Test whether a validation or resolution candidate currently exists. */ exists(location: string): boolean; /** Read link metadata without following a symbolic link. */ lstat(location: string): fs.BigIntStats; /** Read bytes used by project, graph, and host-input fingerprints. */ readFile(location: string): Buffer; /** Enumerate one project or missing-input proof directory. */ readdir(location: string): fs.Dirent[]; /** Resolve one lexical path to its current physical target. */ realpath(location: string): string; /** Read ordinary metadata for file-kind and missing-path checks. */ stat(location: string): fs.Stats; /** Read nanosecond metadata for stable file and directory signatures. */ statBigInt(location: string): fs.BigIntStats; /** Override path parsing when the observed filesystem is not the host. */ platform?: NodeJS.Platform; /** * Open one directory's change notification, or throw when the observed * filesystem cannot provide one. * * Left undefined, generations watch the host filesystem: `fs.watch` on POSIX * and an isolated broker process on Windows. An embedder observing another * filesystem supplies its own; a generation whose watch cannot be opened * keeps validating from recorded state instead of losing its cache. * * Supplying one replaces the Windows broker as well, so an embedder that * wraps Node's own `fs.watch` there gives up the isolation that contains the * native abort Node's Windows fs-event backend can raise when a watched * temporary tree is deleted. */ watch?(directory: string, listener: (eventType: string, filename: string | null) => void, onError: () => void, recursive?: boolean): { close: () => void; }; } /** Normalize one directory entry under the owning filesystem's case policy. */ export declare function normalizeHostInputName(name: string, caseSensitive: boolean): string; /** Create an empty persistent transform cache with isolated filesystem reads. */ export declare function createTtscTransformCache(operations?: Partial): TtscTransformCache; /** * Open a new delivery pass, enabling constant-time first delivery for every * module this pass asks for. * * This deliberately retains the cached generation. The pass boundary is a * statement about _deliveries_ — each module is requested at most once inside * it — not about whether the compiled program is still correct, which the * generation's own recorded snapshot answers and which * {@link matchesCachedSource} proves once at the pass's first delivery. Clearing * here instead made a host whose `buildStart` repeats recompile the whole * project on every rebuild even when no compiler input had changed * (samchon/ttsc#1300). Use {@link resetTtscTransformCache} to actually discard a * generation and its watchers. * * Hosts without a guaranteed pass boundary use persistent validation unless * they have another immutable lifecycle. Bun runtime setup, for example, * defines one process-scoped module-loading session. */ export declare function beginTtscTransformBuild(cache: TtscTransformCache): void; /** * Discard every generation, dispose its watchers, and return the cache to * persistent validation mode. * * This is the unconditional lifecycle boundary, and it is distinct from * {@link beginTtscTransformBuild}: a pass ending is not a reason to throw a * proven compile away, while a session ending is. */ export declare function resetTtscTransformCache(cache: TtscTransformCache): void; /** * What the generation already knows about one derived watch input, handed to * the adapter so it does not rederive it per input per delivery. * * All facts are generation state: the identity is the memoized * {@link pathIdentityKey} of the input, `missing` preserves the original public * existence contract, and `unavailable` distinguishes a failed file predicate * from ordinary absence. An adapter that computes them itself pays a * `realpath`, a case-sensitivity directory listing, and an `existsSync` for * every input of every delivered module, which is O(modules x inputs) for one * build (samchon/ttsc#1246). */ export interface TtscWatchInputEvidence { /** Memoized filesystem identity of the input. */ identity: string; /** Whether the generation recorded this input as unavailable as a file. */ missing: boolean; /** The generation state Metro can compare with its main-process baseline. */ state?: TtscWatchInputState; /** Which unavailable predicate must become true before invalidation. */ unavailable?: "missing" | "not-file"; } /** Exact generation state behind one derived watch input. */ export type TtscWatchInputState = { /** A project-walk or dependency-only input read as ordinary host bytes. */ codec: "host"; hash: string; } | { /** A realized compiler-graph input, including its physical target. */ codec: "graph"; hash: string; realpath: string | null; } | { /** The exact compiler predicates observed for a resolver input. */ codec: "predicates"; observation: ITtscCompilerTransformation.IInputObservation; }; /** Main-process file predicate used only by project discovery. */ export interface TtscWatchInputFileBaseline { fileExists: boolean; identity: string; } /** Main-process state broad enough to compare every watch-input codec. */ export interface TtscWatchInputBaseline extends TtscWatchInputFileBaseline { directoryExists: boolean; graphHash: string; graphReadHash: string | null; hostHash: string; realpath: { ok: false; path?: never; } | { ok: true; path: string; }; stat: "directory" | "file" | "missing"; } /** Baseline shape stored for either a discovery predicate or a full input. */ export type TtscWatchInputKeyBaseline = TtscWatchInputFileBaseline | TtscWatchInputBaseline; /** One derived input and its optional generation proof. */ export interface TtscWatchInput { evidence?: TtscWatchInputEvidence; file: string; } /** * Hooks the bundler adapter passes into {@link transformTtsc} so transform * side-channels (plugin-reported dependencies and host resolver inputs) reach * the bundler without leaking extra fields on the returned `TransformResult`. */ export interface TtscTransformHooks { /** * Invoked once per absolute watch-input path derived for the transformed file * `F`: the plugin-reported `dependencies[F]` list unioned with the host-owned * reference graph's contribution — the reachability closure of `graph.edges` * from `F`, the `graph.globals` files, the `graph.configs` chain, importer * `graph.candidates`, and universal `graph.resolutionInputs`. For a file the * envelope declared `dependenciesComplete`, only `dependencies[F]`, * `graph.candidates`, `graph.resolutionInputs`, and the universal * `graph.configs` chain remain. Adapters forward this to the bundler's * `addWatchFile` so type-only inputs participate in watch-mode and * persistent-cache invalidation. See {@link selectWatchInputs} for the exact * derivation. */ addWatchFile?: (file: string, evidence?: TtscWatchInputEvidence) => void; /** * Batched form of {@link addWatchFile}. When supplied, the transform calls it * once per delivered module and does not call `addWatchFile` for that module. * `failed` marks a recovery batch: a failed compiler can omit inputs from its * previous successful result, so replacing hosts should retain those * spellings until the next successful delivery. */ addWatchFiles?: (inputs: readonly TtscWatchInput[], failed?: boolean) => void; /** * Invoked when the plugin declared the transformed file volatile (the * envelope's `volatile` list): its output depends on non-file inputs that no * file-dependency snapshot can represent. Adapters should mark the module * uncacheable where the bundler exposes that control (e.g. a webpack loader * context's `cacheable(false)`). */ markVolatile?: () => void; } /** * Apply the ttsc plugin transform to a single source file. * * The function is intentionally project-scoped: it compiles the entire tsconfig * project in one shot and extracts the result for `id`. Subsequent calls for * sibling files in the same project reuse the cached result as long as none of * the project's input files have changed (verified by comparing SHA-256 * hashes). * * Returns `undefined` when no transform is needed (declaration files, virtual * modules, disabled plugins, or source unchanged after transform). * * @param id - Bundler module id (may carry a query string or virtual prefix). * @param source - Current file content supplied by the bundler. * @param options - Resolved plugin options. * @param aliases - Raw Vite alias configuration (object or array). * @param cache - Optional project cache. Callers with a real `buildStart` * boundary declare it through {@link beginTtscTransformBuild}; other hosts * retain persistent validation. * @param hooks - Optional adapter callbacks; see {@link TtscTransformHooks}. * Dependency notifications fire on cache hits too; watch registrations are * per build, not per compilation. */ export declare function transformTtsc(id: string, source: string, options: ResolvedTtscUnpluginOptions, aliases?: unknown, cache?: TtscTransformCache, hooks?: TtscTransformHooks): Promise; interface TtscHostInputValidation { /** Lexical input spellings that existed when the generation was captured. */ readonly entries: Map; /** * Lexical spellings the manifest accounts for, omitted from the per-module * dependency loop below. * * Spellings, not identities: a symlink and its target share one identity but * are two inputs, and skipping the alias because the manifest proved the * target would leave the alias's own retarget unvalidated. */ readonly covered: Set; /** * Missing paths grouped by the nearest directory whose listing proves them * absent. */ readonly missing: Map>; } /** * Strip a query string or hash fragment from a bundler module id. * * Vite appends query parameters (e.g. `?raw`, `?url`, `?inline`) to * differentiate import variants of the same file. We must strip them before * using the id as a file-system path. */ export declare function stripQuery(id: string): string; /** * Returns `true` for every declaration-file spelling TypeScript-Go accepts. * Besides the standard `.d.ts`, `.d.mts`, and `.d.cts` forms, TypeScript-Go * treats an arbitrary-extension source such as `styles.d.css.ts` as a * declaration file too. */ export declare function isDeclarationFile(id: string): boolean; /** * Build the unplugin transform result, or `undefined` when the transform * produced no changes. * * Returning `undefined` instead of `{ code: source }` lets the bundler skip the * unnecessary module update and preserves the original source map. */ export declare function createTransformResult(source: string, code: string): TtscTransformResult | undefined; /** Validate one predicate-preserving graph proof against a filesystem view. */ export declare function validateGraphInputObservation(file: string, observation: ITtscCompilerTransformation.IInputObservation, filesystem?: TtscTransformFilesystemOperations): string[]; /** * Hash every input file under `projectRoot` (the same walk universe * {@link matchesCachedSource} validates against), keyed by project-relative * slash path. Exported so hosts without a per-build boundary (`@ttsc/metro`) * can fold the identical input universe into their own cache fingerprints. */ export declare function collectProjectInputHashes(projectRoot: string, identities?: FilesystemPathIdentityContext, filesystem?: TtscTransformFilesystemOperations, policy?: ITtscProjectMembershipPolicy): Record; /** One project-walk hash set together with its completeness proof. */ export interface TtscProjectInputHashSnapshot { complete: boolean; hashes: Record; } /** * Hash the project walk and retain whether every attempted directory and file * was observed coherently. Cache-key hosts must reject an incomplete set. */ export declare function collectProjectInputHashSnapshot(projectRoot: string, identities?: FilesystemPathIdentityContext, filesystem?: TtscTransformFilesystemOperations, policy?: ITtscProjectMembershipPolicy): TtscProjectInputHashSnapshot; /** * Report whether an absolute `file` belongs to the project walk universe of * `root`: it lies under `root`, every component exists without traversing a * symbolic link, the leaf is a regular file, and no segment of the relative * path is ignored. The predicate mirrors {@link walkProjectInputs} exactly, so * "walk-visible" here means "hashed by {@link collectProjectInputHashes}". * Missing paths and files reached through symlinks or Windows junctions are * out-of-walk inputs that only the reference graph can prove relevant. */ export declare function isProjectWalkPath(root: string, file: string, _identities?: FilesystemPathIdentityContext, filesystem?: TtscTransformFilesystemOperations, policy?: ITtscProjectMembershipPolicy): boolean; /** * Hash a list of absolute out-of-walk input paths: content SHA-256 for a * readable file, a stable directory-kind digest for a directory candidate, and * a stable `missing` marker otherwise. Keys use filesystem identity so * case-only spellings share one snapshot entry, while reads retain the original * path supplied by the compiler. The marker is state, not an error — a recorded * input disappearing (or reappearing) must change the comparison exactly like a * content edit. Exported so `@ttsc/metro` can re-hash its recorded snapshot * with identical semantics at cache-key time. */ export declare function collectExternalInputHashes(paths: readonly string[], filesystem?: TtscTransformFilesystemOperations): Record; /** Capture the stable file predicate used by implicit project discovery. */ export declare function captureWatchInputFileBaseline(file: string, filesystem?: TtscTransformFilesystemOperations): TtscWatchInputFileBaseline | undefined; /** * Capture one stable main-process baseline that can be compared with any * generation-owned watch-input evidence. Two equal broad observations are * required so a cache key never publishes a torn path state. */ export declare function captureWatchInputBaseline(file: string, filesystem?: TtscTransformFilesystemOperations): TtscWatchInputBaseline | undefined; /** Compare generation evidence with the main process's exact key baseline. */ export declare function watchInputEvidenceMatchesBaseline(evidence: TtscWatchInputEvidence, baseline: TtscWatchInputKeyBaseline): boolean; /** Validate the complete narrow or broad shape stored in a key baseline. */ export declare function isWatchInputKeyBaseline(baseline: unknown): baseline is TtscWatchInputKeyBaseline; /** * Build a comparison key for a path without changing the spelling handed to a * filesystem or bundler. Windows is case-insensitive; macOS is probed per * existing filesystem location so case-sensitive volumes keep distinct paths. */ export declare function pathIdentityKey(file: string, identities?: FilesystemPathIdentityContext): string; export {};