Repository navigation
fix(optimize-imports): support @heroui/react in Server Components - #4099
Conversation
Unused leaves of the @heroui/react barrel reach react-aria code that calls createContext() at module scope or imports client-only, which failed Server Components importing from the barrel in dev and in the RSC build (#4007).
…in RSC heroui's Toast reaches react-aria-components/Toast, whose other exports call createContext() at module scope and import client-only. Rewrite RSC subpath imports of react-aria-components to the modules that define the imported names, resolving through Vite from the importer and the entry. Re-exports are not followed through use client/use server/use cache modules, and use client modules are skipped in RSC.
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: 06edc50 |
@vinext/cloudflare
create-vinext-app
@vinext/types
vinext
@cloudflare/workers-response-store
commit: |
| const isSubpath = | ||
| !packages.has(importSource) && | ||
| preferReactServer && | ||
| isOptimizedSubpath(importSource, SUBPATH_OPTIMIZE_PACKAGES); |
There was a problem hiding this comment.
This broad match also rewrites direct user imports such as import { Collection } from "react-aria-components/Collection" in a Server Component. The published Collection.mjs entry intentionally imports "client-only" (its comment says this should cause a build-time error from an RSC), but this transform follows the re-export to react-aria/Collection and drops that guard. That changes Next.js-compatible behavior for every RAC subpath in order to fix one server-safe Toast queue export. Please scope this path to the known HeroUI/ToastQueue case (for example by importer and exported name), so arbitrary RAC client entries retain their client-only error.
There was a problem hiding this comment.
Fixed in 2999215: the RSC subpath rewrite now only applies to UNSTABLE_ToastQueue from react-aria-components/Toast (the server-safe react-stately class heroui's toast queue imports). Any other export, namespace import or RAC subpath is left unchanged, so its client-only guard still fails a Server Component import. Added a test for UNSTABLE_Toast, a mixed import, a namespace import and react-aria-components/Collection.
There was a problem hiding this comment.
Went back to following all react-aria-components subpath exports in cff6c2c, after checking real Next.js. Next's default Turbopack build follows re-exports out of these side-effect-free entries and drops their client-only import too: UNSTABLE_ToastQueue renders, and Collection fails with createContext is not a function, not the client-only error. Webpack keeps the guard, but it also fails the #4007 repro itself, even with optimizePackageImports. The export allowlist was stricter than Next's default build.
Each cell is dev / build for a page whose only import is the one in the row (Next.js 16.2.7, @heroui/react 3.2.1, react-aria-components 1.18.0). "client-only" is the 'client-only' cannot be imported error; "createContext" is createContext is not a function.
| Server Component imports | Next webpack | Next webpack + optimizePackageImports: ["@heroui/react"] |
Next Turbopack (default) | vinext main | vinext with only the heroui default (first commit) | vinext with this PR |
|---|---|---|---|---|---|---|
{ UNSTABLE_ToastQueue } from react-aria-components/Toast |
client-only / client-only | (same as webpack) | renders / renders | createContext / client-only | createContext / client-only | renders / renders |
{ Collection } from react-aria-components/Collection |
client-only / client-only | (same as webpack) | createContext / build fails: createContext | client-only / client-only | client-only / client-only | createContext / build passes, request fails: createContext |
{ Toast } from @heroui/react, rendering <Toast.Provider /> (#4007) |
client-only / client-only | client-only / client-only | renders / renders | createContext / client-only | createContext / client-only | renders / renders |
{ Skeleton } from @heroui/react |
client-only / client-only | renders / renders | renders / renders | createContext / client-only | renders / renders | renders / renders |
|
Review: 1 findings. Posted 1 actionable inline comment. Targeted tests pass. |
|
…h export RAC subpath entries import client-only on purpose, so following any subpath export dropped that guard for client components imported from a Server Component. Limit the RSC subpath rewrite to UNSTABLE_ToastQueue from react-aria-components/Toast, which heroui's toast queue needs.
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: 2999215 |
|
Since last review: 1 resolved, 0 still open, 0 new. LGTM! |
…ts again This reverts the export allowlist from 2999215. Next.js's default Turbopack build follows re-exports out of react-aria-components' side-effect-free subpath entries and drops their client-only import, so server-safe exports work and client components still fail when their own modules run. The allowlist was stricter than Next.js.
Performance benchmarksCompared 1 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
/bigbonk review this PR. You have an 8-minute time limit. Head SHA: cff6c2c |
|
Since last review: 0 resolved, 0 still open, 0 new. LGTM! |
|
💬 Approved with comments — The subpath rewrite and directive-boundary handling look correct for the cases traced. Only an informational note on a no-op rewrite path and a small test cleanup remain. Findings: 1 info · 1 suggestion The decision is advisory: this comment never approves, blocks or requests changes on the pull request. 🔧 Fix with your agentPaste the following into your coding agent to investigate and fix these issues. 📋 Code Quality: 💬 1 info · 1 suggestionThe subpath rewrite and directive-boundary logic look correct. A "use client" barrel entry can now be rewritten to itself, and one new test is vacuous. Recommendation: 💬 Optional Nits
View full review
✅ All other checks passed🔒 Security: ✅ LGTMNo security issues found in the import-rewriting changes. 📖 AGENTS.md: ✅ LGTMNo issues found. ⚡ Performance: ✅ LGTMNo performance regressions found.
📝 Documentation: ✅ LGTMNo documentation issues found. Reviewed commit: |
* fix(optimize-imports): only stop at use client modules in RSC #4099 made every directive module the import source for the names it exports, in both server environments. Record the boundary per environment instead: RSC stops at any directive, while SSR only stops at use server and use cache modules, which plugin-rsc and vinext replace with references there. SSR again rewrites imports past use client barrels such as antd's entry. Also skip the rewrite for any package whose entry defines every imported name, not just react-aria-components subpaths, so imports from a use client entry keep their bare specifier in RSC. * test(optimize-imports): e2e for SSR rewrites past a use client barrel * style: sort fixture package.json keys * test: keep the client barrel route out of the Workers runtime fixture * fix(optimize-imports): register and rewrite by the environment's sources * fix(optimize-imports): resolve sub-packages from the environment's own registrations
Fixes #4007
Summary
A Server Component that imports one component from
@heroui/react, such asimport { Toast } from "@heroui/react", failed in dev (createContext is not a function) and in the RSC build ('client-only' cannot be imported in server build). The barrel's unused leaves reach react-aria code that callscreateContext()at module scope or importsclient-only.Two changes to
vinext:optimize-imports:@heroui/reactis now in the default optimized packages, so its barrel imports are rewritten to the component modules. This fixes Skeleton and most other components.toast-queue.jsimportsUNSTABLE_ToastQueuefromreact-aria-components/Toast, and that subpath entry is itself a barrel with the same problem. In the RSC environment, subpath imports ofreact-aria-componentsare now rewritten to the modules that define the imported names, so heroui's toast queue importsToastQueuefrom react-stately directly. This drops the subpath entry'simport "client-only", as Next.js's default Turbopack build does: server-safe exports work, and a client component likeCollectionstill fails, when its own module callscreateContext(). The subpath is resolved through Vite from the importer, and the entry's package imports are resolved from the entry, because react-aria-components can carry its own nested copy of react-stately at a different version than the app's. SSR and the client keep the package's own module graph, so React contexts are never instantiated twice there.So heroui's internal files are transformed in dev too, the transform filter now accepts ids with Vite's
?v=query, as the build already did.Re-exports are no longer followed through modules with a
"use client","use server"or"use cache"directive: names exported by such a module are imported from the module itself. Before, anexport *into a"use client"file could flatten past its client reference. In RSC,"use client"modules are skipped, since plugin-rsc replaces them with client references.Next.js comparison
Each cell is dev / build for a page whose only import is the one in the row (Next.js 16.2.7, @heroui/react 3.2.1, react-aria-components 1.18.0). "client-only" is the
'client-only' cannot be importederror; "createContext" iscreateContext is not a function.optimizePackageImports: ["@heroui/react"]{ UNSTABLE_ToastQueue }fromreact-aria-components/Toast{ Collection }fromreact-aria-components/Collection{ Toast }from@heroui/react, rendering<Toast.Provider />(#4007){ Skeleton }from@heroui/reactTesting
tests/optimize-imports.test.tsfor subpath rewriting (nested package resolution,?v=ids, SSR/client untouched, unresolvable sources, directive boundaries).Toast.Providerdirectly, andToast.Providerwith a client button callingtoast()return 200 and hydrate without console errors in dev and aftervinext build. The prod build used to fail. In prod, clicking the button shows the toast. No heroui or react-aria module is loaded twice in the rsc or ssr module graphs.app-basic. Build time is unchanged.Known limitations
Toast.Providerdirectly from a Server Component and callingtoast()from a client component shows no toast. plugin-rsc serves the provider's internal"use client"file unbundled while the client component uses the pre-bundled@heroui/react, so the two use different toast queues. plugin-rsc warns about this ("client component dependency is inconsistently optimized") and recommendsoptimizeDeps.exclude: ["@heroui/react"]. Production is not affected.react-aria-components. Barrels that are not listed inoptimizePackageImportsstill fail as before.Collectionnow fails withcreateContext is not a function(at request time in a production build) instead of theclient-onlybuild error, the same error Next.js's Turbopack build gives.