Skip to content

chore: enable Metro unstable_enablePackageExports#30386

Merged
andrepimenta merged 6 commits into
mainfrom
chore/drop-dynamic-import-shim-enable-package-exports
May 25, 2026
Merged

chore: enable Metro unstable_enablePackageExports#30386
andrepimenta merged 6 commits into
mainfrom
chore/drop-dynamic-import-shim-enable-package-exports

Conversation

@andrepimenta
Copy link
Copy Markdown
Member

@andrepimenta andrepimenta commented May 19, 2026

Description

Enable Metro's Node-style exports field resolution by setting resolver.unstable_enablePackageExports: true in metro.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 lockdownSerializer was failing because enabling the flag changed module-ID assignment and hardenIntrinsics was firing before require was 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 the unstable_enablePackageExports disable + explanatory comment; the flag is now true.

What does NOT change

  • babel.config.js is untouched. The dynamicImportToRequire visitor stays in place — it turned out to be load-bearing for Jest (not Hermes): NavigationService.ts calls import('../AgenticService/AgenticService') inside an if (__DEV__) { } branch that is active in the Jest VM, and Node's VM refuses dynamic import() callbacks without --experimental-vm-modules. Anything that touches NavigationService (e.g. PerpsTutorialCarousel.view.test.tsx via setupFirstTimeTutorial) needs the transform.
  • The manual @ledgerhq/* lib/<subpath> fallback in resolveRequest is 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_enablePackageExports changes how every package with an exports field resolves its modules. The app-wide blast radius means LavaMoat lockdown ordering, Snaps, and any package that ships an exports map (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:

  1. Release build (Hermes bytecode compile) — produce an iOS release .app and confirm the JS bundle is present and the app cold-starts under Hermes.
  2. LavaMoat lockdown cold start — launch the release build and confirm lockdown completes without hardenIntrinsics ordering errors.
  3. Snaps install + invoke — install a Snap and invoke it end-to-end.
  4. MYX-enabled Perps path — exercise the PerpsController MYX provider dynamic-import path.
  5. Power-user wallet cold start — import a power-user SRP (many accounts + tokens) and verify cold-start time has not regressed.

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
  • I've tested with a power user scenario
  • I've instrumented key operations with Sentry traces for production performance metrics

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Enabling Node-style exports resolution 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 exports maps. The Metro config flips resolver.unstable_enablePackageExports to true, removing the previous defensive disable.

This changes how modules are resolved for packages that define an exports field, 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.

…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>
@metamaskbotv2 metamaskbotv2 Bot added the team-mobile-platform Mobile Platform team label May 19, 2026
@github-actions github-actions Bot added pr-not-ready-for-e2e Skip E2E and block merging. Remove this label once the PR is ready to run the E2E tests. size-S labels May 19, 2026
@andrepimenta andrepimenta removed the pr-not-ready-for-e2e Skip E2E and block merging. Remove this label once the PR is ready to run the E2E tests. label May 19, 2026
andrepimenta and others added 3 commits May 19, 2026 16:33
…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>
@github-actions github-actions Bot added size-XS and removed size-S labels May 20, 2026
@andrepimenta andrepimenta changed the title chore: drop dynamicImportToRequire babel shim and enable Metro packag… chore: enable Metro unstable_enablePackageExports May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeIdentity, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeSwap, SmokeStake, SmokeWalletPlatform, SmokeMoney, SmokePerps, SmokeMultiChainAPI, SmokePredictions, SmokeSeedlessOnboarding, SmokeBrowser, SmokeSnaps
  • Selected Performance tags: @PerformanceLaunch, @PerformanceLogin, @PerformanceOnboarding, @PerformanceAccountList, @PerformanceAssetLoading, @PerformanceSwaps, @PerformancePredict, @PerformancePreps
  • Risk Level: high
  • AI Confidence: 90%
click to see 🤖 AI reasoning details

E2E Test Selection:
The change enables unstable_enablePackageExports: true in Metro's resolver configuration. This was previously explicitly disabled with a comment warning it "breaks LavaMoat's lockdownSerializer (hardenIntrinsics fires before require is set up)".

This is a fundamental build configuration change that affects how Metro resolves ALL npm packages that use the exports field in their package.json. The impact is app-wide:

  1. Module resolution changes globally: Every package that uses the exports field will now be resolved differently. This could change which code paths are loaded for many dependencies.

  2. LavaMoat security layer: The previous comment explicitly warned this breaks LavaMoat's lockdownSerializer. If LavaMoat's security initialization is disrupted, it could cause runtime crashes or security failures across all features.

  3. Broad blast radius: Since this affects the bundler's module resolution strategy, any feature that depends on packages using exports fields could be affected - accounts, confirmations, networking, swaps, snaps, browser, identity sync, etc.

  4. The @LedgerHQ workaround: The existing resolveRequest still has a manual @ledgerhq subpath mapping that was added because unstable_enablePackageExports: false didn't handle it - with this now enabled, that workaround may interact differently.

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:
Enabling package exports resolution changes how Metro bundles the app, which can affect startup time, module initialization order, and overall app performance. Since LavaMoat's lockdown serializer is involved in app initialization, any changes to module resolution order could impact cold start and warm start times. All performance tests should run to detect any regressions introduced by the changed bundling strategy.

View GitHub Actions results

@sonarqubecloud
Copy link
Copy Markdown

@andrepimenta andrepimenta merged commit 6cd7732 into main May 25, 2026
272 of 279 checks passed
@andrepimenta andrepimenta deleted the chore/drop-dynamic-import-shim-enable-package-exports branch May 25, 2026 10:32
@github-actions github-actions Bot locked and limited conversation to collaborators May 25, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.80.0 Issue or pull request that will be included in release 7.80.0 label May 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.80.0 Issue or pull request that will be included in release 7.80.0 size-XS team-mobile-platform Mobile Platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants