Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part 01 — The Roslyn-shaped gap in the TypeScript ecosystem

The argument has to start somewhere honest. The TypeScript ecosystem is not codegen-poor — it has, at a rough count, more than fifteen mature tools whose stated job is some shape of "produce TS files from other inputs". Listing them and dismissing them as inferior would be a mistake, and a reader who has actually shipped a plop template or a ts-patch transformer will close the tab. Nothing in those tools is broken. Something specific is missing, and that is a different claim.

The claim made here, and defended over the next eleven articles, is that the TS ecosystem has no equivalent of the Roslyn shape known since 2020 as ISourceGenerator (and its 2022 refinement IIncrementalGenerator): a multi-stage feedback codegen pipeline in which each stage observes the prior stage's outputs through a virtual filesystem, in which convergence is a fixpoint property and not a hand-counted iteration limit, in which the contract is strictly additive, and in which the commit to disk is atomic. Every tool already in the TS ecosystem solves a different problem; the multi-stage feedback shape is the gap. @frenchexdev/ts-codegen-pipeline fills it.

This article does three things. First, it recalls the Roslyn ISourceGenerator mental model — briefly, since the audience is assumed to be Roslyn refugees in TS land. Second, it surveys the existing TS codegen tools by category and shows precisely where each one stops short of the multi-stage feedback shape. Third, it reproduces the analogue table from the library README and uses it to position @frenchexdev/ts-codegen-pipeline against the C# vocabulary the audience already carries.

What ISourceGenerator is, in three paragraphs

In the C# world, a Roslyn source generator is a class that implements ISourceGenerator (or its incremental successor IIncrementalGenerator). It receives a GeneratorExecutionContext carrying the Compilation — every syntax tree, every symbol, every reference the compiler currently knows. It calls context.AddSource(hintName, sourceText) to inject new C# files into that compilation. Those files become part of the compilation — visible to the type-checker, visible to other generators, visible to the rest of the compiler pipeline. The user's source files are never modified. Generators run as a phase of csc itself; the compiler iterates them until convergence.

The two properties that matter for this series are: (a) feedback — a generator's output is visible as input to subsequent generators in the same build, and (b) strict additivity — generators can never modify user source. Roslyn's analyzer/code-fix pipeline can suggest edits to user code, but a generator can only emit. The two roles are deliberately split: generators add code, fixers transform code with consent. That split is what lets Roslyn iterate generators until a fixpoint without risking infinite oscillation.

The third property, sometimes glossed over, is determinism. Roslyn generators are required to produce the same output for the same input. The compiler caches generated text by an EquatableArray<...> model in IIncrementalGenerator; non-deterministic generators break the cache and turn green builds red. The C# community learnt this the hard way — early ISourceGenerator implementations leaked timestamps into output, and the fix was the harder-to-misuse incremental API. The TS port keeps the determinism contract but does not recreate the incremental machinery. Why it does not is the load-bearing argument of Part 12.

What the TS ecosystem has — and where each tool stops

Surveying TS codegen tools by category, four families emerge. None of them is wrong; none of them is what ISourceGenerator is.

Family 1 — Templating engines

plop, hygen, yeoman, and nx generators are the dominant TS templating engines. Each takes a template and a context object, asks the user a few questions, and produces files on disk. They are excellent for one-shot scaffolding: create a new component, create a new endpoint, bootstrap a new package. They are also, by design, oblivious to the AST of the project they generate into. A plop template that produces a UserController.ts does not know whether UserController already exists, does not type-check the generated file in the consumer's tsconfig, and does not coordinate with another template that might be generating an interfering UserService.ts. They are syntactic, not semantic; one-shot, not iterative; isolated, not composable.

The shape of the gap is: a templating engine cannot do feedback. There is no representation of "stage 2 reads what stage 1 emitted in iteration 0 and emits in iteration 1". The templates are independent invocations, sequenced by a run script, with no shared in-memory project they could read from each other.

