Summary
@deque/cauldron-react ships CJS only today: rollup.config.js emits a single
format: 'cjs' bundle, and package.json exposes only main/types — no exports
map, no module field. As a result the ergonomic barrel import
(import { Modal } from '@deque/cauldron-react') can't be tree-shaken: bundlers keep
the whole component graph.
This ticket adds a dual CJS/ESM build plus "sideEffects": false so the barrel
import prunes unused components automatically — no consumer import-style change.
Payoff
Consumers keep import { X } from '@deque/cauldron-react' and automatically bundle only
what they use — no deep imports, no codemod. A consumer importing only Button should
pull in neither react-aria-components (TreeView) nor react-syntax-highlighter (Code).
Scope notes from an initial code audit
- The JS bundle imports zero CSS. All styling lives in
src/index.css (built
separately by PostCSS into lib/cauldron.css); no .tsx module does import './x.css'.
So the correct declaration is "sideEffects": false, not a ["*.css"] list.
sideEffects: false does not affect the separate cauldron.css artifact — consumers
still import it explicitly.
- The only module-scope side effect found is the
registerLanguage(...) block in
src/components/Code/index.tsx. It's local (only runs when Code is imported) and
therefore tree-shake-safe. All other addEventListener/window/document usage is
inside hooks/lifecycle (runtime, not import-time) and irrelevant to sideEffects.
Code hard-imports CJS subpaths (react-syntax-highlighter/dist/cjs/light). This
is the single most likely thing to break in an ESM consumer graph and must be switched
to ESM paths before the ESM build is validated.
- Adding an
exports map is itself a potentially breaking change: any consumer
currently deep-reaching into lib/* will hard-error once exports is present. The
barrel, ./cauldron.css, and ./package.json must be preserved. Treat as a minor
release with a loud changelog note.
- Icons stay lazy-loaded and are out of scope.
Icon loads SVGs through a dynamic
import(), so they resolve as async chunks rather than eager weight. This ticket does
not change that. The tree-shaking payoff here is pruning unused components and their
heavy deps (react-aria-components, react-syntax-highlighter), not shrinking the icon
set. Zeroing out per-icon weight is a separate follow-up, not part of this work.
Phased breakdown
Sequential; phases 2 and 3 can proceed in parallel. Each phase is its own PR for
reviewability.
Risks
- Dual-package hazard (highest impact). The library creates React context in ≥6
places (theme, Dialog, Combobox, Listbox, Table, ActionList). If a consumer graph loads
both the CJS and ESM copies, contexts become distinct identities and providers silently
stop reaching consumers — no error, just broken theming/compound components.
exports map is a breaking change for deep-import consumers (see scope notes).
react-syntax-highlighter interop under strict ESM.
- Types/runtime mismatch ("masquerading") if condition ordering is wrong under
moduleResolution: node16/nodenext.
- Mis-declared
sideEffects silently dropping needed code, or leaving dead code
un-shakeable.
Verification
Automated (add to CI):
Consumer harness matrix (install from pnpm pack tarball, not workspace symlink):
Behavioral / tree-shaking:
Summary
@deque/cauldron-reactships CJS only today:rollup.config.jsemits a singleformat: 'cjs'bundle, andpackage.jsonexposes onlymain/types— noexportsmap, no
modulefield. As a result the ergonomic barrel import(
import { Modal } from '@deque/cauldron-react') can't be tree-shaken: bundlers keepthe whole component graph.
This ticket adds a dual CJS/ESM build plus
"sideEffects": falseso the barrelimport prunes unused components automatically — no consumer import-style change.
Payoff
Consumers keep
import { X } from '@deque/cauldron-react'and automatically bundle onlywhat they use — no deep imports, no codemod. A consumer importing only
Buttonshouldpull in neither
react-aria-components(TreeView) norreact-syntax-highlighter(Code).Scope notes from an initial code audit
src/index.css(builtseparately by PostCSS into
lib/cauldron.css); no.tsxmodule doesimport './x.css'.So the correct declaration is
"sideEffects": false, not a["*.css"]list.sideEffects: falsedoes not affect the separatecauldron.cssartifact — consumersstill import it explicitly.
registerLanguage(...)block insrc/components/Code/index.tsx. It's local (only runs when Code is imported) andtherefore tree-shake-safe. All other
addEventListener/window/documentusage isinside hooks/lifecycle (runtime, not import-time) and irrelevant to
sideEffects.Codehard-imports CJS subpaths (react-syntax-highlighter/dist/cjs/light). Thisis the single most likely thing to break in an ESM consumer graph and must be switched
to ESM paths before the ESM build is validated.
exportsmap is itself a potentially breaking change: any consumercurrently deep-reaching into
lib/*will hard-error onceexportsis present. Thebarrel,
./cauldron.css, and./package.jsonmust be preserved. Treat as a minorrelease with a loud changelog note.
Iconloads SVGs through a dynamicimport(), so they resolve as async chunks rather than eager weight. This ticket doesnot change that. The tree-shaking payoff here is pruning unused components and their
heavy deps (
react-aria-components,react-syntax-highlighter), not shrinking the iconset. Zeroing out per-icon weight is a separate follow-up, not part of this work.
Phased breakdown
Sequential; phases 2 and 3 can proceed in parallel. Each phase is its own PR for
reviewability.
publintand@arethetypeswrong/cliagainstpnpm packoutput; wire into CI. Minimalpacked-tarball smoke test (Node
require+ Nodeimport). Delivers value againstthe current CJS output immediately; establishes install-from-tarball (not symlink).
Phase 2 — Fix
react-syntax-highlighterimports. SwitchCodefromdist/cjs/...to ESM paths; confirm Code still renders + highlights.Dropped — verified not needed (and the switch would regress Node
resolution, since
react-syntax-highlighterhas noexportsmap). Bundlersalready resolve the
dist/cjspaths correctly; the RSH-path concern foldsinto Phase 3/5 as a documented caveat. See the verification comment below.
lib/cjs+lib/esmwith correctextensions /
typemarker and shared.d.ts. Decide module granularity here:one bundled file per format is the default (see Phase 3b).
preserveModulesfor reliable pruning. Especially if thePhase 5 tree-shaking test shows a single bundle doesn't prune cleanly. Emit each
component as its own module (
preserveModules: true) instead of one bundled fileper format. Module-boundary elimination is more reliable than statement-level
dead-code elimination across one large bundle. It matters most for the module-scope
registerLanguage(...)calls insrc/components/Code/index.tsxand for dropping thereact-aria-componentsimport when a consumer doesn't useTreeView— the exactcases
"sideEffects": falsehas to get right. Cost: many small files per formatinstead of one; no runtime difference through a bundler, slightly more module loads
for an unbundled Node
require.exportsmap + fields. Add conditionalexports(types-firstordering),
module, preserve barrel +./cauldron.css+./package.json.Sub-task: audit module-scope side effects and set
"sideEffects": false(expected).surfaces a cross-module load-time effect. Expected to be a no-op based on the audit
above.
Risks
places (theme, Dialog, Combobox, Listbox, Table, ActionList). If a consumer graph loads
both the CJS and ESM copies, contexts become distinct identities and providers silently
stop reaching consumers — no error, just broken theming/compound components.
exportsmap is a breaking change for deep-import consumers (see scope notes).react-syntax-highlighterinterop under strict ESM.moduleResolution: node16/nodenext.sideEffectssilently dropping needed code, or leaving dead codeun-shakeable.
Verification
Automated (add to CI):
publintclean against packed tarball.@arethetypeswrong/cliclean (no masquerading, all conditions resolve).Consumer harness matrix (install from
pnpm packtarball, not workspace symlink):require(...)resolves CJS; Node nativeimport(...)resolves ESM.<Code>(proves syntax-highlighter interop)."use client"page) — SSR + dual-graph context.tsc --noEmitundermoduleResolution=bundler,node16,node.Behavioral / tree-shaking:
ThemeProvider→ themed component, orTable→TableRow); assert behavior worksand only one context copy is present in the bundle.
Buttonbundles none of the othercomponents,
react-aria-components, orreact-syntax-highlighter..d.tsresolution intact for both entry styles.