* chore(tooling): migrate workspace to TypeScript 7 * fix(docker): stage plugin manifests before native web type checking * perf(guard): batch native parsing with bounded AST retention * fix(tooling): port main's AST tests to native TypeScript
9.5 KiB
TypeScript 7 toolchain and migration evaluation
The workspace uses TypeScript 7.0.2 for tsc commands, including type checks,
JavaScript emission, declaration emission, and the web build's final type gate.
Dependency layout
Every workspace declares @typescript/native: npm:typescript@7.0.2, which
supplies tsc and the pinned native APIs.
Only apps/web retains typescript: npm:@typescript/typescript6@6.0.2 for
Next 16.2.6 configuration/type generation. It supplies the legacy JavaScript
compiler API and tsc6, without competing for tsc. Next still imports the old
API; removing this last alias requires a separate framework compatibility change.
This follows Microsoft's side-by-side installation guidance.
Repository-owned guards and tests no longer import typescript:
- Guards and AST-based tests use
@typescript/native/unstable/astand@typescript/native/unstable/sync. - Semantic regression tests request diagnostics from the native compiler, preserving their error-code and source-position assertions.
- Desktop snippets use Node 24's type stripping. Daemon/packaged test harnesses use esbuild for ESM/CommonJS emission, without pretending this is type checking.
- Guard parsing uses an explicitly scoped native process, no libraries or import resolution, and immutable in-memory projects. Regression tests ensure changed contents and TS/TSX syntax cannot reuse stale ASTs. Native processes and snapshots are released on success and failure. No virtual fixture is written to disk.
The unstable/* API is deliberately pinned to 7.0.2. Compiler upgrades must run
the focused guard and semantic-diagnostic tests, not just tsc --noEmit.
pnpm exec tsc --version reports 7.0.2. The remaining compatibility dependency
is isolated to Next; it is not used for repository type checks or JS emission.
Web build gate
Run pnpm --filter @open-design/web build, which executes:
next build && tsc -b --noEmit
Next generates route types and application assets. Its old in-process type check
is disabled in next.config.ts; native tsc checks the project, including the
generated types, before the package build can succeed. A type error therefore
still fails the build. Calling next build directly bypasses this final gate;
use the package build entry point, including for packaging and CI.
The standalone typecheck script remains tsc -b --noEmit. The project-level
compiler options explicitly list ambient types that older compilers loaded
implicitly. The e2e alias uses a relative paths target instead of the removed
baseUrl option. No strictness checks were disabled for this migration.
Evaluation scope
Baseline: commit 287009d37f, with the original mix of TypeScript 5.9.3 and
6.0.3. Measurements ran sequentially in the same worktree on an Apple M5 Pro
(15 logical CPUs, 48 GiB RAM), macOS arm64, Node 24.13.0, pnpm 10.33.2.
Dependencies and workspace build prerequisites were installed before measuring.
The timings below were captured at the initial compiler migration, before the
follow-up guard/test API migration. They are single-run wall-clock observations, including
pnpm orchestration. Web builds use the default static-export mode and start
without .next caches; the native run also discards the separate web
tsconfig.tsbuildinfo before the build. Full type checking starts without that
incremental cache. These are not fresh OS filesystem-cache measurements or CI
runner predictions. Existing generated package outputs are present on both sides.
| Command | Original | TS 7.0.2 | Speedup | Time saved |
|---|---|---|---|---|
pnpm typecheck |
46.87 s | 10.90 s | 4.30x | 76.8% |
pnpm --filter @open-design/daemon build |
6.11 s | 1.50 s | 4.08x | 75.5% |
pnpm --filter @open-design/desktop build |
0.93 s | 0.28 s | 3.36x | 70.3% |
pnpm --filter @open-design/web build |
36.65 s | 16.79 s | 2.18x | 54.2% |
The baseline Next build reported 20.8 s in its TypeScript stage. The remaining bundling and prerendering work explains why the full web build improves less than type checking alone. Workspace orchestration and esbuild work similarly limit the full-repository speedup.
Repeated compiler-only comparison
To separate compiler performance from pnpm orchestration, each compiler ran three times in alternating order on the same migrated sources and installed workspace outputs. Both checks passed in every round. Web incremental caching was disabled; daemon does not enable it. The comparison uses the original 5.9.3 compiler for both apps versus native 7.0.2, with each compiler's bundled standard library and default worker settings.
| Project | TS 5.9.3 median | TS 7.0.2 median | Speedup |
|---|---|---|---|
apps/web |
22.31 s | 2.51 s | 8.90x |
apps/daemon |
4.56 s | 1.52 s | 3.00x |
Commands from each app directory:
# Run with the baseline 5.9.3 compiler, then the native 7.0.2 compiler.
# Web:
tsc -p tsconfig.json --noEmit --incremental false
# Daemon:
tsc -p tsconfig.json --noEmit
The web measurements ranged from 21.42–25.24 s (5.9.3) and 2.32–2.92 s (7.0.2). The daemon measurements ranged from 4.55–4.66 s and 1.51–1.55 s respectively. This supports a substantial improvement on this machine without implying the same multiplier for every package or CI runner.
Validation
- Frozen-lockfile installation and postinstall builds passed. All 27 child
workspaces plus the root resolve
tsc --versionto 7.0.2. - Full workspace
pnpm typecheckand repositorypnpm guardpassed. - Focused existing tests cover deliverable selection and consumers of the legacy compiler API: 8 files, 150 tests passed across daemon, web, desktop, packaged, and e2e packages.
- Default static-export and standalone web builds, web sidecar, daemon, desktop, packaged, and Closure builds passed.
- A temporary TS2322 error was injected into a web source file: the complete package build exited nonzero at the native compiler gate. The probe was removed and the subsequent standalone build passed.
- Daemon and desktop emitted-file sets are unchanged (2,301 and 105 files). Desktop JavaScript output is byte-identical. Daemon JavaScript differs only in the migrated deliverable-selection expression; other changes are confined to declarations and source maps.
This evaluation does not measure application runtime speed, editor latency, peak memory, Windows/Linux execution, or signed installer production. Full daemon and web test suites were not run; focused tests follow repository guidance.
Follow-up: remove repository-owned legacy API usage
The follow-up migration keeps the compatibility alias only in apps/web for
Next. Full type checking, web build, and all guards passed after removal from the
other 27 manifests. Focused follow-up validation passed 149 tests across 11
files: 42 e2e guard/scope tests, 72 web tests, 10 desktop tests, 12 packaged tests,
and 13 daemon tests. Seven new guard tests cover import-edge preservation,
source changes, TS/TSX parsing, old-tree stability, and failure cleanup.
The final native-API guard run took 20.54 s on the same machine. This is not a claim of improvement over the old JS parser: native AST extraction involves process/protocol overhead, unlike the compiler-only benchmarks above. One native process per full guard run and a one-entry AST cache avoid repeatedly starting the compiler or parsing identical consecutive inputs. Native snapshots are immutable; tests caught stale results when virtual paths were reused, so each distinct parse now opens a fresh virtual project and closes its predecessor.
Guard performance follow-up
CI exposed a regression in the first API migration: the layout-policy step took
131 s in run 39649012290 (52 s in run 39651364456), versus 18 s in the nearby
legacy run 39648868125. Per-file immutable project creation is unsuitable for
repository scans even though it protects correctness.
Cross-app and web import isolation now collect the same inputs and parse them in batches of at most 256 files. Each batch owns one immutable parse-only project and snapshot, with no library or dependency resolution. Native ASTs are fetched lazily; the client cache retains only the immediately preceding tree so adjacent consumers do not transfer it twice. Each batch closes its native process and snapshot, including on failure. Changed or unlisted source contents still use isolated immutable projects. The checking rules and traversal are unchanged.
Three alternating complete pnpm guard runs on the same macOS machine compared
legacy commit 8d820a6989 with this optimization:
| Variant | Round 1 | Round 2 | Round 3 | Median |
|---|---|---|---|---|
| Legacy JS API | 8.37 s | 8.38 s | 8.42 s | 8.38 s |
| Bounded native batches | 7.43 s | 7.18 s | 6.75 s | 7.18 s |
All runs passed. The median wall time improved by 14.3% over the legacy guard.
These are warm local observations, not an all-platform performance guarantee.
/usr/bin/time -l reported maximum resident sizes of 642–648 MB for legacy and
670–716 MB for the final native implementation (decimal units). This is a modest
memory increase, not a memory saving. An unbounded batch prototype reached
2.36 GB and was rejected. No process-wide heap limit or forced GC was added.
Validation includes full guard/typecheck and 41 focused tests across five files, including batch-boundary traversal, changed content, TS/TSX handling, nested failure cleanup, and old-tree stability after releasing a batch. Linux CI timing must be checked independently; local measurements alone do not close the CI performance regression.