ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
245 lines (244 loc) • 10.7 kB
TypeScript
import { type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process";
/** One contributor's resolved Go source plus its target sub-package name. */
export interface ITtscBuildContributor {
/** Sub-package suffix: scratch lands at `<host>/contrib/<name>/`. */
name: string;
/** Absolute path to the contributor's source directory. */
source: string;
}
/** Source-plugin cache locations resolved for one ttsc invocation. */
export interface ITtscSourceBuildCachePaths {
/** Root directory containing all ttsc-owned source build caches. */
root: string;
/** Directory containing content-addressed compiled plugin binaries. */
pluginRoot: string;
/** Directory passed to Go as `GOCACHE` for source-plugin builds. */
goBuildRoot: string;
/** How `goBuildRoot` was selected. */
goBuildRootSource: "ttsc-cache" | "TTSC_GO_CACHE_DIR" | "GOCACHE";
}
/**
* Build one Go source plugin into a cached executable.
*
* `opts.env` is the effective environment for this build — the caller merges `{
* ...process.env, ...context.env }` so a programmatic `TtscCompiler` instance
* can pin its own Go toolchain (`TTSC_GO_BINARY`), Go build cache
* (`TTSC_GO_CACHE_DIR`), and Go build variables (`GOFLAGS`, `CGO_*`, …) without
* mutating the shared `process.env`. CLI callers omit it and inherit
* `process.env`, so ambient behavior is unchanged.
*/
export declare function buildSourcePlugin(opts: {
source: string;
pluginName: string;
baseDir: string;
cacheDir?: string;
contributors?: readonly ITtscBuildContributor[];
env?: NodeJS.ProcessEnv;
label?: string;
overlayDirs?: readonly string[];
quiet?: boolean;
ttscVersion: string;
tsgoVersion: string;
}): string;
/** Opaque identity of one observed lock generation. */
export type PluginBuildLockFence = {
protocol: "legacy" | "v2";
generation: string;
};
/** Ownership token returned only to the process that acquired `current`. */
export type PluginBuildLockLease = {
protocol: "v2";
generation: string;
};
/**
* Atomically acquire the current generation in a v2 coordination directory.
*
* A non-empty candidate is renamed to `current`. Directory rename cannot
* replace a non-empty `current`, so exactly one contender wins without an
* empty-owner publication window. `null` means either another v2 holder won or
* the path is a legacy lock that must be observed before it can be reclaimed.
*
* Exported for deterministic multi-process tests.
*/
export declare function acquirePluginBuildLock(lockDir: string): PluginBuildLockLease | null;
/** Retire a held generation during the holder's `finally`. */
export declare function releasePluginBuildLock(lockDir: string, lease: PluginBuildLockLease): boolean;
/**
* Retire exactly the generation carried by an abandoned observation.
*
* Exported for deterministic multi-process tests.
*/
export declare function reclaimPluginBuildLock(lockDir: string, fence: PluginBuildLockFence): boolean;
/**
* Outcome of one waiting session on another process's plugin build lock.
*
* - `published`: the binary exists and can be reused.
* - `released`: the observed generation no longer exists and no binary appeared —
* the holder freed the key normally, so the caller should retry ordinary
* acquisition without reporting or removing anything.
* - `abandoned`: the lock still exists but is provably stale (dead owner, old
* legacy lock) or the wait budget expired; the caller may report and retire
* precisely the attached generation.
*
* Exported for unit tests.
*/
export type PluginBinaryWaitResult = {
outcome: "published";
} | {
outcome: "released";
} | {
outcome: "abandoned";
reason: string;
fence: PluginBuildLockFence;
};
/**
* Poll for the locked builder to publish its binary, up to `timeoutMs`.
*
* Exported for unit tests.
*/
export declare function waitForPluginBinary(opts: {
binaryPath: string;
lockDir: string;
lockInfo: {
label: string;
pluginName: string;
quiet: boolean;
};
timeoutMs: number;
}): PluginBinaryWaitResult;
/**
* One observation of a plugin build lock directory's state.
*
* - `active`: the lock exists and its owner is alive (or cannot be disproven:
* another host, no metadata but young). Keep waiting.
* - `abandoned`: the lock still exists and the evidence says nobody will ever
* release it — a same-host owner that is no longer running, or an old
* metadata-less legacy lock. Retiring its fenced generation is justified.
* - `released`: the observed generation no longer exists. In v2 the persistent
* coordination root remains while `current` is absent. This is a routine
* handoff, never an infinitely old abandoned lock (issue #421).
*
* Exported for unit tests.
*/
export type PluginBuildLockObservation = {
state: "active";
owner: string;
fence: PluginBuildLockFence;
} | {
state: "abandoned";
reason: string;
fence: PluginBuildLockFence;
} | {
state: "released";
};
/**
* Classify the current state of a plugin build lock directory.
*
* Exported for unit tests.
*/
export declare function inspectPluginBuildLock(lockDir: string, now: number): PluginBuildLockObservation;
/**
* Render a millisecond duration for lock diagnostics (`137ms`, `42s`, `9m 3s`).
*
* Total over every number: no caller produces a non-finite duration anymore
* (the lock state machine reports "released" instead of an Infinity age), but
* as defense in depth a non-finite input renders as `an unknown time` so no
* public diagnostic can ever print `Infinitym NaNs` again (issue #421).
*
* Exported for unit tests.
*/
export declare function formatDuration(ms: number): string;
/**
* Format an absolute filesystem path as a single `go.work`/`go.mod` token.
*
* The modfile grammar shared by `go.mod` and `go.work` (parsed by
* `golang.org/x/mod/modfile`) is whitespace-tokenized, so a `use`/`replace`
* path that contains a space — a home or project directory such as `/Users/John
* Smith/...` or `C:\Users\John Smith\...` — must be emitted as a quoted string
* or `go` cannot parse the generated `go.work`. Normalize Windows separators to
* `/` (the workspace convention) and then delegate to
* {@link autoQuoteGoModToken}, which mirrors `modfile.AutoQuote`.
*
* Separator normalization is itself a quoting trigger. A Windows UNC
* (`\\server\share\...`) or extended-length (`\\?\C:\...`) path normalizes into
* a token that starts with `//`, and the modfile lexer reads `//` as a line
* comment wherever it appears. Emitted bare, such a token turns its whole
* `use`/`replace` line into a comment: `go` exits 0, reports nothing, and the
* overlay module simply disappears from the workspace.
*
* Exported for unit tests.
*/
export declare function formatGoWorkPath(p: string): string;
/**
* Quote `token` for a `go.mod`/`go.work` line exactly as
* `golang.org/x/mod/modfile`'s `AutoQuote` does: return it unchanged when it is
* already a clean bare token, otherwise return its Go double-quoted form so the
* value round-trips through the modfile lexer. A clean bare token is therefore
* emitted byte-for-byte as before; only tokens that would otherwise be split or
* interpreted as comments are quoted.
*
* Exported for unit tests.
*/
export declare function autoQuoteGoModToken(token: string): string;
export declare function spawnGoTool(goBinary: string, args: readonly string[], options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns<string>;
/** Build the fixed cmd.exe switch sequence for one already quoted payload. */
export declare function windowsGoCommandArgs(payload: string): string[];
/**
* Resolve the directory where compiled plugin binaries are cached.
*
* Delegates to {@link resolveSourceBuildCachePaths}; kept as a thin accessor for
* callers (and tests) that only need the plugin-binary root. Triggers the
* opportunistic project-cache GC as a side effect for the default location.
*/
export declare function resolvePluginCacheRoot(projectRoot: string, cacheDir?: string, env?: NodeJS.ProcessEnv): string;
/**
* Resolve all source-plugin build cache directories for one invocation.
*
* `pluginRoot` stores compiled plugin binaries; `goBuildRoot` is the Go object
* cache passed as `GOCACHE` while ttsc builds those binaries. Both live under a
* single `root`, so persisting one directory covers the whole source-build
* cache without depending on ttsc internals.
*/
export declare function resolveSourceBuildCachePaths(projectRoot: string, cacheDir?: string, env?: NodeJS.ProcessEnv): ITtscSourceBuildCachePaths;
/**
* Return every directory `ttsc clean` should remove for `projectRoot`.
*
* Covers the resolved cache root (which holds `plugins/` and, when ttsc-owned,
* `go-build/`), a ttsc-owned Go build cache that lives OUTSIDE that root
* (`TTSC_GO_CACHE_DIR`), and the two legacy project-local caches. A
* user-provided `GOCACHE` is never removed. Pure over `env`, so the CLI passes
* `process.env` and a programmatic caller can pass an injected environment.
*/
export declare function resolveCleanTargets(projectRoot: string, cacheDir?: string, env?: NodeJS.ProcessEnv): string[];
/**
* Machine-global cache directories created by pre-0.17 ttsc releases (XDG /
* AppData / Library / `~/.cache`). ttsc no longer writes to any of these, but
* an upgraded machine can still hold a multi-GB orphaned cache here, so `ttsc
* clean` offers them for removal to reclaim that disk. Each entry is the whole
* `<userCacheRoot>/ttsc` directory (both its `plugins` and `go-build`), which
* was entirely ttsc-owned in those releases and is safe to remove.
*/
export declare function legacyGlobalCacheTargets(): string[];
/** Report whether `child` equals `parent` or is nested beneath it. */
export declare function isPathWithin(child: string, parent: string): boolean;
/**
* Compute a deterministic SHA-256 cache key for a plugin build.
*
* The key covers every input that can produce a different binary: ttsc/tsgo
* versions, platform, entry package, Go compiler identity, Go build environment
* variables, overlay module sources, plugin source files, and contributor
* source files. Contributors are sorted by name so declaration order does not
* affect the key.
*
* Exposed for testing and for the `ttsc cache` CLI command.
*/
export declare function computeCacheKey(inputs: {
contributors?: readonly ITtscBuildContributor[];
dir: string;
entry: string;
env?: NodeJS.ProcessEnv;
goBinary?: string;
overlayDirs?: readonly string[];
ttscVersion: string;
tsgoVersion: string;
}): string;