From 99b2bacfbb56c395919bad3f441aec7d12f78c7e Mon Sep 17 00:00:00 2001 From: Anthony Mittaz Date: Fri, 3 Jul 2026 14:38:50 +1000 Subject: [PATCH 1/2] fix(metro): defer transform worker resolution for non-expo projects --- CONTEXT.md | 1 + .../src/bundler/adapters/metro/metro.ts | 5 ++- .../src/bundler/adapters/metro/transformer.ts | 31 +++++++++++++------ packages/uniwind/src/bundler/types.ts | 1 + 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 09fdcee2..6952bd82 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -113,6 +113,7 @@ Metro integration: - `withUniwindConfig(config, uniwindConfig)` patches Metro graph support for uncached modules. - Metro adds `css` as source extension and removes it from asset extensions. - Metro transformer handles the configured CSS entry file specially. +- Metro transformer worker selection is lazy and follows whether the incoming Metro config uses Expo's transformer path. - Native platform CSS transforms into a JS module that calls `Uniwind.__reinit(...)`. - Web platform CSS transforms into CSS plus web runtime setup. - Resolver swaps React Native component imports to Uniwind-aware implementations where needed. diff --git a/packages/uniwind/src/bundler/adapters/metro/metro.ts b/packages/uniwind/src/bundler/adapters/metro/metro.ts index 106868bb..0cf81bdd 100644 --- a/packages/uniwind/src/bundler/adapters/metro/metro.ts +++ b/packages/uniwind/src/bundler/adapters/metro/metro.ts @@ -24,7 +24,10 @@ export const withUniwindConfig = ( transformerPath: require.resolve('./transformer.cjs'), transformer: { ...config.transformer, - uniwind: bundlerConfig.toMetroConfig(), + uniwind: { + ...bundlerConfig.toMetroConfig(), + isExpoProject: config.transformerPath?.includes('@expo/metro-config'), + }, }, resolver: { ...config.resolver, diff --git a/packages/uniwind/src/bundler/adapters/metro/transformer.ts b/packages/uniwind/src/bundler/adapters/metro/transformer.ts index 1addaf5a..6bed1cf3 100644 --- a/packages/uniwind/src/bundler/adapters/metro/transformer.ts +++ b/packages/uniwind/src/bundler/adapters/metro/transformer.ts @@ -9,18 +9,28 @@ import path from 'path' const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css') -let worker: typeof MetroTransformWorker +let worker: typeof MetroTransformWorker | undefined -try { - try { - const { unstable_transformerPath } = require('@expo/metro-config') as typeof ExpoMetroConfig - - worker = require(unstable_transformerPath) - } catch { - worker = require('@expo/metro-config/build/transform-worker/transform-worker.js') +const getTransformWorker = (isExpoProject?: boolean): typeof MetroTransformWorker => { + if (worker) { + return worker } -} catch { - worker = require('metro-transform-worker') + + const resolvedWorker: typeof MetroTransformWorker = isExpoProject + ? (() => { + try { + const { unstable_transformerPath } = require('@expo/metro-config') as typeof ExpoMetroConfig + + return require(unstable_transformerPath) + } catch { + return require('@expo/metro-config/build/transform-worker/transform-worker.js') + } + })() + : require('metro-transform-worker') + + worker = resolvedWorker + + return resolvedWorker } export const transform = async ( @@ -32,6 +42,7 @@ export const transform = async ( data: Buffer, options: JsTransformOptions, ) => { + const worker = getTransformWorker(config.uniwind.isExpoProject) const isCss = options.type !== 'asset' && path.join(process.cwd(), config.uniwind.cssEntryFile) === path.join(projectRoot, filePath) if (filePath.endsWith('/components/web/metro-injected.js')) { diff --git a/packages/uniwind/src/bundler/types.ts b/packages/uniwind/src/bundler/types.ts index 704c7e60..5f7d831a 100644 --- a/packages/uniwind/src/bundler/types.ts +++ b/packages/uniwind/src/bundler/types.ts @@ -11,5 +11,6 @@ export type Polyfills = { export type UniwindMetroConfig = UniwindConfig & { polyfills?: Polyfills debug?: boolean + isExpoProject?: boolean isTV?: boolean } From e664f439bd2c8985655bd5bf909336c98c5ebc1c Mon Sep 17 00:00:00 2001 From: Anthony Mittaz Date: Fri, 3 Jul 2026 15:00:20 +1000 Subject: [PATCH 2/2] fix(metro): harden transform worker selection --- CONTEXT.md | 2 +- .../src/bundler/adapters/metro/metro.ts | 25 ++++++++++++++++++- .../src/bundler/adapters/metro/transformer.ts | 13 ++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 6952bd82..bdcfe76b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -113,7 +113,7 @@ Metro integration: - `withUniwindConfig(config, uniwindConfig)` patches Metro graph support for uncached modules. - Metro adds `css` as source extension and removes it from asset extensions. - Metro transformer handles the configured CSS entry file specially. -- Metro transformer worker selection is lazy and follows whether the incoming Metro config uses Expo's transformer path. +- Metro transformer worker selection is lazy, cached per Expo/non-Expo config type, and follows Expo transformer paths or Expo-specific config markers. - Native platform CSS transforms into a JS module that calls `Uniwind.__reinit(...)`. - Web platform CSS transforms into CSS plus web runtime setup. - Resolver swaps React Native component imports to Uniwind-aware implementations where needed. diff --git a/packages/uniwind/src/bundler/adapters/metro/metro.ts b/packages/uniwind/src/bundler/adapters/metro/metro.ts index 0cf81bdd..7216c737 100644 --- a/packages/uniwind/src/bundler/adapters/metro/metro.ts +++ b/packages/uniwind/src/bundler/adapters/metro/metro.ts @@ -9,6 +9,29 @@ import { nativeResolver, webResolver } from './resolvers' const isUniwindRequest = (moduleName: string) => moduleName === 'uniwind' || moduleName.startsWith('uniwind/') +type ExpoTransformerConfig = NonNullable & { + _expoRelativeProjectRoot?: string + _expoRouterPath?: string + expo_customTransformerPath?: string | false + postcssHash?: string | null +} + +const isExpoMetroConfig = (config: MetroConfig) => { + const transformerPath = config.transformerPath + const transformer = config.transformer as ExpoTransformerConfig | undefined + const hasExpoTransformerField = transformer + ? '_expoRelativeProjectRoot' in transformer + || '_expoRouterPath' in transformer + || 'expo_customTransformerPath' in transformer + || 'postcssHash' in transformer + : false + + return Boolean( + transformerPath?.includes('@expo/metro-config') + || hasExpoTransformerField, + ) +} + export const withUniwindConfig = ( config: T, uniwindConfig: UniwindConfig, @@ -26,7 +49,7 @@ export const withUniwindConfig = ( ...config.transformer, uniwind: { ...bundlerConfig.toMetroConfig(), - isExpoProject: config.transformerPath?.includes('@expo/metro-config'), + isExpoProject: isExpoMetroConfig(config), }, }, resolver: { diff --git a/packages/uniwind/src/bundler/adapters/metro/transformer.ts b/packages/uniwind/src/bundler/adapters/metro/transformer.ts index 6bed1cf3..e645bdc4 100644 --- a/packages/uniwind/src/bundler/adapters/metro/transformer.ts +++ b/packages/uniwind/src/bundler/adapters/metro/transformer.ts @@ -9,14 +9,17 @@ import path from 'path' const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css') -let worker: typeof MetroTransformWorker | undefined +const workerCache = new Map() const getTransformWorker = (isExpoProject?: boolean): typeof MetroTransformWorker => { - if (worker) { - return worker + const cacheKey = Boolean(isExpoProject) + const cachedWorker = workerCache.get(cacheKey) + + if (cachedWorker) { + return cachedWorker } - const resolvedWorker: typeof MetroTransformWorker = isExpoProject + const resolvedWorker: typeof MetroTransformWorker = cacheKey ? (() => { try { const { unstable_transformerPath } = require('@expo/metro-config') as typeof ExpoMetroConfig @@ -28,7 +31,7 @@ const getTransformWorker = (isExpoProject?: boolean): typeof MetroTransformWorke })() : require('metro-transform-worker') - worker = resolvedWorker + workerCache.set(cacheKey, resolvedWorker) return resolvedWorker }