chore: enable Metro unstable_enablePackageExports#30386
Conversation
…e exports Both were added defensively during the RN 0.81.5 / Expo SDK 54 upgrade (#29195) and verified unnecessary locally: - babel.config.js: remove the inline `import() -> Promise.resolve().then(() => require())` visitor. babel-preset-expo / Metro's own dynamic-import handling already covers the PerpsController MYX dynamic import and lilconfig's ESM loader, so Hermes no longer trips on raw `import()` syntax. - metro.config.js: enable `unstable_enablePackageExports`. The LavaMoat lockdownSerializer ordering issue documented in the RN upgrade PR no longer reproduces; the manual @ledgerhq/* lib/<subpath> fallback in `resolveRequest` can be cleaned up in a follow-up. TODO before merge: validate a release build (Hermes bytecode compile + LavaMoat lockdown cold start), Snaps install + invoke, the MYX-enabled Perps path, and a power-user wallet cold start. Co-authored-by: Cursor <cursoragent@cursor.com>
…lity
The babel transform removed in the prior commit was doing two jobs, not
just the Hermes one its original commit message described:
1. Hermes release-bytecode parser (PerpsController.ts, lilconfig): turns
out to already be covered by babel-preset-expo, so dropping the
transform was safe for that case.
2. Jest VM context: Node refuses `import()` callbacks unless launched
with `--experimental-vm-modules`. NavigationService.ts uses
`import('../AgenticService/AgenticService')` inside an
`if (__DEV__) { }` branch (active in Jest), so any test that touches
NavigationService — e.g. PerpsTutorialCarousel.view.test.tsx via
`setupFirstTimeTutorial` — fails with:
"TypeError: A dynamic import callback was invoked without
--experimental-vm-modules".
Restore the inline visitor so `import(x)` is rewritten to
`Promise.resolve().then(() => require(x))` at the AST level. Production
release bundles get the same shape (no behavioural change) and Jest can
execute the call because it's now a plain `require`.
Keep `unstable_enablePackageExports: true` from the previous commit —
that side of the cleanup is genuinely safe and unrelated to this issue.
Co-authored-by: Cursor <cursoragent@cursor.com>
The dynamicImportToRequire transform itself was never actually removed on this branch — the first commit removed it, the second restored it, and only the surrounding comment ended up modified. Restore the comment to main's wording too so this PR's diff is strictly the metro.config.js `unstable_enablePackageExports` change. Co-authored-by: Cursor <cursoragent@cursor.com>
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: This is a fundamental build configuration change that affects how Metro resolves ALL npm packages that use the
Given that this change touches the fundamental module resolution mechanism for the entire app bundle, ALL test suites should run to validate that no feature area is broken by the changed module resolution behavior. Performance Test Selection: |
|



Description
Enable Metro's Node-style
exportsfield resolution by settingresolver.unstable_enablePackageExports: trueinmetro.config.js.The flag was added but immediately disabled during the RN 0.81.5 / Expo SDK 54 upgrade (#29195) out of caution: at the time, LavaMoat's
lockdownSerializerwas failing because enabling the flag changed module-ID assignment andhardenIntrinsicswas firing beforerequirewas set up (expo/expo#36551). That ordering issue no longer reproduces locally with the current Metro + LavaMoat versions, so the defensive disable is no longer needed.What changes
metro.config.js: remove theunstable_enablePackageExportsdisable + explanatory comment; the flag is nowtrue.What does NOT change
babel.config.jsis untouched. ThedynamicImportToRequirevisitor stays in place — it turned out to be load-bearing for Jest (not Hermes):NavigationService.tscallsimport('../AgenticService/AgenticService')inside anif (__DEV__) { }branch that is active in the Jest VM, and Node's VM refuses dynamicimport()callbacks without--experimental-vm-modules. Anything that touchesNavigationService(e.g.PerpsTutorialCarousel.view.test.tsxviasetupFirstTimeTutorial) needs the transform.@ledgerhq/*lib/<subpath>fallback inresolveRequestis kept as-is and can be cleaned up in a follow-up once Metro's exports resolution is verified to handle those paths directly.Risk
Medium.
unstable_enablePackageExportschanges how every package with anexportsfield resolves its modules. The app-wide blast radius means LavaMoat lockdown ordering, Snaps, and any package that ships anexportsmap (Ledger, Predict, Perps, etc.) all need to be exercised before merge.Changelog
CHANGELOG entry: null
Related issues
Follow-up to: #29195
Manual testing steps
Before merge:
.appand confirm the JS bundle is present and the app cold-starts under Hermes.hardenIntrinsicsordering errors.PerpsControllerMYX provider dynamic-import path.Screenshots/Recordings
Before
After
Pre-merge author checklist
Performance checks (if applicable)
Pre-merge reviewer checklist
Note
Medium Risk
Enabling Node-style
exportsresolution changes module resolution semantics across many dependencies, which can impact bundling, runtime behavior, and LavaMoat serialization/lockdown ordering. Risk is mostly integration/regression in build/startup paths rather than localized logic changes.Overview
Metro now resolves package
exportsmaps. The Metro config flipsresolver.unstable_enablePackageExportstotrue, removing the previous defensive disable.This changes how modules are resolved for packages that define an
exportsfield, potentially altering bundle module IDs and dependency entrypoint selection during build/runtime.Reviewed by Cursor Bugbot for commit 78a083c. Bugbot is set up for automated code reviews on this repo. Configure here.