expo icon indicating copy to clipboard operation
expo copied to clipboard

feat(cli): Implement experimental sticky native module Metro resolution

Open kitten opened this issue 6 months ago • 1 comments

Why

expo-modules-autolinking resolves Expo modules and React Native modules which may be duplicated in the dependency tree. While this doesn't cause issues in native autolinking, since duplicates are detected and discarded, Metro itself isn't aware of this.

This introduces several challenges for our usual Metro resolution, since two installations of the same dependency may be bundled. This means that while the autolinking process may link a single instance of any given native module into the native app, the JS bundle may contain multiple versions of it. This is an error that's hard to detect for our users and leads to inconsistencies when making dependency changes.

Usually dependencies are duplicated due to:

  • auto-installing peer dependencies, particularly with npm
  • occasionally during dependency installation and upgrades with Yarn Classic
  • due to hoisting errors with Yarn Berry
  • due to pnpm isolated dependencies or other monorepo setups when no dedup has been run

While we can also add detection for this in expo install --check and offer deduplication, resolving dependencies in line with autolinking in Metro itself has other benefits:

  • this matches the expectation that native modules can only be bundled exactly once
  • prevents users from running unsupported deduplication tools
  • increases trust in their lockfiles and gradual updates
  • prevents many SDK upgrades from failing outright

How

The implementation is similar to the aliasResolver and the fallbackResolver. However, since expo-modules-autolinking is asynchronous and resolvers in Metro aren't, resolution is split up into two steps.

In the first step, we run Expo Modules autolinking (via findModulesAsync; expo-modules-autolinking search) and React Native autolinking (via createReactNativeConfigAsync; expo-modules-autolinking react-native-config) ahead of time. This creates a configuration per platform with a regular expression matching sticky modules and a mapping for their absolute paths.

In the second step, this is passed on to the sticky resolver itself, which uses it on module imports/requires and resolves them from the absolute path. This is similar to the requestAlias resolver and not an entirely new concept for our custom resolvers.

This logic is only enabled when EXPO_USE_STICKY_RESOLVER is enabled.

Follow-up opportunities

The expo-modules-autolinking logic could use some improvements:

  • I'd love for it not to allow all autolinking options per platform. Allowing searchPaths and ignorePaths per platform seems counter-intuitive, since excludes already exists. This makes module discovery in the CLI more complicated than it needs to be
  • This enables us, in theory, to make autolinking consistent. We could update react-native-config and search to be combined, i.e. for the former to use the same logic and to allow for transitive dependencies

Test Plan

  • [x] tested against bycedric/expo-monorepo-example
  • [x] tested against a freshly created Expo app
  • [x] verified output from DEBUG=expo:start:server:metro:sticky-resolver manually
  • [x] unit test for the sticky resolver itself
  • [ ] tested against a "broken" monorepo example

I did a test on a non-monorepo project with DEBUG=expo:start:server:metro:sticky-resolver. This provides output that verifies which native modules were detected and output for each sticky resolution as it happens. The time it takes for exports to complete seems to not have changed.

We should test this against invalid projects. We currently don't have an example for this, but since this option is experimental, if we're confident it'll not break anything, this will be testable gradually. An initial test would be to create a monorepo, then manually add a pinned expo module dependency that duplicates another to a local package that's included in the app.

Checklist

  • [x] I added a changelog.md entry and rebuilt the package sources according to this short guide
  • [ ] This diff will work correctly for npx expo prebuild & EAS Build (eg: updated a module plugin).
  • [ ] Conforms with the Documentation Writing Style Guide

kitten avatar Jun 03 '25 13:06 kitten

Subscribed to pull request

File Patterns Mentions
packages/@expo/cli/** @EvanBacon, @bycedric

Generated by CodeMention

github-actions[bot] avatar Jun 03 '25 13:06 github-actions[bot]