Skip to content

Commit eb5a2ab

Browse files
fix(core): Lazy-load Metro internal modules to prevent Expo 55 import errors
Defer requiring Metro internals (baseJSBundle, sourceMapString, bundleToString) until they're actually needed during serialization, rather than at module import time. This prevents "Cannot find module 'metro/src/DeltaBundler/Serializers/baseJSBundle'" errors when Metro is only a transitive dependency through Expo. The fix moves Metro module requires from module-level to inside createDefaultMetroSerializer(), ensuring the config can be imported successfully even when Metro internals aren't immediately available. Fixes #5957 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 3817909 commit eb5a2ab

2 files changed

Lines changed: 77 additions & 40 deletions

File tree

packages/core/src/js/tools/vendor/metro/utils.ts

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,48 +32,9 @@ import type * as bundleToStringType from 'metro/private/lib/bundleToString';
3232

3333
import type { MetroSerializer } from '../../utils';
3434

35-
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
36-
let baseJSBundleModule: any;
37-
try {
38-
baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
39-
} catch {
40-
baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle');
41-
}
42-
43-
const baseJSBundle: typeof baseJSBundleType =
44-
typeof baseJSBundleModule === 'function'
45-
? baseJSBundleModule
46-
: (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default);
47-
48-
let sourceMapString: typeof sourceMapStringType;
49-
try {
50-
const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString');
51-
sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
52-
} catch (e) {
53-
sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString');
54-
if ('sourceMapString' in sourceMapString) {
55-
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
56-
// Metro 0.81.0 and 0.80.10 patch
57-
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
58-
}
59-
}
60-
61-
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
62-
let bundleToStringModule: any;
63-
try {
64-
bundleToStringModule = require('metro/private/lib/bundleToString');
65-
} catch {
66-
bundleToStringModule = require('metro/src/lib/bundleToString');
67-
}
68-
69-
const bundleToString: typeof bundleToStringType =
70-
typeof bundleToStringModule === 'function'
71-
? bundleToStringModule
72-
: (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default);
73-
7435
type NewSourceMapStringExport = {
7536
// Since Metro v0.80.10 https://github.com/facebook/metro/compare/v0.80.9...v0.80.10#diff-1b836d1729e527a725305eef0cec22e44605af2700fa413f4c2489ea1a03aebcL28
76-
sourceMapString: typeof sourceMapString;
37+
sourceMapString: typeof sourceMapStringType;
7738
};
7839

7940
/**
@@ -108,6 +69,49 @@ export const getSortedModules = (
10869
* https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/Server.js#L244-L277
10970
*/
11071
export const createDefaultMetroSerializer = (): MetroSerializer => {
72+
// Lazy-load Metro internals only when serializer is created
73+
// This defers requiring Metro modules until they're actually needed (during build),
74+
// avoiding import-time failures when Metro is only a transitive dependency
75+
76+
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
77+
let baseJSBundleModule: any;
78+
try {
79+
baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle');
80+
} catch {
81+
baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle');
82+
}
83+
84+
const baseJSBundle: typeof baseJSBundleType =
85+
typeof baseJSBundleModule === 'function'
86+
? baseJSBundleModule
87+
: (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default);
88+
89+
let sourceMapString: typeof sourceMapStringType;
90+
try {
91+
const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString');
92+
sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
93+
} catch (e) {
94+
sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString');
95+
if ('sourceMapString' in sourceMapString) {
96+
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
97+
// Metro 0.81.0 and 0.80.10 patch
98+
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString;
99+
}
100+
}
101+
102+
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
103+
let bundleToStringModule: any;
104+
try {
105+
bundleToStringModule = require('metro/private/lib/bundleToString');
106+
} catch {
107+
bundleToStringModule = require('metro/src/lib/bundleToString');
108+
}
109+
110+
const bundleToString: typeof bundleToStringType =
111+
typeof bundleToStringModule === 'function'
112+
? bundleToStringModule
113+
: (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default);
114+
111115
return (entryPoint, preModules, graph, options) => {
112116
// baseJSBundle assigns IDs to modules in a consistent order
113117
let bundle = baseJSBundle(entryPoint, preModules, graph, options);

packages/core/test/tools/sentryMetroSerializer.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,39 @@ describe('Sentry Metro Serializer', () => {
232232
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
233233
});
234234
});
235+
236+
test('createDefaultMetroSerializer can be created without Metro internals being loaded at import time', () => {
237+
// This test verifies that the lazy-loading of Metro internals works correctly.
238+
// The createDefaultMetroSerializer function should be callable without triggering
239+
// module-level requires of Metro internals at import time.
240+
// See: https://github.com/getsentry/sentry-react-native/issues/5957
241+
242+
// Import the function
243+
const { createDefaultMetroSerializer: createSerializer } = require('../../src/js/tools/vendor/metro/utils');
244+
245+
// Create the serializer - this should succeed without loading Metro internals
246+
const serializer = createSerializer();
247+
expect(typeof serializer).toBe('function');
248+
249+
// Verify the serializer can be invoked with proper arguments and produces output
250+
const [entryPoint, preModules, graph, options] = mockMinSerializerArgs();
251+
const result = serializer(entryPoint, preModules, graph, options);
252+
253+
// The serializer returns either a string (hot mode) or {code, map}
254+
// if (typeof result === 'string') {
255+
// // Hot mode: returns just code
256+
// expect(typeof result).toBe('string');
257+
// } else {
258+
// Non-hot mode: returns {code, map}
259+
expect(result).toHaveProperty('code');
260+
expect(result).toHaveProperty('map');
261+
expect(typeof result.code).toBe('string');
262+
expect(typeof result.map).toBe('string');
263+
// Both code and map should exist (even if minimal for empty bundle)
264+
expect(result.code).toBeDefined();
265+
expect(result.map).toBeDefined();
266+
// }
267+
});
235268
});
236269

237270
function mockMinSerializerArgs(options?: {

0 commit comments

Comments
 (0)