Skip to content

fix(build): break the vendor <-> vendor-react chunk cycle - #45

Open
a-effort wants to merge 2 commits into
mainfrom
fix/vendor-chunk-cycle
Open

fix(build): break the vendor <-> vendor-react chunk cycle#45
a-effort wants to merge 2 commits into
mainfrom
fix/vendor-chunk-cycle

Conversation

@a-effort

Copy link
Copy Markdown
Contributor

What

manualChunks in vite.config.ts matched packages by bare substring, so id.includes("/react-dom/") also caught @floating-ui/react-dom and swept it into vendor-react — which then imported @floating-ui/dom back out of vendor.

That makes the two chunks circular. Rollup has said so on every build for a while, buried among the chunk-size advice:

Circular chunk: vendor -> vendor-react -> vendor. Please adjust the manual chunk logic for these chunks.

Why it matters

A cycle leaves rollup to pick an execution order, and the losing side evaluates against a namespace object that doesn't exist yet. When that order flips, the app dies at module-eval and serves a blank page whose only console output is:

Uncaught TypeError: Cannot set properties of undefined (setting 'Activity')
    at vendor-react-*.js

Activity is a React 19 export, so nothing in that message points at bundling — it reads like a React or auth fault. This is how it was found: an unrelated one-component change shifted chunk composition, flipped the order, and took the whole app down.

The cycle is latent, not new. Any change that disturbs the bundle can flip it, so the failure attaches to whichever commit happened to disturb it rather than to the real cause.

The fix

Anchor the match to the package root so only the real react packages land in vendor-react, and hold that chunk to react, react-dom, scheduler, react-is. A leaf chunk imports nothing from its siblings, so it can't be one end of a cycle no matter what moves between chunks later. react-intl, @formatjs, sonner and the react-remove-scroll family move to vendor, alongside helpers they already import from there.

Verification

Circular chunk warning     gone
vendor-react cross-chunk   zero imports (true leaf)
tsc -b / eslint / prettier clean
vitest                     167 files, 2908 passed, 1 skipped

Because no CI job builds (see below), I verified the actual failure by loading the built entry chunk under jsdom and catching module-eval throws — it reproduces the browser error exactly. Before: FAIL … setting 'Activity'. After: PASS.

⚠️ CI does not cover this

No workflow runs npm run build:

workflow runs
client-lint-test generate, format:check, lint, test:coverage
client-generate generate, tsc -b --noEmit
client-e2e Playwright against npm run dev:e2e — the Vite dev server

manualChunks is a rollup build-only option, so the dev server never applies it and E2E can't see this class of bug. A PR that reintroduces the cycle goes green on every check.

Adding npm run build to client-lint-test would have caught this in seconds. Filing that separately rather than bundling it here.

Related

Blocks the avatar PR, which trips this cycle and cannot merge before this lands.

manualChunks matched packages by bare substring, so /react-dom/ also
caught @floating-ui/react-dom and swept it into vendor-react, which then
imported @floating-ui/dom back out of vendor. Rollup reported the result
as "Circular chunk: vendor -> vendor-react -> vendor", one warning among
the chunk-size advice it prints on every build.

A cycle leaves rollup to pick an execution order, and the losing side
evaluates against a namespace object that does not exist yet. When that
order flips, the app dies at module-eval with "Cannot set properties of
undefined (setting 'Activity')" — Activity being a React 19 export — and
serves a blank page with no other console output. Nothing about the
message points at bundling, so it reads like a React or auth fault.

Anchor the match to the package root so only the real react packages
land in vendor-react, and hold that chunk to react, react-dom, scheduler
and react-is. A leaf chunk imports nothing from its siblings and so
cannot be one end of a cycle, whatever else moves between chunks later.
react-intl, @formatjs, sonner and the react-remove-scroll family move to
vendor, alongside the helpers they already import from there.

This was latent rather than new: rollup happened to pick a working order
for as long as chunk composition held still. Any unrelated change that
shifts it can flip the order, so the failure surfaces attached to
whichever commit disturbed the bundle rather than to this one.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
@a-effort a-effort self-assigned this Aug 19, 2026
@a-effort a-effort added the bug Something isn't working label Aug 19, 2026

@marekdano marekdano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

LGTM 🚀

The header profile trigger rendered an empty rounded square, since no
avatar data exists to put in it. Fill it with a lucide UserRound behind
an Avatar primitive, so the frame that already reserved the space reads
as a person rather than a gap.

Colors come from the muted / muted-foreground tokens, which index.css
redefines under .dark, so light and dark need no conditional logic here
and no dark: variants. A test asserts the fallback carries no dark:
prefix, to keep a future hardcoded override from creeping back in.

UserAvatar owns the fallback ladder rather than HeaderProfileMenu
inlining an icon, and takes an optional src. The API exposes no avatar
field today, so that leg is unused — but Radix Avatar is what handles
the image load/error swap, which is the part that gets ugly to retrofit
once profile pictures land. The primitive comes from the radix-ui
umbrella package, already a dependency, so this adds none.

Sizing stays size-6 rounded-md with overflow-hidden: an image later
fills the same box and the header geometry does not move. The trigger
button already carries aria-label={displayName}, so the icon is
decorative and adds no second accessible name and no new i18n strings.

Signed-off-by: Anna Effort <anna.effort@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants