diff --git a/CHANGELOG.md b/CHANGELOG.md index 6418b28f9a..002c91cb0a 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)) + ### Dependencies - Bump Cocoa SDK from v9.8.0 to v9.9.0 ([#5956](https://github.com/getsentry/sentry-react-native/pull/5956)) 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..444fb7219f 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -232,6 +232,32 @@ 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); + + 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?: {