From d0d73f6cb1c1464a58300fabf2ccbf5b5aa3abb0 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Thu, 9 Jul 2026 14:11:18 -0700 Subject: [PATCH] experimentalImportSupport: Fix exported names under relabelling and rest spread (#1772) Summary: Fixes https://github.com/react/metro/issues/1460 Fixes two related bugs in Metro's `experimentalImportSupport`. ## Exports with inline relabelling ```js export const { A: B } = {}; ``` **Currently transforms to** ```js const { A: B } = {}; exports.A = A; ``` Which is invalid: `A` is a reference error. **After this diff** ```js const { A: B } = {}; exports.A = B; ``` ## Exports with spreads Currently both of these fail to transform: **Object spread** ```js export const {A, ...others} = {A: 'A', B: 'B', C: 'C'}; ``` Plugin throws `TypeError: Cannot read properties of undefined (reading 'name')` when attempting to iterate over the object for key: val pairs. **Array spread** ```js export const [A, ...others] = ['A', 'B', 'C']; ``` Plugin throws `Property name expected type of string but got undefined` after extracting an `undefined` name and then attempting to use it at as prop of `exports`. **After this diff** ```js const {A, ...others} = {A: 'A', B: 'B', C: 'C'}; exports.A = A; exports.others = others; ``` and ```js const [A, ...others] = ['A', 'B', 'C']; exports.A = A; exports.others = others; ``` ## Spec Almost goes without saying in this case - exported variable declarations should expose the declaration's bound names. ECMA-262 defines those names through Static Semantics: `BoundNames`, including `BindingPattern` forms: https://tc39.es/ecma262/#sec-static-semantics-boundnames ## Implementation Metro had naive `ObjectPattern`/`ArrayPattern` extraction that read object keys or array elements directly. This fixes the same issues Expo did in [#38058](https://github.com/expo/expo/pull/38058) and [#38118](https://github.com/expo/expo/pull/38118), but uses Babel's own `getBindingIdentifiers` for a simpler implementation. ## Changelog ``` - **[Experimental]**: experimentalImportSupport: Fix exports renamed by destructuring, and support exports of rest spreads. ``` Reviewed By: GijsWeterings, huntie Differential Revision: D111013897 --- .../__tests__/import-export-plugin-test.js | 45 +++++++++++++++++++ .../src/import-export-plugin.js | 37 ++------------- .../__snapshots__/import-export-test.js.snap | 8 ++++ .../__tests__/import-export-test.js | 10 +++++ .../import-export/export-destructuring.js | 26 +++++++++++ .../basic_bundle/import-export/index.js | 10 +++++ 6 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 packages/metro/src/integration_tests/basic_bundle/import-export/export-destructuring.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 de4a65164a..4eade05492 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 @@ -255,6 +255,36 @@ test('exports destructured named object members', () => { compare([importExportPlugin], code, expected, opts); }); +test('exports destructured renamed object members', () => { + const code = ` + export const {foo: bar, baz} = {foo: 'bar', baz: 'baz'}; + `; + + const expected = ` + Object.defineProperty(exports, '__esModule', {value: true}); + const {foo: bar,baz} = {foo: 'bar', baz: 'baz'}; + exports.bar = bar; + exports.baz = baz; + `; + + compare([importExportPlugin], code, expected, opts); +}); + +test('exports destructured object rest members', () => { + const code = ` + export const {foo, ...bar} = {foo: 'foo', bar: 'bar', baz: 'baz'}; + `; + + const expected = ` + Object.defineProperty(exports, '__esModule', {value: true}); + const {foo,...bar} = {foo: 'foo', bar: 'bar', baz: 'baz'}; + exports.foo = foo; + exports.bar = bar; + `; + + compare([importExportPlugin], code, expected, opts); +}); + test('exports destructured named array members', () => { const code = ` export const [foo,bar] = ['bar','baz']; @@ -270,6 +300,21 @@ test('exports destructured named array members', () => { compare([importExportPlugin], code, expected, opts); }); +test('exports destructured array rest members', () => { + const code = ` + export const [foo, ...bar] = ['foo','bar','baz']; + `; + + const expected = ` + Object.defineProperty(exports, '__esModule', {value: true}); + const [foo,...bar] = ['foo','bar','baz']; + exports.foo = foo; + exports.bar = bar; + `; + + compare([importExportPlugin], code, expected, opts); +}); + test('exports members of another module directly from an import (as all)', () => { const code = ` export * from 'bar'; diff --git a/packages/metro-transform-plugins/src/import-export-plugin.js b/packages/metro-transform-plugins/src/import-export-plugin.js index cf5c3d4dbf..db8295491f 100644 --- a/packages/metro-transform-plugins/src/import-export-plugin.js +++ b/packages/metro-transform-plugins/src/import-export-plugin.js @@ -224,40 +224,9 @@ export default function importExportPlugin({ if (declaration) { if (isVariableDeclaration(declaration)) { - declaration.declarations.forEach(d => { - switch (d.id.type) { - case 'ObjectPattern': - { - const properties = d.id.properties; - properties.forEach(p => { - // $FlowFixMe[incompatible-use] Flow error uncovered by typing Babel more strictly - // $FlowFixMe[prop-missing] - const name = p.key.name; - // $FlowFixMe[incompatible-type] - state.exportNamed.push({local: name, remote: name, loc}); - }); - } - break; - case 'ArrayPattern': - { - const elements = d.id.elements; - elements.forEach(e => { - // $FlowFixMe[incompatible-use] Flow error uncovered by typing Babel more strictly - // $FlowFixMe[prop-missing] - const name = e.name; - // $FlowFixMe[incompatible-type] - state.exportNamed.push({local: name, remote: name, loc}); - }); - } - break; - default: - { - const name = d.id.name; - // $FlowFixMe[incompatible-type] - state.exportNamed.push({local: name, remote: name, loc}); - } - break; - } + const bindings = t.getBindingIdentifiers(declaration); + Object.keys(bindings).forEach(name => { + state.exportNamed.push({local: name, remote: name, loc}); }); } else { const id = declaration.id || path.scope.generateUidIdentifier(); 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 5233683c96..b20e405717 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 @@ -16,6 +16,10 @@ Object { }, "default": "export-4: FOO", "extraData": Object { + "arrayFirst": "export-destructuring: ARRAY_FIRST", + "arrayRest": Array [ + "export-destructuring: ARRAY_REST", + ], "foo": "export-null: FOO", "importStar": Object { "default": "export-2: DEFAULT", @@ -27,8 +31,12 @@ Object { "myFunction": "export-1: MY_FUNCTION", "namespaceReExportDefault": "export-2: DEFAULT", "namespaceReExportFoo": "export-2: FOO", + "objectRest": Object { + "remaining": "export-destructuring: OBJECT_REST", + }, "primitiveDefault": "export-primitive-default: DEFAULT", "primitiveFoo": "export-primitive-default: FOO", + "renamedObject": "export-destructuring: RENAMED_OBJECT", }, "namedDefaultExported": "export-3: 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 55646a2625..a26f5da727 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,16 @@ test('builds a simple bundle', async () => { const object = execBundle(result.code); const cjs = await object.asyncImportCJS; + expect(object.extraData.renamedObject).toBe( + 'export-destructuring: RENAMED_OBJECT', + ); + expect(object.extraData.objectRest).toEqual({ + remaining: 'export-destructuring: OBJECT_REST', + }); + expect(object.extraData.arrayFirst).toBe('export-destructuring: ARRAY_FIRST'); + expect(object.extraData.arrayRest).toEqual([ + 'export-destructuring: ARRAY_REST', + ]); expect(object.extraData.namespaceReExportDefault).toBe('export-2: DEFAULT'); expect(object.extraData.namespaceReExportFoo).toBe('export-2: FOO'); expect(object).toMatchSnapshot(); diff --git a/packages/metro/src/integration_tests/basic_bundle/import-export/export-destructuring.js b/packages/metro/src/integration_tests/basic_bundle/import-export/export-destructuring.js new file mode 100644 index 0000000000..8d36febb73 --- /dev/null +++ b/packages/metro/src/integration_tests/basic_bundle/import-export/export-destructuring.js @@ -0,0 +1,26 @@ +/** + * 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'; + +const objectSource = { + original: 'export-destructuring: RENAMED_OBJECT', + remaining: 'export-destructuring: OBJECT_REST', +}; + +export const {original: renamedObject, ...objectRest} = objectSource; + +const arraySource = [ + 'export-destructuring: ARRAY_FIRST', + 'export-destructuring: ARRAY_REST', +]; + +// $FlowFixMe[invalid-exported-annotation] Flow can't infer an exported annotation for an array rest binding +export const [arrayFirst, ...arrayRest] = arraySource; 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 9c3ceadfb5..c0574ed7e9 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,12 @@ import type {RequireWithUnstableImportMaybeSync} from './utils'; import {default as myDefault, foo as myFoo, myFunction} from './export-1'; import * as importStar from './export-2'; +import { + arrayFirst, + arrayRest, + objectRest, + renamedObject, +} from './export-destructuring'; import {namespaceReExport} from './export-namespace'; import {foo} from './export-null'; import primitiveDefault, { @@ -26,6 +32,8 @@ export {default as namedDefaultExported} from './export-3'; export {foo as default} from './export-4'; export const extraData = { + arrayFirst, + arrayRest, foo, importStar, myDefault, @@ -33,8 +41,10 @@ export const extraData = { myFunction: myFunction() as string, namespaceReExportDefault: namespaceReExport.default, namespaceReExportFoo: namespaceReExport.foo, + objectRest, primitiveDefault, primitiveFoo, + renamedObject, }; export const asyncImportCJS = import('./export-5');