Internals: Build and Cache
This page is for debugging source plugin builds.
Cold Build Path
When ttsc sees a plugin source descriptor:
- Resolve
source. - Hash the plugin source and host versions.
- Reuse the cached binary on hit.
- Copy source into a scratch directory on miss.
- Generate a
go.workoverlay pointing atttscand its shims, plus (when the plugin’sgo.moddoes not declaregithub.com/samchon/ttsc/packages/ttsc) a synthesizedreplace github.com/samchon/ttsc/packages/ttsc v0.0.0 => <workspace overlay>block. Thereplaceblock is the seam that lets a plugin’s barerequire github.com/samchon/ttsc/packages/ttsc v0.0.0resolve to the workspacettscsource at build time. - Classify the source package. A
package mainsource is an executable sidecar. A non-maintransform source is a linked plugin package that must register throughdriver.RegisterPlugin. - For executable sidecars, run
go build -o .ttsc-plugin <entry>in the scratch directory. The leading-dot placeholder name makes the in-progress binary easy to spot ingo buildstderr; the cache move renames it toplugin(orplugin.exeon Windows). - For linked transform sources, synthesize or select a native host that blank-imports the copied package, then build that host binary instead.
- Move the built binary into the cache.
- Invoke executable sidecars with
check,transform,build,fix, orformat. Linked transform packages do not receive argv directly; the native host receives the subcommand and dispatches to registered driver hooks.
Cache Key Inputs
The cache key for a built plugin binary is sha256 of, in order:
ttsc=${ttscVersion}tsgo=${tsgoVersion}: the resolvedtypescriptversion.platform=${process.platform}/${process.arch}entry=${plugin entry package relative to its go.mod}go=${resolveGoCompilerIdentity(goBinary)}: the resolved Go binary’s file content hash andgo versiontext, not the install path.- Every effective Go build env value in
GO_BUILD_ENV_KEYS:GOOS,GOARCH,GOAMD64,GOARM,GOARM64,GO386,GOMIPS,GOMIPS64,GOPPC64,GORISCV64,GOWASM,GOFLAGS,GOEXPERIMENT,GOFIPS140,GO_EXTLINK_ENABLED,GCCGO,GCCGOTOOLDIR,CGO_ENABLED, the cgo compiler/linker variables,GOTOOLCHAIN, andGOROOTby toolchain-content fingerprint. Command-valued cgo settings also include the resolved tool binary identity. - Plugin source-directory hash. Skipped paths:
node_modules,.git,.ttsc,go.work,go.work.sum,*.tgz,*.tar.gz,.DS_Store,Thumbs.db, and editor backup files ending in~. - Overlay-directory hashes, sorted by directory name (
ttscand shim source under the host’snode_modules). - Contributor source-directory hashes, sorted by contributor name.
The Go-binary identity (5) and GO_BUILD_ENV_KEYS (6) are the load-bearing reason GOOS=linux GOARCH=arm64 ttsc does not poison the native-platform cache slot. Pathless Go identity also lets pnpm installs with different virtual-store paths share the same cached binary when the bundled compiler content is identical.
The cache key does not include:
- consumer TypeScript source files;
- plugin options such as lint
rules/extends,banner, orcalls; - CLI flags such as
--emitor--outDir.
README, JSON, schema, and other data files inside the plugin source directory do affect the cache key. This keeps //go:embed and other file-backed plugin data aligned with the built binary. Local workspace changes to ttsc’s Go host or shim overlay also invalidate the source plugin cache automatically.
Cache Locations
Default (project-local, the find-cache-dir convention):
<workspaceRoot>/node_modules/.cache/ttsc/plugins/<key>/plugin (plugin.exe on Windows)
<workspaceRoot>/node_modules/.cache/ttsc/go-build (GOCACHE while building)<workspaceRoot> is the nearest enclosing workspace root: the nearest ancestor holding a pnpm-workspace.yaml or a package.json with a workspaces field, else the nearest ancestor containing a node_modules, else the project root. Every package in a monorepo therefore shares ONE cache and a plugin builds once per workspace, not once per package. rm -rf node_modules (or deleting the repo) reclaims it. ttsc keeps no machine-global cache — a user-wide cache silently grew to hundreds of GB across tsgo/plugin version bumps, so it was removed outright.
Override:
TTSC_CACHE_DIR=/tmp/ttsc-cache npx ttsc --emit
TTSC_GO_CACHE_DIR=/tmp/ttsc-go-build npx ttsc --emit--cache-dir and TTSC_CACHE_DIR set the cache root for source-plugin binaries. A relative --cache-dir resolves from the effective ttsc working directory (--cwd when set); a relative TTSC_CACHE_DIR resolves from the project root; both store plugin binaries below <root>/plugins. The Go object cache used while building source plugins defaults to <root>/go-build; TTSC_GO_CACHE_DIR overrides that path, and an existing user GOCACHE is honored (and never treated as ttsc-owned) when TTSC_GO_CACHE_DIR is unset. For ttsx, only an explicit --cache-dir also anchors temporary runtime output below <cache-dir>/project/<pid>.
ttsc cache paths --json prints the active cache root, plugin binary cache root, Go build cache root, and cacheableRoots. Use that command in CI when the workflow wants to persist the right directories without hardcoding paths.
Clean. Removes the ttsc-owned cache subdirectories (plugins/, go-build/, and an external TTSC_GO_CACHE_DIR) plus the legacy project-local caches (node_modules/.ttsc, .ttsc) — never the parent cache root (it may be shared with other tools) and never a user GOCACHE:
npx ttsc clean
npx ttsc clean --cache-dir /tmp/ttsc-cacheThe cache is content-addressed, so it is not tied to a package-manager layout: npm installs, pnpm virtual-store paths, and workspace packages share a binary whenever the source plugin, contributors, ttsc overlay, Go toolchain content, platform, and build environment produce the same cache key.
ttsc updates a small last-used marker whenever a binary is reused. Once per day it opportunistically prunes default-cache entries unused for 30 days and trims the cache by least-recently-used order when it grows beyond the size budget. Explicit --cache-dir and TTSC_CACHE_DIR caches are not garbage-collected automatically because callers usually use them for CI or test isolation.
Build environment
The plugin builder reads Go’s effective build environment for the cache key. It sets GOWORK=auto, infers GOROOT from the bundled Go binary when GOROOT is unset, and passes a stable GOCACHE to go build for source-plugin compilation. The GOCACHE path itself is not part of the binary cache key because it is a build object store, not a binary input. The builder also injects the TTSC_*_BINARY trio into each plugin process so plugins can re-spawn ttsc helpers.
Cache-key inputs: GO_BUILD_ENV_KEYS
These effective Go build env values fold into the cache key when non-empty, so cross-compile targets, build-tag tweaks, cgo compiler/linker choices, FIPS mode, and toolchain roots never collide with the native build slot. ttsc reads them through go env, so go env -w settings are covered too. GOROOT is normalized to a toolchain-content fingerprint instead of the install path, preserving pnpm path sharing for identical bundled toolchains. Command-valued entries such as CC, CXX, AR, GCCGO, and PKG_CONFIG include the resolved executable content hash as well as the command string.
GOOS GOARCH GOAMD64 GOARM GOARM64 GO386 GOMIPS GOMIPS64 GOPPC64
GORISCV64 GOWASM GOFLAGS GOEXPERIMENT GOFIPS140 GO_EXTLINK_ENABLED
GCCGO GCCGOTOOLDIR CGO_ENABLED AR CC CXX FC PKG_CONFIG
CGO_CFLAGS CGO_CFLAGS_ALLOW CGO_CFLAGS_DISALLOW
CGO_CPPFLAGS CGO_CPPFLAGS_ALLOW CGO_CPPFLAGS_DISALLOW
CGO_CXXFLAGS CGO_CXXFLAGS_ALLOW CGO_CXXFLAGS_DISALLOW
CGO_FFLAGS CGO_FFLAGS_ALLOW CGO_FFLAGS_DISALLOW
CGO_LDFLAGS CGO_LDFLAGS_ALLOW CGO_LDFLAGS_DISALLOW
GOTOOLCHAIN GOROOTttsc also folds selected external cgo/pkg-config environment values into the key when they are set, including include/library search path variables (CPATH, LIBRARY_PATH, INCLUDE, LIB, LIBPATH), pkg-config search variables (PKG_CONFIG_PATH, PKG_CONFIG_LIBDIR, PKG_CONFIG_SYSROOT_DIR), and Apple SDK/deployment variables (SDKROOT, MACOSX_DEPLOYMENT_TARGET).
Source: packages/ttsc/src/plugin/internal/buildSourcePlugin.ts (GO_BUILD_ENV_KEYS and EXTERNAL_GO_BUILD_ENV_KEYS; the cache-key loop folds each non-empty effective value into the hash).
Injected into the plugin process
Before spawning a plugin, ttsc sets three env vars (TTSC_NODE_BINARY, TTSC_TSGO_BINARY, TTSC_TTSX_BINARY) in the child environment so the plugin can re-spawn the ttsc toolchain without resolving paths itself, plus a fourth (TTSC_PLUGIN_CONFIG_DIR) when the embedder declared a plugin config anchor:
TTSC_NODE_BINARY: absolute path of the Node binary that launchedttsc.TTSC_TSGO_BINARY: resolved tsgo native binary used by thisttscinvocation.TTSC_TTSX_BINARY: resolvedttsxlauncher.TTSC_PLUGIN_CONFIG_DIR(only when the embedding setspluginConfigDir): the directory that plugin config-file discovery and relativeconfigFileresolution anchor at. Build integrations that compile through a generated wrapper tsconfig set it to the real project root (the bundler adapters write such a wrapper into the system temp directory for aliases andcompilerOptionsoverlays); absent it, plugins anchor at the tsconfig’s own directory.
A plugin that wants to re-spawn ttsx or tsgo (for example to type-check a generated file) should call these vars rather than re-resolving from $PATH. They guarantee the same host the consumer ran. A plugin that discovers its own config file should anchor at TTSC_PLUGIN_CONFIG_DIR when set; the shipped utility plugins do this through driver.PluginConfigBaseDir. Sources: runBuild.ts and transformProjectInMemory.ts. See Recipes → Re-spawn ttsx or tsgo from a plugin for usage.
GOROOT is auto-injected only when unset
When the resolved Go binary is the bundled SDK shipped by @ttsc/{platform}-{arch}, ttsc infers GOROOT from the binary’s parent directory and injects it into the build env, but only if process.env.GOROOT is empty. A stale GOROOT exported by a shell rc file from a different Go install will be honored as-is and silently bind the bundled compiler to the wrong stdlib. If a freshly-installed plugin build fails with errors like runtime/runtime0.go: no such file or directory, unset GOROOT before re-running.
Go Toolchain Resolution
ttsc resolves the Go compiler in this order, first match wins:
TTSC_GO_BINARY, when set to a non-empty path.- The installed
@ttsc/{platform}-{arch}package’s bundled Go SDK, resolved viacreateRequireagainst@ttsc/{platform}-{arch}/bin/go/bin/go. - The workspace-development fallback
<repo>/packages/ttsc-{platform}-{arch}/bin/go/bin/go. - The legacy local-workspace fallback
<repo>/native/go/bin/go. $HOME/go-sdk/go/bin/go: the convention used byscripts/build-platform-package.cjsand by maintainers who bootstrap a bundled-Go layout without going through the platform package.- Literal
"go": a$PATHlookup; the development fallback.
Published consumer installs should land on step 2. Steps 3–5 exist for development workflows; step 6 should only fire when none of the above are present.
Debugging Build Failures
If go build fails, ttsc prints the Go stderr:
ttsc: building plugin "my-plugin" via "go build" failed:
.ttsc-plugin/main.go:42:8: undefined: shimast.SomethingCheck:
- missing shim
requireingo.mod; - missing source files in the npm tarball;
- wrong source directory or package entry;
- stale cache, fixed with
npx ttsc clean; - pnpm local dev layout, fixed in Editor setup;
- on POSIX,
ttscchmod 0755s the bundledgobinary and every file underpkg/tool/before invoking it; a sandbox or read-only mount that blocks chmod will surface here as a Go spawn failure.
Concurrency
Concurrent ttsc processes may build the same missing key at the same time. Scratch directories are unique, and the final move is atomic. This is wasteful but safe.
See also
- Walkthroughs: shipped plugins as worked examples.
- Pitfalls: failure modes specific to plugin authoring (including the
goToolchainNotFoundMessageexact text). - Driver API: the Go façade plugin authors should call.
- Plugin Protocol: the wire contract.