Skip to content

Add an internal Expo-project marker to the Uniwind Metro config and use it to lazily choose the correct transform worker.#593

Open
sync wants to merge 2 commits into
uni-stack:mainfrom
sync:fix/issue-bundling-when-expo-and-rn-same-monorepo
Open

Add an internal Expo-project marker to the Uniwind Metro config and use it to lazily choose the correct transform worker.#593
sync wants to merge 2 commits into
uni-stack:mainfrom
sync:fix/issue-bundling-when-expo-and-rn-same-monorepo

Conversation

@sync

@sync sync commented Jul 3, 2026

Copy link
Copy Markdown

This fixes monorepos that contain both an Expo app and a plain React Native app, where the plain RN app can accidentally resolve Expo's transform worker and fail to build.

Repro app: https://github.com/sync/uniwee

On main, which has this patch applied, pnpm build:ios succeeds. On the without-patch branch, pnpm build:ios fails with:

Unexpected module with full source map found: node_modules/metro-runtime/src/polyfills/require.js

error Unexpected module with full source map found: /Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-runtime/src/polyfills/require.js.
Error: Unexpected module with full source map found: /Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-runtime/src/polyfills/require.js
    at processNextModule (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-source-map/src/source-map.js:78:13)
    at workLoop (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-source-map/src/source-map.js:88:22)
    at fromRawMappingsImpl (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-source-map/src/source-map.js:103:3)
    at /Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-source-map/src/source-map.js:122:5
    at new Promise (<anonymous>)
    at fromRawMappingsNonBlocking (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro-source-map/src/source-map.js:121:10)
    at sourceMapGeneratorNonBlocking (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro/src/DeltaBundler/Serializers/sourceMapGenerator.js:75:57)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async sourceMapStringNonBlocking (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro/src/DeltaBundler/Serializers/sourceMapString.js:18:21)
    at async Server._serializeGraph (/Users/anthonymittaz/Projects/expo/uniwee/node_modules/metro/src/Server.js:216:19)
 ELIFECYCLE  Command failed with exit code 1.

Summary by CodeRabbit

  • New Features
    • Enhanced Metro integration to better detect Expo-based Metro configurations and select the appropriate transformer path.
    • Added an isExpoProject setting to Metro bundling configuration.
  • Bug Fixes
    • Updated transform worker handling to select the correct worker per build, using lazy loading with caching.
  • Documentation
    • Refreshed Metro integration notes to clarify how transformer worker selection is determined for Expo configurations.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66f4c096-800b-4f8b-8ed7-494f05feedd9

📥 Commits

Reviewing files that changed from the base of the PR and between 99b2bac and e664f43.

📒 Files selected for processing (3)
  • CONTEXT.md
  • packages/uniwind/src/bundler/adapters/metro/metro.ts
  • packages/uniwind/src/bundler/adapters/metro/transformer.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/uniwind/src/bundler/adapters/metro/metro.ts
  • packages/uniwind/src/bundler/adapters/metro/transformer.ts

📝 Walkthrough

Walkthrough

The Metro adapter now records whether a config uses Expo’s transformer path, passes that flag through Uniwind’s Metro config, and selects the transform worker lazily per call with caching.

Changes

Metro Transformer Worker Selection

Layer / File(s) Summary
Config type and Expo detection wiring
packages/uniwind/src/bundler/types.ts, packages/uniwind/src/bundler/adapters/metro/metro.ts
Adds isExpoProject to UniwindMetroConfig, detects Expo Metro configs from transformerPath and Expo-specific transformer fields, and stores the flag in transformer.unwind.
Lazy transform worker resolution
packages/uniwind/src/bundler/adapters/metro/transformer.ts, CONTEXT.md
Replaces eager worker initialization with cached per-call worker resolution keyed by isExpoProject, and updates the Metro integration note to describe lazy worker selection.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • uni-stack/uniwind#523: Both PRs modify the Metro adapter’s worker selection and caching logic in the same codepath.

Suggested reviewers: jpudysz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an Expo-project marker and using it to select the Metro transform worker lazily.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR updates Uniwind's Metro integration to choose the right transform worker for Expo and plain React Native projects. The main changes are:

  • Adds an internal Expo-project marker to the Metro transformer config.
  • Detects Expo configs from Expo transformer paths and Expo-specific transformer fields.
  • Lazily loads and caches transform workers separately for Expo and non-Expo builds.
  • Documents the new Metro worker-selection behavior.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
