feat(vite): add a Vite resolver so className works outside Metro - #406
feat(vite): add a Vite resolver so className works outside Metro#406p-larson wants to merge 2 commits into
Conversation
NativeWind 5 removed the JSX runtime export that made className work under any bundler, so on Vite the react-native -> react-native-css/components swap never happens and every className is silently dropped. Stories and pages still render, just unstyled, which makes it easy to miss. This adds a `react-native-css/vite` export applying the same mapping `nativeResolver` already uses: resolve `react-native` to `react-native-css/components`, which re-exports react-native with the className-aware wrappers layered on top. `webResolver`'s path rewriting is deliberately not ported. Rollup resolvers do not run during dependency pre-bundling, so it would require excluding react-native-web from optimizeDeps and re-adding each of its CommonJS dependencies by hand. It also rewrites react-native-web's own internal imports, which is the circular-import crash in nativewind#380. Two details the implementation depends on: - Imports originating inside react-native-css are skipped, mirroring `isFromThisModule`. The barrel re-exports react-native and the wrappers use their base component at module scope, so redirecting them would cycle. - Both `react-native` and `react-native-web` are matched, because vite-plugin-rnw applies that alias before user plugins run. The mapping is registered as a Vite plugin for source files and as an esbuild plugin in optimizeDeps for pre-bundled dependencies, since esbuild does not run Rollup resolvers. vite is added as an optional peer dependency so Metro-only users are unaffected.
|
Following up on the obvious question — why a resolver rather than the existing It does work for app source. It is the weaker layer for one structural reason, plus two packaging bugs that are worth fixing regardless of this PR. It does not reach code inside dependencies
Widening the filter to cover Resolution also happens once per specifier instead of an AST pass per module. Two packaging bugs found while testingThese are independent of this PR — happy to split them into their own issue, or fix them here if you prefer. 1. The ESM build of the babel preset calls Source is export default function () {
return {
plugins: [
require("./import-plugin").default,
"react-native-worklets/plugin",
],
};
}Under Metro this resolves to the CommonJS build so it never surfaces, but the ESM build is unusable by any ESM-based babel config loader. 2. If you would rather solve Vite through babel than a resolver, fixing those two would be the prerequisite — though the dependency-coverage gap above would remain. |
The Metro resolvers and babel plugin anchor isFromThisModule on resolve(__dirname, "../../../dist"), which only lands on the package when running from the built output. From src -- the source export condition -- it resolves outside the package, so the guard never matches. Vite resolves through whichever condition the consumer configured, so match on the package directory instead. Adds a case for Vite's query suffixes.
|
One refinement pushed, and it surfaces a latent issue in the existing code worth flagging separately. I initially copied const thisModuleDist = resolve(__dirname, "../../../dist");That depth is correct for the built output ( So from The same appears to apply to Full suite still green: 1057 passing, 56 suites, plus |
Closes #405 — opened there first per CONTRIBUTING's guidance on API changes. Happy to rework or close this if you would rather take a different approach.
Problem
Under Vite nothing performs the
react-native→react-native-css/componentsswap, so everyclassNameis dropped. The page still renders — same DOM, same text, no error and no warning — it is just unstyled.NativeWind 4 exported a JSX runtime, so the documented Other Bundlers recipe worked anywhere. On v5 its third requirement cannot be satisfied:
nativewind@5.0.0-preview.4exports only.,./babel,./metro,./types,./theme, and settingjsxImportSource: "nativewind"fails withMissing "./jsx-dev-runtime" specifier in "nativewind" package.Approach
Adds a
react-native-css/viteexport applying the mappingnativeResolveralready uses:react-native-css/componentsis already a barrel re-exportingreact-nativewith the wrappers layered on top, so no new module is introduced and the component list cannot drift as wrappers are added.Why not port
webResolverRewriting
react-native-web/dist/exports/<X>/index.js→components/<X>was tried first and abandoned:react-native-webfromoptimizeDeps, which exposes its CommonJS dependencies to the browser as raw CJS —inline-style-prefixer,fbjs,styleqand others, each needing its ownoptimizeDeps.includeentry, down to deep paths likeinline-style-prefixer/lib/plugins/crossFade.js.react-native-web's own internal imports, producing the initialization cycle reported in babel import-plugin still rewrites react-native-web internals, causing a circular-import crash on web (incomplete fix of #196 / #202) #380.Following
nativeResolveravoids both:react-native-webstays pre-bundled and its internals are never touched.Implementation notes
react-native-cssare skipped, mirroringisFromThisModule. The barrel re-exportsreact-nativeand each wrapper uses its base component at module scope (copyComponentProperties(RNView, …)runs during evaluation), so redirecting them recreates the babel import-plugin still rewrites react-native-web internals, causing a circular-import crash on web (incomplete fix of #196 / #202) #380 cycle.react-nativeandreact-native-webare matched, becausevite-plugin-rnwapplies that alias before user plugins run. Matching onlyreact-nativesilently no-ops.optimizeDeps.esbuildOptions.pluginsfor pre-bundled dependencies, since esbuild does not run Rollup resolvers.viteis an optional peer dependency, so Metro-only users are unaffected.Testing
src/__tests__/vite/resolver.test.tscovering both specifiers, theisFromThisModuleguard, unrelated specifiers, and the optimizeDeps registration.yarn lint,yarn typecheck, andyarn test(1056 passing, 56 suites) all green.react-native-css/viteexport against a reproduction repo: https://github.com/p-larson/nativewind-vite-storybook-reproThat repo has two apps, byte-identical except for this plugin, and asserts computed styles rather than "did it render":
Docs
README's "Other bundlers" section is replaced with a "Vite based projects" section covering plain Vite and Storybook's
react-native-web-vite.