From 99a91f26287b57e967dcd43ca794f5b8791d433a Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 16 Sep 2025 12:11:24 +0200 Subject: [PATCH] [METRO PLUGIN] Support Metro 0.83+ imports --- package.json | 2 +- .../__tests__/__utils__/serializerUtils.ts | 10 +- .../core/src/metro/__tests__/metro.test.ts | 254 ++++++++++++++++ packages/core/src/metro/metro.d.ts | 8 +- .../core/src/metro/plugin/debugIdHelper.ts | 6 +- .../core/src/metro/plugin/metroSerializer.ts | 17 +- packages/core/src/metro/plugin/utils.ts | 275 ++++++++++++------ yarn.lock | 200 ++++++------- 8 files changed, 565 insertions(+), 207 deletions(-) diff --git a/package.json b/package.json index 6f47c528e..5b33fa717 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "genversion": "3.0.2", "jest": "^29.7.0", "lerna": "8.1.6", - "metro": "^0.82.0", + "metro": "^0.83.1", "pod-install": "0.1.14", "prettier": "2.2.0", "react": "18.3.1", diff --git a/packages/core/src/metro/__tests__/__utils__/serializerUtils.ts b/packages/core/src/metro/__tests__/__utils__/serializerUtils.ts index 743f865a8..fb0c38fc7 100644 --- a/packages/core/src/metro/__tests__/__utils__/serializerUtils.ts +++ b/packages/core/src/metro/__tests__/__utils__/serializerUtils.ts @@ -4,16 +4,16 @@ * Copyright 2016-Present Datadog, Inc. */ -// eslint-disable-next-line import/no-extraneous-dependencies -import countLines from 'metro/src/lib/countLines'; - import type { MetroSerializer, MixedOutput, Module, MetroVirtualModuleOutput } from '../../plugin/types/metroTypes'; -import { createCountingSet } from '../../plugin/utils'; +import { + getCreateCountingSetFunction, + getCountLinesFunction +} from '../../plugin/utils'; export const mockSerializerArgsForEmptyModule = (): Parameters => { return [ @@ -48,6 +48,8 @@ export const mockSerializerArgsForEmptyModule = (): Parameters }; export const mockSerializerArgsForSourceMappingURLModule = (): Parameters => { + const countLines = getCountLinesFunction(); + const createCountingSet = getCreateCountingSetFunction(); const mockedCode = '//# sourceMappingURL=index.android.bundle.map'; return [ 'index.js', diff --git a/packages/core/src/metro/__tests__/metro.test.ts b/packages/core/src/metro/__tests__/metro.test.ts index 6f9faaa9b..98ebafae5 100644 --- a/packages/core/src/metro/__tests__/metro.test.ts +++ b/packages/core/src/metro/__tests__/metro.test.ts @@ -27,6 +27,10 @@ const DEBUG_ID_CODE_SNIPPET = 'var _datadogDebugIds,_datadogDebugIdMeta;void 0===_datadogDebugIds&&(_datadogDebugIds={});try{var stack=(new Error).stack;stack&&(_datadogDebugIds[stack]="__datadog_debug_id_placeholder__",_datadogDebugIdMeta="datadog-debug-id-__datadog_debug_id_placeholder__")}catch(e){}'; describe('Datadog Metro Plugin', () => { + afterEach(() => { + jest.resetModules(); + }); + describe('Datadog Metro Serializer', () => { test('generates bundle and source map with UUID v5 Debug ID', async () => { const codeSnippetHash = createHash('md5'); @@ -404,4 +408,254 @@ describe('Datadog Metro Plugin', () => { expect(result.map).toBe('{"testMap":"test"}'); }); }); + + describe('Import Utils', () => { + test('M getDefaultExport extracts the default export if it exists', () => { + // GIVEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const getDefaultExport = require('../plugin/utils') + .getDefaultExport; + + const exampleModuleWithDefault = { + default: 'default export', + namedExport: 'named export' + }; + + // WHEN + const result = getDefaultExport(exampleModuleWithDefault); + + // THEN + expect(result).toBe('default export'); + }); + + test('M getDefaultExport returns the module as it is if default does not exist', () => { + // GIVEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const getDefaultExport = require('../plugin/utils') + .getDefaultExport; + + const exampleModule1 = { + namedExport: 'named export' + }; + + const exampleModule2 = 'just a string'; + + // WHEN + const result1 = getDefaultExport(exampleModule1); + const result2 = getDefaultExport(exampleModule2); + + // THEN + expect(result1).toEqual({ namedExport: 'named export' }); + expect(result2).toBe('just a string'); + }); + + test('M getDefaultExport returns undefined if the module is null or undefined', () => { + // GIVEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const getDefaultExport = require('../plugin/utils') + .getDefaultExport; + + const exampleModule1 = null; + const exampleModule2 = undefined; + + // WHEN + const result1 = getDefaultExport(exampleModule1); + const result2 = getDefaultExport(exampleModule2); + + // THEN + expect(result1).toBeUndefined(); + expect(result2).toBeUndefined(); + }); + + test('M createCountingSet correctly imports function from metro/src when metro/private is not available', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock('metro/private/lib/CountingSet', () => { + throw new Error('Module not found'); + }); + + jest.doMock( + 'metro/src/lib/CountingSet', + () => ({ + default: class CountingSetMock { + test: string = 'constructor_not_called'; + constructor() { + this.test = 'constructor_called'; + } + } + }), + { virtual: true } + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const createCountingSet = utils.getCreateCountingSetFunction(); + const result = createCountingSet(); + + // THEN + expect(result).toHaveProperty('test'); + expect(result.test).toBe('constructor_called'); + }); + }); + + test('M sourcemapString correctly imports function from metro/src when metro/private is not available', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock( + 'metro/private/DeltaBundler/Serializers/sourceMapString', + () => { + throw new Error('Module not found'); + } + ); + + jest.doMock( + 'metro/src/DeltaBundler/Serializers/sourceMapString', + () => ({ + default: ( + modules: unknown[], + options: object + ): string => + `test-modules_length:${ + modules.length + },options_keys:${Object.keys(options).length}` + }), + { virtual: true } + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const metroSourceMapString = utils.getSourceMapStringFunction(); + const result = metroSourceMapString([{}, {}, {}], { + excludeSource: true, + shouldAddToIgnoreList: () => true + }); + + // THEN + expect(result).toBe('test-modules_length:3,options_keys:2'); + }); + }); + + test('M sourcemapString returns the correct function when retrieved as named export', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock( + 'metro/private/DeltaBundler/Serializers/sourceMapString', + () => ({ + sourceMapString: ( + modules: unknown[], + options: object + ): string => + `test-modules_length:${ + modules.length + },options_keys:${Object.keys(options).length}` + }) + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const metroSourceMapString = utils.getSourceMapStringFunction(); + const result = metroSourceMapString([{}, {}, {}], { + excludeSource: true, + shouldAddToIgnoreList: () => true + }); + // THEN + expect(result).toBe('test-modules_length:3,options_keys:2'); + }); + }); + + test('M sourcemapString returns the correct function when retrieved as default export', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock( + 'metro/private/DeltaBundler/Serializers/sourceMapString', + () => ({ + default: ( + modules: unknown[], + options: object + ): string => + `test-modules_length:${ + modules.length + },options_keys:${Object.keys(options).length}` + }) + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const metroSourceMapString = utils.getSourceMapStringFunction(); + const result = metroSourceMapString([{}, {}, {}], { + excludeSource: true, + shouldAddToIgnoreList: () => true + }); + // THEN + expect(result).toBe('test-modules_length:3,options_keys:2'); + }); + }); + + test('M getBaseJSBundle correctly imports function from metro/src when metro/private is not available', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock( + 'metro/private/DeltaBundler/Serializers/baseJSBundle', + () => { + throw new Error('Module not found'); + } + ); + + jest.doMock( + 'metro/src/DeltaBundler/Serializers/baseJSBundle', + () => () => ({ + test: 'ok' + }), + { virtual: true } + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const baseJSBundle = utils.getBaseJSBundleFunction(); + const result = baseJSBundle(); + + // THEN + expect(result).toHaveProperty('test'); + expect(result.test).toBe('ok'); + }); + }); + + test('M getCountLines correctly imports function from metro/src when metro/private is not available', () => { + // GIVEN + jest.isolateModules(() => { + // GIVEN + jest.doMock('metro/private/lib/countLines', () => { + throw new Error('Module not found'); + }); + + // Random int between 10 and 100 to ensure the mock is used + const randomInt = Math.floor(Math.random() * 90) + 10; + + jest.doMock( + 'metro/src/lib/countLines', + () => (str: string): number => str.length + randomInt, + { virtual: true } + ); + + // WHEN + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const utils = require('../plugin/utils'); + const getCountLines = utils.getCountLinesFunction(); + const result = getCountLines('test-string'); + + // THEN + expect(result).toBe('test-string'.length + randomInt); + }); + }); + }); }); diff --git a/packages/core/src/metro/metro.d.ts b/packages/core/src/metro/metro.d.ts index b087f4cfe..60d0053b4 100644 --- a/packages/core/src/metro/metro.d.ts +++ b/packages/core/src/metro/metro.d.ts @@ -28,7 +28,7 @@ declare type Bundle = { }; // https://github.com/facebook/metro/blob/a3d021a0d021b5706372059f472715c63019e044/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js#L25 -declare module 'metro/src/DeltaBundler/Serializers/baseJSBundle' { +declare module 'metro/private/DeltaBundler/Serializers/baseJSBundle' { const baseJSBundle: ( entryPoint: string, preModules: ReadonlyArray, @@ -39,7 +39,7 @@ declare module 'metro/src/DeltaBundler/Serializers/baseJSBundle' { } // https://github.com/facebook/metro/blob/a3d021a0d021b5706372059f472715c63019e044/packages/metro/src/lib/bundleToString.js#L22 -declare module 'metro/src/lib/bundleToString' { +declare module 'metro/private/lib/bundleToString' { const bundleToString: ( bundle: Bundle ) => { @@ -51,7 +51,7 @@ declare module 'metro/src/lib/bundleToString' { } // https://github.com/facebook/metro/blob/a3d021a0d021b5706372059f472715c63019e044/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js#L22 -declare module 'metro/src/DeltaBundler/Serializers/sourceMapString' { +declare module 'metro/private/DeltaBundler/Serializers/sourceMapString' { import type { MixedOutput, Module } from 'metro'; const sourceMapString: ( @@ -67,7 +67,7 @@ declare module 'metro/src/DeltaBundler/Serializers/sourceMapString' { } // https://github.com/facebook/metro/blob/a3d021a0d021b5706372059f472715c63019e044/packages/metro/src/lib/countLines.js#L16 -declare module 'metro/src/lib/countLines' { +declare module 'metro/private/lib/countLines' { const countLines: (code: string) => number; export = countLines; } diff --git a/packages/core/src/metro/plugin/debugIdHelper.ts b/packages/core/src/metro/plugin/debugIdHelper.ts index d25ade873..48c0ecea6 100644 --- a/packages/core/src/metro/plugin/debugIdHelper.ts +++ b/packages/core/src/metro/plugin/debugIdHelper.ts @@ -9,8 +9,6 @@ import { createHash } from 'crypto'; import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs'; -// eslint-disable-next-line import/no-extraneous-dependencies -import countLines from 'metro/src/lib/countLines'; import path from 'path'; import type { @@ -20,7 +18,7 @@ import type { MetroBundleWithMap, DatadogDebugIdModule } from './types/metroTypes'; -import { createCountingSet } from './utils'; +import { getCreateCountingSetFunction, getCountLinesFunction } from './utils'; /** * Regex to match the Debug ID comment in the bundle. @@ -63,6 +61,8 @@ const DEBUG_ID_METADATA_PREFIX = 'datadog-debug-id-'; */ export const createDebugIdModule = (debugId: string): DatadogDebugIdModule => { let debugIdCode = createDebugIdSnippet(debugId); + const countLines = getCountLinesFunction(); + const createCountingSet = getCreateCountingSetFunction(); return { setSource: (code: string) => { diff --git a/packages/core/src/metro/plugin/metroSerializer.ts b/packages/core/src/metro/plugin/metroSerializer.ts index 369491bf3..828bf15df 100644 --- a/packages/core/src/metro/plugin/metroSerializer.ts +++ b/packages/core/src/metro/plugin/metroSerializer.ts @@ -6,12 +6,6 @@ * Portions of this code are adapted from Sentry's Metro configuration: * https://github.com/getsentry/sentry-react-native/blob/17c0c2e8913030e4826d055284a735efad637312/packages/core/src/js/tools/sentryMetroSerializer.ts */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import baseJSBundle from 'metro/src/DeltaBundler/Serializers/baseJSBundle'; -// eslint-disable-next-line import/no-extraneous-dependencies -import bundleToString from 'metro/src/lib/bundleToString'; - import { DEBUG_ID_PLACEHOLDER, addDebugIdModule, @@ -32,9 +26,11 @@ import type { ReadOnlyGraph } from './types/metroTypes'; import { + getBaseJSBundleFunction, + getBundleToStringFunction, convertSerializerOutput as getMetroBundleWithMap, getSortedModules, - metroSourceMapString + getSourceMapStringFunction } from './utils'; /** @@ -105,8 +101,9 @@ export const createDatadogMetroSerializer = ( */ export const createDefaultMetroSerializer = (): MetroSerializer => { return (entryPoint, preModules, graph, options) => { - // Default metro bundle + // Creates the default metro bundle // https://github.com/facebook/metro/blob/a3d021a0d021b5706372059f472715c63019e044/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js#L25 + const baseJSBundle = getBaseJSBundleFunction(); let bundle = baseJSBundle(entryPoint, preModules, graph, options); // Modify the bundle through the datadogBundleCallback, if we are not in hot-reload mode @@ -118,6 +115,7 @@ export const createDefaultMetroSerializer = (): MetroSerializer => { } // Retrieves the processed code from the bundle + const bundleToString = getBundleToStringFunction(); const { code } = bundleToString(bundle); // If we are in hot-reload mode, we skip sourcemaps generation, and only return the code. @@ -126,7 +124,8 @@ export const createDefaultMetroSerializer = (): MetroSerializer => { } // Force generation of sourcemaps - const map = metroSourceMapString( + const sourceMapString = getSourceMapStringFunction(); + const map = sourceMapString( [...preModules, ...getSortedModules(graph, options)], { processModuleFilter: options.processModuleFilter, diff --git a/packages/core/src/metro/plugin/utils.ts b/packages/core/src/metro/plugin/utils.ts index 41d114250..5ae9b29c0 100644 --- a/packages/core/src/metro/plugin/utils.ts +++ b/packages/core/src/metro/plugin/utils.ts @@ -3,29 +3,75 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import sourceMapString from 'metro/src/DeltaBundler/Serializers/sourceMapString'; -// eslint-disable-next-line import/no-extraneous-dependencies +import type baseJSBundle from 'metro/private/DeltaBundler/Serializers/baseJSBundle'; +import type sourceMapString from 'metro/private/DeltaBundler/Serializers/sourceMapString'; +import type bundleToString from 'metro/private/lib/bundleToString'; +import type countLines from 'metro/private/lib/countLines'; import type CountingSet from 'metro/src/lib/CountingSet'; import type { ReadOnlyGraph, Module, MixedOutput } from 'metro'; import type { DefaultConfigOptions } from './types/expoTypes'; import type { MetroSerializerOutput, - MetroBundleWithMap, - SourceMapString + MetroBundleWithMap } from './types/metroTypes'; /** - * Lazy-init reference for {@link createCountingSet} resolved function. + * Lazy-init cache for modules loaded through `safeRequireDefaultExport`. + */ +const lazyLoadedModules: { [key: string]: any } = {}; + +/** + * Lazy-init reference for {@link getSourceMapStringFunction} resolved function. + */ +let sourceMapStringFunction: typeof sourceMapString | undefined; + +/** + * Safely loads and returns the `baseJSBundle` function from Metro's internal modules. + * Supports multiple Metro versions with different internal paths. + */ +export const getBaseJSBundleFunction = (): typeof baseJSBundle => + lazyRequireDefaultExport([ + 'metro/private/DeltaBundler/Serializers/baseJSBundle', + 'metro/src/DeltaBundler/Serializers/baseJSBundle' + ]); + +/** + * Safely loads and returns the `countLines` function from Metro's internal modules. + * Supports multiple Metro versions with different internal paths. + */ +export const getCountLinesFunction = (): typeof countLines => + lazyRequireDefaultExport([ + 'metro/private/lib/countLines', + 'metro/src/lib/countLines' + ]); + +/** + * Safely loads and returns the `bundleToString` function from Metro's internal modules. + * Supports multiple Metro versions with different internal paths. */ -let createCountingSetFunc: (() => CountingSet) | undefined; +export const getBundleToStringFunction = (): typeof bundleToString => + lazyRequireDefaultExport([ + 'metro/private/lib/bundleToString', + 'metro/src/lib/bundleToString' + ]); /** - * Lazy-init reference for {@link metroSourceMapString} resolved function. + * CountingSet was added in Metro 0.71.2 - a modified `Set` that only deletes items when the + * number of `delete(item)` calls matches the number of `add(item)` calls. + * + * https://github.com/facebook/metro/commit/fc29a1177f883144674cf85a813b58567f69d545 */ -let sourceMapStringFunc: SourceMapString | undefined; +export const getCreateCountingSetFunction: () => () => CountingSet = () => { + const CountingSetClass = safeLazyRequireDefaultExport([ + 'metro/private/lib/CountingSet', + 'metro/src/lib/CountingSet' + ]); + + return CountingSetClass + ? () => new CountingSetClass() + : () => (new Set() as unknown) as CountingSet; +}; /** * Lazy-init reference for {@link getDefaultExpoConfig} resolved function. @@ -37,6 +83,45 @@ let getDefaultExpoConfigFunc: ) => DefaultConfigOptions) | undefined; +/** + * In Metro versions prior to v0.80.10, `sourceMapString` was exported as a default export. + * Starting with v0.80.10, it became a named export. + * + * @returns The resolved `sourceMapString` function, of type {@link sourceMapString} + */ +export const getSourceMapStringFunction = (): typeof sourceMapString => { + if (sourceMapStringFunction) { + return sourceMapStringFunction; + } + + const sourceMapStringModule = safeRequireDefaultExport([ + 'metro/private/DeltaBundler/Serializers/sourceMapString', + 'metro/src/DeltaBundler/Serializers/sourceMapString' + ]); + + if (typeof sourceMapStringModule === 'function') { + sourceMapStringFunction = sourceMapStringModule as typeof sourceMapString; + } else if ( + sourceMapStringModule && + typeof sourceMapStringModule === 'object' && + typeof (sourceMapStringModule as { sourceMapString: any })[ + 'sourceMapString' + ] === 'function' + ) { + sourceMapStringFunction = (sourceMapStringModule as { + sourceMapString: typeof sourceMapString; + }).sourceMapString; + } + + if (!sourceMapStringFunction) { + throw new Error( + "[DATADOG METRO PLUGIN] Unexpected error: Cannot resolve sourceMapString function from Metro's internal modules." + ); + } + + return sourceMapStringFunction; +}; + /** * This function ensures that modules in source maps are sorted in the same * order as in a plain JS bundle. @@ -60,71 +145,6 @@ export function getSortedModules( ); } -/** - * CountingSet was added in Metro 0.71.2 - a modified `Set` that only deletes items when the - * number of `delete(item)` calls matches the number of `add(item)` calls. - * - * https://github.com/facebook/metro/commit/fc29a1177f883144674cf85a813b58567f69d545 - */ -export const createCountingSet = () => { - if (createCountingSetFunc) { - return createCountingSetFunc(); - } - - try { - createCountingSetFunc = () => { - const { - default: MetroCountingSet - // eslint-disable-next-line global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires - } = require('metro/src/lib/CountingSet'); - return new MetroCountingSet(); - }; - } catch (_) { - createCountingSetFunc = () => - (new Set() as unknown) as CountingSet; - } - - return createCountingSetFunc(); -}; -/** - * In Metro versions prior to v0.80.10, `sourceMapString` was exported as a default export. - * Starting with v0.80.10, it became a named export. - * - * @returns The resolved `sourceMapString` function, of type {@link SourceMapString} - */ -export const metroSourceMapString: SourceMapString = ( - modules: Module[], - options: { - excludeSource?: boolean; - processModuleFilter?: (module: Module) => boolean; - shouldAddToIgnoreList?: (module: Module) => boolean; - } -): string => { - if (sourceMapStringFunc) { - return sourceMapStringFunc(modules, options); - } - - if (typeof sourceMapString === 'function') { - sourceMapStringFunc = sourceMapString; - } else if ( - sourceMapString && - typeof sourceMapString === 'object' && - typeof sourceMapString['sourceMapString'] === 'function' - ) { - sourceMapStringFunc = (sourceMapString as { - sourceMapString: SourceMapString; - }).sourceMapString; - } - - if (!sourceMapStringFunc) { - throw new Error( - "[DATADOG METRO PLUGIN] Cannot find sourceMapString function in 'metro/src/DeltaBundler/Serializers/sourceMapString'" - ); - } - - return sourceMapStringFunc(modules, options); -}; - /** * Converts the serializer result of type {@link MetroSerializerOutput} to {@link MetroBundleWithMap}. */ @@ -160,18 +180,101 @@ export const getDefaultExpoConfig = ( projectRoot: string, options: DefaultConfigOptions ): DefaultConfigOptions => { - if (!getDefaultExpoConfigFunc) { + if (getDefaultExpoConfigFunc) { + return getDefaultExpoConfigFunc(projectRoot, options); + } + try { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const metroConfig = require('expo/metro-config'); + getDefaultExpoConfigFunc = metroConfig.getDefaultConfig; + return metroConfig.getDefaultConfig(projectRoot, options); + } catch (e) { + throw new Error( + 'Cannot load `expo/metro-config`. Make sure Expo is properly set up in your project.' + ); + } +}; + +/** + * Safely requires a module from multiple possible paths, returning its default export if found. + * Caches the result to avoid redundant requires. + * Useful for supporting multiple Metro versions with different internal paths. + * @param modulePaths The list of possible module paths to try. + * @returns the default export of the first successfully required module. + * @throws if none of the module paths could be resolved. + */ +export const lazyRequireDefaultExport = (modulePaths: string[]): T => { + const resolvedModule = safeLazyRequireDefaultExport(modulePaths); + if (!resolvedModule) { + throw new Error( + `[DATADOG METRO PLUGIN] Unexpected error: cannot find module in any of the following paths: ${modulePaths.join( + ', ' + )}` + ); + } + return resolvedModule; +}; + +/** + * Safely requires a module from multiple possible paths, returning its default export if found. + * Caches the result to avoid redundant requires. + * Useful for supporting multiple Metro versions with different internal paths. + * @param modulePaths The list of possible module paths to try. + * @returns the default export of the first successfully required module, or `undefined` if none could be resolved. + */ +export const safeLazyRequireDefaultExport = ( + modulePaths: string[] +): T | undefined => { + const cacheKey = modulePaths.join('|'); + if (lazyLoadedModules[cacheKey]) { + return lazyLoadedModules[cacheKey] as T; + } + + const resolvedModule = safeRequireDefaultExport(modulePaths); + + lazyLoadedModules[cacheKey] = resolvedModule; + return resolvedModule; +}; + +/** + * Safely requires a module from multiple possible paths, returning its default export if found. + * Useful for supporting multiple Metro versions with different internal paths. + * @param modulePaths The list of possible module paths to try. + * @returns the default export of the first successfully required module, or `undefined` if none could be resolved. + */ +export function safeRequireDefaultExport( + modulePaths: string[] +): T | undefined { + let resolvedModule: T | undefined; + for (const modulePath of modulePaths) { try { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - const metroConfig = require('expo/metro-config'); - getDefaultExpoConfigFunc = metroConfig.getDefaultConfig; - return metroConfig.getDefaultConfig(projectRoot, options); - } catch (e) { - throw new Error( - 'Cannot load `expo/metro-config`. Make sure Expo is properly set up in your project.' - ); + // eslint-disable-next-line global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires, import/no-dynamic-require + const mod = require(modulePath); + const defaultExport = getDefaultExport(mod); + if (defaultExport) { + return defaultExport; + } + } catch (_) { + // Try next path } + } + + return resolvedModule; +} + +/** + * Safely extracts the default export from a module, if it exists. + * Useful for supporting both CommonJS and ESM-syntax exports. + * Supports upcoming Metro v0.83.2 https://github.com/facebook/metro/commit/5d301d7177af455fc6a94a0ba464639677cdf4b4. + */ +export function getDefaultExport(module: { default: T } | T): T | undefined { + if (!module) { + return undefined; + } + + if (typeof module === 'object' && 'default' in module) { + return module.default as T; } else { - return getDefaultExpoConfigFunc(projectRoot, options); + return module as T; } -}; +} diff --git a/yarn.lock b/yarn.lock index 6dd42ebb0..cfb6af2b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10343,7 +10343,7 @@ __metadata: genversion: 3.0.2 jest: ^29.7.0 lerna: 8.1.6 - metro: ^0.82.0 + metro: ^0.83.1 pod-install: 0.1.14 prettier: 2.2.0 react: 18.3.1 @@ -12951,10 +12951,10 @@ __metadata: languageName: node linkType: hard -"hermes-estree@npm:0.28.1": - version: 0.28.1 - resolution: "hermes-estree@npm:0.28.1" - checksum: 4f7b4e0491352012a6cb799315a0aae16abdcc894335e901552ee6c64732d0cf06f0913c579036f9f452b7c4ad9bb0b6ab14e510c13bd7e5997385f77633ab00 +"hermes-estree@npm:0.29.1": + version: 0.29.1 + resolution: "hermes-estree@npm:0.29.1" + checksum: a72fe490d99ba2f56b3e22f3d050ca7757cc8dc9ebcb9d907104e46aaabdea9d32b445f73cca724a2537090fad3dde3cce0dc733bad6d7b3930c6bcde484d45c languageName: node linkType: hard @@ -12976,12 +12976,12 @@ __metadata: languageName: node linkType: hard -"hermes-parser@npm:0.28.1": - version: 0.28.1 - resolution: "hermes-parser@npm:0.28.1" +"hermes-parser@npm:0.29.1": + version: 0.29.1 + resolution: "hermes-parser@npm:0.29.1" dependencies: - hermes-estree: 0.28.1 - checksum: 0d95280d527e1ad46e8caacd56b24d07e4aec39704de86cf164600f2c4fb00f406dd74a37b2103433ef7ec388a549072da20438e224bd47def21f973c36aab7d + hermes-estree: 0.29.1 + checksum: 3a7cd5cbdb191579f521dcb17edf199e24631314b9f69d043007e91762b53cd1f38eeb7688571f5be378b1c118e99af42040139e5f00e74a7cfd5c52c9d262e0 languageName: node linkType: hard @@ -15562,15 +15562,15 @@ __metadata: languageName: node linkType: hard -"metro-babel-transformer@npm:0.82.4": - version: 0.82.4 - resolution: "metro-babel-transformer@npm:0.82.4" +"metro-babel-transformer@npm:0.83.1": + version: 0.83.1 + resolution: "metro-babel-transformer@npm:0.83.1" dependencies: "@babel/core": ^7.25.2 flow-enums-runtime: ^0.0.6 - hermes-parser: 0.28.1 + hermes-parser: 0.29.1 nullthrows: ^1.1.1 - checksum: 03587a3f3e84180eb560b5652ffa62b08e89a0ff9a3bd8292e39c4ccae7836ce5e2d156f9cb33b56b3a0e9ed51453b458db626df7eee1515c02cf9dfd1cb6457 + checksum: 4cb47742ee89821eaaae76c6622c2848004292c25d010cb1a1673ae1a603b5540021c71faa654d6cf0e795a48fc8756d979d79ffdfbc03bbef5a96bca0b8fea1 languageName: node linkType: hard @@ -15583,12 +15583,12 @@ __metadata: languageName: node linkType: hard -"metro-cache-key@npm:0.82.4": - version: 0.82.4 - resolution: "metro-cache-key@npm:0.82.4" +"metro-cache-key@npm:0.83.1": + version: 0.83.1 + resolution: "metro-cache-key@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 - checksum: a6ab3908295b5ba346d4d991595cc8baf1d22be39fbd4bdf794b617868a003a4f9925d2d01fdbc4c616ff74196783cb4ea5f98dcb6a7c1b510e72e075d9f6b24 + checksum: 5a021798a961f9936537e0e48760347a33c4b1fb2b4c4814448547cefd2d7bf1486b721ffd1eb23120295007a76adf8c8481c9bf0ebc96a36b3cbe2c8b901c1d languageName: node linkType: hard @@ -15603,15 +15603,15 @@ __metadata: languageName: node linkType: hard -"metro-cache@npm:0.82.4": - version: 0.82.4 - resolution: "metro-cache@npm:0.82.4" +"metro-cache@npm:0.83.1": + version: 0.83.1 + resolution: "metro-cache@npm:0.83.1" dependencies: exponential-backoff: ^3.1.1 flow-enums-runtime: ^0.0.6 https-proxy-agent: ^7.0.5 - metro-core: 0.82.4 - checksum: 8c6d9126872fc42de66bc8ebd8e827f7ed9da6c4f421db57e3efd7f43f1b44d664bcaea97c7d5b364e1391d5e0e4fc16581681f0b1c7f94db07c19569e2f80a5 + metro-core: 0.83.1 + checksum: 3221b6236cef81a5712cd89cefe94dbbd6a1bd0a5286647fe638b2d6c8dcd87c52362c76a68eb03b0e62e96e4f143706ad1b4379c0903acc276910b407784df6 languageName: node linkType: hard @@ -15631,19 +15631,19 @@ __metadata: languageName: node linkType: hard -"metro-config@npm:0.82.4": - version: 0.82.4 - resolution: "metro-config@npm:0.82.4" +"metro-config@npm:0.83.1": + version: 0.83.1 + resolution: "metro-config@npm:0.83.1" dependencies: connect: ^3.6.5 cosmiconfig: ^5.0.5 flow-enums-runtime: ^0.0.6 jest-validate: ^29.7.0 - metro: 0.82.4 - metro-cache: 0.82.4 - metro-core: 0.82.4 - metro-runtime: 0.82.4 - checksum: 05daf4477e5db1dfda26ce5631de23510f5d893f3486bb259e00a576ab4f16b613b3e1b97eee160cf64ef75aaf4b2560cb3cc840e6149b04aa00409b27f6cbfe + metro: 0.83.1 + metro-cache: 0.83.1 + metro-core: 0.83.1 + metro-runtime: 0.83.1 + checksum: d20ef15b46cf25d0e597d2ee441a06a77fe3d7ff24b82773ee07b1745dbe19a987c76667638fd44294c799ffc5d03db6be14b37a8ef0b4aa9803af4a29943c62 languageName: node linkType: hard @@ -15658,14 +15658,14 @@ __metadata: languageName: node linkType: hard -"metro-core@npm:0.82.4": - version: 0.82.4 - resolution: "metro-core@npm:0.82.4" +"metro-core@npm:0.83.1": + version: 0.83.1 + resolution: "metro-core@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 lodash.throttle: ^4.1.1 - metro-resolver: 0.82.4 - checksum: bb17d2f1adcd32e6402000a0a27b3a1682b2cc1835cc29f1bae0136fd31b97b37c79f1def55bd60f3a2a85028d073c671c82e72a9b1eef1465f1dae5ce02d3c8 + metro-resolver: 0.83.1 + checksum: 27c654890e35dbe36d165381b919973a23ea7726a00921e9c04f308b14a0d9a91d8ddd1df548c1ce3df00867e84293d2ce2b65001d662d0433949fc0a2940b0a languageName: node linkType: hard @@ -15686,9 +15686,9 @@ __metadata: languageName: node linkType: hard -"metro-file-map@npm:0.82.4": - version: 0.82.4 - resolution: "metro-file-map@npm:0.82.4" +"metro-file-map@npm:0.83.1": + version: 0.83.1 + resolution: "metro-file-map@npm:0.83.1" dependencies: debug: ^4.4.0 fb-watchman: ^2.0.0 @@ -15699,7 +15699,7 @@ __metadata: micromatch: ^4.0.4 nullthrows: ^1.1.1 walker: ^1.0.7 - checksum: f5f24c5bcae7acbfbd2606707df35e1178e196c3e00d2a69bb8a4443942851989918e9f07e8301a7f8fb83d3fb17e9fd2320b9de322a2acfeb6f03f565c6bbf6 + checksum: 3db913e35ed5ce82fdd3f8a13ad97de9da9bb6de8a172a6fead63e1888b8622c770299625c7c9243a280d79578a8df8a7badd0874e9c02a02835e6120f98ecfa languageName: node linkType: hard @@ -15713,13 +15713,13 @@ __metadata: languageName: node linkType: hard -"metro-minify-terser@npm:0.82.4": - version: 0.82.4 - resolution: "metro-minify-terser@npm:0.82.4" +"metro-minify-terser@npm:0.83.1": + version: 0.83.1 + resolution: "metro-minify-terser@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 terser: ^5.15.0 - checksum: 23170c34f519ebaa57189283f51847108395f53ebfcb798e2907bf28e3fce8649f80ff4d1b3f0ed2e321287b61ea84ff825923d8879d23d36f7a9bcbbb804294 + checksum: e5246676b0e90932afafc88098da920a221bec79f264f177dd4f41bd260e7da359acebe57f8e0cdc4c66d1f778f7c5bf664c8ee07f0afbba061b4113b9b73498 languageName: node linkType: hard @@ -15732,12 +15732,12 @@ __metadata: languageName: node linkType: hard -"metro-resolver@npm:0.82.4": - version: 0.82.4 - resolution: "metro-resolver@npm:0.82.4" +"metro-resolver@npm:0.83.1": + version: 0.83.1 + resolution: "metro-resolver@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 - checksum: d4833712d70516930e60cfd59fa7695eacb23eb064b89819903e27f53f1350ed4acfaa02011655f8aacc63f41d15b0781489db17a167994701596192054d791e + checksum: 3bd82898c278544a91471c02f23846eb79300a45fbc70318503773fdadd4fbd74b8c67e686a05d08b24a200122bac7faeab59bf0dcebea620f70153e3d68f446 languageName: node linkType: hard @@ -15751,13 +15751,13 @@ __metadata: languageName: node linkType: hard -"metro-runtime@npm:0.82.4": - version: 0.82.4 - resolution: "metro-runtime@npm:0.82.4" +"metro-runtime@npm:0.83.1": + version: 0.83.1 + resolution: "metro-runtime@npm:0.83.1" dependencies: "@babel/runtime": ^7.25.0 flow-enums-runtime: ^0.0.6 - checksum: a0fa5004db83c6e132f2228c6d91aa56a31d97406c252b27b8e1bdff8f2ff6e453290cc44c4f07b4f0e458fc01eb28c3b85b7d915f6caffb3cc4d2c10f38abd9 + checksum: 2b5d1cf7f6e26a82ddf0eaab4e64389edddb63affb2175895e37fee6eb33de49baad04a03e337fecacf3dc6770bea05d17a7b118db807e6f20ad598f3cae2cb7 languageName: node linkType: hard @@ -15779,21 +15779,21 @@ __metadata: languageName: node linkType: hard -"metro-source-map@npm:0.82.4": - version: 0.82.4 - resolution: "metro-source-map@npm:0.82.4" +"metro-source-map@npm:0.83.1": + version: 0.83.1 + resolution: "metro-source-map@npm:0.83.1" dependencies: "@babel/traverse": ^7.25.3 "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3" "@babel/types": ^7.25.2 flow-enums-runtime: ^0.0.6 invariant: ^2.2.4 - metro-symbolicate: 0.82.4 + metro-symbolicate: 0.83.1 nullthrows: ^1.1.1 - ob1: 0.82.4 + ob1: 0.83.1 source-map: ^0.5.6 vlq: ^1.0.0 - checksum: 41a5efbf6eff61db338922b5651ed69ca0cb42786100dc794c273147c9af2698ee3f3d7d967232b7591e9b8875d416c12fe7e1f10bb9cf8cb46c9d6a13c10773 + checksum: 8913599c549042e064c0fff305a7cc52dba1ef18cf011f8a904016108d50e8be634b62f2348eccc24a305d938011c4f609f6cc8965ab3d394601634a5655b4cd languageName: node linkType: hard @@ -15813,19 +15813,19 @@ __metadata: languageName: node linkType: hard -"metro-symbolicate@npm:0.82.4": - version: 0.82.4 - resolution: "metro-symbolicate@npm:0.82.4" +"metro-symbolicate@npm:0.83.1": + version: 0.83.1 + resolution: "metro-symbolicate@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 invariant: ^2.2.4 - metro-source-map: 0.82.4 + metro-source-map: 0.83.1 nullthrows: ^1.1.1 source-map: ^0.5.6 vlq: ^1.0.0 bin: metro-symbolicate: src/index.js - checksum: dbe92d7eea7d71ebbfd35cc901d3e428774d7a4747d10781a8f4350a6c341edd352f3b25939a7c3f174a07deffac019a92bab04f32fe8cb7e8c3a708eab11115 + checksum: fadaf52309d3844cebdc344aa7b77292fb359a6d7404e14b56b07c45a04040cf0eaa688f5915cab5299185fc9a65fe8248de6fb376a9f65194a1786b9ec15e30 languageName: node linkType: hard @@ -15843,9 +15843,9 @@ __metadata: languageName: node linkType: hard -"metro-transform-plugins@npm:0.82.4": - version: 0.82.4 - resolution: "metro-transform-plugins@npm:0.82.4" +"metro-transform-plugins@npm:0.83.1": + version: 0.83.1 + resolution: "metro-transform-plugins@npm:0.83.1" dependencies: "@babel/core": ^7.25.2 "@babel/generator": ^7.25.0 @@ -15853,7 +15853,7 @@ __metadata: "@babel/traverse": ^7.25.3 flow-enums-runtime: ^0.0.6 nullthrows: ^1.1.1 - checksum: b1a04093b41a8becd700ddae93278a87424f3c35b86bc0912eb5734948ea7f9d54c2440240277315cfabffc1dd9c4d4155c5286464546a97c5656981a97ce42d + checksum: 487c0ac1b5117dd74814d336a11949be37d86e9eb98802c51c5190004c80b94d76933188322c105daeea3faa7ef686bee26e1ec798b3d02c3454af81337951c6 languageName: node linkType: hard @@ -15878,24 +15878,24 @@ __metadata: languageName: node linkType: hard -"metro-transform-worker@npm:0.82.4": - version: 0.82.4 - resolution: "metro-transform-worker@npm:0.82.4" +"metro-transform-worker@npm:0.83.1": + version: 0.83.1 + resolution: "metro-transform-worker@npm:0.83.1" dependencies: "@babel/core": ^7.25.2 "@babel/generator": ^7.25.0 "@babel/parser": ^7.25.3 "@babel/types": ^7.25.2 flow-enums-runtime: ^0.0.6 - metro: 0.82.4 - metro-babel-transformer: 0.82.4 - metro-cache: 0.82.4 - metro-cache-key: 0.82.4 - metro-minify-terser: 0.82.4 - metro-source-map: 0.82.4 - metro-transform-plugins: 0.82.4 + metro: 0.83.1 + metro-babel-transformer: 0.83.1 + metro-cache: 0.83.1 + metro-cache-key: 0.83.1 + metro-minify-terser: 0.83.1 + metro-source-map: 0.83.1 + metro-transform-plugins: 0.83.1 nullthrows: ^1.1.1 - checksum: 5d17296ba1ca6ce939c4ffbd99d7372a6033ba6f6d2da42634509a9c121055440ae5c5eea8677d9201e06d71d811729b313c3f6b54f69cb87d05c5b9f92c6334 + checksum: d164656d4f72a202d162cf2739845716ba6139e3cff24c76e0b7a6b6e2b3902400e5535ebdfdac9be680df8f229fd635833e3c0c038ea317616bd399a9fade11 languageName: node linkType: hard @@ -15949,9 +15949,9 @@ __metadata: languageName: node linkType: hard -"metro@npm:0.82.4, metro@npm:^0.82.0": - version: 0.82.4 - resolution: "metro@npm:0.82.4" +"metro@npm:0.83.1, metro@npm:^0.83.1": + version: 0.83.1 + resolution: "metro@npm:0.83.1" dependencies: "@babel/code-frame": ^7.24.7 "@babel/core": ^7.25.2 @@ -15968,24 +15968,24 @@ __metadata: error-stack-parser: ^2.0.6 flow-enums-runtime: ^0.0.6 graceful-fs: ^4.2.4 - hermes-parser: 0.28.1 + hermes-parser: 0.29.1 image-size: ^1.0.2 invariant: ^2.2.4 jest-worker: ^29.7.0 jsc-safe-url: ^0.2.2 lodash.throttle: ^4.1.1 - metro-babel-transformer: 0.82.4 - metro-cache: 0.82.4 - metro-cache-key: 0.82.4 - metro-config: 0.82.4 - metro-core: 0.82.4 - metro-file-map: 0.82.4 - metro-resolver: 0.82.4 - metro-runtime: 0.82.4 - metro-source-map: 0.82.4 - metro-symbolicate: 0.82.4 - metro-transform-plugins: 0.82.4 - metro-transform-worker: 0.82.4 + metro-babel-transformer: 0.83.1 + metro-cache: 0.83.1 + metro-cache-key: 0.83.1 + metro-config: 0.83.1 + metro-core: 0.83.1 + metro-file-map: 0.83.1 + metro-resolver: 0.83.1 + metro-runtime: 0.83.1 + metro-source-map: 0.83.1 + metro-symbolicate: 0.83.1 + metro-transform-plugins: 0.83.1 + metro-transform-worker: 0.83.1 mime-types: ^2.1.27 nullthrows: ^1.1.1 serialize-error: ^2.1.0 @@ -15995,7 +15995,7 @@ __metadata: yargs: ^17.6.2 bin: metro: src/cli.js - checksum: cc155f963e393f0d1c6c2f5b094e9e2f3b1a0354d5bdf5bead5a5655ddbb2457fbd16fb101ba41bb0784b44970b7f72ba980229a24d2754598eabcefcccfb13f + checksum: f7782a76a8085b7b86d9d80922d2c4fbd6fa2da1c092480c650aa9cbba7192cdf7d76042fc1429c85c5ef18cc7df9bc595e9f6a07796d04aab30c16a588d23e7 languageName: node linkType: hard @@ -16865,12 +16865,12 @@ __metadata: languageName: node linkType: hard -"ob1@npm:0.82.4": - version: 0.82.4 - resolution: "ob1@npm:0.82.4" +"ob1@npm:0.83.1": + version: 0.83.1 + resolution: "ob1@npm:0.83.1" dependencies: flow-enums-runtime: ^0.0.6 - checksum: 8385f5a20195c5c6e61bd18528a10baebe2287dd67fcf5721efeffe89dc61c7ab2b6c56ae9c6649687dda80a20663c33c18e4fc5cc651fd53e6befed3b9d9cf1 + checksum: defa2261aefb89449613278efe16a3414350088166c9ec7cbaaef24dd9eab5fe5c2b751cf2e401d0f834eb78f18631528913a03212d8f2e0c18e3e451abec85a languageName: node linkType: hard