ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
330 lines (329 loc) • 16.8 kB
TypeScript
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 {};