Two coexisting TypeScript compiler lanes
The framework can compile a package with either the established TypeScript compiler or the new native one, package by package, with a single switch to roll everything back.
- What it is
- Mechanism — something you declare.
- What it does
- Two compiler versions coexist in the framework repository, and each package is on one of them by an explicit entry in a ledger — so a migration is a per-package promotion and a fleet-wide rollback is one variable.
- What you decide
- Promote a package by adding its name to the ledger. Roll the fleet back with the override variable — which beats the ledger for every package at once, which is what an operator needs at the moment they need it. A separate compatibility harness measures diagnostic and emit parity between versions before a package is promoted.
- Where it stops
- This is framework-repository machinery, deliberately not propagated.
The framework compiles itself with two TypeScript compilers at once. A ledger names which packages use the newer native compiler; everything else uses the established one. Both are installed side by side under distinct package names, so promoting a package is adding a name to a list, and one environment variable puts the entire fleet back on the compiler that has always worked.
The problem it solves
Changing the compiler of a large codebase is normally an all-or-nothing event. You bump one dependency, every package retargets at once, and whatever breaks breaks together, in packages maintained by different people, with no way to tell a real defect from an artefact of the move. The usual mitigations are worse than the problem: a long-lived branch that diverges, or a per-package build script edit, which means the rollback is as many edits as the migration was and a partial revert is the state nobody notices.
The second problem is quieter. Once two compilers can be installed, the name of the binary stops identifying which one runs. Whichever package wins the shared executable name is an accident of install order, and a build that used the wrong compiler produces output that looks correct.
What it does
Two compiler versions coexist in the framework repository, and each package is on one of them by an explicit entry in a ledger — so a migration is a per-package promotion and a fleet-wide rollback is one variable.
A lane is a named pair of a package and an expected identity, resolved by ASKING the package what it is: its own manifest is read, the name is asserted and — for the new lane — the exact version; then the executable that package itself declares is spawned with an argument array. Nothing resolves a bare binary name, and nothing goes through a shell, so a checkout path containing a space is inert. The established lane deliberately asserts no version, because its version is owned by the workspace pin and duplicating it here would create a second authority that drifts.
The compiler is resolved from the invoking package’s own workspace, and a workspace that has not installed the lane’s compiler is REFUSED rather than allowed to borrow one from a parent. That refusal is the boundary that keeps a nested application workspace from silently inheriting the framework’s compiler: resolution walks upwards, the borrowed binary exists and works, and nothing would ever report it.
Incremental build state is scoped per lane, so state written by one compiler can never be read by the other — rewritten by the runner rather than left to each caller to remember, because a shared state file fails as a confusing stale-type error far from its cause.
The lane only decides which compiler emits into the private staging area. The finalizer, the publication journal, the writer lock and the publication ordering are all downstream and unchanged — so a lane flip cannot corrupt a published artifact; the worst case is a package that fails to compile, which fails closed and leaves the previous generation standing.
Because the split is held together by four surfaces that cannot import one another, a dedicated check keeps them in agreement: the declared version against the asserted one, the separate declaration a nested workspace must carry, a ledger entry naming a package that does not exist, a ledger entry for a package with no lane to select, and the executable staying a registered binary of a workspace package.
What you decide
Promote a package by adding its name to the ledger. Roll the fleet back with the override variable — which beats the ledger for every package at once, which is what an operator needs at the moment they need it. A separate compatibility harness measures diagnostic and emit parity between versions before a package is promoted.
Limits
This is framework-repository machinery, deliberately not propagated. The compiler is on the excluded list of dependency alignment, on the stated ground that a compiler version is an application’s own choice — so a generated application pins its own and this ledger says nothing about it.
The parity comparison is keyed on the POSITIONS a compiler rejects rather than on the diagnostic codes it reports, because across a major version the same defect at the same position can be reported under a different code — and a code-keyed comparison reports complete agreement as a regression.
One subsystem stays on the established compiler by decision rather than by omission. Its build inputs are sealed by content and the new compiler’s executable is platform-specific, so sealing what actually runs would give the same source a different identity on a developer machine and on a build server.