From eb5a2ab516f72d7358409f41e2cd44e7992fb8b4 Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 6 Apr 2026 23:35:10 +0100 Subject: [PATCH 1/4] 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 --- .../core/src/js/tools/vendor/metro/utils.ts | 84 ++++++++++--------- .../test/tools/sentryMetroSerializer.test.ts | 33 ++++++++ 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/packages/core/src/js/tools/vendor/metro/utils.ts b/packages/core/src/js/tools/vendor/metro/utils.ts index 3dc4fcffe6..5dc78e25ab 100644 --- a/packages/core/src/js/tools/vendor/metro/utils.ts +++ b/packages/core/src/js/tools/vendor/metro/utils.ts @@ -32,48 +32,9 @@ import type * as bundleToStringType from 'metro/private/lib/bundleToString'; import type { MetroSerializer } from '../../utils'; -// oxlint-disable-next-line typescript-eslint(no-explicit-any) -let baseJSBundleModule: any; -try { - baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle'); -} catch { - baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle'); -} - -const baseJSBundle: typeof baseJSBundleType = - typeof baseJSBundleModule === 'function' - ? baseJSBundleModule - : (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default); - -let sourceMapString: typeof sourceMapStringType; -try { - const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString'); - sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString; -} catch (e) { - sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString'); - if ('sourceMapString' in sourceMapString) { - // Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b - // Metro 0.81.0 and 0.80.10 patch - sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString; - } -} - -// oxlint-disable-next-line typescript-eslint(no-explicit-any) -let bundleToStringModule: any; -try { - bundleToStringModule = require('metro/private/lib/bundleToString'); -} catch { - bundleToStringModule = require('metro/src/lib/bundleToString'); -} - -const bundleToString: typeof bundleToStringType = - typeof bundleToStringModule === 'function' - ? bundleToStringModule - : (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default); - type NewSourceMapStringExport = { // Since Metro v0.80.10 https://github.com/facebook/metro/compare/v0.80.9...v0.80.10#diff-1b836d1729e527a725305eef0cec22e44605af2700fa413f4c2489ea1a03aebcL28 - sourceMapString: typeof sourceMapString; + sourceMapString: typeof sourceMapStringType; }; /** @@ -108,6 +69,49 @@ export const getSortedModules = ( * https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/Server.js#L244-L277 */ export const createDefaultMetroSerializer = (): MetroSerializer => { + // Lazy-load Metro internals only when serializer is created + // This defers requiring Metro modules until they're actually needed (during build), + // avoiding import-time failures when Metro is only a transitive dependency + + // oxlint-disable-next-line typescript-eslint(no-explicit-any) + let baseJSBundleModule: any; + try { + baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle'); + } catch { + baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle'); + } + + const baseJSBundle: typeof baseJSBundleType = + typeof baseJSBundleModule === 'function' + ? baseJSBundleModule + : (baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default); + + let sourceMapString: typeof sourceMapStringType; + try { + const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString'); + sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString; + } catch (e) { + sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString'); + if ('sourceMapString' in sourceMapString) { + // Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b + // Metro 0.81.0 and 0.80.10 patch + sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString; + } + } + + // oxlint-disable-next-line typescript-eslint(no-explicit-any) + let bundleToStringModule: any; + try { + bundleToStringModule = require('metro/private/lib/bundleToString'); + } catch { + bundleToStringModule = require('metro/src/lib/bundleToString'); + } + + const bundleToString: typeof bundleToStringType = + typeof bundleToStringModule === 'function' + ? bundleToStringModule + : (bundleToStringModule?.bundleToString ?? bundleToStringModule?.default); + return (entryPoint, preModules, graph, options) => { // baseJSBundle assigns IDs to modules in a consistent order let bundle = baseJSBundle(entryPoint, preModules, graph, options); diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index 104b230e12..e13e8cfb83 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -232,6 +232,39 @@ describe('Sentry Metro Serializer', () => { 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}$/); }); }); + + test('createDefaultMetroSerializer can be created without Metro internals being loaded at import time', () => { + // This test verifies that the lazy-loading of Metro internals works correctly. + // The createDefaultMetroSerializer function should be callable without triggering + // module-level requires of Metro internals at import time. + // See: https://github.com/getsentry/sentry-react-native/issues/5957 + + // Import the function + const { createDefaultMetroSerializer: createSerializer } = require('../../src/js/tools/vendor/metro/utils'); + + // Create the serializer - this should succeed without loading Metro internals + const serializer = createSerializer(); + expect(typeof serializer).toBe('function'); + + // Verify the serializer can be invoked with proper arguments and produces output + const [entryPoint, preModules, graph, options] = mockMinSerializerArgs(); + const result = serializer(entryPoint, preModules, graph, options); + + // The serializer returns either a string (hot mode) or {code, map} + // if (typeof result === 'string') { + // // Hot mode: returns just code + // expect(typeof result).toBe('string'); + // } else { + // Non-hot mode: returns {code, map} + expect(result).toHaveProperty('code'); + expect(result).toHaveProperty('map'); + expect(typeof result.code).toBe('string'); + expect(typeof result.map).toBe('string'); + // Both code and map should exist (even if minimal for empty bundle) + expect(result.code).toBeDefined(); + expect(result.map).toBeDefined(); + // } + }); }); function mockMinSerializerArgs(options?: { From 46a1d437b1339d210227168daa25debdc8457250 Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 6 Apr 2026 23:41:42 +0100 Subject: [PATCH 2/4] missing test cleanup --- packages/core/test/tools/sentryMetroSerializer.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index e13e8cfb83..a0e255b7a9 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -250,12 +250,6 @@ describe('Sentry Metro Serializer', () => { const [entryPoint, preModules, graph, options] = mockMinSerializerArgs(); const result = serializer(entryPoint, preModules, graph, options); - // The serializer returns either a string (hot mode) or {code, map} - // if (typeof result === 'string') { - // // Hot mode: returns just code - // expect(typeof result).toBe('string'); - // } else { - // Non-hot mode: returns {code, map} expect(result).toHaveProperty('code'); expect(result).toHaveProperty('map'); expect(typeof result.code).toBe('string'); From 6502c19e526ed6c0bb4cd7152cc88a1822cc553d Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 6 Apr 2026 23:43:26 +0100 Subject: [PATCH 3/4] changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b75411f9..064c68f40f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Enable "Open Sentry" button in Playground for Expo apps ([#5947](https://github.com/getsentry/sentry-react-native/pull/5947)) +### Fixes + +- Lazy-load Metro internal modules to prevent Expo 55 import errors ([#5958](https://github.com/getsentry/sentry-react-native/pull/5958)) + ## 8.7.0 ### Features From 29c86f80e81739e2ec6d357cbe42f96335efe1f7 Mon Sep 17 00:00:00 2001 From: LucasZF Date: Tue, 7 Apr 2026 11:02:09 +0100 Subject: [PATCH 4/4] Update packages/core/test/tools/sentryMetroSerializer.test.ts Co-authored-by: Antonis Lilis --- packages/core/test/tools/sentryMetroSerializer.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index a0e255b7a9..444fb7219f 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -257,7 +257,6 @@ describe('Sentry Metro Serializer', () => { // Both code and map should exist (even if minimal for empty bundle) expect(result.code).toBeDefined(); expect(result.map).toBeDefined(); - // } }); });