From cf6e12274d055502c3e983b27bfb05b839850c2a Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Mon, 29 Jun 2026 21:51:55 -0700 Subject: [PATCH 1/5] Fix RAM bundle passing the deps function (not its result) to getTransformOptions getRamBundleInfo gives custom getTransformOptions a callback to look up a module's transitive dependencies, but it called `Array.from(getDependencies)` on the function itself instead of `Array.from(getDependencies(x))`. Since the closure has arity 1, Array.from treated it as array-like and returned [undefined], so any getTransformOptions relying on this callback received garbage regardless of the file queried. Invoke getDependencies(x). Existing tests never exercised the callback, which masked this. Adds a regression test that calls the callback and asserts the real transitive deps (fails before / passes after). --- .../__tests__/getRamBundleInfo-test.js | 41 +++++++++++++++++++ .../Serializers/getRamBundleInfo.js | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index c988afebe4..6fc33a61d2 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -112,6 +112,47 @@ test('should return the RAM bundle info', async () => { ).toMatchSnapshot(); }); +test('passes a working transitive-dependency lookup to getTransformOptions', async () => { + let resolvedDeps; + await getRamBundleInfo( + '/root/entry.js', + pre, + {...graph, entryPoints: new Set(['/root/entry.js'])}, + { + asyncRequireModulePath: '', + // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? + createModuleId: path => path, + dev: true, + excludeSource: false, + getRunModuleStatement, + getTransformOptions: async (entryPoints, opts, getDependenciesOf) => { + resolvedDeps = await getDependenciesOf('/root/foo.js'); + return {preloadedModules: {}, ramGroups: []}; + }, + globalPrefix: '', + includeAsyncPaths: false, + inlineSourceMap: false, + modulesOnly: false, + platform: null, + processModuleFilter: module => true, + projectRoot: '/root', + runBeforeMainModule: [], + runModule: true, + serverRoot: '/root', + shouldAddToIgnoreList: () => false, + sourceMapUrl: 'http://localhost/bundle.map', + sourceUrl: null, + getSourceUrl: null, + }, + ); + // foo depends on bar, baz, qux — the callback must return those, not [undefined]. + expect([...resolvedDeps].sort()).toEqual([ + '/root/bar.js', + '/root/baz.js', + '/root/qux.js', + ]); +}); + test('emits x_google_ignoreList based on shouldAddToIgnoreList', async () => { expect( await getRamBundleInfo( diff --git a/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js b/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js index 9b8047ec08..997f6561cc 100644 --- a/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js +++ b/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js @@ -160,7 +160,7 @@ async function _getRamOptions( /* $FlowFixMe[incompatible-type](>=0.99.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.99 was deployed. To see the error, delete this * comment and run Flow. */ - async (x: string) => Array.from(getDependencies), + async (x: string) => Array.from(getDependencies(x)), ); return { From 502e8f442ab08f357fa289be7b086985034fd56b Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:00:21 +0100 Subject: [PATCH 2/5] Fix Flow error in the new getRamBundleInfo regression test `resolvedDeps` was declared without a type or initialiser, so spreading it fails `flow check` with "possibly uninitialized variable is incompatible with $Iterable". Annotate it and assert it was assigned before spreading, which also gives a clearer failure if `getTransformOptions` is never called with a dependency lookup. Changelog: [Internal] --- .../Serializers/__tests__/getRamBundleInfo-test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index 6fc33a61d2..1921f670b2 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -113,7 +113,7 @@ test('should return the RAM bundle info', async () => { }); test('passes a working transitive-dependency lookup to getTransformOptions', async () => { - let resolvedDeps; + let resolvedDeps: ?Array; await getRamBundleInfo( '/root/entry.js', pre, @@ -146,6 +146,9 @@ test('passes a working transitive-dependency lookup to getTransformOptions', asy }, ); // foo depends on bar, baz, qux — the callback must return those, not [undefined]. + if (resolvedDeps == null) { + throw new Error('getTransformOptions was not called with a dependency lookup'); + } expect([...resolvedDeps].sort()).toEqual([ '/root/bar.js', '/root/baz.js', From 68a7cfc444461acc98500dbe9df6dd35c96de866 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:00:35 +0100 Subject: [PATCH 3/5] Remove stale $FlowFixMe above the getTransformOptions dependency callback The suppression dates to Flow v0.99 in 2019 and no longer suppresses anything: `flow check` reports no error on this line with it removed, either with the fix in this stack or with the original `Array.from(getDependencies)`. Worth noting Flow did not catch this bug in either form, for the same reason it was silent at runtime - `Array.from` accepts array-likes, and a function satisfies that via `.length`. Changelog: [Internal] --- .../metro/src/DeltaBundler/Serializers/getRamBundleInfo.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js b/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js index 002dbebf63..dbfddf6585 100644 --- a/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js +++ b/packages/metro/src/DeltaBundler/Serializers/getRamBundleInfo.js @@ -157,9 +157,6 @@ async function _getRamOptions( const {preloadedModules, ramGroups} = await getTransformOptions( [entryFile], {dev: options.dev, hot: true, platform: options.platform}, - /* $FlowFixMe[incompatible-type](>=0.99.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.99 was deployed. To see the error, delete this - * comment and run Flow. */ async (x: string) => Array.from(getDependencies(x)), ); From 3dc81136cfdb9df19d99c13cd270ebe5df1b2faf Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:03:42 +0100 Subject: [PATCH 4/5] Apply Prettier formatting to the regression test The guard added in the previous commit exceeded the print width, failing `prettier --check` and so `yarn lint`. Changelog: [Internal] --- .../Serializers/__tests__/getRamBundleInfo-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index 1921f670b2..d60b196831 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -147,7 +147,9 @@ test('passes a working transitive-dependency lookup to getTransformOptions', asy ); // foo depends on bar, baz, qux — the callback must return those, not [undefined]. if (resolvedDeps == null) { - throw new Error('getTransformOptions was not called with a dependency lookup'); + throw new Error( + 'getTransformOptions was not called with a dependency lookup', + ); } expect([...resolvedDeps].sort()).toEqual([ '/root/bar.js', From 337cc829f91a2b26bcda95c3d5351398ff8e0065 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:08:18 +0100 Subject: [PATCH 5/5] Share base options across the getRamBundleInfo tests Each test repeated the full serializer options object. Extract it into `baseOptions` so each test overrides only what it exercises. --- .../__tests__/getRamBundleInfo-test.js | 122 +++++------------- 1 file changed, 31 insertions(+), 91 deletions(-) diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index d60b196831..9e9678dbfd 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -76,38 +76,40 @@ const pre = [createModule('pre', [], 'js/script')[1]]; const getRunModuleStatement = (moduleId: string | number) => `require(${JSON.stringify(moduleId)});`; +const baseOptions = { + asyncRequireModulePath: '', + // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? + createModuleId: (path: string): number => path, + dev: true, + excludeSource: false, + getRunModuleStatement, + getTransformOptions: async () => ({ + preloadedModules: {}, + ramGroups: [], + }), + globalPrefix: '', + includeAsyncPaths: false, + inlineSourceMap: false, + modulesOnly: false, + platform: null, + processModuleFilter: (module: Module<>) => true, + projectRoot: '/root', + runBeforeMainModule: [], + runModule: true, + serverRoot: '/root', + shouldAddToIgnoreList: () => false, + sourceMapUrl: 'http://localhost/bundle.map', + sourceUrl: null, + getSourceUrl: null, +}; + test('should return the RAM bundle info', async () => { expect( await getRamBundleInfo( '/root/entry.js', pre, {...graph, entryPoints: new Set(['/root/entry.js'])}, - { - asyncRequireModulePath: '', - // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? - createModuleId: path => path, - dev: true, - excludeSource: false, - getRunModuleStatement, - getTransformOptions: async () => ({ - preloadedModules: {}, - ramGroups: [], - }), - globalPrefix: '', - includeAsyncPaths: false, - inlineSourceMap: false, - modulesOnly: false, - platform: null, - processModuleFilter: module => true, - projectRoot: '/root', - runBeforeMainModule: [], - runModule: true, - serverRoot: '/root', - shouldAddToIgnoreList: () => false, - sourceMapUrl: 'http://localhost/bundle.map', - sourceUrl: null, - getSourceUrl: null, - }, + baseOptions, ), ).toMatchSnapshot(); }); @@ -119,30 +121,11 @@ test('passes a working transitive-dependency lookup to getTransformOptions', asy pre, {...graph, entryPoints: new Set(['/root/entry.js'])}, { - asyncRequireModulePath: '', - // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? - createModuleId: path => path, - dev: true, - excludeSource: false, - getRunModuleStatement, + ...baseOptions, getTransformOptions: async (entryPoints, opts, getDependenciesOf) => { resolvedDeps = await getDependenciesOf('/root/foo.js'); return {preloadedModules: {}, ramGroups: []}; }, - globalPrefix: '', - includeAsyncPaths: false, - inlineSourceMap: false, - modulesOnly: false, - platform: null, - processModuleFilter: module => true, - projectRoot: '/root', - runBeforeMainModule: [], - runModule: true, - serverRoot: '/root', - shouldAddToIgnoreList: () => false, - sourceMapUrl: 'http://localhost/bundle.map', - sourceUrl: null, - getSourceUrl: null, }, ); // foo depends on bar, baz, qux — the callback must return those, not [undefined]. @@ -164,32 +147,7 @@ test('emits x_google_ignoreList based on shouldAddToIgnoreList', async () => { '/root/entry.js', pre, {...graph, entryPoints: new Set(['/root/entry.js'])}, - { - asyncRequireModulePath: '', - // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? - createModuleId: path => path, - dev: true, - excludeSource: false, - getRunModuleStatement, - getTransformOptions: async () => ({ - preloadedModules: {}, - ramGroups: [], - }), - globalPrefix: '', - includeAsyncPaths: false, - inlineSourceMap: false, - modulesOnly: false, - platform: null, - processModuleFilter: module => true, - projectRoot: '/root', - runBeforeMainModule: [], - runModule: true, - serverRoot: '/root', - shouldAddToIgnoreList: () => true, - sourceMapUrl: 'http://localhost/bundle.map', - sourceUrl: null, - getSourceUrl: null, - }, + {...baseOptions, shouldAddToIgnoreList: () => true}, ), ).toMatchSnapshot(); }); @@ -205,29 +163,11 @@ test('should use the preloadedModules and ramGroup configs to build a RAM bundle pre, {...graph, entryPoints: new Set(['/root/entry.js'])}, { - asyncRequireModulePath: '', - // $FlowFixMe[incompatible-type] createModuleId assumes numeric IDs - is this too strict? - createModuleId: path => path, - dev: true, - excludeSource: false, - getRunModuleStatement, + ...baseOptions, /* $FlowFixMe[incompatible-type] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ getTransformOptions, - globalPrefix: '', - includeAsyncPaths: false, inlineSourceMap: null, - modulesOnly: false, - platform: null, - processModuleFilter: module => true, - projectRoot: '/root', - runBeforeMainModule: [], - runModule: true, - serverRoot: '/root', - shouldAddToIgnoreList: () => false, - sourceMapUrl: 'http://localhost/bundle.map', - sourceUrl: null, - getSourceUrl: null, }, );