From b24713624db5ad73d34f7f3e270cbc8c9ede8020 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Tue, 2 Sep 2025 03:37:24 -0700 Subject: [PATCH] Pass `globalPrefix` through to serializer and expose to `getRunModuleStatement` Summary: To allow prefixing the global function `__r` (e.g., https://github.com/facebook/metro/pull/1512), expose the prefix to the configured [`getRunModuleStatement`](https://metrobundler.dev/docs/configuration/#getrunmodulestatement), which returns the `__r()` call as a string. ``` - **[Feature]** Expose `globalPrefix` to `getRunModuleStatement` ``` ## Alternative? Runtime prefixing We could alternatively add prefix at runtime by emitting: ``` 'globalThis[__METRO_GLOBAL_PREFIX__ + '__r'](/*...*/)' ``` There are a couple of downsides - - These runModule statements are currently outside IIFEs that inject `global` based on (something like) `global = globalThis ?? global ?? window` - we don't rely on the existence of `globalThis` elsewhere (though it's probably safe to now - that's a separate breaking change). - Concatenation/interpolation + object access is more verbose and slightly slower, and there's just no need for it in code emitted by Metro itself (as opposed to framework/userland code, which must use it). Differential Revision: D81476621 --- docs/Configuration.md | 2 +- packages/metro-config/src/defaults/index.js | 2 +- packages/metro-config/src/types.js | 5 ++++- packages/metro-config/types/types.d.ts | 5 ++++- .../__tests__/baseJSBundle-test.js | 20 +++++++++++++++++-- .../__tests__/getRamBundleInfo-test.js | 3 +++ .../DeltaBundler/Serializers/baseJSBundle.js | 1 + packages/metro/src/DeltaBundler/types.js | 6 +++++- packages/metro/src/Server.js | 3 +++ packages/metro/src/lib/getAppendScripts.js | 4 +++- packages/metro/types/DeltaBundler/types.d.ts | 5 ++++- 11 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index d5275666f1..f0c05ed723 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -562,7 +562,7 @@ This option only has an effect under the default [`transformerPath`](#transforme #### `getRunModuleStatement` -Type: `(number | string) => string` +Type: `(moduleId: number | string, globalPrefix: string) => string` Specify the format of the initial require statements that are appended at the end of the bundle. By default is `__r(${moduleId});`. diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index 30ceac7e38..175b81ab5e 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -58,7 +58,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({ serializer: { polyfillModuleNames: [], - getRunModuleStatement: (moduleId: number | string) => + getRunModuleStatement: (moduleId: number | string, globalPrefix: string) => `__r(${JSON.stringify(moduleId)});`, getPolyfills: () => [], getModulesRunBeforeMainModule: () => [], diff --git a/packages/metro-config/src/types.js b/packages/metro-config/src/types.js index 67e4fbd2a6..bb773c99e9 100644 --- a/packages/metro-config/src/types.js +++ b/packages/metro-config/src/types.js @@ -134,7 +134,10 @@ type SerializerConfigT = { ) => mixed, getModulesRunBeforeMainModule: (entryFilePath: string) => Array, getPolyfills: ({platform: ?string, ...}) => $ReadOnlyArray, - getRunModuleStatement: (number | string) => string, + getRunModuleStatement: ( + moduleId: number | string, + globalPrefix: string, + ) => string, polyfillModuleNames: $ReadOnlyArray, processModuleFilter: (modules: Module<>) => boolean, isThirdPartyModule: (module: $ReadOnly<{path: string, ...}>) => boolean, diff --git a/packages/metro-config/types/types.d.ts b/packages/metro-config/types/types.d.ts index 5deb6e65d6..a5c84143c9 100644 --- a/packages/metro-config/types/types.d.ts +++ b/packages/metro-config/types/types.d.ts @@ -132,7 +132,10 @@ export interface SerializerConfigT { ) => unknown; getModulesRunBeforeMainModule: (entryFilePath: string) => string[]; getPolyfills: (options: {platform: string | null}) => ReadonlyArray; - getRunModuleStatement: (moduleId: number | string) => string; + getRunModuleStatement: ( + moduleId: number | string, + globalPrefix: string, + ) => string; polyfillModuleNames: ReadonlyArray; processModuleFilter: (modules: Module) => boolean; isThirdPartyModule: (module: {readonly path: string}) => boolean; diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js index d1a6d4764c..9314c75f24 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js @@ -94,8 +94,10 @@ const nonAsciiModule: Module<> = { getSource: () => Buffer.from('bar-source'), }; -const getRunModuleStatement = (moduleId: number | string) => - `require(${JSON.stringify(moduleId)});`; +const getRunModuleStatement = jest.fn( + (moduleId: number | string, globalPrefix: string) => + `require(${JSON.stringify(moduleId)});`, +); const transformOptions: TransformInputOptions = { customTransformOptions: {}, @@ -107,6 +109,10 @@ const transformOptions: TransformInputOptions = { unstable_transformProfile: 'default', }; +beforeEach(() => { + jest.clearAllMocks(); +}); + test('should generate a very simple bundle', () => { expect( baseJSBundle( @@ -126,6 +132,7 @@ test('should generate a very simple bundle', () => { createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: 'customPrefix', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -157,6 +164,8 @@ test('should generate a very simple bundle', () => { "pre": "__d(function() {/* code for polyfill */});", } `); + + expect(getRunModuleStatement).toHaveBeenCalledWith('foo', 'customPrefix'); }); test('should generate a bundle with correct non ascii characters parsing', () => { @@ -177,6 +186,7 @@ test('should generate a bundle with correct non ascii characters parsing', () => createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -235,6 +245,7 @@ test('should add runBeforeMainModule statements if found in the graph', () => { createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -274,6 +285,7 @@ test('should handle numeric module ids', () => { createModuleId: createModuleIdFactory(), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -322,6 +334,7 @@ test('outputs custom runModule statements', () => { dev: true, getRunModuleStatement: moduleId => `export default require(${JSON.stringify(moduleId)}).default;`, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -360,6 +373,7 @@ test('should add an inline source map to a very simple bundle', () => { createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: true, modulesOnly: false, @@ -411,6 +425,7 @@ test('emits x_google_ignoreList based on shouldAddToIgnoreList', () => { createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: true, modulesOnly: false, @@ -462,6 +477,7 @@ test('does not add polyfills when `modulesOnly` is used', () => { createModuleId: filePath => path.basename(filePath), dev: true, getRunModuleStatement, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: true, diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js index d57b6c987f..8eea8c6d04 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/getRamBundleInfo-test.js @@ -94,6 +94,7 @@ test('should return the RAM bundle info', async () => { preloadedModules: {}, ramGroups: [], }), + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -129,6 +130,7 @@ test('emits x_google_ignoreList based on shouldAddToIgnoreList', async () => { preloadedModules: {}, ramGroups: [], }), + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: false, modulesOnly: false, @@ -167,6 +169,7 @@ test('should use the preloadedModules and ramGroup configs to build a RAM bundle /* $FlowFixMe[incompatible-type] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ getTransformOptions, + globalPrefix: '', includeAsyncPaths: false, inlineSourceMap: null, modulesOnly: false, diff --git a/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js b/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js index 6dc3cd3ee2..e60ea42650 100644 --- a/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js +++ b/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js @@ -59,6 +59,7 @@ export default function baseJSBundle( asyncRequireModulePath: options.asyncRequireModulePath, createModuleId: options.createModuleId, getRunModuleStatement: options.getRunModuleStatement, + globalPrefix: options.globalPrefix, inlineSourceMap: options.inlineSourceMap, runBeforeMainModule: options.runBeforeMainModule, runModule: options.runModule, diff --git a/packages/metro/src/DeltaBundler/types.js b/packages/metro/src/DeltaBundler/types.js index b39a5ad162..1fc1e8e3f5 100644 --- a/packages/metro/src/DeltaBundler/types.js +++ b/packages/metro/src/DeltaBundler/types.js @@ -168,7 +168,11 @@ export type SerializerOptions = $ReadOnly<{ asyncRequireModulePath: string, createModuleId: string => number, dev: boolean, - getRunModuleStatement: (number | string) => string, + getRunModuleStatement: ( + moduleId: number | string, + globalPrefix: string, + ) => string, + globalPrefix: string, includeAsyncPaths: boolean, inlineSourceMap: ?boolean, modulesOnly: boolean, diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index b3382990c8..11c00edfb7 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -235,6 +235,7 @@ export default class Server { processModuleFilter: this._config.serializer.processModuleFilter, createModuleId: this._createModuleId, getRunModuleStatement: this._config.serializer.getRunModuleStatement, + globalPrefix: this._config.transformer.globalPrefix, dev: transformOptions.dev, includeAsyncPaths: graphOptions.lazy, projectRoot: this._config.projectRoot, @@ -380,6 +381,7 @@ export default class Server { excludeSource: serializerOptions.excludeSource, getRunModuleStatement: this._config.serializer.getRunModuleStatement, getTransformOptions: this._config.transformer.getTransformOptions, + globalPrefix: this._config.transformer.globalPrefix, includeAsyncPaths: graphOptions.lazy, platform: transformOptions.platform, projectRoot: this._config.projectRoot, @@ -1069,6 +1071,7 @@ export default class Server { processModuleFilter: this._config.serializer.processModuleFilter, createModuleId: this._createModuleId, getRunModuleStatement: this._config.serializer.getRunModuleStatement, + globalPrefix: this._config.transformer.globalPrefix, includeAsyncPaths: graphOptions.lazy, dev: transformOptions.dev, projectRoot: this._config.projectRoot, diff --git a/packages/metro/src/lib/getAppendScripts.js b/packages/metro/src/lib/getAppendScripts.js index 9eae15f1b9..d7904a91a2 100644 --- a/packages/metro/src/lib/getAppendScripts.js +++ b/packages/metro/src/lib/getAppendScripts.js @@ -21,7 +21,8 @@ import nullthrows from 'nullthrows'; type Options = $ReadOnly<{ asyncRequireModulePath: string, createModuleId: string => T, - getRunModuleStatement: T => string, + getRunModuleStatement: (moduleId: T, globalPrefix: string) => string, + globalPrefix: string, inlineSourceMap: ?boolean, runBeforeMainModule: $ReadOnlyArray, runModule: boolean, @@ -46,6 +47,7 @@ export default function getAppendScripts( if (modules.some((module: Module<>) => module.path === path)) { const code = options.getRunModuleStatement( options.createModuleId(path), + options.globalPrefix, ); output.push({ path: `require-${path}`, diff --git a/packages/metro/types/DeltaBundler/types.d.ts b/packages/metro/types/DeltaBundler/types.d.ts index 43caa6f472..25103e7690 100644 --- a/packages/metro/types/DeltaBundler/types.d.ts +++ b/packages/metro/types/DeltaBundler/types.d.ts @@ -148,7 +148,10 @@ export interface SerializerOptions { readonly asyncRequireModulePath: string; readonly createModuleId: (filePath: string) => number; readonly dev: boolean; - readonly getRunModuleStatement: (moduleId: string | number) => string; + readonly getRunModuleStatement: ( + moduleId: number | string, + globalPrefix: string, + ) => string; readonly includeAsyncPaths: boolean; readonly inlineSourceMap?: boolean; readonly modulesOnly: boolean;