Skip to content

className is silently dropped under Vite: no bundler integration exists outside Metro #405

Description

@p-larson

Summary

Under Vite there is nothing that performs the react-nativereact-native-css/components swap, so every className is dropped. The page still renders — same DOM, same text, no error, no warning — it is just unstyled. That makes it easy to ship without noticing.

Opening this before a PR, per CONTRIBUTING's guidance to discuss API changes first. I have a working implementation and would be glad to submit it, adjust the approach, or drop it if you would rather solve this differently.

Reproduction: https://github.com/p-larson/nativewind-vite-storybook-repro

Two apps, byte-identical except for one Vite plugin, plus a script that asserts computed styles rather than "did it render":

[broken]  className in DOM: false   computed size: null x null
[fixed]   className in DOM: true    computed size: 320px x 160px
broken fixed
unstyled styled

Why it happens

NativeWind 4 exported a JSX runtime, so the documented Other Bundlers recipe worked anywhere. Its third requirement is no longer satisfiable on v5 — nativewind@5.0.0-preview.4 exports only ., ./babel, ./metro, ./types, ./theme, and setting jsxImportSource: "nativewind" fails the build:

Error: Missing "./jsx-dev-runtime" specifier in "nativewind" package

The work moved here, into a Metro-only resolver. I understand from the README that Metro is the officially supported bundler and that contributions for others are welcome — this is that contribution.

Proposed approach

Add a react-native-css/vite export applying the mapping nativeResolver already uses:

if (moduleName === "react-native") {
  return resolver(context, `react-native-css/components`, platform);
}

react-native-css/components is already a barrel re-exporting react-native with the wrappers layered on top, so no new module is introduced and the component list cannot drift.

I first tried porting webResolver (rewriting react-native-web/dist/exports/<X>/index.jscomponents/<X>) and abandoned it:

  1. Rollup resolvers do not run during dependency pre-bundling. Making the rewrite fire requires excluding react-native-web from optimizeDeps, which exposes its CommonJS dependencies to the browser as raw CJS — inline-style-prefixer, fbjs, styleq, and others, each needing its own optimizeDeps.include entry, down to deep paths like inline-style-prefixer/lib/plugins/crossFade.js.

  2. It rewrites react-native-web's own internal imports, producing exactly the initialization cycle in babel import-plugin still rewrites react-native-web internals, causing a circular-import crash on web (incomplete fix of #196 / #202) #380.

Following nativeResolver avoids both — react-native-web stays pre-bundled and its internals are never touched.

Two details that are easy to get wrong:

The mapping needs registering twice: as a Vite plugin for source files, and as an esbuild plugin in optimizeDeps.esbuildOptions.plugins for pre-bundled dependencies, since esbuild does not run Rollup resolvers.

Status

Implemented on p-larson/react-native-css@feat/vite-resolver: yarn lint, yarn typecheck, and the full yarn test suite (1056 passing) are green, with 5 new resolver tests. vite is added as an optional peer dependency so Metro-only users are unaffected. I also packed the branch and ran the reproduction against the real react-native-css/vite export end-to-end.

Happy to open the PR if the approach looks reasonable.

Environment

Package Version
react-native-css 3.0.7
nativewind 5.0.0-preview.4
react-native-web 0.21
vite 7.2
@storybook/react-native-web-vite 10.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions