Skip to content

Make the ESM barrel tree-shakeable so consumers bundle only the CSS they use - #1415

Closed
stefankp wants to merge 1 commit into
mainfrom
stefankp/brand-esm-barrel-fix
Closed

Make the ESM barrel tree-shakeable so consumers bundle only the CSS they use#1415
stefankp wants to merge 1 commit into
mainfrom
stefankp/brand-esm-barrel-fix

Conversation

@stefankp

Copy link
Copy Markdown
Contributor

Summary

The ESM barrel (@primer/react-brand/esm) was flagged as side-effectful in package.json sideEffects (via "./esm/index.esm.js"). That forced bundlers (webpack/rspack/vite) to fully evaluate the barrel and retain every component's co-located CSS, even when a consumer imported only a couple of components — so a 2-component island still pulled in the entire ~60-component CSS surface.

This PR removes the barrel module from sideEffects (keeping **/*.css and the base ./esm/css/stylesheets.js side-effectful) so bundlers can tree-shake through the barrel and emit only the CSS of components that are actually imported. It also adds a granular ./esm/* subpath export (mirroring the existing ./lib/*) so consumers can import individual component modules and their types directly. The change is additive packaging metadata only — no runtime or source changes — and all existing entry points are preserved.

List of notable changes:

  • updated sideEffects in packages/react/package.json to drop "./esm/index.esm.js", making the ESM barrel tree-shakeable while still emitting used components' CSS and always retaining the ~21 KB base layer.
  • added "./esm/*": "./esm/*" to the exports map, exposing per-component ESM modules + co-located types, e.g. @primer/react-brand/esm/Button/Button.js.
  • added a minor changeset describing the behavior change and the CSS-drop risk for consumers implicitly relying on all Brand CSS being present.

What should reviewers focus on?

  • Confirm the sideEffects change cannot drop used CSS: **/*.css and ./esm/css/stylesheets.js remain side-effectful, so any imported component still emits its CSS and the base layer (design tokens, light/dark color modes, reset, utilities) is always retained.
  • Confirm the change is purely additive: ., ./esm, ./lib, ./lib/*, ./fonts/*, and ./package.json are unchanged; only ./esm/* is added and one sideEffects entry removed.
  • Consider the consumer-facing risk: bundlers will now prune CSS for components that aren't imported. A consumer that relied on Brand CSS being present without importing the component must now import it (or the base stylesheet / @primer/brand-css) explicitly. This is called out in the changeset.

Steps to test:

Measured with a throwaway consumer that imports only {ThemeProvider, MinimalFooter} from @primer/react-brand/esm, packed from this build and bundled with both webpack and rspack, extracting the emitted CSS:

Scenario Emitted CSS (raw / gzip)
Before — pure sideEffects pruning (all 73 modules retained) 771 KB / 93.7 KB
Before — webpack/rspack production defaults 63 KB / 10.2 KB
After — this PR (webpack + rspack, usedExports on and off) 42 KB / 6.1 KB

The base layer (:root tokens for light + dark, reset, utilities) is present in every scenario; only unused components' CSS is removed. The barrel entry @primer/react-brand/esm resolves unchanged, and a granular @primer/react-brand/esm/Button/Button.js import resolves both JS and .d.ts.

To reproduce:

  1. npm ci && npm run build:lib
  2. cd packages/react && npm pack --ignore-scripts, then install the tarball into a scratch app.
  3. Bundle import {ThemeProvider, MinimalFooter} from '@primer/react-brand/esm' and inspect the emitted CSS size before vs. after this change.

Supporting resources (related issues, external links, etc):

  • Surfaced by a @primer/react-brand consumer investigation (marketing islands) where a 2-component island emitted ~22.6 KB compressed of Brand CSS.

Contributor checklist:

  • All new and existing CI checks pass (tsc --noEmit clean, eslint 0 warnings, 1228 Jest tests pass locally)
  • Tests prove that the feature works and covers both happy and unhappy paths — this is packaging metadata; validated via bundler CSS measurement (see Steps to test) rather than unit tests
  • Any drop in coverage, breaking changes or regressions have been documented above
  • UI Changes contain new visual snapshots — N/A, no UI or visual changes
  • All developer debugging and non-functional logging has been removed
  • Related issues have been referenced in the PR description — N/A

Remove `./esm/index.esm.js` from `sideEffects` so bundlers can prune
unused components (and their co-located CSS) when consumers import from
the `@primer/react-brand/esm` barrel. Keep `**/*.css` and
`./esm/css/stylesheets.js` side-effectful so used components' CSS and the
base layer are always retained.

Add a `./esm/*` subpath export (mirroring the existing `./lib/*`) so
consumers can import individual component ESM modules and types directly,
e.g. `@primer/react-brand/esm/Button/Button.js`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db807c62-fba1-4868-bb40-cd661ef1a30f
@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7e78b28

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@primer/react-brand Minor
@primer/brand-docs Minor
@primer/brand-css Minor
@primer/brand-primitives Minor
@primer/brand-e2e Minor
@primer/brand-fonts Minor
@primer/brand-mcp Minor
@primer/brand-config Minor
@primer/brand-storybook Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

🟢 No design token changes found

@stefankp

Copy link
Copy Markdown
Contributor Author

Closing temporarily pending internal sign-off on the description — will reopen shortly. No code change; branch stays as-is.

@stefankp stefankp closed this Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🟢 Bundle size report

CheckMainBranchChange
UMD — full bundle (JS)101.39 kB101.39 kB🟢 No change
UMD — full bundle (CSS)67.01 kB67.01 kB🟢 No change
ESM — full bundle (JS + CSS)1.51 MB1.51 MB🟢 No change
ESM — tree-shaken simple (Button)69.92 kB69.92 kB🟢 No change
ESM — tree-shaken complex (ActionMenu)78.82 kB78.82 kB🟢 No change

@github-actions

Copy link
Copy Markdown
Contributor

🟢 No unit test coverage changes found

All components and hooks with tests maintain the same coverage as the main branch.

@github-actions

Copy link
Copy Markdown
Contributor

🟢 No visual differences found

Our visual comparison tests did not find any differences in the UI.

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