Reference Plugin Tour
A guided source-code reading of every plugin that ships with ttsc. By the end you should be able to open packages/<name>/ in your editor and follow what every line does.
Prerequisite: skim Concepts. The tour assumes the vocabulary: descriptor, sidecar, stage, subcommand, shim, auto-discovery, and does not redefine those terms inline.
The shipped plugins are ordered by difficulty:
@ttsc/banner: insert a JSDoc preamble at the top of every emitted file. Smallest possible transform; the right place to start.@ttsc/strip: deleteconsole.log,debugger;, and matching call statements before emit. Your first real AST mutation: filter aNodeList, recurse into block-bodied statements, replace embedded statements with anEmptyStatement.@ttsc/paths: rewriteimport "@lib/x"intoimport "./modules/x.js"after consultingcompilerOptions.paths,rootDir,outDir, and the actual Program source files. Introduces tsconfig parsing, AST-leaf-text mutation (the synthesize-flag invariant), and the visitor that finds every module-specifier shape TypeScript supports.@ttsc/lint: full diagnostics engine, plus its public Go API for third-party rule contributors. The advanced tier: rule registry, severity ladder, fix/format dispatch, build-time contributor merging.
Pages 1β3 each take a single Go file and walk it line by line. Page 4 is a longer architectural tour because the lint engine is many files, but the reading pattern stays the same: open the file in the repo, then read this page as the commentary track.
Pre-reading: the two halves
Every plugin in this tour follows the two-halves model:
packages/<name>/
βββ package.json β "ttsc": { "plugin": { "transform": "@ttsc/<name>" } }
βββ src/
β βββ index.ts (or .cjs) β JS descriptor β exports { name, source, stage }
βββ go.mod β Go module declaration for the native source
βββ plugin/
βββ main.go β Optional executable sidecar entrypoint
βββ <name>.go β Package-local helper
βββ driver/
βββ <name>.go β Optional linked transform packageThe JS descriptor lives in node_modules on consumer machines and tells ttsc where the Go source is. The selected Go source is built once and cached; executable package main sources are spawned with subcommands like transform, build, or check, while linked transform packages run inside the selected native host.
If anything in that paragraph feels new, read Concepts β Plugin protocol first.
Where the real logic lives
The package-local sidecars at packages/banner/plugin/main.go, packages/strip/plugin/main.go, and packages/paths/plugin/main.go are small standalone utility dispatchers. The published descriptors for those transform plugins point at driver/, so consumer builds run the linked transform package inside the selected native host; the statement-removal code lives in packages/strip/driver/strip.go.
Transform packages can also expose linked Go packages instead of executable command packages. A linked package calls driver.RegisterPlugin from init() and implements driver.SourcePreamblePlugin and/or driver.ProgramPlugin; ttsc copies it into the selected native host at build time. This is capability-based, not package-name based: any transform descriptor whose source resolves to a non-main Go package uses the same path.
Practical consequence, flagged again on each walkthrough as it becomes relevant:
- For learning, the shipped plugins are the best worked examples in the repo: production-quality code, small enough to read end-to-end. The walkthroughs point you at the right files (
driver/<name>.gofor linked transform logic;plugin/main.gofor the entrypoint contract). - For your own plugin, use
driver.LoadProgramwhen you own an executable host, or export a linked Go package withdriver.RegisterPluginwhen your transform is a library that should ride inside another host. The canonical executable fixture istests/projects/go-source-plugin-checker.
@ttsc/lint is different: its real engine lives in packages/lint/linthost/, and the sidecar at packages/lint/plugin/main.go is a linthost.Main one-liner. That setup is reusable, the lint deep dive shows how to copy the pattern when your plugin grows past one file.
How to read these pages
Each walkthrough has the same structure:
- What it does for the consumer. A two-line summary plus a copyable
compilerOptions.plugins[]entry, so you can sanity-check what the plugin promises before you read its code. - Directory layout. Where every file lives in the repo, and which file is βthe real oneβ to read.
- JS descriptor. Short: usually 10 lines, but it pins the descriptor shape every plugin must satisfy.
- Go module file. What goes in
go.modand why. Especially for Go beginners. - Native entrypoint or linked source. The
main.godispatcher ordriver/package, line by line. - The real logic. The bulk of the page: a guided read of the actual transform / check code in production.
- Beginner annotations. Sidebars that explain Go-specific constructs (
flag.NewFlagSet, pointer receivers,defer, etc.) the first time they appear. - What to copy, what to ignore. Closing summary of which patterns generalize to plugin authors and which are just package-local implementation details.
You do not need to run anything. Open the cited file in your editor, scroll along with the walkthrough, and the patterns stick faster than building a fresh plugin from scratch.
Once these are familiar, the canonical βbuild a third-party plugin from scratchβ exercises are:
- Authoring β Getting started: the Hello plugin (~80 lines of
main.go). - Authoring β End-to-end transform:
debugger;removal with real AST mutation.
The tour pages assume you have read Concepts but not necessarily Authoring. If you prefer βwrite your own first, then read someone elseβs,β do Authoring first.
Combined fixture
After all four plugins look familiar, this is the smallest tsconfig that uses every one of them in one project:
{
"compilerOptions": {
"paths": {
"@lib/*": ["./src/modules/*"],
},
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"plugins": [
{ "transform": "@ttsc/lint" },
{ "transform": "@ttsc/banner" },
{ "transform": "@ttsc/strip" },
{ "transform": "@ttsc/paths" },
],
},
"include": ["src"],
}lint.config.ts:
import type { ITtscLintConfig } from "@ttsc/lint";
export default {
rules: { "no-var": "error" },
} satisfies ITtscLintConfig;banner.config.ts:
import type { ITtscBannerConfig } from "@ttsc/banner";
export default {
text: "License MIT",
} satisfies ITtscBannerConfig;@ttsc/lint runs first as a check-stage plugin and aborts emit on error-severity findings. The three transform-stage plugins then run inside one compiler emit pass through linked packages: @ttsc/banner overlays the source preamble, @ttsc/strip filters statements, @ttsc/paths rewrites module specifiers. TypeScript-Go owns the final emit of JavaScript, declarations, and source maps.
This exact fixture is locked by tests/test-ttsc/src/features/utility-plugins/test_ttsc_utility_plugins_lint_banner_paths_and_strip_run_together_in_ttsc_build.ts, if anything in the walkthroughs ever drifts from the shipped behavior, that test catches it first.
See also
- Architecture: the build/cache pipeline behind every plugin in this tour.
- Driver API: the curated Go faΓ§ade every third-party plugin should call.
- AST & Checker: the shim surface every plugin in this tour imports.
- Pitfalls: the failure modes the walkthroughs reference but do not redocument.