Use a module request for asyncRequireModulePath so Metro's transform cache is shareable across checkouts - #58584
Open
janicduplessis wants to merge 1 commit into
Conversation
janicduplessis
force-pushed
the
@janic/metro-config-async-require-specifier
branch
3 times, most recently
from
September 18, 2026 03:22
f80ac38 to
20e2708
Compare
…cache is shareable across checkouts @react-native/metro-config set asyncRequireModulePath with require.resolve(), which embeds the absolute path of the checkout in Metro's global transform cache key. Two checkouts of the same project (git worktrees, CI workspaces) never shared a transform cache entry. Add a react-native/async-require secondary entry point, like react-native/asset-registry, and point the config at it: a module request that resolves from any module in the project, hoisted or not, because react-native is the app's direct dependency and depends on metro-runtime itself.
janicduplessis
force-pushed
the
@janic/metro-config-async-require-specifier
branch
from
September 18, 2026 03:25
20e2708 to
4da2b4d
Compare
janicduplessis
marked this pull request as ready for review
September 18, 2026 03:29
Contributor
|
What would it take to allow |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
@react-native/metro-configsetsasyncRequireModulePathwithrequire.resolve(), an absolute path into the current checkout. Metro hashes the whole transformer config into its global transform cache key (metro-transform-workergetCacheKey), so two checkouts of the same project (git worktrees, CI workspaces, a renamed directory) never share a transform cache entry. Nothing fails; the cache is just cold for every checkout.The value has to be a module request, which is what Metro's own default is and what
@expo/metro-configdoes withexpo/internal/async-require-module("must be a module request, rather than an absolute path to keep the cache clean"). Metro inlines it into every module that usesimport()and resolves it from that module, so it also has to resolve from anywhere in the project.Metro's bare
metro-runtime/src/modules/asyncRequiredoes not: I confirmed with pnpmnode-linker=isolatedthatmetro-runtimeis not in the project'snode_modules(it is not a direct dependency), soimport()in app code fails withUnable to resolve module metro-runtime/src/modules/asyncRequire from index.js, while the current absolute path works there.So this follows the precedent already in this repo:
react-native/asset-registryis an untyped secondary entry point that exists only to be referenced from Metro'stransformer.assetRegistryPath, for the same reason (a module request Metro can inline and resolve from anywhere).react-native/async-requireis its twin fortransformer.asyncRequireModulePath:src/async-require.jsre-exportsmetro-runtime/src/modules/asyncRequire, with a matchingexportsentry and theno-deep-importsallowlist update, exactly likeasset-registry.react-nativeis every app's direct dependency and already depends onmetro-runtime, so the request resolves from app code and from packages in any layout, hoisted or not. This is also what Expo does withexpo/internal/async-require-module.Packaging:
srcis in the package'sfiles, andnpm pack --dry-runlistssrc/async-require.jsnext tosrc/asset-registry.jsandsrc/setup-env.js.Changelog:
[GENERAL] [FIXED] - Make Metro's transform cache shareable across checkouts by pointing
asyncRequireModulePathat a newreact-native/async-requireentry point instead of an absolute pathTest Plan:
yarn flow-check: no errors.yarn linton the changed files andyarn test packages/eslint-plugin-react-nativepass.Cache key. The transformer-config segment of Metro's global cache key, computed with
metro-transform-worker'sgetCacheKeyfor two checkouts:Resolution.
Metro.runBuildof an entry withimport('./src/lazy'), usinggetDefaultConfigfrom@react-native/metro-config@0.86.3with this change applied to the installed packages:require.resolve)metro-runtime/...react-native/async-require(this PR)node-linker=isolatedUnable to resolve module metro-runtime/src/modules/asyncRequire from index.jsEnd to end. A fresh
@react-native-community/cli initapp on 0.86.3 withApp.tsxrenderingReact.lazy(() => import('./src-lazy')), this change applied to the installed packages, built and launched on an iOS 26.5 simulator. The lazily imported component renders and the app logs have no errors.Cache sharing across checkouts: same app as a second
git worktreewithnode_modulescopied over,$TMPDIR/metro-cachewiped first, plainreact-native startin each,DEBUG=Metro:Transformerfor the base hash, first bundle request timed withcurl(dev, iOS):e63a95bf…e63a95bf…require.resolveb6349718…The template app is ~600 modules, so the absolute numbers are small; the point is that B only shares A's transforms with this change.