packages/uniwind/src/bundler/adapters/metro/metro.ts Adds Expo Metro config detection and passes the result through transformer.uniwind.isExpoProject.
packages/uniwind/src/bundler/adapters/metro/transformer.ts Changes transform worker resolution to lazy loading with separate cache entries for Expo and non-Expo configs.
packages/uniwind/src/bundler/types.ts Extends the internal Metro config shape with the optional Expo-project marker.
CONTEXT.md Documents the Metro worker-selection behavior.

Reviews (2): Last reviewed commit: "fix(metro): harden transform worker sele..." | Re-trigger Greptile

Comment thread packages/uniwind/src/bundler/adapters/metro/transformer.ts Outdated
Comment thread packages/uniwind/src/bundler/adapters/metro/metro.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/uniwind/src/bundler/adapters/metro/transformer.ts (1)

12-34: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Cache not keyed by isExpoProject.

getTransformWorker caches the first resolved worker in the module-level worker variable regardless of the isExpoProject value on subsequent calls. This is fine as long as one Metro process only ever transforms files for a single project/config, but if this transformer module is ever reused across configs with differing isExpoProject within the same process, the stale cached worker would be returned — the same class of bug this PR is fixing.

♻️ Defensive fix: key the cache by isExpoProject
-let worker: typeof MetroTransformWorker | undefined
+const workerCache = new Map<boolean, typeof MetroTransformWorker>()

 const getTransformWorker = (isExpoProject?: boolean): typeof MetroTransformWorker => {
-    if (worker) {
-        return worker
+    const cacheKey = Boolean(isExpoProject)
+    const cached = workerCache.get(cacheKey)
+    if (cached) {
+        return cached
     }

     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
+    workerCache.set(cacheKey, resolvedWorker)

     return resolvedWorker
 }

Please confirm whether a single Node process can host Metro transforms for multiple projects/configs in this monorepo's build tooling (e.g. programmatic multi-target builds); if not, this is purely defensive.

Also applies to: 45-45

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/uniwind/src/bundler/adapters/metro/transformer.ts` around lines 12 -
34, The module-level cache in getTransformWorker is not keyed by isExpoProject,
so a worker resolved for one project type can be incorrectly reused for another.
Update getTransformWorker in transformer.ts to cache separate resolved workers
for Expo and non-Expo calls (or otherwise key the cache by isExpoProject) while
keeping the existing lazy resolution logic around MetroTransformWorker and
ExpoMetroConfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/uniwind/src/bundler/adapters/metro/transformer.ts`:
- Around line 12-34: The module-level cache in getTransformWorker is not keyed
by isExpoProject, so a worker resolved for one project type can be incorrectly
reused for another. Update getTransformWorker in transformer.ts to cache
separate resolved workers for Expo and non-Expo calls (or otherwise key the
cache by isExpoProject) while keeping the existing lazy resolution logic around
MetroTransformWorker and ExpoMetroConfig.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8917567-d31c-4cee-aae9-59ce3844ca9f

📥 Commits

Reviewing files that changed from the base of the PR and between 6b498f2 and 99b2bac.

📒 Files selected for processing (4)
  • CONTEXT.md
  • packages/uniwind/src/bundler/adapters/metro/metro.ts
  • packages/uniwind/src/bundler/adapters/metro/transformer.ts
  • packages/uniwind/src/bundler/types.ts

@Brentlok Brentlok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @sync thanks for the PR, left some minor comments, but overall the idea is very good! ✨

Comment on lines +12 to +34
type ExpoTransformerConfig = NonNullable<MetroConfig['transformer']> & {
_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,
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could just scan all transformer keys to start with expo or _expo? Having those manually defined doesn't seem easy to maintain, especially because it looks like some expo's internal stuff

Comment on lines -27 to +53
uniwind: bundlerConfig.toMetroConfig(),
uniwind: {
...bundlerConfig.toMetroConfig(),
isExpoProject: isExpoMetroConfig(config),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extend toMetroConfig method to accept isExpoProject param instead of spreading object there to add new property

const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css')

let worker: typeof MetroTransformWorker
const workerCache = new Map<boolean, typeof MetroTransformWorker>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boolean as a Map key seems random at first glance, please add a small comment there explaining it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants