Repository navigation
Use a module request for asyncRequireModulePath so Metro's transform cache is shareable across checkouts - #58584
Conversation
20e2708 to
4da2b4d
Compare
|
What would it take to allow |
…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.
4da2b4d to
dbc06cf
Compare
|
@javache The other option that works is using + extraNodeModules: {
+ 'metro-runtime': path.dirname(
+ require.resolve('metro-runtime/package.json'),
+ ),
+ },in the default metro config, but I thought adding the file seemed like a better idea since we do it for other configs (although as you said it is to access internal RN module instead of other package). |
|
@rubennorte @motiz88 Little ping on this :) |
|
I don't think this approach is safe unfortunately. Currently, Absolute paths obviously work because they're not sensitive to the origin, but no relative or bare specifier is safe on its own - under isolated node modules neither Expo gets away with it because the specifier is intercepted by a custom resolver and pointed at a known location, but on the default resolver this PR would break folks.
Otherwise, I think the cleanest approach is to use the new Separately, if cache portability has indeed been accidentally broken by this for most users, I'd want to make sure fixing cache re-use doesn't inadvertently expose issues with common setups. |
|
I've opened react/metro#2083 to allow us to point at |
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.