You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consuming external StyleX libraries in Vite: externalPackages + optimizeDeps guide could help
Hi all, it's me again :-)
So, some context, I maintain a design system library that ships uncompiled StyleX — stylex.create(), defineVars(), etc. — meant to be compiled on the consumer side at build time. I recently set up a Vite + React template that consumes this library, and while I got everything working (so far, just finished an hour ago, and not really doing much with the repo yet), the path to get there had some hiccups. I wanted to share what I learned in case it helps others, and to suggest some documentation.
Our setup
Vite 6 with @stylexjs/unplugin v0.17.5
Design system package (@acme): ships an ESM bundle containing uncompiled stylex.create() and stylex.props() calls, with many peer deps (react-aria-components, @react-aria/, @react-stately/, @fortawesome/*, etc.)
Tokens package (@acme-tokens): ships .stylex.js files with defineVars() calls
This correctly tells the unplugin to run the Babel transform on these packages. Production builds work perfectly — 67KB of extracted CSS, everything compiles.
The dev server problem
Where we got stuck was the dev server. With the config above, the page was completely blank. The browser console showed:
Uncaught SyntaxError: The requested module
'.../use-sync-external-store/shim/index.js'
doesn't provide an export named: 'useSyncExternalStore'
The chain of causation:
externalPackages causes the unplugin's config() hook to add those packages to optimizeDeps.exclude. This is necessary — if esbuild pre-bundles them first, the Babel transform never runs on them and no CSS gets extracted.
When our design system package is excluded from optimizeDeps, Vite serves it raw from node_modules. Its imports to peer deps like react-aria-components are also not pre-bundled, because Vite's dep scanner only finds imports in app source code — it never scans inside excluded packages.
Deep in the peer dep tree, some packages import CJS modules (like use-sync-external-store/shim). Since those aren't pre-bundled either, Vite tries to serve the raw CJS files as ESM, and the browser can't parse them.
The fix: Vite's nested dependency syntax
The solution is Vite's optimizeDeps.include with the parent > dep syntax:
This tells Vite: "even though acme is excluded from pre-bundling, go ahead and pre-bundle its peer deps (and their full transitive trees, including CJS-to-ESM conversion)."
This is documented Vite behavior and it's the right tool for this job. It keeps the StyleX packages excluded so the Babel transform works, while ensuring their peer deps are served as proper ESM.
What would have helped us
We got to a working solution using first-class features of both StyleX and Vite. Nothing we did is a hack. But some documentation would have helped:
The externalPackages docs don't mention the optimizeDeps.exclude side effect. The unplugin docs describe externalPackages as telling the transform to process those packages, but they don't mention that it also excludes them from Vite's dependency pre-bundling, or why that's necessary. Understanding this is critical to debugging the CJS errors that will almost certainly follow.
There's no mention of optimizeDeps.include as the companion config. If you use externalPackages on a library with peer deps (which is the common case), you almost certainly need the nested optimizeDeps.include syntax for those peer deps. These two configs should be documented together as a pair.
There's no "consuming external StyleX libraries" guide for any bundler. I remember there used to be examples showing how to use a StyleX version of OpenProps, which was a great reference point for this use case. That example seems to be gone now, and there's nothing to replace it. The current Vite installation docs only cover local StyleX usage. For those of us building design systems or other libraries that ship uncompiled StyleX, there's no end-to-end guide showing the full config needed — for Vite, Next.js/webpack, or any other bundler.
The externalPackages option is missing from the TypeScript types in v0.17.5. It works fine at runtime, but the missing types meant we had to cast to Record<string, unknown>, which made it feel unofficial or unsupported.
For reference: the full working Vite config
Here's our complete vite.config.ts for anyone else trying to do this:
/// <reference types="vitest/config" />import{defineConfig}from'vite';importreactfrom'@vitejs/plugin-react';importstylexfrom'@stylexjs/unplugin';exportdefaultdefineConfig({optimizeDeps: {include: [// The StyleX unplugin excludes external StyleX packages from Vite's// optimizeDeps (so their uncompiled code reaches the Babel transform).// But their peer deps still need pre-bundling so Vite can serve them// as ESM and convert any CJS transitive deps. The `>` syntax tells// Vite to pre-bundle these even though their parent is excluded.'@acme > react-aria-components','@acme > react-aria','@acme > react-stately','@acme > @react-aria/form','@acme > @react-aria/i18n','@acme > @react-aria/label','@acme > @react-aria/toast','@acme > @react-stately/form','@acme > @react-stately/toast','@acme > @react-stately/data','@acme > @fortawesome/react-fontawesome','@acme > @fortawesome/fontawesome-svg-core',],},plugins: [stylex.vite({dev: process.env.NODE_ENV!=='production',useCSSLayers: true,runtimeInjection: false,externalPackages: ['@acme, '@acme-tokens',],}asRecord<string,unknown>),react(),],test: {globals: true,environment: 'jsdom',setupFiles: './src/test-setup.ts',css: true,server: {deps: {inline: ['@acme','@acme-tokens','@stylexjs/stylex',],},},},});
Hope this helps anyone else working through the same thing. Happy to answer questions or provide more details about any part of this.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Consuming external StyleX libraries in Vite:
externalPackages+optimizeDepsguide could helpHi all, it's me again :-)
So, some context, I maintain a design system library that ships uncompiled StyleX —
stylex.create(),defineVars(), etc. — meant to be compiled on the consumer side at build time. I recently set up a Vite + React template that consumes this library, and while I got everything working (so far, just finished an hour ago, and not really doing much with the repo yet), the path to get there had some hiccups. I wanted to share what I learned in case it helps others, and to suggest some documentation.Our setup
@stylexjs/unpluginv0.17.5@acme): ships an ESM bundle containing uncompiledstylex.create()andstylex.props()calls, with many peer deps (react-aria-components, @react-aria/, @react-stately/, @fortawesome/*, etc.)@acme-tokens): ships.stylex.jsfiles withdefineVars()callsWhat worked (and what we wish we'd known sooner)
The
externalPackagesconfig works as intendedThis correctly tells the unplugin to run the Babel transform on these packages. Production builds work perfectly — 67KB of extracted CSS, everything compiles.
The dev server problem
Where we got stuck was the dev server. With the config above, the page was completely blank. The browser console showed:
The chain of causation:
externalPackagescauses the unplugin'sconfig()hook to add those packages tooptimizeDeps.exclude. This is necessary — if esbuild pre-bundles them first, the Babel transform never runs on them and no CSS gets extracted.When our design system package is excluded from optimizeDeps, Vite serves it raw from
node_modules. Its imports to peer deps likereact-aria-componentsare also not pre-bundled, because Vite's dep scanner only finds imports in app source code — it never scans inside excluded packages.Deep in the peer dep tree, some packages import CJS modules (like
use-sync-external-store/shim). Since those aren't pre-bundled either, Vite tries to serve the raw CJS files as ESM, and the browser can't parse them.The fix: Vite's nested dependency syntax
The solution is Vite's
optimizeDeps.includewith theparent > depsyntax:This tells Vite: "even though
acmeis excluded from pre-bundling, go ahead and pre-bundle its peer deps (and their full transitive trees, including CJS-to-ESM conversion)."This is documented Vite behavior and it's the right tool for this job. It keeps the StyleX packages excluded so the Babel transform works, while ensuring their peer deps are served as proper ESM.
What would have helped us
We got to a working solution using first-class features of both StyleX and Vite. Nothing we did is a hack. But some documentation would have helped:
The
externalPackagesdocs don't mention theoptimizeDeps.excludeside effect. The unplugin docs describeexternalPackagesas telling the transform to process those packages, but they don't mention that it also excludes them from Vite's dependency pre-bundling, or why that's necessary. Understanding this is critical to debugging the CJS errors that will almost certainly follow.There's no mention of
optimizeDeps.includeas the companion config. If you useexternalPackageson a library with peer deps (which is the common case), you almost certainly need the nestedoptimizeDeps.includesyntax for those peer deps. These two configs should be documented together as a pair.There's no "consuming external StyleX libraries" guide for any bundler. I remember there used to be examples showing how to use a StyleX version of OpenProps, which was a great reference point for this use case. That example seems to be gone now, and there's nothing to replace it. The current Vite installation docs only cover local StyleX usage. For those of us building design systems or other libraries that ship uncompiled StyleX, there's no end-to-end guide showing the full config needed — for Vite, Next.js/webpack, or any other bundler.
The
externalPackagesoption is missing from the TypeScript types in v0.17.5. It works fine at runtime, but the missing types meant we had to cast toRecord<string, unknown>, which made it feel unofficial or unsupported.For reference: the full working Vite config
Here's our complete
vite.config.tsfor anyone else trying to do this:Hope this helps anyone else working through the same thing. Happy to answer questions or provide more details about any part of this.
Beta Was this translation helpful? Give feedback.
All reactions