UNPKG

ttsc

Version:

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

330 lines (329 loc) 16.8 kB
import { type ProjectInputPathIdentityContext } from "../../internal/projectInputPathIdentity"; import type { ITtscProjectInputSnapshot } from "../../structures/internal/ITtscProjectInputSnapshot"; import type { TtscBuildOptions } from "../../structures/internal/TtscBuildOptions"; type WatchTopologyOptions = Pick<TtscBuildOptions, "binary" | "emit" | "env" | "outDir" | "passthrough" | "projectRoot" | "tsconfig"> & { cwd: string; files: readonly string[]; }; type WatchTopologyCallbacks = { onError(location: string, error: unknown): void; onInputChange(change: WatchInputChange): void; onProjectInputWatchUnavailable?(roots: readonly string[]): void; onProjectInputWatchRoots?(roots: readonly string[]): void; onTopologyChange(): void; }; export type WatchInputChange = { /** Keep the resident process but cold-load its compiler Program. */ invalidate?: boolean; kind: "compiler" | "config" | "plugin" | "project"; path?: string; }; export type CompilerDirectoryWatchEventPlan = { changes: string[]; rearm: string[]; refresh: boolean; }; /** * Keeps the launcher watch set aligned with the compiler's current program. * * TypeScript-Go's `--listFilesOnly` output is the authority for source and * declaration inputs. Configuration files, project-reference roots, and the * source trees of selected native plugins supplement that list, while compiler * outputs are filtered before any watcher is installed. */ export declare class WatchTopology { private readonly options; private readonly callbacks; private analysisOnly; private closed; private compilerPostRegistrationMembershipRefresh; private compilerPostRegistrationReconciliationScheduled; private compilerPostRegistrationSkipUnobservedProjectInputWatchRoots; private directories; private directoryWatchers; private extraInputs; private extraWatchers; private compilerFileSnapshots; private files; private fileWatchers; private observedDirectories; private outputFiles; private outputs; private projectInputFingerprints; private projectInputMatches; private projectInputs; private declaredProjectInputs; private projectInputRecoveryScheduled; private projectInputPostRegistrationReconciliationScheduled; private projectInputRejectedWatchRoots; private projectInputRequiredWatchRoots; private projectInputUnobservedWatchRoots; private projectInputWatchRoots; private projectInputWatchers; private projectInputLinkWatchers; private projectInputCompilerOutputOverlaps; private projectInputCompilerAcknowledgements; private reloadFiles; constructor(options: WatchTopologyOptions, callbacks: WatchTopologyCallbacks); /** Re-resolve compiler inputs and notify only when their membership changed. */ refresh(notify: boolean): void; private refreshCompilerInputs; /** * Hand one Program-membership transition from the compiler lane to the * overlapping project-input lane. * * Windows can deliver the compiler membership refresh before the recursive * project watcher names the same creation. The rebuild scheduled here already * consumes the current project bytes, so publishing their strong fingerprints * keeps the later parent event from rediscovering the same population delta. * A newly tracked compiler file also remembers that fingerprint until its * first named content delivery; identical bytes are the delayed creation, * while different bytes are a real later edit and remain observable even * inside filesystem timestamp resolution. */ private acknowledgeProjectInputCompilerMembership; /** Add Go plugin source trees discovered by the real build lane. */ setExtraInputs(inputs: readonly string[]): void; /** * Reconcile project-rule dependencies, retaining absent files and empty glob * populations as live topology. */ setProjectInputs(inputs: ITtscProjectInputSnapshot): void; /** Close every watcher so SIGINT/SIGTERM can drain the event loop. */ close(): void; private syncFileWatchers; /** Compare a tracked file's content and physical owner with its snapshot. */ private compilerFileMovement; private syncDirectoryWatchers; /** * Reconcile tracked compiler files after a newly registered watcher returns. * * A file or directory watcher can be returned before its backend is ready to * deliver the first event. The compiler-file stamps were captured before * registration, so one coalesced microtask can recover a change in that * handoff window. A real event updates the same stamp first and makes this * bounded scan a no-op. */ private scheduleCompilerPostRegistrationReconciliation; /** * Narrow a plan's changes to the tracked files that actually moved. * * A backend that cannot name what changed forces the plan to nominate every * tracked file under the watched directory, which is the only safe answer it * can give from an event carrying no filename. macOS delivers such events for * ordinary activity elsewhere in the project, so the compiler lane would wake * for sources nobody touched. Only a content notification passes through: it * is the one event that claims the bytes moved. A rename claims the directory * entry was rewritten and an unnamed event claims nothing, so both are * decided from the bytes, which is the question neither of them answered. */ private compilerChangesToReport; private recordCompilerFileSnapshot; private rearmFileWatchers; private syncExtraWatchers; private syncProjectInputWatchers; /** * Watch the directory that holds a declaration which is itself a link. * * A recursive watcher cannot report the link being replaced. The backend that * keys its handles by path skips an entry it already knows, and the handle it * put on the entry followed the link to the target's inode, which unlinking * and recreating the link never touches. A plain directory watch has neither * property: it reports the entry by name the moment it moves. These are kept * apart from the recursive roots because they are not roots — they observe * one directory, they are never reported as watch roots, and an ancestor * covering them does not make them redundant. */ private syncProjectInputLinkWatchers; /** Drop the watcher that just reported a directory replacement. */ private retireProjectInputWatcher; private retainProjectInputWatchRoot; /** * Retry a failed root on the next reconciliation instead of retiring it for * the session. * * This immediate recovery pass still honors the rejected root so it can * install a safe ancestor where one exists. Only the recovery fixpoint * reports a genuinely uncovered lane; transient gaps between fallback * candidates are not user-visible. The rejection then expires. A later * compiler refresh or an unchanged project-input republication can retry the * original root, while a permanently failing backend costs at most one * attempt per sync. */ private scheduleProjectInputWatcherRecovery; /** * Reconcile the snapshot-to-watcher handoff after the caller's current turn. * * A recursive watcher can return before its backend is ready to deliver the * first event. The publication baseline is necessarily captured before that * watcher exists, so an input materialized synchronously after * `setProjectInputs()` would otherwise depend entirely on that startup event. * The ordinary fingerprint update makes this scan and a real backend event * race safely: whichever arrives first records the new population and the * other becomes a no-op. */ private scheduleProjectInputPostRegistrationReconciliation; /** * Re-resolve declarations after watcher registration. * * A missing path can become a symlink before the handoff scan. The retained * normalized snapshot still names the pre-link spelling in that case, so a * scan can find the first target file without installing the physical owner * that must observe later target changes. */ private refreshPublishedProjectInputIdentities; /** Drop retained owner choices for declarations no longer published. */ private pruneProjectInputWatchRoots; /** Report only newly uncovered project-input roots as an observation loss. */ private reportUnobservedProjectInputWatchRoots; /** * One snapshot holding every spelling of every declaration. * * Consumers that decide from a population rather than from a single path have * to see both, or half of them answer from the file a link pointed at when * the snapshot was published while the event they are judging resolved to the * file it points at now. */ private projectInputPopulation; /** * Every spelling of one declaration that has to be anchored separately. * * The retained snapshot is normalized to physical identities, which is what * every comparison needs but not what every watcher needs: a declaration * reached through a symlink resolves to its target's directory, so anchoring * the normalized form alone watches the bytes and never the link. Retargeting * or replacing the link then goes unobserved, even though it is exactly what * decides which bytes the declaration names next. Both spellings are planned * through the same root selection, so the project-root hoist and the * nearest-existing-ancestor boundary still bound each of them, and the active * set drops one again whenever they coincide or share an ancestor. */ private projectInputDeclarations; private projectInputWatchRoot; private refreshProjectInputs; private collectProjectInputMatches; private refreshFromDirectory; private isCompilerOutputDirectory; private isCompilerOutput; private isProjectInputCompilerOutputDirectory; private isProjectInputCompilerOutput; private isProjectInputDirectory; private classifyCompilerInput; private isPluginInput; } export declare function reloadInputsForFailedTopologyRefresh(reloadFiles: Iterable<string>, changed?: string): string[]; type SynchronizedWatcher = { close(): void; on(event: "error", listener: (error: Error) => void): unknown; }; export declare function syncWatchers<T extends SynchronizedWatcher>(watchers: Map<string, T>, desired: ReadonlyMap<string, string>, create: (location: string, key: string) => T, onError: (location: string, error: unknown) => void, shouldContinue?: () => boolean): boolean; export declare function literalGlobRoot(pattern: string): string; /** * Chooses the one stable recursive watcher root owned by a project-input * declaration. * * Inputs inside the project share its physical root so directory replacement * cannot strand a child handle. External inputs use the nearest existing * ancestor of their declared parent, which is the explicit boundary for * observing a currently missing external tree without polling every file -- * except that the boundary never rises to a directory holding the project, * since such a root outranks the project's own in the active merge and leaves * one handle over a shared system directory to carry everything. An external * declaration that can only be owned that way falls back to its own tree, and * is left unwatched when even that would contain the project. */ export declare function projectInputWatchDirectories(target: string, projectRoot: string): string[]; /** * Removes recursive roots already covered by an ancestor without rewriting the * declaration-specific roots retained by WatchTopology. */ export declare function projectInputActiveWatchDirectories(directories: Iterable<string>, identities?: import("../../internal/projectInputPathIdentity").FilesystemPathIdentityContext): string[]; export declare function projectInputAvailableWatchDirectory(location: string, rejected: ReadonlySet<string>, identities?: ProjectInputPathIdentityContext, projectRoot?: string): string | undefined; /** * Whether a replacement at this path leaves a recursive watcher bound to the * object that was replaced. * * Only one backend needs the answer. Node routes a recursive watch to its own * per-directory implementation when the platform is neither macOS nor Windows, * and that implementation keys its handles by path: the handle for a directory * renamed away stays bound to the object that left, and any child whose name * survives the swap is skipped as already known. The native subtree backends * both other platforms use follow the path, so retiring their watcher would buy * nothing and would open a window in which no events are delivered. * * The answer is deliberately narrower than the rescan rule, because * reinstalling a root costs one watch descriptor per entry beneath it, which an * install storm would pay thousands of times. A directory appearing inside a * glob root deserves a rescan but replaces nothing a root stands on, and a * reload directory anchors the directory that contains it rather than itself, * since its fingerprint is a digest of its own immediate entries and nothing * below it can reach the declared corpus. */ export declare function projectInputReplacementStrandsWatchers(snapshot: ITtscProjectInputSnapshot, location: string, identities?: import("../../internal/projectInputPathIdentity").FilesystemPathIdentityContext, platform?: NodeJS.Platform): boolean; /** * Decide whether an event that named no declared input can still have moved * one. * * The admitted set is the only bound on how often a watch session re-reads and * re-hashes its declared corpus, and both directions cost: too narrow drops an * atomic replacement, too wide re-fingerprints on every entry an install * creates. Exported so that boundary is pinned directly instead of being * inferred from a rebuild that a silent rescan and a skipped rescan produce * identically. */ export declare function projectInputTopologyMayAffect(snapshot: ITtscProjectInputSnapshot, location: string, previous: ReadonlyMap<string, string>, identities?: import("../../internal/projectInputPathIdentity").FilesystemPathIdentityContext): boolean; export declare function projectInputEventShouldNotify(input: { contentChanged: boolean; directlyMatched: boolean; membershipChanged: boolean; }): boolean; /** * Classify an exact execution-selection input ahead of ordinary project data. * * `changedInputs` carries fingerprint or membership deltas, so a filename-less * event can still select the cold lane. A named exact event selects the cold * lane only after the surrounding change detector admits the event; unchanged * bytes remain quiet before this classifier is observed. */ export declare function projectInputReloadEventShouldNotify(input: { causedBy?: readonly string[]; changed?: string; changedInputs: readonly string[]; reloadDirectories?: readonly string[]; reloadFiles: readonly string[]; globs?: readonly string[]; }): boolean; /** * Return whether a project-input population transition can reshape a Program. * * JSON is data to a ProjectRule but may simultaneously be a `resolveJsonModule` * source. TypeScript and JavaScript paths can likewise overlap a project-input * declaration. Their creation or deletion therefore requires a cold Program * inside the existing resident process. A filename-less event cannot identify * the changed member and is conservatively invalidating whenever the population * moved. */ export declare function projectInputMembershipInvalidatesProgram(input: { changed?: string; changedInputs?: readonly string[]; contentChanged?: boolean; next: ReadonlyMap<string, string>; previous: ReadonlyMap<string, string>; }): boolean; /** * Plan one compiler-directory event without relying on backend timing. * * POSIX file watchers own ordinary content changes. A named rename re-arms the * replaced file; an unnamed event conservatively re-arms and reports every * surviving tracked input below the watch root. Windows has no per-file * watchers here, so both named and unnamed directory events report inputs. */ export declare function planCompilerDirectoryWatchEvent(input: { changed?: string; event: string; exists(location: string): boolean; location: string; platform: NodeJS.Platform; trackedFiles: ReadonlyMap<string, string>; }): CompilerDirectoryWatchEventPlan; export {};