From 8ce1569cb3c1cf4b160f49f327c57fe5fb7a83e4 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Fri, 10 Jul 2026 02:36:32 -0700 Subject: [PATCH] experimentalImportSupport: Place export-all assignments before explicit exports, fix precedence of `export default` > `export * from` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Fixes a bug in `experimentalImportSupport` where `export * from 'foo'` would incorrectly overwrite an `export default`, if `'foo'` also has a default export. # Example ## Input source ```js // foo.js (the star source) export default 'star default'; export const overridden = 'star named'; export const sourceOnly = 'source only'; ``` ```js // entry.js export * from 'foo'; export const overridden = 'explicit named'; export default 'explicit default'; ``` Per the ES spec, importing from `entry.js` should give `overridden === 'explicit named'`, `default === 'explicit default'`, and `sourceOnly === 'source only'`. Explicit exports take precedence over names supplied via `export *`, regardless of the position of export declarations. ## Before ```js 'use strict'; Object.defineProperty(exports, '__esModule', {value: true}); var _overridden = 'explicit named'; var _default = 'explicit default'; exports.overridden = _overridden; // 'explicit named' exports.default = _default; // 'explicit default' var _foo = require('foo'); for (var _key in _foo) { // star copy runs LAST... exports[_key] = _foo[_key]; // ...clobbers the explicit exports } ``` Runtime result (wrong): ```js exports.overridden // 'star named' ❌ should be 'explicit named' exports.default // 'star default' ❌ should be 'explicit default' exports.sourceOnly // 'source only' ✅ ``` ## After ```js 'use strict'; Object.defineProperty(exports, '__esModule', {value: true}); var _overridden = 'explicit named'; var _default = 'explicit default'; var _foo = require('foo'); for (var _key in _foo) { // star copy runs FIRST exports[_key] = _foo[_key]; } exports.overridden = _overridden; // explicit exports now win exports.default = _default; ``` Runtime result (spec-correct): ```js exports.overridden // 'explicit named' ✅ exports.default // 'explicit default' ✅ exports.sourceOnly // 'source only' ✅ ``` # Spec [ECMA-262 Source Text Module Record](https://tc39.es/ecma262/#sec-source-text-module-records-resolveexport-exportname-resolve-set) `ResolveExport` checks local `ExportEntries` and indirect `ExportEntries` before considering `StarExportEntries`, so explicit exports take precedence over names supplied through `export *`. # Provenance This is part of the change made in Expo's fork in [#38111](https://github.com/expo/expo/pull/38111) (though that's mainly concerned with introducing live bindings), so this is "in the wild" already for Expo users. # Implementation This is just a straightforward ordering swap of the emitted assignments. # Changelog ``` - **[Experimental]**: `experimentalImportSupport` - fix precedence of `export default` over `export * from`. ``` Reviewed By: huntie Differential Revision: D111013892 --- .../__tests__/import-export-plugin-test.js | 69 ++++++++++++++++++- .../src/import-export-plugin.js | 28 ++++---- .../__snapshots__/import-export-test.js.snap | 3 + .../__tests__/import-export-test.js | 9 +++ .../import-export/export-star-overrides.js | 17 +++++ .../import-export/export-star-source.js | 17 +++++ .../basic_bundle/import-export/index.js | 7 ++ 7 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js create mode 100644 packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.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 4eade05492..6fbfd15ba6 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 @@ -15,8 +15,10 @@ import collectDependencies from 'metro/private/ModuleGraph/worker/collectDepende const {compare, transformToAst} = require('../__mocks__/test-helpers'); const importExportPlugin = require('../import-export-plugin'); +const generate = require('@babel/generator').default; // $FlowFixMe[untyped-import] @babel/code-frame const {codeFrameColumns} = require('@babel/code-frame'); +const vm = require('vm'); const opts = { importAll: '_$$_IMPORT_ALL', @@ -156,8 +158,8 @@ test('exports members of another module directly from an import (as default)', ( var _foo = require('bar').foo; var _baz = require('bar').baz; - exports.default = _foo; exports.baz = _baz; + exports.default = _foo; `; compare([importExportPlugin], code, expected, opts); @@ -233,8 +235,8 @@ test('allows mixed esm and cjs exports', () => { exports.bar = 'bar'; module.exports.baz = 'baz'; class _default {} - exports.default = _default; exports.foo = foo; + exports.default = _default; `; compare([importExportPlugin], code, expected, opts); @@ -360,6 +362,69 @@ test('exports members of another module directly from an import (as namespace)', `); }); +test('places export all above explicit exports', () => { + const code = ` + export * from 'foo'; + export {baz} from 'bar'; + const bax = 'bax'; + export default bax; + `; + + const expected = ` + Object.defineProperty(exports, '__esModule', {value: true}); + + var _baz = require('bar').baz; + const bax = 'bax'; + + var _default = bax; + + var _foo = require("foo"); + + for (var _key in _foo) { + exports[_key] = _foo[_key]; + } + + exports.baz = _baz; + exports.default = _default; + `; + + compare([importExportPlugin], code, expected, opts); +}); + +test('explicit exports override export all at runtime', () => { + const transformedCode = generate( + transformToAst( + [importExportPlugin], + ` + export * from 'foo'; + export const overridden = 'explicit named'; + export default 'explicit default'; + `, + opts, + ), + ).code; + const context = { + exports: {} as {[string]: unknown}, + require: (id: string) => { + if (id !== 'foo') { + throw new Error(`Unexpected module: ${id}`); + } + return { + default: 'star default', + overridden: 'star named', + sourceOnly: 'source only', + }; + }, + }; + + vm.runInNewContext(transformedCode, context); + + expect(context.exports.__esModule).toBe(true); + expect(context.exports.default).toBe('explicit default'); + expect(context.exports.overridden).toBe('explicit named'); + expect(context.exports.sourceOnly).toBe('source only'); +}); + 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 db8295491f..a838355d37 100644 --- a/packages/metro-transform-plugins/src/import-export-plugin.js +++ b/packages/metro-transform-plugins/src/import-export-plugin.js @@ -522,20 +522,6 @@ export default function importExportPlugin({ body.unshift(e.node); }); - state.exportDefault.forEach( - (e: {local: string, loc: ?SourceLocation, ...}) => { - body.push( - withLocation( - exportTemplate({ - LOCAL: t.identifier(e.local), - REMOTE: t.identifier('default'), - }), - e.loc, - ), - ); - }, - ); - state.exportAll.forEach( (e: {file: string, loc: ?SourceLocation, ...}) => { body.push( @@ -569,6 +555,20 @@ export default function importExportPlugin({ }, ); + state.exportDefault.forEach( + (e: {local: string, loc: ?SourceLocation, ...}) => { + body.push( + withLocation( + exportTemplate({ + LOCAL: t.identifier(e.local), + REMOTE: t.identifier('default'), + }), + e.loc, + ), + ); + }, + ); + if ( state.exportDefault.length || state.exportAll.length || 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 b20e405717..7418d1fa9b 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 @@ -20,6 +20,9 @@ Object { "arrayRest": Array [ "export-destructuring: ARRAY_REST", ], + "exportStarDefault": "export-star-overrides: DEFAULT", + "exportStarOverridden": "export-star-overrides: OVERRIDDEN", + "exportStarSourceOnly": "export-star-source: SOURCE_ONLY", "foo": "export-null: FOO", "importStar": Object { "default": "export-2: DEFAULT", 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 a26f5da727..9b5d7b3a33 100644 --- a/packages/metro/src/integration_tests/__tests__/import-export-test.js +++ b/packages/metro/src/integration_tests/__tests__/import-export-test.js @@ -39,6 +39,15 @@ test('builds a simple bundle', async () => { ]); expect(object.extraData.namespaceReExportDefault).toBe('export-2: DEFAULT'); expect(object.extraData.namespaceReExportFoo).toBe('export-2: FOO'); + expect(object.extraData.exportStarDefault).toBe( + 'export-star-overrides: DEFAULT', + ); + expect(object.extraData.exportStarOverridden).toBe( + 'export-star-overrides: OVERRIDDEN', + ); + expect(object.extraData.exportStarSourceOnly).toBe( + 'export-star-source: SOURCE_ONLY', + ); expect(object).toMatchSnapshot(); expect(cjs).toEqual(expect.objectContaining(cjs.default)); 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 new file mode 100644 index 0000000000..d17eef6ef9 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-overrides.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +export * from './export-star-source'; + +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 new file mode 100644 index 0000000000..db1c34a509 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-star-source.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict + */ + +'use strict'; + +export default 'export-star-source: DEFAULT'; + +export const overridden = 'export-star-source: OVERRIDDEN'; + +export const sourceOnly = 'export-star-source: SOURCE_ONLY'; 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 c0574ed7e9..7a67bd69a8 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 @@ -25,6 +25,10 @@ import {foo} from './export-null'; import primitiveDefault, { foo as primitiveFoo, } from './export-primitive-default'; +import exportStarDefault, { + overridden as exportStarOverridden, + sourceOnly as exportStarSourceOnly, +} from './export-star-overrides'; declare var require: RequireWithUnstableImportMaybeSync; @@ -34,6 +38,9 @@ export {foo as default} from './export-4'; export const extraData = { arrayFirst, arrayRest, + exportStarDefault, + exportStarOverridden, + exportStarSourceOnly, foo, importStar, myDefault,