Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 44 additions & 40 deletions packages/core/src/js/tools/vendor/metro/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions packages/core/test/tools/sentryMetroSerializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,33 @@ 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?: {
Expand Down
Loading