Skip to content

feat!: move the React bindings to material-theme-builder/react - #161

Merged
abernier merged 1 commit into
mainfrom
feat/react-subpath
Jul 31, 2026
Merged

feat!: move the React bindings to material-theme-builder/react#161
abernier merged 1 commit into
mainfrom
feat/react-subpath

Conversation

@abernier

Copy link
Copy Markdown
Owner

Supersedes #160, same problem, opposite shape.

#160 was right about the diagnosis and the measurement — I reproduced both. It added material-theme-builder/builder as a React-free subpath. This does the inversion instead: the framework-agnostic core stays at the root, the React bindings move behind /react.

- import { Mcu, useMcu, ExportButton } from "material-theme-builder";
+ import { Mcu, useMcu, ExportButton } from "material-theme-builder/react";

  import { builder } from "material-theme-builder";  // unchanged

Why this way round

A subpath for the React-free half fixes the bundle but leaves the default import the expensive one — you'd have to know that a framework registers client references through a re-export to think of reaching for it. Since that's now the second pass on the same invariant (#156, then #160), removing the required knowledge beats documenting the workaround.

It also stops the package name from stuttering: material-theme-builder/builder on a package called material-theme-builder is a sign the root holds something other than what it says. And it's how the ecosystem splits this — motion / motion/react, @floating-ui/dom / @floating-ui/react.

Measured

Next output: "export" app, layout does nothing but builder(...).toCss() into a <style>, <Mcu> never imported. Identical app source on both sides; only the installed package shape differs (published 2.2.0 vs this branch, packed).

client chunks (gzip) chunk files tonalSpot present SSR'd CSS
2.2.0 198 715 9 yes 19 398 chars
this branch 166 679 8 no 19 398 chars

−32 036 bytes gzip, byte-identical output. Within 90 bytes of #160's figure, measured on a different app — the mechanism reproduces.

Worth noting the saving is larger than dist/react.js weighs standalone (26 184 gzip): dropping it also collapses a whole chunk. I'd flagged that arithmetic as suspicious when reviewing #160; it holds up.

Build

dist/index.js no longer references ./react.js at all, so the index build stops holding it external:

dist/index.js   33 KB   no directive -- builder, zero occurrences of "react"
dist/react.js  138 KB   "use client" -- Mcu, useMcu, ExportButton
dist/cli.js     36 KB   the CLI

attw 🟢 on all four resolution modes for all three entries, 40 tests pass, pnpm run lgtm clean.

Also

The root now exports the McuConfig type, which was previously unreachable from any entry point — it was declared in react.d.ts but never exported, and the root only re-exported builder.

Left alone

dist/react.js still carries its own copy of the builder and @material/material-color-utilities (argbFromHex ×12, TonalPalette ×10 in both bundles). Pre-existing, unchanged by this PR, and only costs anything to a client component importing builder from the root and rendering <Mcu>. Deduping needs the React sources to import through the root entry, which is its own change.

🤖 Generated with Claude Code

The root entry re-exported `Mcu`, `useMcu` and `ExportButton`, so a framework
that splits server and client graphs registered every export of the client
module through that re-export. `import { builder } from "material-theme-builder"`
in a server component therefore shipped the React surface and the color
utilities to the browser, for a component the page never rendered -- and no
amount of tree-shaking undoes a client-reference registration.

Root is now `builder` alone; the bindings live behind `/react`, which is
where the ecosystem puts framework bindings (`motion/react`,
`@floating-ui/react`). A subpath for the React-free half would have worked
too, but left the default import the expensive one -- you'd have to know
about client references to reach for the cheap path.

Measured on a Next `output: "export"` app calling only `builder(...).toCss()`,
same source both sides: client chunks drop from 198 715 to 166 679 bytes gzip
(9 chunks to 8), server-rendered CSS byte-identical at 19 398 chars.

The root also now exports the `McuConfig` type, previously unreachable from
any entry point.

BREAKING CHANGE: `Mcu`, `useMcu` and `ExportButton` move to
`material-theme-builder/react`. `builder`, the CLI and
`material-theme-builder/tailwind.css` are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abernier
abernier merged commit d7ab3f0 into main Jul 31, 2026
6 checks passed
@abernier
abernier deleted the feat/react-subpath branch July 31, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant