From 06521aade4da68d261031d99cbd5d3dda28db3a4 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Sun, 27 Apr 2025 15:07:30 +0100 Subject: [PATCH] [rfc] Support import.meta with experimentalImportSupport Currently, Metro leaves `import.meta` accesses untransformed, but provides no `import` pseudoglobal, so (unless polyfilled) this throws at runtime. `import.meta` is [specified](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-meta-properties) as a writable null-proto object. Though the spec is silent on any properties, it provides a mechanism ([`HostGetImportMetaProperties`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-hostgetimportmetaproperties)) for hosts to set properties, given a module object. Eg: `url`, `resolve` are defined by Node.js and browsers (and notably `env` by Vite). We emulate that mechanism here with the `__getImportMetaProperties(module) => {[key: string]: mixed}` optional, framework-defined global function. --- .../metro-runtime/src/polyfills/require.js | 16 +++++++++++ .../__tests__/import-export-plugin-test.js | 24 ++++++++++++++++ .../src/import-export-plugin.js | 28 +++++++++++++++++-- packages/metro-transform-worker/src/index.js | 4 +++ .../__snapshots__/import-export-test.js.snap | 1 + .../__snapshots__/server-test.js.snap | 2 ++ .../__tests__/import-export-test.js | 9 +++++- .../__tests__/server-test.js | 14 ++++++++++ .../basic_bundle/import-export/export-8.js | 1 + 9 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/metro-runtime/src/polyfills/require.js b/packages/metro-runtime/src/polyfills/require.js index a0b9312400..24fdba504f 100644 --- a/packages/metro-runtime/src/polyfills/require.js +++ b/packages/metro-runtime/src/polyfills/require.js @@ -467,6 +467,22 @@ function loadModuleImplementation( } moduleObject.id = moduleId; + if (!moduleObject.importMeta) { + let importMeta; + Object.defineProperty(moduleObject, 'importMeta', { + enumerable: false, + configurable: false, + get: () => + importMeta ?? + (importMeta = { + __proto__: null, + ...global[`${__METRO_GLOBAL_PREFIX__}__getImportMetaProperties`]?.( + moduleObject, + ), + }), + }); + } + // keep args in sync with with defineModuleCode in // metro/src/Resolver/index.js // and metro/src/ModuleGraph/worker.js diff --git a/packages/metro-transform-plugins/src/__tests__/import-export-plugin-test.js b/packages/metro-transform-plugins/src/__tests__/import-export-plugin-test.js index d0f347b842..fd0353571a 100644 --- a/packages/metro-transform-plugins/src/__tests__/import-export-plugin-test.js +++ b/packages/metro-transform-plugins/src/__tests__/import-export-plugin-test.js @@ -22,6 +22,10 @@ const collectDependencies = require('metro/src/ModuleGraph/worker/collectDepende const opts = { importAll: '_$$_IMPORT_ALL', importDefault: '_$$_IMPORT_DEFAULT', + transformImportMeta: { + objectName: 'module', + propertyName: '_importMeta', + }, }; test('correctly transforms and extracts "import" statements', () => { @@ -375,6 +379,26 @@ test('supports `import {default as LocalName}`', () => { `); }); +test('transforms import.meta', () => { + const code = ` + function foo() { + const module = 'bar'; + console.log(import.meta.url); + return module; + } + `; + + const expected = ` + function foo() { + const _module = 'bar'; + console.log(module._importMeta.url); + return _module; + } + `; + + compare([importExportPlugin], code, expected, opts); +}); + function showTransformedDeps(code: string) { const {dependencies} = collectDependencies( transformToAst([importExportPlugin], code, opts), diff --git a/packages/metro-transform-plugins/src/import-export-plugin.js b/packages/metro-transform-plugins/src/import-export-plugin.js index 8f1a44ea35..448532e0e5 100644 --- a/packages/metro-transform-plugins/src/import-export-plugin.js +++ b/packages/metro-transform-plugins/src/import-export-plugin.js @@ -31,6 +31,10 @@ export type Options = $ReadOnly<{ importDefault: string, importAll: string, resolve: boolean, + transformImportMeta: ?{ + objectName: string, + propertyName: string, + }, out?: {isESModule: boolean, ...}, }>; @@ -46,6 +50,10 @@ type State = { imports: Array<{node: Statement}>, importDefault: BabelNode, importAll: BabelNode, + transformImportMeta: ?{ + renameBinding: string, + node: BabelNode, + }, opts: Options, ... }; @@ -138,8 +146,6 @@ declare function withLocation( ): Array; // eslint-disable-next-line no-redeclare -/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ function withLocation(node, loc) { if (Array.isArray(node)) { return node.map(n => withLocation(n, loc)); @@ -155,6 +161,15 @@ function importExportPlugin({types: t}: {types: Types, ...}): PluginObj { return { visitor: { + MetaProperty(path, state): void { + if ( + state.transformImportMeta != null && + path.node.meta.name === 'import' + ) { + path.scope.rename(state.transformImportMeta.renameBinding); + path.replaceWith(t.cloneNode(state.transformImportMeta.node)); + } + }, ExportAllDeclaration( path: NodePath, state: State, @@ -497,6 +512,15 @@ function importExportPlugin({types: t}: {types: Types, ...}): PluginObj { state.imports = []; state.importAll = t.identifier(state.opts.importAll); state.importDefault = t.identifier(state.opts.importDefault); + state.transformImportMeta = state.opts.transformImportMeta + ? { + renameBinding: state.opts.transformImportMeta.objectName, + node: t.memberExpression( + t.identifier(state.opts.transformImportMeta.objectName), + t.identifier(state.opts.transformImportMeta.propertyName), + ), + } + : null; // Rename declarations at module scope that might otherwise conflict // with arguments we inject into the module factory. diff --git a/packages/metro-transform-worker/src/index.js b/packages/metro-transform-worker/src/index.js index 6bca8fccb7..7aac148cc7 100644 --- a/packages/metro-transform-worker/src/index.js +++ b/packages/metro-transform-worker/src/index.js @@ -305,6 +305,10 @@ async function transformJS( importAll, importDefault, resolve: false, + transformImportMeta: { + objectName: 'module', + propertyName: 'importMeta', + }, } as ImportExportPluginOptions, ]); } diff --git a/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap b/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap index c7ef3f9cb1..8441eee8d2 100644 --- a/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap +++ b/packages/metro/src/integration_tests/__tests__/__snapshots__/import-export-test.js.snap @@ -13,6 +13,7 @@ Object { "asyncImportMaybeSyncESM": Object { "default": "export-8: DEFAULT", "foo": "export-8: FOO", + "url": "metro:///11", }, "default": "export-4: FOO", "extraData": Object { diff --git a/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap b/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap index 6eb28f0f19..859930db50 100644 --- a/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap +++ b/packages/metro/src/integration_tests/__tests__/__snapshots__/server-test.js.snap @@ -58,6 +58,7 @@ exports[`Metro development server serves bundles via HTTP should serve lazy bund Object { "default": "export-8: DEFAULT", "foo": "export-8: FOO", + "url": "metro://module/11", } `; @@ -90,6 +91,7 @@ exports[`Metro development server serves bundles via HTTP should serve non-lazy Object { "default": "export-8: DEFAULT", "foo": "export-8: FOO", + "url": "metro://module/11", } `; diff --git a/packages/metro/src/integration_tests/__tests__/import-export-test.js b/packages/metro/src/integration_tests/__tests__/import-export-test.js index 6261d124c4..5c3c5e82b2 100644 --- a/packages/metro/src/integration_tests/__tests__/import-export-test.js +++ b/packages/metro/src/integration_tests/__tests__/import-export-test.js @@ -26,7 +26,14 @@ test('builds a simple bundle', async () => { entry: 'import-export/index.js', }); - const object = execBundle(result.code); + const object = execBundle(result.code, { + // Framework/host-defined props + __getImportMetaProperties: module => { + return { + url: new URL(String(module.id), 'metro://'), + }; + }, + }); const cjs = await object.asyncImportCJS; expect(object).toMatchSnapshot(); diff --git a/packages/metro/src/integration_tests/__tests__/server-test.js b/packages/metro/src/integration_tests/__tests__/server-test.js index 796f78292a..359996625b 100644 --- a/packages/metro/src/integration_tests/__tests__/server-test.js +++ b/packages/metro/src/integration_tests/__tests__/server-test.js @@ -92,6 +92,13 @@ describe('Metro development server serves bundles via HTTP', () => { test('should serve lazy bundles', async () => { const object = await downloadAndExec( '/import-export/index.bundle?platform=ios&dev=true&minify=false&lazy=true', + { + __getImportMetaProperties: module => { + return { + url: new URL(`metro://module/${module.id}`), + }; + }, + }, ); await expect(object.asyncImportCJS).resolves.toMatchSnapshot(); await expect(object.asyncImportESM).resolves.toMatchSnapshot(); @@ -111,6 +118,13 @@ describe('Metro development server serves bundles via HTTP', () => { test('should serve non-lazy bundles by default', async () => { const object = await downloadAndExec( '/import-export/index.bundle?platform=ios&dev=true&minify=false', + { + __getImportMetaProperties: module => { + return { + url: new URL(`metro://module/${module.id}`), + }; + }, + }, ); await expect(object.asyncImportCJS).resolves.toMatchSnapshot(); await expect(object.asyncImportESM).resolves.toMatchSnapshot(); diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js index 80ea23e156..3a84b67fbc 100644 --- a/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-8.js @@ -13,3 +13,4 @@ export default 'export-8: DEFAULT'; export const foo = 'export-8: FOO'; +export const url = import.meta.url;