Programmatic API (ttsc JS class)
For tool authors that need to drive ttsc from JavaScript or TypeScript.
This page is for embedders, anyone wrapping ttsc inside a Node program (test harnesses, dev-server middleware, playground hosts, code-generation pipelines). Day-to-day users should keep using the ttsc, ttsx, and ttscserver CLIs instead.
For browser embedding (running ttsc inside a Web Worker / WebAssembly), see the WASM guide and the @ttsc/playground package built on top of it.
When to use
Reach for the programmatic API when you need to:
- Compile or type-check a project from inside another Node tool and consume the result as structured data, not parsed terminal output.
- Capture every emitted text artifact in memory without writing JavaScript, declarations, maps or incremental state into the user’s project tree.
- Pay the lazy plugin-build cost ahead of time in CI warmup steps.
- Manage the ttsc cache directory from your tool’s own lifecycle.
- Compare watched or cached filesystem paths with the same physical-path and per-directory case rules as ttsc.
If you only need to run the same command a user would type, shell out to npx ttsc instead. The CLI is the public, stable surface for that.
Install
npm install ttsc typescriptBoth packages are runtime dependencies. typescript ships the TypeScript-Go binary that ttsc drives.
Minimal example
import { TtscCompiler } from "ttsc";
const compiler = new TtscCompiler({
cwd: "/abs/path/to/project",
// tsconfig: "tsconfig.build.json", // optional; auto-discovered when omitted
});
const result = compiler.compile();
if (result.type === "success") {
for (const [path, text] of Object.entries(result.output)) {
// path is project-relative ("dist/index.js", "dist/index.d.ts", ...)
console.log(path, text.length);
}
} else if (result.type === "failure") {
for (const diag of result.diagnostics) {
console.error(`${diag.file}:${diag.line}:${diag.character} ${diag.messageText}`);
}
} else {
// type === "exception"
console.error("ttsc host failed:", result.error);
}Plugin-backed compile() keeps all compiler outputs in a private temporary lifetime, including incremental metadata from diagnostic recovery after a plugin fails. Separate declaration directories and bundled outputs retain their original returned keys. Source maps use their original output coordinates. Existing caller output and build-info files remain untouched; compiler/plugin caches and arbitrary plugin effects retain their normal ownership.
Lifecycle
TtscCompiler is construct-once, reuse. The constructor captures one project context (working directory, tsconfig, plugin list, cache root, env) and every operation runs against that context. The context is intentionally not replaceable per call, create another instance to compile a different project. Explicit tsconfig, projectRoot and pluginConfigDir keep their documented authority. Compiler CLI forwarding does not become a per-operation project override on this API. Internal build and watch lanes share one selected config while retaining a separate original compiler argument directory for response and relative command-line options.
// One compiler per project, kept around across calls.
const compiler = new TtscCompiler({ cwd: projectDir });
await ensurePluginsReady(compiler); // optional warmup
const first = compiler.compile();
const second = compiler.compile(); // safe to call againThe constructor copies the context and environment overrides and captures each explicit plugin entry’s JSON payload. Later changes to the original options or nested plugin configuration do not change that captured input. Each operation receives a fresh copy of the payload.
Plugin payload capture uses JSON serialization, including Date and custom toJSON conversion. Conversion runs at construction; cyclic values and BigInt can therefore throw before any compilation starts. Host selection fields (transform, enabled, and a string configFile) are captured separately from that JSON representation. Worker operations transfer both parts and restore them before loading plugins.
Operations
compile()
Runs the configured project through the TypeScript-Go pipeline and returns the structured result as a discriminated union.
For projects with no plugins, ttsc starts its native TypeScript-Go host in a separate process. That host captures WriteFile callbacks and returns the output map through api-compile. For projects with plugins, ttsc runs the plugin pipeline against a private system temporary directory, reads the generated text back into memory, and removes the temp directory before returning.
Result shape (ITtscCompilerResult):
type ITtscCompilerResult =
| { type: "success"; diagnostics?: ITtscCompilerDiagnostic[]; output: Record<string, string> }
| { type: "failure"; diagnostics: ITtscCompilerDiagnostic[]; output: Record<string, string> }
| { type: "exception"; error: unknown };success: TypeScript-Go completed with no error-class diagnostics. Theoutputmap contains every text artifact (JS,.d.ts, source maps, declaration maps).diagnosticsis omitted when empty; warnings appear here when present.failure: Compilation produced error-class diagnostics or a plugin failed after ttsc could establish a build result. Before returning a plugin-backed failure, ttsc runs an independent no-emit TypeScript check so pure TypeScript errors remain indiagnostics;outputmay still be partial.exception: The host or plugin setup failed before ttsc could establish a build result (cache lockup, missing binary, invalid project setup, etc.). Treat theerrorfield as opaque diagnostic material; do not assume it’s anErrorinstance.
Exception transport retains a finite description of the failure: Error name, message, stack, cause and aggregate errors, together with enumerable outcome data. Repeated objects use $ttscReference JSON-pointer markers. Source objects shaped like reference or literal-object envelopes are escaped as { $ttscValue: "object", $ttscProperties: ... } so their fields remain distinguishable from transport markers. Values that JSON cannot represent, accessors and objects that cannot be inspected use $ttscValue markers. The description does not invoke getters or preserve foreign class internal slots.
transform()
Source-to-source. Returns a typescript map (project-relative path → TypeScript text), never JS or .d.ts.
const t = compiler.transform();
if (t.type === "success") {
// t.typescript["src/index.ts"] holds the post-transform source
}When no transform-stage plugin is configured, the map contains the unmodified files as TypeScript-Go loaded them. With a transform plugin (e.g. typia’s), the map contains the rewritten text.
t.sourceMaps, keyed the same way, holds a version 3 source map for each file whose text differs from its source, from that text back to the source. Its sources are relative to the file’s directory, and its sourcesContent is the text the file was transformed from, so a consumer can confirm the map describes the text it holds. A file without an entry has no map, which is always the case for an executable plugin host that writes none.
Successful and compiler-failure transformations may also carry hostInputs: the exact tsconfig ancestry, every package manifest inspected by auto-discovery, the JavaScript descriptor’s loaded module graph, absolute paths declared by each plugin descriptor, and inputs a linked native plugin reports while evaluating configuration. First-party plugins use the reporting channels for implicit config candidates and evaluated config dependencies; missing paths are retained because creating a higher-priority candidate changes resolution. hostInputHashes carries the matching evaluation-time SHA-256 digest, or null for an observed missing candidate, while hostInputRealpaths carries its physical path or null. Bundler adapters require both when supplied before narrowly reusing a persistent generation, so concurrent content changes and symlink or junction retargeting cannot authorize stale output. Arbitrary unclassified project files remain absent.
observationsComplete: false means the producer could not establish complete input observation. hostInputProofFailures may identify paths whose proof was unavailable with the reason "observation-unavailable". These fields preserve known hashes and physical paths; the markers alone grant no reuse authority and cannot excuse a changed input.
@ttsc/unplugin can deliver a coherent locally compiled success when its only missing proof is explicitly unavailable host observation. Delivery requires an explicitly nonwatching pass, validated known input proofs, a stable build and a host that can withdraw each module from its cache.
The common owner releases the input capture and permits only each distinct module’s first delivery in that same pass. Repeated modules and later passes recapture; no persistent project record or shared publication is retained. Watching hosts, unknown lifecycle states and hosts without that cache control reject the incomplete result. See Validation paths.
A transformation that ran a plugin also carries pluginSources: every Go source directory its plugins supplied to their binaries, by absolute path, with the state the build keyed the binary on. That state covers the directory’s files and the environment a build there runs in: its Go compiler, what go env reports, and cgo’s toolchain. These are each plugin’s Go module root and each contributor’s source; ttsc’s own sources change only with ttsc itself and are not listed. A plugin edited in place, or built under another GOFLAGS or Go toolchain, changes nothing else a consumer can see, so a consumer that caches the output proves each state with pluginSourceStateHolds from ttsc/plugin-source and observes each directory as a whole subtree.
transformAsync()
The same transformation, run on a worker thread so it never blocks the event loop. The envelope, the failure semantics, and the descriptor-resilient launches are the same as transform()’s.
const t = await compiler.transformAsync();A host that serves other work while it compiles uses this one; @ttsc/unplugin does, so a dev server keeps answering requests during a compile. Plugin loading and the native compile both run on the worker, and the worker adopts process.env as it is at the call. A host that points TEMP at a scratch directory around the call therefore covers the whole transform, and nothing that changes the environment afterward reaches it. Idle workers are kept for the next call, so plugin loading’s in-process caches stay warm, and they never keep the process alive.
prepare()
Builds every configured Go source plugin into the cache up front. Use this in CI warmup steps so the first user-triggered compile doesn’t pay the plugin build cost.
compiler.prepare(); // returns compiled binary paths (debug aid)prepare() does not run diagnostics, does not create a Program, and does not emit files. It is purely a cache warmer.
clean()
Removes the cache directories owned by this compiler’s context. Returns the list of paths that were actually removed (existing directories only).
compiler.clean();When the context sets an explicit cacheDir, clean() selects that directory as a whole, plus the legacy node_modules/.ttsc and .ttsc caches, following ttsc clean --cache-dir <dir>. An explicit cacheDir must name a directory dedicated to ttsc. clean() refuses a filesystem root or any directory that equals or contains the project before removing anything, so values such as ., .., and a workspace ancestor are rejected.
A caller-provided GOCACHE remains protected in both explicit and default cleanup modes. A selected target is preserved if it contains that cache, lies inside it, or its physical relationship cannot be safely established. This protection can therefore prevent removal of an explicit cacheDir that overlaps the caller’s Go cache.
For the default workspace-local node_modules/.cache/ttsc, or a root resolved from TTSC_CACHE_DIR, it removes only the ttsc-owned pieces:
- the compiled plugin binaries (
<cache-root>/plugins); - the descriptor, capability, and orphan-lowering caches (
<cache-root>/descriptors,<cache-root>/capabilities,<cache-root>/ttsx-orphan); - the runtime directories of
ttsxandttsc/registerruns that have ended (<cache-root>/ttsx), keeping the directory of a run still in progress; - the ttsc-owned Go object cache (
<cache-root>/go-buildwhen the root is the default or its basename isttsc, plus an explicitTTSC_GO_CACHE_DIRwhen it points elsewhere); - the legacy project-local caches (
node_modules/.ttsc,.ttsc).
In that case the parent cache root itself is never removed (it may be shared with other tools), and a user-provided GOCACHE is treated as external and left untouched. Before deleting anything, clean() rejects every resolved target that equals or contains the project or a filesystem root, including physical aliases selected through a symlink or junction.
Runtime cleanup and a new run’s owner publication use one filesystem lock. A run with an unreadable or absent owner record is kept because its owner cannot be proved gone; an unreadable run index makes cleanup fail without removing the runtime directory.
Resident transform service (TtscService)
TtscService is the resident, incremental counterpart to transform(). Where transform() reloads and recompiles the whole project on every call, TtscService compiles the project once into a warm host and then answers per-file requests from it. A single process that transforms many files (a watch server, an editor, a long-running codegen tool) pays the project compile once.
import { TtscService } from "ttsc";
const service = new TtscService({ cwd: "/abs/path/to/project" });
try {
// The file's post-plugin TypeScript, or undefined when it is not in the program.
const request = new AbortController();
const code = await service.transformFile("src/index.ts", {
signal: request.signal,
});
// Apply an unsaved edit and re-transform; a later transformFile reflects it.
const ok = await service.updateFile("src/index.ts", "export const x: number = 2;\n");
const next = await service.transformFile("src/index.ts");
} finally {
service.dispose();
}The constructor takes the same ITtscCompilerContext and launches the host synchronously; the host compiles in the background, so the first transformFile resolves once that compile lands. There is no request deadline: the host is running your own project’s transforms, and how long that takes is not the service’s call. A request still settles the moment the host dies, because the reader closing is what the client waits on.
transformFile(name)→Promise<string | undefined>. The file’s post-plugin TypeScript source. A relativenameresolves against the project root. Both relative and absolute names then follow their filesystem links, including links inside the project, so a file is named as the resident Program loaded it. Resolvesundefinedwhen the physical file is outside the compiled program. Rejects when the project failed to compile, carrying the host’s diagnostics in the rejection.updateFile(name, content)→Promise<boolean>. Apply new in-memory content for a file and re-transform.nameresolves as it does fortransformFile. Returns whether the rebuild succeeded;falsemeans the edit did not compile and the previous transform is still in effect.dispose(). Terminate the resident host and reject any in-flight requests. Always call it (usetry/finally) so the host process does not leak.
Pass { signal } as the optional final argument to transformFile or updateFile to cancel one call. Passing an already-aborted signal rejects only that call before it enters the host’s FIFO. Once a request has been written, cancelling it retires the host because a later reply could no longer be safely paired. Any concurrent requests reject with a distinct host-retirement error. TtscService then fails closed: later calls reject with that error rather than respawning a host. Construct a new service when the caller wants a fresh resident program.
undefined means an absent file, and false means an edit that did not compile. A reply the host could never validly send (a malformed line, a non-object, or a well-formed object of the wrong operation shape) is a protocol failure: the call rejects, and a corrupt line can no longer be mispaired with a later request.
TtscService runs through the linked-plugin host, the only host that serves resident requests, so the project must declare at least one transform-stage plugin. It is source-to-source: it returns TypeScript, never JS or .d.ts (use compile() for emit).
The service is resident within one process. It does not share a host across separate worker processes (for example a Metro worker pool); each process that constructs a TtscService runs its own host. A shared cross-process daemon is tracked in #255 .
Context options
| Field | Purpose |
|---|---|
cwd | Working directory. Defaults to process.cwd(). |
tsconfig | Path to a project config file. Relative paths resolve against cwd. Omit to auto-discover the nearest tsconfig. |
projectRoot | Source root for source membership, descriptor and native plugin process cwd, cache anchoring and returned relative Go sources. Omit to use the resolved config directory; a common ancestor admits sibling workspace sources. Relative paths resolve against cwd. |
pluginConfigDir | Consumer directory for plugin package resolution, factory contexts, plugin config-file discovery and relative configFile resolution. Omit to use the selected tsconfig’s directory independently of projectRoot. Generated wrappers supply the original consumer directory. Relative paths resolve against cwd. |
binary | Pin a specific TypeScript-Go binary. For test rigs and embedded toolchains; normal callers omit it. |
env | Environment overrides for executable selection, source builds and child processes. |
cacheDir | Root for compiled ttsc artifacts. Relative paths resolve against cwd. Defaults to TTSC_CACHE_DIR, else the workspace-local node_modules/.cache/ttsc. |
plugins | undefined = read project plugins. false = ignore project plugins for this instance. Array = explicit entries. |
Construction copies env overrides; each operation merges them over its current process.env. The resulting environment governs executable selection, compiler option and project-reference inspection, Go source builds for plugins and the native compiler host, child processes, and cleanup. An unrelated override preserves inherited TTSC_TSGO_BINARY and TTSC_BINARY. The explicit binary option takes precedence over TTSC_TSGO_BINARY.
On Windows, environment names are case-insensitive, so a constructor ttsc_cache_dir overrides inherited TTSC_CACHE_DIR; within one supplied environment object, the lexicographically first spelling wins. On POSIX, differently cased names remain distinct.
An own undefined entry overrides the inherited entry, after which selectors apply their normal fallback. Empty cache selectors also use their normal fallback. An explicit cacheDir takes precedence over environment cache selectors. Cleanup protects the selected caller GOCACHE, including when it overlaps an explicit whole-cache target.
Filesystem path identity
Tool hosts that deduplicate watch inputs, cache keys, or workspace roots can use the same filesystem identity contract as ttsc through the ttsc/path-identity subpath.
import { createFilesystemPathIdentityContext } from "ttsc/path-identity";
const identities = createFilesystemPathIdentityContext();
const workspace = identities.resolve("/work/project");
const config = identities.resolve("/work/project/tsconfig.json");
console.log(config.key); // canonical comparison key
console.log(config.path); // canonical physical spelling
console.log(identities.isWithin(workspace.path, config.path)); // trueCreate one context per filesystem transaction and reuse it for every comparison in that transaction. The context memoizes normalized input identities, each realpath prefix at its first probe, and each directory case decision when missing-suffix resolution or caseSensitive() first needs it. Resolving a descendant can therefore populate an ancestor’s realpath cache before that ancestor is resolved directly, while resolving an existing directory need not probe its case behavior. This keeps repeated comparisons consistent and avoids repeated filesystem probes, but it is not an atomic filesystem snapshot: a probe first reached later can observe a newer filesystem state.
Existing path segments use their physical spelling. For a prospective path whose nearest existing prefix is a directory, missing suffixes fold ASCII letters only when that directory is observed case-insensitive. Sensitive or unknown directory behavior preserves spelling; an OS name supplies no fallback case policy. Non-ASCII names retain their spelling because an ASCII probe does not establish native Unicode equivalence. Windows drive and UNC roots are matched case-insensitively.
resolve(location) returns { key, path }. Use key for maps and sets, and retain path when a filesystem operation needs canonical spelling. isWithin(root, candidate) compares the same canonical identities. caseSensitive(directory) returns true for observed sensitivity, false for observed insensitivity, or undefined when the probe cannot establish either. With unknown behavior, different keys for missing spellings do not prove distinct physical entries or complete watcher coverage.
For observation routing, lexicalKey, lexicalMatches and lexicalIsWithin retain link and short-name components. Matching admits possible ASCII case aliases under unknown policy and conservatively retains non-ASCII component pairs as candidates. These methods select observations to examine; they do not establish physical equality or authorize cache reuse.
Realpath errors other than a missing entry throw by default. A bundler or editor that deliberately tolerates files moving during a scan can construct the context with { throwOnRealpathError: false }; unreadable locations are then treated as missing for that transaction. The optional platform, realpath, and caseSensitive operations are for controlled host integration and tests. Most callers should use the defaults.
Platform binary
Tool hosts that run one of the platform binary’s helper commands find the binary the way ttsc does through the ttsc/binary subpath: an absolute TTSC_BINARY, then the @ttsc/<platform>-<arch> package, then the repository’s own dev layout. @ttsc/unplugin starts its Linux watch helper this way, so the helper always comes from the binary ttsc itself uses.
import { resolveBinary } from "ttsc/binary";
const binary = resolveBinary(); // absolute path, or null when none is installedConfig reading
Tool hosts that read a project’s tsconfig.json themselves, for its paths or for the files its include admits, read it by ttsc’s rules through the ttsc/tsconfig subpath. A host that parsed comments, followed extends, or resolved a plugin path its own way could disagree with the compiler about what the project says. @ttsc/unplugin reads the configs behind its alias overlay and project membership this way.
import fs from "node:fs";
import { parseJsonc, resolveTsconfigExtends } from "ttsc/tsconfig";
const config = parseJsonc(fs.readFileSync("tsconfig.json", "utf8"));
const base = resolveTsconfigExtends("tsconfig.json", "./tsconfig.base");parseJsonc(text)parses a config as TypeScript reads one. A byte-order mark, comments, and trailing commas are allowed, and a syntax error keeps the line and column of the original text.resolveTsconfigExtends(tsconfig, specifier)returns the path anextendsvalue names, by TypeScript-Go’s rule. After\is folded into/, a rooted,./, or../specifier names the file or the file with.jsonappended, under the spelling it was reached by. Any other specifier is resolved as a module to its physical path, and a bare package root is resolved through itspackage.json#tsconfig. It throws when nothing matches.tsconfigExtendsFileCandidates(tsconfig, specifier)returns the exact files a file specifier tries, in order, orundefinedfor a module specifier. A host that watches for a missing base config to appear watches these.isRelativePluginSpecifier(specifier)tells whether a plugin entry’stransformis a relative path, which resolves from the config that wrote it, rather than a package, which resolves from the project root.
Plugin source
Tool hosts that cache a transform’s output prove its pluginSources through the ttsc/plugin-source subpath, with the rule the plugin build itself applies. @ttsc/unplugin and @ttsc/metro prove each plugin’s Go source this way, so a plugin edited in place, or built under another environment, invalidates what it produced.
import { pluginSourceStateHolds } from "ttsc/plugin-source";
const stale = Object.entries(t.pluginSources ?? {}).some(
([directory, state]) => !pluginSourceStateHolds(directory, state),
);-
pluginSourceState(directory, options?)returns the state the build keys a binary on. It is a SHA-256 over two things:- the relative path and bytes of every file below the directory, in sorted order, skipping files below a
node_modules,.git, or.ttscdirectory, generatedgo.workfiles, package tarballs, editor sidecars, and backups ending in~; - the build environment there: the Go compiler ttsc resolves for the directory, the values
go envreports for the variables the build keys on (GOFLAGS,GOOS,CGO_ENABLED, the C toolchain’s commands by content, GOROOT by content, and the others), and cgo’s external toolchain variables.
The sources are read on every call. Reading the environment costs a
go envrun and a GOROOT walk. Underprocess.env, the process keeps that reading per directory and set of variables while its witnesses still match: the Go tool, the filego env -wwrites, the C toolchain executables and GOROOT.Changed witness metadata triggers a fresh reading at the next call. This requires edits to alter the captured identity or timestamps; reproducing the entire metadata signature falls outside that proof.
options.envrequests a fresh reading under the supplied environment. An unreadable listed file throws, as it would during a build. - the relative path and bytes of every file below the directory, in sorted order, skipping files below a
-
pluginSourceStateHolds(directory, state, options?)tells whether a state still holds. It compares under the process’s reading of the environment, which follows a changed toolchain aspluginSourceStatedescribes, and when the state still does not match it reads the environment again before it refutes the state, so a consumer never keeps refuting output its own compiler just built from a fresh read. Prove a reported state with this rather than by comparingpluginSourceState.options.sourceDigesthands over the sources’ digest from a reading the host can vouch for, which the proof then does not read again. -
processPluginBuildEnvironment(directory, refresh?)returns this process’s reading of the environment a build in the directory is keyed on, the onepluginSourceStatecombines with the sources. A host that skips re-proving a plugin source while its watcher stays silent compares this reading too, since a watcher over the sources never sees the toolchain outside them;@ttsc/unpluginrecords it beside each source directory it proved.refreshreads it again rather than reusing the kept reading. -
pluginBuildVersions(projectRoot)returns the ttsc version and the version of thetypescriptpackage the project resolves, the two versions every plugin build is keyed on. A host that keeps output beyond the process that produced it names the output by them, as@ttsc/unplugin’s shared compile store does. -
collectPluginSourceFiles(directory)returns the files the state’s sources part reads, as absolute paths in sorted order, andpluginSourceDigest(directory)their digest. A host that proves a source on every delivery may keep a digest while the metadata of exactly these files holds still, provided each stamp had provably left its clock tick before the read, and hand it topluginSourceStateHolds.pluginSourceFilesSignature(directory, evidence)takes that metadata: it signs exactly these files, each by its relative path and the signatureevidencegives it, and tells whether every stamp is separable, which only the host can judge, since the host owns the clock reference.@ttsc/unpluginkeeps a digest this way, so a source its watch cannot vouch for costs a metadata walk rather than a read of every file, and so doesresolveCapabilityPlugins’ cache across processes. That cache records each descriptor input with the proof the plugin load took while the descriptors evaluated, not a later reading, and records no answer with an input the load could not prove, so the next call walks again. -
prunesPluginSourceDirectory(name)tells whether the state passes over a directory of that name. A host watching a plugin source as a subtree skips such a directory, rather than watching a nested package tree or a repository’s object store. -
pluginSourceCovers(directory, file, kind)tells whether a path below a plugin source directory can bear on its state: none of the directories on the way to it is one the state passes over. With"directory"the path is a directory a host would watch, and its own name counts. With"entry"it is the path an event names, which may be a file the build reads whatever it is called, so only the directories above it count.ttsc --watchand@ttsc/unplugindecide what they hear of a plugin source with it.
Diagnostics
ITtscCompilerDiagnostic is a flat, JSON-friendly shape independent of TypeScript-Go’s internal Go types.
interface ITtscCompilerDiagnostic {
file: string | null; // null for global diagnostics
category: "error" | "warning" | "suggestion" | "message";
code: number | string; // numeric for TS, string for plugin codes
start?: number; // 0-based char offset
length?: number;
line?: number; // 1-based, display convention
character?: number; // 1-based, display convention
messageText: string; // chains are pre-flattened
}When the host process exits non-zero but produced no parsable diagnostics, ttsc synthesizes a { code: "TTSC_PROCESS", category: "error" } entry that carries the captured stderr/stdout. The same entry is seeded when a plugin fails without reporting structured diagnostics and ttsc recovers pure TypeScript errors through its independent no-emit check, so the recovered type errors never displace the plugin failure. Embedders can branch on code to detect this synthesized case.
Plugins
Project plugins are taken from compilerOptions.plugins in the project’s tsconfig and from installed package markers. The constructor’s plugins field lets embedders override that resolution for one compiler instance:
// Run the project without any plugins at all.
const bare = new TtscCompiler({ cwd, plugins: false });
// Run the project with a different plugin set than the tsconfig requests.
const lintOnly = new TtscCompiler({
cwd,
plugins: [{ transform: "@ttsc/lint" }],
});See ITtscProjectPluginConfig for the entry shape and Plugin authoring for how to ship a plugin package.
Patterns
Long-lived service
Build one TtscCompiler per watched project at startup. Call prepare() once, then compile() whenever you need a fresh build; each compile() reloads the project, so the construction cost is just descriptor wiring, not the compile itself.
When the long-lived tool transforms the same project’s files repeatedly rather than emitting full builds, use TtscService instead: it keeps the host warm and answers per-file transform requests without recompiling per call.
CI warmup
const compiler = new TtscCompiler({ cwd });
compiler.prepare(); // pre-build native plugins into cachePair this with caching TTSC_CACHE_DIR between CI runs to skip the rebuild on subsequent invocations. That root contains both source-plugin binaries and, unless TTSC_GO_CACHE_DIR or GOCACHE is set, the Go object cache used while building them.
Driving a Web Worker / Browser
TtscCompiler is a Node API, it spawns native processes. For browser embedding, use @ttsc/wasm directly or the higher-level @ttsc/playground package.
Stability
TtscCompiler, TtscService, the result types, ITtscCompilerContext, and the ttsc/path-identity, ttsc/binary, ttsc/tsconfig, and ttsc/plugin-source subpaths are the public programmatic surface. CLI launchers, the rest of the project config reader, and native build helpers stay internal, depending on them will break. The package exports field enforces this.