Minor Changes
-
bdda185: build(DST-1315): unbundle the build output for tree-shaking
@marigold/componentspreviously shipped its entire ESM build as a single concatenated barrel (index.mjsre-exporting 74+ components). Because every export lived in one module, bundlers could not statically prove which parts were unused, so importing a single leaf component (e.g.Stack) pulled in essentially the whole library. On rspack this meant ~57 kB (≈82% of the lib) for aStack + Text + Cardimport.The build now emits one file per source module (
unbundle: true, thepreserveModulesequivalent) while keeping the.barrel import fully backward compatible. Consumer bundlers (rspack, vite, esbuild) can now drop unused components: the sameStack + Text + Cardimport drops to ~0.8 kB, and a singleButtonimport drops from ~57 kB to ~1.7 kB.Why a build flag alone wasn't enough (source changes explained)
Flipping
unbundle: trueis necessary but not sufficient. The old single-barrel build concatenated every module into one file, which hid problems that only matter once each module stands on its own and a bundler starts deciding, per module, what it can safely drop.unbundleexposed those problems, so the following source changes were required to make tree-shaking actually work — without them the flag delivers little or no benefit:- Toast queue: removed a module-scope side effect.
ToastProviderexportedconst queue = new ToastQueue(...)at module top level. That constructor runs the moment the module is imported and touchesdocument(view-transition setup). The package declaressideEffects: false, which tells bundlers "importing any module here does nothing observable, so unused ones are safe to delete." A top-levelnewthat touchesdocumentdirectly contradicts that promise: it's a real side effect on import, it can break SSR, and it makes thesideEffects: falseclaim dishonest (risking either dropped-needed-code or kept-unneeded-code depending on the bundler). Fix: construct the queue lazily on first use viagetToastQueue(), keeping the singleton but making the module genuinely side-effect-free. (Tests, stories, andToastProviderwere updated to callgetToastQueue().) motion: switched off the non-shakeablemotionproxy.import { motion } from 'motion/react'pulls motion's entire feature set, and themotion.*proxy is by design not tree-shakeable — referencingmotion.divdrags in the whole renderer (~34 kB). In the old concatenated barrel this cost was paid once and amortized across the whole library, so it was easy to miss. Underunbundle, that cost attaches to each module that importsmotion(ActionBar,Tabs,Tray), so importing any one of them would re-pull motion's full bundle — defeating the point. Fix: use the lightweightmcomponents frommotion/react-m(tiny core, no features) and load thedomMaxfeature set through aLazyMotionboundary via a dynamicimport()of a local module (motionFeatures.ts), so bundlers split it into its own async chunk that only loads when an animated component actually renders. (The dynamic import targets a local file rather thanmotion/reactdirectly because importing the dep dynamically made vite's optimizer re-bundle mid-run and drop named exports during tests.)hooksbarrel: replacedexport *with explicit named re-exports.export * from './hooks'forces a bundler to pull in and consider the entire namespace of the re-exported module; explicit named re-exports let it trace precisely which symbols are reachable. Minor on its own, butexport *chains are a classic way to silently anchor unused code.react-select: externalized and declared as a dependency. Underunbundle, rolldown copies any non-externalized dependency into the output per-importing-module.react-select(~2 MB with@emotion) was being bundled into the dist without even being a declared dependency — invisible in the old barrel, but under unbundle it bloated every chunk that referenced it. Fix: mark itexternalintsdown.config.tsand add it todependenciesso it resolves transitively at install time. It's used only by the deprecatedMultiselect; drop both together in the next major.- Test/mock updates that follow the source changes.
Tray.test.tsx'svi.mock('motion/react')had to gainLazyMotion/domMax, plus a newvi.mock('motion/react-m')providingcreate, becauseTrayModalnow importscreatefrommotion/react-m.Toast.test.tsx/Toast.stories.tsxswitched from the removed module-levelqueueexport togetToastQueue(). - A
size-limitbudget gate (pnpm --filter @marigold/components size, run in CI) was added so these wins don't silently regress — e.g. someone re-introducing amotionproxy import or a module-scope side effect.
No public API changes: all imports from
@marigold/componentscontinue to work exactly as before. - Toast queue: removed a module-scope side effect.
Patch Changes
-
a609642: chore(DST-1512): import
I18nProviderfromreact-aria-componentsin remaining stories/tests/demosFollow-up to DST-1505. Migrates the remaining
I18nProviderimports off the@react-aria/i18nshell package to stay consistent with the RAC-first principle (import an API fromreact-aria-componentswhenever it re-exports it, so provider and consumers share oneI18nContext). Component stories/tests now import fromreact-aria-components/I18nProvider, and docs demos use the public@marigold/componentsexport. Thepackages/systemformatter tests intentionally stay on@react-aria/i18n, because the formatters under test read locale from that package directly andpackages/systemdoes not depend onreact-aria-components. -
60b6e03: fix(DST-1507): make
Table.EditableCellinline editing work after SSR hydrationIn a server-rendered app (for example Next.js), editable cells were inert after hydration: clicking a cell did not open its inline editor until an unrelated re-render (such as a window resize) happened to occur. React Aria builds the table collection in a separate render pass, and the editing state previously lived in that build pass, so the rendered cell content stayed bound to the server pass's closures and never reconnected to the live component after hydration. The editing state and overlay now live in an inner component rendered inside the
Cell, i.e. in the collection's content pass, so interaction reconnects on its own after hydration (the same structure React Spectrum's S2TableViewuses).- @marigold/system@17.8.0