spike(GMT-1715): tsdown build — go/no-go criterion #4 - #3407
Draft
dreamwasp wants to merge 2 commits into
Draft
Conversation
The last unspiked criterion from the reboot recommendation §5. It works, and it answers the question that was blocking Phase ordering. package scope current tsdown speedup variance pure TS 17.8s 4.4s ~4x gamut-styles 16 tsx, 20 emotion imports 21.3s 2.8s ~7x gamut 426 files, 106 styled ~17.3s 2.3s ~7x All three emit bundled ESM + CJS plus .d.mts/.d.cts from one run. HEADLINE: no Babel bridge is needed, so the build migration does NOT depend on Emotion leaving first. @emotion/babel-plugin is configured only for sourceMap + autoLabel (DX, not correctness), 0 of 2 snapshot files depend on emotion class names, and tsdown built both emotion-heavy packages with no Babel at all. Output shape validated end to end: real exports maps resolving through the package name, the mono deep-import preserved as a real subpath (@codecademy/gamut-styles/AssetProvider → createFontLinks), and the old dist/AssetProvider path correctly blocked with ERR_PACKAGE_PATH_NOT_EXPORTED. Four findings: - Today's Babel output is not valid Node ESM — extensionless specifiers, 10 in variance alone. It only works because consumers bundle. Corroborates the bundled-output decision independently. - Latent type-only re-export debt that Babel's file-by-file transpile hides: 6 names across 2 files needed `export type`. Neither isolatedModules nor verbatimModuleSyntax is enabled anywhere, which is why it accumulated. - .css imports need @tsdown/css or externalizing; externalizing matches the current build, which already cpy's CSS. - Packages must migrate together — gamut-styles couldn't load until variance was also tsdown-built, because variance's Babel ESM is unresolvable. Also worth re-testing: tsdown's bundled dts came out SMALLER than tsc's per-file output (396kB in 1 file vs 481kB across 47), so the RFC's "keep tsc --emitDeclarationOnly" assumption didn't survive contact. variance and gamut-styles deliberately point at dist-tsdown, so a whole-repo build won't work on this branch. That's finding 4 made concrete, not breakage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ompose
An earlier revision of TSDOWN-SPIKE.md suggested tsdown's own dts might remove
the need for tsc --emitDeclarationOnly. That was wrong, and build-tooling-rfc's
original recommendation to keep tsc is correct.
The first measurement was taken while gamut-styles still resolved
@codecademy/variance to its Babel per-file .d.ts. Once variance ships bundled
declarations — which is what migrating variance to tsdown means — the
downstream build fails:
variance declarations gamut-styles dts build
per-file (tsc) 0 errors, 1.6s
bundled (tsdown) 50 x TS4023 "cannot be named"
The downstream package can't name types the upstream's bundled .d.ts doesn't
re-export. Causation verified by flipping variance's types field back and
forth — 0 vs 50, nothing else changed.
Two related fixes in this commit:
- dts: true alone fails on these packages ("You have references in your
tsconfig") because the root tsconfig is a references-only shell.
dts: { tsconfig: 'tsconfig.lib.json' } gets past it.
- variance now uses the finding-driven hybrid: JS from dist-tsdown/,
declarations from dist/ via the existing tsc step.
Also corrected: I called gamut-styles/dist/AssetProvider the highest-priority
break that must become a public export. It already is one — src/index.ts:3
star-exports it and both createFontLinks and AssetProvider resolve off the
root, verified by importing them. So it's a rename to the root specifier, not
a promotion to a subpath, and shipping the ./AssetProvider subpath would
enshrine one nobody requires.
Credit to the RSC session for hitting the dts wall independently on the same
package and tracing it to variance's bundled .d.ts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run-many --target=build --all |
❌ Failed | 13s | View ↗ |
💡 Dealing with memory or CPU issues? See memory and CPU details with the resource usage add-on ↗.
☁️ Nx Cloud last updated this comment at 2026-08-11 14:02:35 UTC
|
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Overview
Spike, not for merge. Closes go/no-go criterion #4 for the 1.0 reboot: can
tsdown replace
tsc --emitDeclarationOnly+babel ./src --out-dir ./dist, producingthe reboot's decided output shape (bundled per entry + a strict
exportsmap)?Yes. Written up in
TSDOWN-SPIKE.mdat the repo root.Built three real packages —
variance(pure TS),gamut-styles(16.tsx, 20 Emotionimports) and
gamut(426 files, 106 Emotionstyled) — each producing bundled ESM.d.mts/.d.cts, 4–7x faster than the currenttsc+babelpair(
gamut: ~17.3s → 2.3s).The load-bearing result: no Babel bridge is needed
The concern was that a Phase 1 tsdown migration would need a scoped
@rolldown/plugin-babelbridge for@emotion/babel-plugin, which Phase 2 would thendelete. It doesn't — tsdown built both Emotion-heavy packages with no Babel at all.
Consequence: the build migration is independent of the styling work and can land
first. That reversed the planned phase ordering.
Second commit: the declarations claim, corrected
8c2ce2b03corrects the first commit. Bundled declarations don't compose, so the shapeis a hybrid: JS from tsdown, declarations from
tsc. The part worth keeping iswhy it works rather than that it works —
rolldown-plugin-dts#174
(directives leaking into
.d.ts) cannot occur in a hybrid build, becausedeclarations carry no statements and there is nowhere to put a directive prologue.
Structurally unreachable rather than luckily avoided, so nobody re-verifies it after a
version bump. One
exportsmap serves both output shapes.Output contract validated end to end
exportsmaps resolved through the package name(
@codecademy/gamut-styles/AssetProvider→createFontLinks)dist/AssetProviderpath correctly blocked withERR_PACKAGE_PATH_NOT_EXPORTEDTwo findings worth carrying regardless of this spike's fate
variancealone. It only works because consumers bundle.across 2 files, fixed here. Neither
isolatedModulesnorverbatimModuleSyntaxisenabled anywhere, which is why it accumulated.
TSDOWN-SPIKE.md, not yet corrected in the fileTSDOWN-SPIKE.md:33-34says@emotion/babel-pluginis "DX, not correctness". That iswrong. The three configured options (
sourceMap,autoLabel,labelFormat) arecosmetic, but the plugin also stamps
targetunconditionally, outside any option —verified in the plugin's own dist, where
targetis pushed with no guard whilelabeltwo lines later is gated on
autoLabel !== 'never'.targetis load-bearing for component selectors (${Component}in selectorposition). Without it,
@emotion/styledreturnsNO_COMPONENT_SELECTOR, which@emotion/serializethrows on in development and silently renders as.undefinedinproduction. Real exposure: 1 site here
(
packages/gamut/src/BarChart/BarRow/elements.tsx:64) and 22 confirmed consumer sites.The conclusion survives — no Babel bridge is needed — but the supporting sentence
doesn't, and the removal work item is larger than "readable class names".
PR Checklist
the build itself plus the resolution checks in
TSDOWN-SPIKE.mdCodecademy repositories — no. See limitations.
Testing Instructions
Compare timings against
yarn buildonmainfor the same packages.Limitations — please read before drawing conclusions
tscinvocation, and no consumer build has run anywhere. Not mono's Nextbuild, not platform's Rspack, not front's Webpack.
gamut-styles, whose declaration layout happens to line upwith its entry names. That is a property of that package, not of the approach.
packages/gamut(426 files) is untested and is where the entry list stops beingobvious.
exportsmap here is not yet designed around the inventory of ~66 deepdist/imports across mono (19) and platform (47). Two of our own spikes have alreadybroken on it accidentally.
PR Links and Envs
Related reboot spikes: #3402 (styling proof of concept) · #3405 (Panda under the hood) ·
#3406 (engine-neutral DTCG tokens).