diff --git a/package.json b/package.json index aec3faf..4de7827 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,5 @@ ] } }, - "dependencies": { - "esbuild": "^0.27.4" - } + "dependencies": {} } diff --git a/packages/vitest-react-native/package.json b/packages/vitest-react-native/package.json index 003d094..ac4d7d4 100644 --- a/packages/vitest-react-native/package.json +++ b/packages/vitest-react-native/package.json @@ -59,13 +59,13 @@ }, "dependencies": { "@react-native/polyfills": "^2.0.0", - "esbuild": "^0.27.4", "flow-remove-types": "^2.257.1", "pirates": "^4.0.6", "regenerator-runtime": "^0.14.1" }, "devDependencies": { "@types/node": "^22.10.7", + "esbuild": "^0.27.4", "typescript": "^5.7.3", "vite": "^8.0.3", "vitest": "^4.1.2" diff --git a/packages/vitest-react-native/src/plugin.ts b/packages/vitest-react-native/src/plugin.ts index b5bb3ae..7ea3595 100644 --- a/packages/vitest-react-native/src/plugin.ts +++ b/packages/vitest-react-native/src/plugin.ts @@ -2,12 +2,28 @@ import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import type { Plugin, UserConfig } from 'vite'; -import * as esbuild from 'esbuild'; const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const removeTypes = require('flow-remove-types'); +// Lazily resolved rolldown transform (ships with Vite 8+) +type TransformSyncFn = (filename: string, code: string, options?: { lang?: string }) => { code: string }; +let _transformSync: TransformSyncFn | null = null; +let _transformSyncLoading: Promise | null = null; +const ensureTransformSync = async (): Promise => { + if (_transformSync) return; + if (!_transformSyncLoading) { + _transformSyncLoading = (async () => { + const viteRequire = createRequire(require.resolve('vite')); + const rolldownPath = viteRequire.resolve('rolldown/utils'); + const mod = await import(rolldownPath); + _transformSync = mod.transformSync; + })(); + } + await _transformSyncLoading; +}; + export interface VitestReactNativePluginOptions { /** * Additional file extensions to resolve for iOS platform @@ -55,6 +71,9 @@ export function reactNative(options: VitestReactNativePluginOptions = {}): Plugi return { name: 'vitest-plugin-react-native', enforce: 'pre', + async buildStart() { + await ensureTransformSync(); + }, config(): UserConfig { return { resolve: { @@ -107,10 +126,7 @@ export function reactNative(options: VitestReactNativePluginOptions = {}): Plugi // Strip Flow types then transform JSX const flowStripped = removeTypes(code, { all: true }).toString(); - const result = esbuild.transformSync(flowStripped, { - loader: 'jsx', - sourcefile: id, - }); + const result = _transformSync!(id, flowStripped, { lang: 'jsx' }); return { code: result.code, map: null }; }, }; diff --git a/packages/vitest-react-native/src/setup.ts b/packages/vitest-react-native/src/setup.ts index 4d37752..18090c5 100644 --- a/packages/vitest-react-native/src/setup.ts +++ b/packages/vitest-react-native/src/setup.ts @@ -101,7 +101,6 @@ g.performance = globalThis.performance || { now: Date.now }; import { addHook } from 'pirates'; import removeTypes from 'flow-remove-types'; -import * as esbuild from 'esbuild'; import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -109,6 +108,10 @@ import { createRequire } from 'module'; const require = createRequire(import.meta.url); +// Resolve esbuild through vite's dependencies (no direct dependency needed) +const viteRequire = createRequire(require.resolve('vite')); +const esbuild = viteRequire('esbuild') as typeof import('esbuild'); + // ============================================================================ // STEP 3: Set up cache directory for transformed files // ============================================================================ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54c2170..452a314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,6 @@ settings: importers: .: - dependencies: - esbuild: - specifier: ^0.27.4 - version: 0.27.4 devDependencies: '@changesets/cli': specifier: ^2.29.8 @@ -160,9 +156,6 @@ importers: '@react-native/polyfills': specifier: ^2.0.0 version: 2.0.0 - esbuild: - specifier: ^0.27.4 - version: 0.27.4 flow-remove-types: specifier: ^2.257.1 version: 2.298.0 @@ -182,6 +175,9 @@ importers: '@types/node': specifier: ^22.10.7 version: 22.19.7 + esbuild: + specifier: ^0.27.4 + version: 0.27.4 typescript: specifier: ^5.7.3 version: 5.9.3 diff --git a/test/__tests__/JsxTransform.spec.tsx b/test/__tests__/JsxTransform.spec.tsx index 2508886..c5533e2 100644 --- a/test/__tests__/JsxTransform.spec.tsx +++ b/test/__tests__/JsxTransform.spec.tsx @@ -28,6 +28,11 @@ describe('JSX transform for third-party packages (issue #4)', () => { transformPackages: ['react-native-modal-datetime-picker'], }); + // Initialize rolldown (normally done by Vite via buildStart) + if (plugin.buildStart) { + await (plugin.buildStart as (...args: any[]) => any).call({}); + } + const jsxCode = ` import React from 'react'; export default function Picker() { diff --git a/vitest.config.ts b/vitest.config.ts index 2bf618c..6ebb3fe 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,11 +3,15 @@ import { defineConfig } from 'vitest/config'; import { resolve } from 'path'; import { createRequire } from 'module'; import type { Plugin } from 'vite'; -import * as esbuild from 'esbuild'; const require = createRequire(import.meta.url); const removeTypes = require('flow-remove-types'); +// Resolve rolldown through vite's dependencies (pnpm nests it) +const viteRequire = createRequire(require.resolve('vite')); +const rolldownPath = viteRequire.resolve('rolldown/utils'); +const { transformSync } = await import(rolldownPath) as { transformSync: (filename: string, code: string, options?: { lang?: string }) => { code: string } }; + function reactNativeDependencyTransformPlugin(): Plugin { const rnSpecificExts = ['.ios.js', '.ios.jsx', '.android.js', '.android.jsx', '.native.js', '.native.jsx']; const transformableExts = ['.js', '.jsx', ...rnSpecificExts]; @@ -36,7 +40,7 @@ function reactNativeDependencyTransformPlugin(): Plugin { if (!isRN) return; const flowStripped = removeTypes(code, { all: true }).toString(); - const result = esbuild.transformSync(flowStripped, { loader: 'jsx', sourcefile: id }); + const result = transformSync(id, flowStripped, { lang: 'jsx' }); return { code: result.code, map: null }; }, };