From fa9210a41c7a168a8ec562fcbf3c4ab90654fc4a Mon Sep 17 00:00:00 2001 From: p-larson Date: Thu, 6 Aug 2026 15:36:56 -0500 Subject: [PATCH 1/2] feat(vite): add a Vite resolver so className works outside Metro 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 #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. --- README.md | 34 +- package.json | 22 +- src/__tests__/vite/resolver.test.ts | 88 ++++ src/vite/index.ts | 111 +++++ yarn.lock | 634 +++++++++++++++++++++++++++- 5 files changed, 884 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/vite/resolver.test.ts create mode 100644 src/vite/index.ts diff --git a/README.md b/README.md index bafb230..a76c6ae 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,41 @@ Follow the Expo instructions, but replace the `expo` package with `@expo/metro-c + import { getDefaultConfig } from "@expo/metro-config"; ``` +### Vite based projects + +Add the `reactNativeCSS` plugin to your Vite configuration: + +```ts +import { defineConfig } from "vite"; +import { reactNativeCSS } from "react-native-css/vite"; + +export default defineConfig({ + plugins: [reactNativeCSS()], +}); +``` + +For Storybook's `react-native-web-vite` framework, add it in `viteFinal`: + +```ts +import { mergeConfig } from "vite"; +import { reactNativeCSS } from "react-native-css/vite"; + +const config = { + framework: "@storybook/react-native-web-vite", + viteFinal: (config) => + mergeConfig(config, { plugins: [reactNativeCSS()] }), +}; + +export default config; +``` + +Vite does not process CSS through the Metro transformer, so import your +Tailwind entry stylesheet directly (for example in `.storybook/preview.ts`) +and let Vite's PostCSS pipeline handle it. + ### Other bundlers -`react-native-css` officially only supports Metro as the bundler, but we welcome community contributions to support other bundlers like Webpack, Vite or Turbopack. +`react-native-css` officially supports Metro and Vite, but we welcome community contributions to support other bundlers like Webpack or Turbopack. More documentation coming soon. diff --git a/package.json b/package.json index 29c0423..b8cf20c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,17 @@ "default": "./dist/commonjs/metro/index.js" } }, + "./vite": { + "source": "./src/vite/index.ts", + "import": { + "types": "./dist/typescript/module/src/vite/index.d.ts", + "default": "./dist/module/vite/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/src/vite/index.d.ts", + "default": "./dist/commonjs/vite/index.js" + } + }, "./native": { "source": "./src/native/index.ts", "import": { @@ -207,7 +218,13 @@ "@expo/metro-config": ">=54", "lightningcss": ">=1.27.0", "react": ">=19", - "react-native": ">=0.81" + "react-native": ">=0.81", + "vite": ">=5" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } }, "devDependencies": { "@babel/core": "^7.28.0", @@ -250,7 +267,8 @@ "tailwindcss": "^4.1.12", "tailwindcss-safe-area": "^1.1.0", "typescript": "^5.9.2", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.40.0", + "vite": "^7.2.2" }, "react-native-builder-bob": { "source": "src", diff --git a/src/__tests__/vite/resolver.test.ts b/src/__tests__/vite/resolver.test.ts new file mode 100644 index 0000000..624d73b --- /dev/null +++ b/src/__tests__/vite/resolver.test.ts @@ -0,0 +1,88 @@ +import { sep } from "node:path"; + +import { reactNativeCSS } from "../../vite"; + +type ResolveIdHook = ( + this: { resolve: jest.Mock }, + source: string, + importer: string | undefined, + options: Record, +) => Promise; + +function setup() { + const plugin = reactNativeCSS(); + const resolve = jest.fn().mockResolvedValue({ id: "/resolved/components" }); + const resolveId = plugin.resolveId as unknown as ResolveIdHook; + + const call = (source: string, importer?: string) => + resolveId.call({ resolve }, source, importer, {}); + + return { plugin, resolve, call }; +} + +const rncFile = `${sep}node_modules${sep}react-native-css${sep}dist${sep}components${sep}View.js`; +const appFile = `${sep}app${sep}src${sep}Button.tsx`; + +describe("vite resolver", () => { + it("redirects react-native to react-native-css/components", async () => { + const { resolve, call } = setup(); + + await call("react-native", appFile); + + expect(resolve).toHaveBeenCalledWith( + "react-native-css/components", + appFile, + expect.objectContaining({ skipSelf: true }), + ); + }); + + it("redirects react-native-web, which bundlers alias to before plugins run", async () => { + const { resolve, call } = setup(); + + await call("react-native-web", appFile); + + expect(resolve).toHaveBeenCalledWith( + "react-native-css/components", + appFile, + expect.objectContaining({ skipSelf: true }), + ); + }); + + it("does not redirect imports from react-native-css itself", async () => { + const { resolve, call } = setup(); + + // The components barrel re-exports react-native and each wrapper uses its + // base component at module scope, so redirecting these would cycle. + await expect(call("react-native", rncFile)).resolves.toBeNull(); + expect(resolve).not.toHaveBeenCalled(); + }); + + it("ignores unrelated specifiers", async () => { + const { resolve, call } = setup(); + + for (const source of [ + "react", + "react-native-svg", + "react-native-web/dist/exports/View", + "./View", + ]) { + await expect(call(source, appFile)).resolves.toBeNull(); + } + + expect(resolve).not.toHaveBeenCalled(); + }); + + it("registers the same mapping for dependency pre-bundling", () => { + const { plugin } = setup(); + + const config = ( + plugin.config as unknown as () => { + optimizeDeps: { esbuildOptions: { plugins: { name: string }[] } }; + } + )(); + + expect(config.optimizeDeps.esbuildOptions.plugins).toEqual([ + expect.objectContaining({ name: "react-native-css" }), + ]); + }); +}); diff --git a/src/vite/index.ts b/src/vite/index.ts new file mode 100644 index 0000000..1c1cc03 --- /dev/null +++ b/src/vite/index.ts @@ -0,0 +1,111 @@ +import { sep } from "node:path"; + +import type { Plugin } from "vite"; + +/** + * The specifier `vite-plugin-rnw` and most React Native Web setups alias + * `react-native` to. The alias is applied before user plugins run, so both + * names have to be matched or the plugin silently never fires. + */ +const REACT_NATIVE_SPECIFIER = /^react-native(-web)?$/; + +const COMPONENTS = "react-native-css/components"; + +const ESBUILD_PLUGIN_NAME = "react-native-css"; + +/** + * Equivalent of the Metro resolvers' `isFromThisModule`. + * + * `react-native-css/components` re-exports `react-native`, and each wrapper + * uses its base component at module scope, so redirecting our own imports + * would create an initialization cycle. + */ +function isFromThisModule(importer: string | undefined): boolean { + if (!importer) { + return false; + } + + const filename = importer.split("?")[0] ?? importer; + + return filename.includes(`${sep}react-native-css${sep}`); +} + +/** + * Vite plugin that enables `className` under Vite-based bundlers, the same way + * `withReactNativeCSS` does for Metro. + * + * ```ts + * // vite.config.ts + * import { reactNativeCSS } from "react-native-css/vite"; + * + * export default defineConfig({ + * plugins: [reactNativeCSS()], + * }); + * ``` + * + * It applies the 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 approach (rewriting `react-native-web`'s internal module + * paths) is deliberately not used here. Rollup resolvers do not run during + * dependency pre-bundling, so it would require excluding `react-native-web` + * from `optimizeDeps` and then 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 #380. + */ +export function reactNativeCSS(): Plugin { + return { + name: "react-native-css", + enforce: "pre", + + config() { + return { + optimizeDeps: { + /** + * Pre-bundled dependencies are resolved by esbuild, which does not + * run Rollup resolvers, so the same mapping is registered there. + */ + esbuildOptions: { + plugins: [ + { + name: ESBUILD_PLUGIN_NAME, + setup(build) { + build.onResolve( + { filter: REACT_NATIVE_SPECIFIER }, + async (args) => { + if (isFromThisModule(args.importer)) { + return undefined; + } + + const resolved = await build.resolve(COMPONENTS, { + kind: args.kind, + resolveDir: args.resolveDir, + }); + + return resolved.errors.length > 0 ? undefined : resolved; + }, + ); + }, + }, + ], + }, + }, + }; + }, + + async resolveId(source, importer, options) { + if (!REACT_NATIVE_SPECIFIER.test(source)) { + return null; + } + + if (isFromThisModule(importer)) { + return null; + } + + return this.resolve(COMPONENTS, importer, { ...options, skipSelf: true }); + }, + }; +} + +export default reactNativeCSS; diff --git a/yarn.lock b/yarn.lock index 8fe7596..fb13556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,6 +2686,188 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" @@ -4012,6 +4194,13 @@ __metadata: languageName: node linkType: hard +"@napi-rs/lzma-linux-x64-gnu@npm:1.5.1": + version: 1.5.1 + resolution: "@napi-rs/lzma-linux-x64-gnu@npm:1.5.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.12": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -4416,6 +4605,181 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.62.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-android-arm64@npm:4.62.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.62.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.62.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-arm64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.62.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-x64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.62.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.62.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.62.4" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.62.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.62.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.62.4" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.62.4" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.62.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.62.4" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.62.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-musl@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.62.4" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.62.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.62.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.62.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openbsd-x64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-openbsd-x64@npm:4.62.4" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.62.4" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.62.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.62.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.62.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.62.4": + version: 4.62.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.62.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@simple-libs/child-process-utils@npm:^1.0.0": version: 1.0.2 resolution: "@simple-libs/child-process-utils@npm:1.0.2" @@ -4739,6 +5103,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.9": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -7225,6 +7596,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.27.0 || ^0.28.0": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -8007,7 +8467,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -8017,7 +8477,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -11828,6 +12288,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4": + version: 4.0.5 + resolution: "picomatch@npm:4.0.5" + checksum: 10c0/947bc6b6e1ff1e6c5aaf95b107a0839d12802f4f7b867663f67d47accba939ca1cb582cf99dfc30438efa1c4648ac5990967e783e8929c36b03e8440704ef1bd + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -12307,11 +12774,16 @@ __metadata: tailwindcss-safe-area: "npm:^1.1.0" typescript: "npm:^5.9.2" typescript-eslint: "npm:^8.40.0" + vite: "npm:^7.2.2" peerDependencies: "@expo/metro-config": ">=54" lightningcss: ">=1.27.0" react: ">=19" react-native: ">=0.81" + vite: ">=5" + peerDependenciesMeta: + vite: + optional: true languageName: unknown linkType: soft @@ -12873,6 +13345,99 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.43.0": + version: 4.62.4 + resolution: "rollup@npm:4.62.4" + dependencies: + "@napi-rs/lzma-linux-x64-gnu": "npm:1.5.1" + "@rollup/rollup-android-arm-eabi": "npm:4.62.4" + "@rollup/rollup-android-arm64": "npm:4.62.4" + "@rollup/rollup-darwin-arm64": "npm:4.62.4" + "@rollup/rollup-darwin-x64": "npm:4.62.4" + "@rollup/rollup-freebsd-arm64": "npm:4.62.4" + "@rollup/rollup-freebsd-x64": "npm:4.62.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.62.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.62.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.62.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.62.4" + "@rollup/rollup-linux-loong64-gnu": "npm:4.62.4" + "@rollup/rollup-linux-loong64-musl": "npm:4.62.4" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.62.4" + "@rollup/rollup-linux-ppc64-musl": "npm:4.62.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.62.4" + "@rollup/rollup-linux-riscv64-musl": "npm:4.62.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.62.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.62.4" + "@rollup/rollup-linux-x64-musl": "npm:4.62.4" + "@rollup/rollup-openbsd-x64": "npm:4.62.4" + "@rollup/rollup-openharmony-arm64": "npm:4.62.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.62.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.62.4" + "@rollup/rollup-win32-x64-gnu": "npm:4.62.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.62.4" + "@types/estree": "npm:1.0.9" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@napi-rs/lzma-linux-x64-gnu": + optional: true + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/d83bcc89f02db337963dcd0b0d16620129cee263f8437464c02d782dde4e1bb89a6b5b511cd230317ce2c5959fb858884305a9a1a33d1d3da4eef9fe6368414b + languageName: node + linkType: hard + "run-applescript@npm:^7.0.0": version: 7.1.0 resolution: "run-applescript@npm:7.1.0" @@ -13718,6 +14283,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -14156,6 +14731,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:^7.2.2": + version: 7.3.6 + resolution: "vite@npm:7.3.6" + dependencies: + esbuild: "npm:^0.27.0 || ^0.28.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/c6d359f84ad362f5c97ff7988b77c90add04162c60cdf6059375a7d9e3217848c53a8cb6b6c48517bef84b6bba4c9bc85319a889b5236acb7d5a2bc8cb7b9a8d + languageName: node + linkType: hard + "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1" From f875fd9f2d4abc5e6476d0e0805f3b354db2b47c Mon Sep 17 00:00:00 2001 From: p-larson Date: Thu, 6 Aug 2026 15:54:51 -0500 Subject: [PATCH 2/2] refactor(vite): anchor the self-import guard on the package directory 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. --- src/__tests__/vite/resolver.test.ts | 22 +++++++++++++++++----- src/vite/index.ts | 21 ++++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/__tests__/vite/resolver.test.ts b/src/__tests__/vite/resolver.test.ts index 624d73b..3c53f3b 100644 --- a/src/__tests__/vite/resolver.test.ts +++ b/src/__tests__/vite/resolver.test.ts @@ -1,4 +1,4 @@ -import { sep } from "node:path"; +import { resolve as resolvePath } from "node:path"; import { reactNativeCSS } from "../../vite"; @@ -20,8 +20,11 @@ function setup() { return { plugin, resolve, call }; } -const rncFile = `${sep}node_modules${sep}react-native-css${sep}dist${sep}components${sep}View.js`; -const appFile = `${sep}app${sep}src${sep}Button.tsx`; +/** Inside this package, so `isFromThisModule` must skip it. */ +const ownFile = resolvePath( + "/app/node_modules/react-native-css/dist/module/components/View.js", +); +const appFile = resolvePath("/app/src/Button.tsx"); describe("vite resolver", () => { it("redirects react-native to react-native-css/components", async () => { @@ -48,12 +51,21 @@ describe("vite resolver", () => { ); }); - it("does not redirect imports from react-native-css itself", async () => { + it("does not redirect imports from this module", async () => { const { resolve, call } = setup(); // The components barrel re-exports react-native and each wrapper uses its // base component at module scope, so redirecting these would cycle. - await expect(call("react-native", rncFile)).resolves.toBeNull(); + await expect(call("react-native", ownFile)).resolves.toBeNull(); + expect(resolve).not.toHaveBeenCalled(); + }); + + it("strips Vite's query suffix before checking the importer", async () => { + const { resolve, call } = setup(); + + await expect( + call("react-native", `${ownFile}?v=abc123`), + ).resolves.toBeNull(); expect(resolve).not.toHaveBeenCalled(); }); diff --git a/src/vite/index.ts b/src/vite/index.ts index 1c1cc03..09eb4d3 100644 --- a/src/vite/index.ts +++ b/src/vite/index.ts @@ -13,12 +13,23 @@ const COMPONENTS = "react-native-css/components"; const ESBUILD_PLUGIN_NAME = "react-native-css"; +const THIS_MODULE_DIR = `${sep}react-native-css${sep}`; + /** - * Equivalent of the Metro resolvers' `isFromThisModule`. + * Serves the same purpose as `isFromThisModule` in the Metro resolvers and the + * babel plugin: `react-native-css/components` re-exports `react-native`, and + * each wrapper uses its base component at module scope, so redirecting our own + * imports would create an initialization cycle. + * + * Those copies anchor 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 and the check + * silently never matches. Vite resolves through whichever condition the + * consumer configured, so this matches on the package directory instead, which + * holds for both layouts. * - * `react-native-css/components` re-exports `react-native`, and each wrapper - * uses its base component at module scope, so redirecting our own imports - * would create an initialization cycle. + * Vite also appends query suffixes to ids (`?v=`, `?import`), so strip those + * before comparing. */ function isFromThisModule(importer: string | undefined): boolean { if (!importer) { @@ -27,7 +38,7 @@ function isFromThisModule(importer: string | undefined): boolean { const filename = importer.split("?")[0] ?? importer; - return filename.includes(`${sep}react-native-css${sep}`); + return filename.includes(THIS_MODULE_DIR); } /**