Family 2 — Compiler-pipeline transformers

ts-patch and ttypescript (and the older ttypescript-by-config approach used by vite-plugin-typescript-transform, ttsc, and similar) hook the TypeScript compiler itself. The transformer receives the AST mid-compilation, mutates it in memory, and the compiler emits the mutated form. This is the canonical way to do constexpr-style code rewriting in TS today: macro-style libraries like ts-nameof, typescript-is, ts-transformer-keys all work this way.

Two things separate this family from ISourceGenerator. First, transformers mutate — they rewrite the AST of user code, which is exactly the operation Roslyn generators are forbidden to perform. Second, transformers do not have a multi-stage feedback story. You can chain transformers, but the chain is linear and one-pass; there is no fixpoint loop, no concept of "this transformer reads what that transformer emitted on the previous iteration". The compiler is invoked once, the chain runs once, the result is the result.

Compiler-pipeline transformers are also famously version-fragile. ts-patch literally patches the tsc binary; ttypescript re-implements parts of the compiler driver. Every TypeScript point release risks breaking the patch, and the compiler team has been consistently cool to a stable transformer API. That fragility is structural, not incidental; it is why the family has stayed small relative to the templating one.

Family 3 — Bundler-side and Rust-side codegen

unplugin, swc plugins, esbuild plugins, and the more specialised vite-plugin- ecosystem do codegen at bundle time, often by consuming non-TS inputs (GraphQL schemas, .proto files, OpenAPI specs) and producing TS files that get bundled into the application. They are excellent at what they do; tools like gql.tada, proto-loader, and openapi-typescript ship serious value through this path.

Two things separate this family too. First, the AST work is delegated to specialised parsers — swc plugins live in Rust, esbuild plugins are constrained by the bundler's plugin API, neither has a ts-morph-grade view of the consumer's TS project. Second, the codegen step is a side-effect of bundling, not an independently runnable phase. You cannot say "run my pipeline to fixpoint, commit, then build" — you say "start the bundler" and codegen happens as one of its passes.

The shape of the gap remains: the codegen is single-pass, externally driven by the bundler's lifecycle, and the inputs are non-TS schemas. The fixpoint property — iterate until no further emissions — does not appear because the bundler is the loop and the codegen is one of its inner steps.

Family 4 — Project-system generators (Nx, Turborepo, custom)

nx generators deserve a separate mention because they are the closest organisational match to a Roslyn source generator. They run as a phase of the monorepo tool, they can read the project graph, they can emit files. They are how a serious Nx-based shop scaffolds new libraries today.

Even so, they are one-shot scaffolders, not feedback codegen. An nx generator produces files, the user reviews them, and the next time the generator runs it generates new files for new targets — not because it iterated and converged, but because the user invoked it again. There is no virtual filesystem, no fixpoint loop, no notion of stage 0 reading stage 2's output. They are good at their job; their job is not multi-stage codegen.

Naming the gap

Across all four families, the missing primitive is the same: a virtual filesystem in which generators emit, observed by other generators in subsequent passes, terminating by fixpoint, committed atomically.

That sentence has four load-bearing terms. Virtual filesystem — emissions live in memory, not on disk, until convergence. Observed by other generators — stage 2 sees what stage 1 emitted; stage 1, on the next iteration, can see what stage 2 emitted in response. Terminating by fixpoint — the loop stops when an iteration produces no new emissions; iteration counts are a circuit-breaker, not a configuration knob. Committed atomically — the disk receives every emission together or not at all; a mid-loop failure leaves the prior disk state intact.

No tool in the TS ecosystem provides those four properties together. plop has none of them. ts-patch has half of committed atomically (the compilation either emits or fails) but no virtual filesystem of generator-visible outputs and no multi-stage feedback. swc plugins have a private memory model but no fixpoint. nx generators have everything except feedback and fixpoint. The composite is not present anywhere.

Roslyn does provide all four properties. That is what makes it the right reference point. The library this series introduces is the TS port of those four properties — minimum viable, no aspiration, no extra primitives. What it took from Roslyn it took deliberately. What it left behind it left behind deliberately. Both decisions are argued in Part 12.

The analogue table, reproduced

The library README states the mapping in five rows. They are reproduced here because every later article in this series will refer back to one of them.

Concept (TS, this library) Roslyn analogue
SourceGenerator (interface) ISourceGenerator
GenerationContext GeneratorExecutionContext
virtFs.addSource(...) ctx.AddSource(name, text)
Fixpoint loop Multi-pass codegen with feedback
Strictly-additive emission Roslyn's "generators can never modify user source"

A reader new to Roslyn can take the right column on faith and pick up the left column from this series. A reader who knows Roslyn can read the right column as a contract: every Roslyn property in that table holds in the TS port, with the caveats discussed in Part 12. A reader who knows neither can use the table as a glossary; the four engine chapters (03, 04, 05, 06) walk every left-column term against the running example.

The only row that needs commentary in this article is the last one. Strictly additive is the contract that a TS reader is most likely to misunderstand on first reading, because the TS ecosystem does include tools (ts-patch, the macro libraries) that do mutate user code and produce useful results. The Roslyn community fought that battle and concluded that mutation must be split off into a separate role (analyzer/code-fix, with explicit user consent) so that codegen itself can be safely iterated to a fixpoint. The TS port inherits that conclusion. A generator does not mutate src/foo.ts. It emits src/foo.companion.generated.ts, or generated/foo.dto.generated.ts, and the user composes the two via inheritance, mixin, or TS module augmentation. Part 04 shows the constraint enforced under load.

What this article does not claim

Three claims would be tempting to make here and are deliberately not made.

The first non-claim is that the existing TS codegen tools are bad. They are not. plop is excellent for scaffolding. ts-patch is the right tool for in-place AST rewriting. swc plugins are the right tool for bundle-time, schema-driven codegen. The argument is not that any of those tools should be replaced; it is that the multi-stage feedback shape is a fifth, distinct primitive that the ecosystem currently lacks, and the existence of the four other families does not fill it.

The second non-claim is that @frenchexdev/ts-codegen-pipeline is the only possible shape of a TS source generator API. It is one shape, taken seriously from Roslyn, with a small set of explicit non-goals (no IIncrementalGenerator parallel, no syntax-tree visitor framework — those decisions are argued in Part 12). A different design, more ambitious or more minimal, could occupy the same niche. The argument is for the shape, not for this specific implementation as the unique correct one.

The third non-claim is that feedback codegen is the most important missing primitive in the TS ecosystem. That is a comparative claim across many primitives and not the kind of claim a focused series should make. The claim made here is narrower: this primitive is missing, the gap is real, this library fills it, and the next eleven articles show what falls out.

Bridge to the rest of the series

Part 02 tours the canonical example — packages/sourcegen-example — that the rest of the series treats as curriculum. The example is not a hello-world: it weaves three decorator families across ten stages and reaches fixpoint in three iterations through a single deliberate backward-edge stage. Every later article references one stage of that pipeline or one row of the analogue table reproduced above.

The reader who wants to confirm the gap claim before continuing is invited to: open packages/ts-codegen-pipeline/src/runner/run-fixpoint.ts and read the convergence loop; open packages/sourcegen-example/sourcegen.config.ts and read the ten-generator list; then try to reproduce the backward edge in plop, ts-patch, or nx generators. The exercise is short and persuasive.

The Feature for this article is FEAT-TSGEN-01Locating the Roslyn-shaped gap in the TypeScript codegen landscape — declared in assets/features.ts. Its acceptance criteria are: the Roslyn mental model is recalled; templating tooling is compared and distinguished; compiler-hook tooling is compared and distinguished; the multi-stage feedback gap is stated; the Roslyn analogue table is reproduced. Every section above maps to one of those ACs.

⬇ Download