From 79088b3c325378a99d622c713665c54cd3636052 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Fri, 10 Jul 2026 04:37:02 -0700 Subject: [PATCH] Hoist re-export dependencies before module body Summary: In ESM, static imports (including `export * from`) must be evaluated before the current module's body. Metro's `import-export-plugin` currently evaluates named re-exports at their declared position within the body (by turning them into a CJS `require` at that posiiton), and `export *` after the body - both are incorrect. Move static re-export helper requires and export-star copies into the import prelude instead of leaving them at the original export declaration location or after the module body. ## `inlineRequires` Under `inlineRequires`, which runs after this plugin, evaluation of imported modules may be deferred back to where they were before this diff - contrary to the spec but by design for startup performance. ## Spec - Source Text Module Records collect imported and re-exported module specifiers as requested modules: https://tc39.es/ecma262/#sec-source-text-module-records - ECMA-262 `InnerModuleEvaluation` evaluates requested modules before executing the current module body: https://tc39.es/ecma262/#sec-innermoduleevaluation ## Expo Along with the previous diff, this is another change introduced in Expo last year in [#38111](https://github.com/expo/expo/pull/38111), on by default for Expo apps. ## Changelog ```javascript - **[Experimental]**: `experimentalImportSupport`: Move `export from` imports to be evaluated at module prelude, rather than within/after module body ``` Reviewed By: huntie Differential Revision: D111013896 --- .../__tests__/import-export-plugin-test.js | 43 +++++++++++--- .../src/import-export-plugin.js | 59 ++++++++----------- .../__snapshots__/import-export-test.js.snap | 4 ++ .../__tests__/import-export-test.js | 4 ++ .../import-export/export-star-overrides.js | 6 ++ .../import-export/export-star-source.js | 4 ++ .../basic_bundle/import-export/index.js | 2 + 7 files changed, 82 insertions(+), 40 deletions(-) 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 6fbfd15ba6..082d654d2f 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 @@ -325,7 +325,7 @@ test('exports members of another module directly from an import (as all)', () => const expected = ` Object.defineProperty(exports, '__esModule', {value: true}); - var _bar = require("bar"); + var _bar = require('bar'); for (var _key in _bar) { exports[_key] = _bar[_key]; @@ -373,17 +373,17 @@ test('places export all above explicit exports', () => { const expected = ` Object.defineProperty(exports, '__esModule', {value: true}); - var _baz = require('bar').baz; - const bax = 'bax'; - - var _default = bax; - - var _foo = require("foo"); + var _foo = require('foo'); for (var _key in _foo) { exports[_key] = _foo[_key]; } + var _baz = require('bar').baz; + const bax = 'bax'; + + var _default = bax; + exports.baz = _baz; exports.default = _default; `; @@ -425,6 +425,35 @@ test('explicit exports override export all at runtime', () => { expect(context.exports.sourceOnly).toBe('source only'); }); +test('re-export dependencies evaluate before module body at runtime', () => { + const transformedCode = generate( + transformToAst( + [importExportPlugin], + ` + events.push('body'); + export {value} from 'foo'; + export * from 'bar'; + `, + opts, + ), + ).code; + const events = []; + const context = { + events, + exports: {} as {[string]: unknown}, + require: (id: string) => { + events.push(`require ${id}`); + return id === 'foo' ? {value: 'foo value'} : {star: 'bar star'}; + }, + }; + + vm.runInNewContext(transformedCode, context); + + expect(events).toEqual(['require foo', 'require bar', 'body']); + expect(context.exports.value).toBe('foo value'); + expect(context.exports.star).toBe('bar star'); +}); + test('enables module exporting when something is exported', () => { const code = ` foo(); diff --git a/packages/metro-transform-plugins/src/import-export-plugin.js b/packages/metro-transform-plugins/src/import-export-plugin.js index a838355d37..5d941bab5f 100644 --- a/packages/metro-transform-plugins/src/import-export-plugin.js +++ b/packages/metro-transform-plugins/src/import-export-plugin.js @@ -169,11 +169,23 @@ export default function importExportPlugin({ path: NodePath, state: State, ): void { + const loc = path.node.loc; + const file = path.node.source; + state.exportAll.push({ - file: path.node.source.value, - loc: path.node.loc, + file: file.value, + loc, }); + withLocation( + exportAllTemplate({ + FILE: resolvePath(t.cloneNode(file), state.opts.resolve), + REQUIRED: path.scope.generateUidIdentifier(file.value), + KEY: path.scope.generateUidIdentifier('key'), + }), + loc, + ).forEach(node => state.imports.push({node})); + path.remove(); }, @@ -258,8 +270,8 @@ export default function importExportPlugin({ const source = nullthrows(path.node.source); const temp = path.scope.generateUidIdentifier(remote.name); - path.insertBefore( - withLocation( + state.imports.push({ + node: withLocation( importTemplate({ IMPORT: t.cloneNode(state.importAll), FILE: resolvePath(t.cloneNode(source), state.opts.resolve), @@ -267,7 +279,7 @@ export default function importExportPlugin({ }), loc, ), - ); + }); state.exportNamed.push({ local: temp.name, @@ -286,8 +298,8 @@ export default function importExportPlugin({ // $FlowFixMe[incompatible-type] // $FlowFixMe[incompatible-use] if (local.name === 'default') { - path.insertBefore( - withLocation( + state.imports.push({ + node: withLocation( importTemplate({ IMPORT: t.cloneNode(state.importDefault), FILE: resolvePath( @@ -298,7 +310,7 @@ export default function importExportPlugin({ }), loc, ), - ); + }); state.exportNamed.push({ local: temp.name, @@ -306,8 +318,8 @@ export default function importExportPlugin({ loc, }); } else if (remote.name === 'default') { - path.insertBefore( - withLocation( + state.imports.push({ + node: withLocation( importNamedTemplate({ FILE: resolvePath( t.cloneNode(nullthrows(path.node.source)), @@ -318,12 +330,12 @@ export default function importExportPlugin({ }), loc, ), - ); + }); state.exportDefault.push({local: temp.name, loc}); } else { - path.insertBefore( - withLocation( + state.imports.push({ + node: withLocation( importNamedTemplate({ FILE: resolvePath( t.cloneNode(nullthrows(path.node.source)), @@ -334,7 +346,7 @@ export default function importExportPlugin({ }), loc, ), - ); + }); state.exportNamed.push({ local: temp.name, @@ -522,25 +534,6 @@ export default function importExportPlugin({ body.unshift(e.node); }); - state.exportAll.forEach( - (e: {file: string, loc: ?SourceLocation, ...}) => { - body.push( - // $FlowFixMe[incompatible-call] - ...withLocation( - exportAllTemplate({ - FILE: resolvePath( - t.stringLiteral(e.file), - state.opts.resolve, - ), - REQUIRED: path.scope.generateUidIdentifier(e.file), - KEY: path.scope.generateUidIdentifier('key'), - }), - e.loc, - ), - ); - }, - ); - state.exportNamed.forEach( (e: {local: string, remote: string, loc: ?SourceLocation, ...}) => { body.push( 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 7418d1fa9b..40e4368fad 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 @@ -21,6 +21,10 @@ Object { "export-destructuring: ARRAY_REST", ], "exportStarDefault": "export-star-overrides: DEFAULT", + "exportStarEvaluationOrder": Array [ + "source module", + "barrel body", + ], "exportStarOverridden": "export-star-overrides: OVERRIDDEN", "exportStarSourceOnly": "export-star-source: SOURCE_ONLY", "foo": "export-null: FOO", 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 9b5d7b3a33..3ed3cc79b2 100644 --- a/packages/metro/src/integration_tests/__tests__/import-export-test.js +++ b/packages/metro/src/integration_tests/__tests__/import-export-test.js @@ -42,6 +42,10 @@ test('builds a simple bundle', async () => { expect(object.extraData.exportStarDefault).toBe( 'export-star-overrides: DEFAULT', ); + expect(object.extraData.exportStarEvaluationOrder).toEqual([ + 'source module', + 'barrel body', + ]); expect(object.extraData.exportStarOverridden).toBe( 'export-star-overrides: OVERRIDDEN', ); diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js index d17eef6ef9..00319cde38 100644 --- a/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js @@ -12,6 +12,12 @@ export * from './export-star-source'; +(global.__exportStarEvents || (global.__exportStarEvents = [])).push( + 'barrel body', +); + +export const evaluationOrder = global.__exportStarEvents; + export const overridden = 'export-star-overrides: OVERRIDDEN'; export default 'export-star-overrides: DEFAULT'; diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.js index db1c34a509..2d3a39fbe3 100644 --- a/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.js +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.js @@ -10,6 +10,10 @@ 'use strict'; +(global.__exportStarEvents || (global.__exportStarEvents = [])).push( + 'source module', +); + export default 'export-star-source: DEFAULT'; export const overridden = 'export-star-source: OVERRIDDEN'; diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/index.js b/packages/metro/src/integration_tests/basic_bundle/import-export/index.js index 7a67bd69a8..0c331a5c62 100644 --- a/packages/metro/src/integration_tests/basic_bundle/import-export/index.js +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/index.js @@ -26,6 +26,7 @@ import primitiveDefault, { foo as primitiveFoo, } from './export-primitive-default'; import exportStarDefault, { + evaluationOrder as exportStarEvaluationOrder, overridden as exportStarOverridden, sourceOnly as exportStarSourceOnly, } from './export-star-overrides'; @@ -39,6 +40,7 @@ export const extraData = { arrayFirst, arrayRest, exportStarDefault, + exportStarEvaluationOrder, exportStarOverridden, exportStarSourceOnly, foo,