From 3527da8d6d7e5bc4f17820fe6ea9dfc8aa7d81b0 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 8 Jul 2026 09:35:25 -0700 Subject: [PATCH] Add namespace re-export support to import-export plugin Summary: Add static import/export plugin (`experimentalImportSupport`) support for namespace re-exports, i.e. `export * as Name from "module"`. This was added to Expo's fork in https://github.com/expo/expo/pull/38055, (h/t krystofwoldrich). Despite this being perfectly valid syntax it currently fails to transform under `experimantalImportSupport`, with: ``` Cannot read properties of undefined (reading 'name') at <..>/metro-transform-plugins/src/import-export-plugin.js:289:67 ``` (On access of `local.name`, because this type of export has no `local` binding) ## Spec ECMA-262 includes namespace re-export syntax in the ExportDeclaration grammar: https://tc39.es/ecma262/#prod-ExportDeclaration ## Implementation Metro's existing `experimentalImportSupport` transform already has namespace import helper plumbing via `importAll`. This change reuses that helper and then emits an explicit export assignment for the namespace object, keeping the change isolated to the static transform path. ``` - **[Experimental]** Support `export * as Name from "module"` under `experimentalImportSupport` ``` Reviewed By: huntie Differential Revision: D111013905 --- .../__tests__/import-export-plugin-test.js | 21 +++++++++++++ .../src/import-export-plugin.js | 30 +++++++++++++++++-- .../types/import-export-plugin.d.ts | 4 +-- .../__snapshots__/import-export-test.js.snap | 2 ++ .../__tests__/import-export-test.js | 2 ++ .../import-export/export-namespace.js | 13 ++++++++ .../basic_bundle/import-export/index.js | 3 ++ 7 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 packages/metro/src/integration_tests/basic_bundle/import-export/export-namespace.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 d6efe7b5c8..de4a65164a 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 @@ -294,6 +294,27 @@ test('exports members of another module directly from an import (as all)', () => `); }); +test('exports members of another module directly from an import (as namespace)', () => { + const code = ` + export * as AppleIcons from 'apple-icons'; + `; + + const expected = ` + Object.defineProperty(exports, '__esModule', {value: true}); + + var _AppleIcons = _$$_IMPORT_ALL('apple-icons'); + exports.AppleIcons = _AppleIcons; + `; + + compare([importExportPlugin], code, expected, opts); + + expect(showTransformedDeps(code)).toMatchInlineSnapshot(` + " + > 2 | export * as AppleIcons from 'apple-icons'; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dep #0 (apple-icons)" + `); +}); + 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 f93e04aea6..cf5c3d4dbf 100644 --- a/packages/metro-transform-plugins/src/import-export-plugin.js +++ b/packages/metro-transform-plugins/src/import-export-plugin.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions 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. @@ -9,6 +9,8 @@ * @oncall react_native */ +// Portions Copyright (c) 2015-present 650 Industries, Inc. (aka Expo), under MIT. + import type {PluginObj} from '@babel/core'; import type {NodePath} from '@babel/traverse'; import type { @@ -274,7 +276,6 @@ export default function importExportPlugin({ const specifiers = path.node.specifiers; if (specifiers) { specifiers.forEach(s => { - const local = s.local; const remote = s.exported; if (remote.type === 'StringLiteral') { @@ -284,6 +285,31 @@ export default function importExportPlugin({ ); } + if (s.type === 'ExportNamespaceSpecifier') { + const source = nullthrows(path.node.source); + const temp = path.scope.generateUidIdentifier(remote.name); + + path.insertBefore( + withLocation( + importTemplate({ + IMPORT: t.cloneNode(state.importAll), + FILE: resolvePath(t.cloneNode(source), state.opts.resolve), + LOCAL: temp, + }), + loc, + ), + ); + + state.exportNamed.push({ + local: temp.name, + remote: remote.name, + loc, + }); + return; + } + + const local = s.local; + if (path.node.source) { // $FlowFixMe[incompatible-use] const temp = path.scope.generateUidIdentifier(local.name); diff --git a/packages/metro-transform-plugins/types/import-export-plugin.d.ts b/packages/metro-transform-plugins/types/import-export-plugin.d.ts index 869c88655c..4506bdf8b7 100644 --- a/packages/metro-transform-plugins/types/import-export-plugin.d.ts +++ b/packages/metro-transform-plugins/types/import-export-plugin.d.ts @@ -1,12 +1,12 @@ /** - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Portions 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. * * @noformat * @oncall react_native - * @generated SignedSource<> + * @generated SignedSource<<587cbe7792cfd21ce4abb4bfb5bac2f8>> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-transform-plugins/src/import-export-plugin.js 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..5233683c96 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 @@ -25,6 +25,8 @@ Object { "myDefault": "export-1: DEFAULT", "myFoo": "export-1: FOO", "myFunction": "export-1: MY_FUNCTION", + "namespaceReExportDefault": "export-2: DEFAULT", + "namespaceReExportFoo": "export-2: FOO", "primitiveDefault": "export-primitive-default: DEFAULT", "primitiveFoo": "export-primitive-default: 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 e384dba72f..55646a2625 100644 --- a/packages/metro/src/integration_tests/__tests__/import-export-test.js +++ b/packages/metro/src/integration_tests/__tests__/import-export-test.js @@ -27,6 +27,8 @@ test('builds a simple bundle', async () => { const object = execBundle(result.code); const cjs = await object.asyncImportCJS; + expect(object.extraData.namespaceReExportDefault).toBe('export-2: DEFAULT'); + expect(object.extraData.namespaceReExportFoo).toBe('export-2: FOO'); expect(object).toMatchSnapshot(); expect(cjs).toEqual(expect.objectContaining(cjs.default)); diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-namespace.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-namespace.js new file mode 100644 index 0000000000..a510a10938 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-namespace.js @@ -0,0 +1,13 @@ +/** + * 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 * as namespaceReExport from './export-2'; 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 654a2e9b65..9c3ceadfb5 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 @@ -14,6 +14,7 @@ import type {RequireWithUnstableImportMaybeSync} from './utils'; import {default as myDefault, foo as myFoo, myFunction} from './export-1'; import * as importStar from './export-2'; +import {namespaceReExport} from './export-namespace'; import {foo} from './export-null'; import primitiveDefault, { foo as primitiveFoo, @@ -30,6 +31,8 @@ export const extraData = { myDefault, myFoo, myFunction: myFunction() as string, + namespaceReExportDefault: namespaceReExport.default, + namespaceReExportFoo: namespaceReExport.foo, primitiveDefault, primitiveFoo, };