Skip to content

Commit c306f3f

Browse files
shubh73claude
andcommitted
Honour Metro's experimentalImportSupport in react-native/babel-preset via the Babel caller
`experimentalImportSupport: true` in a Metro config asks Metro to lower `import`/`export` itself, so `react-native/babel-preset` must leave ESM intact. `react-native/metro-babel-transformer` only passes the matching `disableImportExportTransform` preset option when the project has no Babel config of its own. Apps from the template have a `babel.config.js` naming the preset, so the preset lowers to CommonJS regardless and the Metro option is silently a no-op (react/metro#909). It also keeps default imports eager under `inlineRequires`, which only inlines bare `require` calls. Pass `experimentalImportSupport` through the Babel caller and resolve it in the preset as `options.disableImportExportTransform ?? babel.caller(...) ?? false`, the channel #57973 added for `inlinePlatform`. Preset options still take precedence and the default is unchanged. Changelog: [General][Fixed] - `experimentalImportSupport` in a Metro config now takes effect when `react-native/babel-preset` is named in a project Babel config Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 1cfc5f2 commit c306f3f

5 files changed

Lines changed: 210 additions & 1 deletion

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
// $FlowExpectedError[untyped-import] - Preset is untyped
14+
const preset = require('../index');
15+
const babel = require('@babel/core');
16+
17+
const FILENAME = '/app/src/App.js';
18+
const SRC = "import foo from './foo';\nexport const bar = foo;";
19+
20+
type PresetOptions = {
21+
disableImportExportTransform?: boolean,
22+
};
23+
24+
type CallerOptions = {
25+
experimentalImportSupport?: boolean,
26+
};
27+
28+
function transform({
29+
presetOptions = {},
30+
caller = {},
31+
}: {
32+
presetOptions?: PresetOptions,
33+
caller?: CallerOptions,
34+
} = {}): string {
35+
const result = babel.transformSync(SRC, {
36+
babelrc: false,
37+
caller: {name: 'test', ...caller},
38+
compact: false,
39+
configFile: false,
40+
filename: FILENAME,
41+
presets: [[preset, {dev: false, ...presetOptions}]],
42+
sourceMaps: false,
43+
});
44+
const code = result?.code;
45+
if (code == null) {
46+
throw new Error('Expected the transform to produce code');
47+
}
48+
return code;
49+
}
50+
51+
function isLowered(code: string): boolean {
52+
return code.includes('require(') && !/^import\b/m.test(code);
53+
}
54+
55+
describe('import/export lowering is skipped when the caller lowers it', () => {
56+
test('lowers by default', () => {
57+
expect(isLowered(transform())).toBe(true);
58+
});
59+
60+
test('keeps ESM when opted out via preset options', () => {
61+
expect(
62+
isLowered(
63+
transform({presetOptions: {disableImportExportTransform: true}}),
64+
),
65+
).toBe(false);
66+
});
67+
68+
test('keeps ESM when the Babel caller lowers imports itself', () => {
69+
// The only channel available when the preset is named in a babel.config.js,
70+
// where Babel supplies no preset options. Metro's transformer sets this from
71+
// its `experimentalImportSupport` transform option.
72+
expect(
73+
isLowered(transform({caller: {experimentalImportSupport: true}})),
74+
).toBe(false);
75+
});
76+
77+
test('lowers when the Babel caller does not lower imports', () => {
78+
expect(
79+
isLowered(transform({caller: {experimentalImportSupport: false}})),
80+
).toBe(true);
81+
});
82+
83+
test('preset options take precedence over the caller', () => {
84+
expect(
85+
isLowered(
86+
transform({
87+
presetOptions: {disableImportExportTransform: false},
88+
caller: {experimentalImportSupport: true},
89+
}),
90+
),
91+
).toBe(true);
92+
});
93+
});

packages/react-native-babel-preset/src/configs/main.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ function getInlinePlatform(caller) {
6060
return caller?.inlinePlatform ?? false;
6161
}
6262

63+
// Boolean, whether the caller lowers `import`/`export` itself (Metro's
64+
// `experimentalImportSupport`). When it does, the preset must leave ESM intact.
65+
function getExperimentalImportSupport(caller) {
66+
return caller?.experimentalImportSupport ?? false;
67+
}
68+
6369
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
6470
const loose = true;
6571

@@ -78,6 +84,11 @@ const getPreset = (src, options, babel) => {
7884
const inlinePlatform =
7985
options.inlinePlatform ?? babel?.caller(getInlinePlatform) ?? false;
8086

87+
const disableImportExportTransform =
88+
options.disableImportExportTransform ??
89+
babel?.caller(getExperimentalImportSupport) ??
90+
false;
91+
8192
// Hermes V1 uses more optimised transform profiles. There is currently no
8293
// difference between stable and canary, but canary may in future be used to
8394
// test features in pre-prod Hermes V1 versions.
@@ -151,7 +162,7 @@ const getPreset = (src, options, babel) => {
151162
extraPlugins.push([require('@react-native/babel-plugin-codegen')]);
152163
}
153164

154-
if (!options.disableImportExportTransform) {
165+
if (!disableImportExportTransform) {
155166
extraPlugins.push(
156167
[require('@babel/plugin-proposal-export-default-from')],
157168
[
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
'use strict';
11+
12+
// A project Babel config that names the preset with no options - the shape of
13+
// the app template. The preset then only learns about Metro's transform
14+
// options through the Babel caller.
15+
module.exports = {
16+
presets: [require.resolve('@react-native/babel-preset')],
17+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
const generate = require('@babel/generator').default;
14+
const path = require('node:path');
15+
16+
const PROJECT_ROOT = path.sep === '/' ? '/my/project' : 'C:\\my\\project';
17+
const PROJECT_BABEL_CONFIG = path.join(
18+
__dirname,
19+
'__fixtures__',
20+
'babel.config.js',
21+
);
22+
const SRC = "import foo from './foo';\nexport const bar = foo;";
23+
24+
// The transformer memoizes its resolved Babel config in a module-level
25+
// closure, so a fresh module instance is required for every distinct config.
26+
beforeEach(() => {
27+
jest.resetModules();
28+
});
29+
30+
function transformToCode({
31+
experimentalImportSupport,
32+
extendsBabelConfigPath,
33+
}: {
34+
experimentalImportSupport: boolean,
35+
extendsBabelConfigPath?: string,
36+
}): string {
37+
const {transform} = require('../index.js');
38+
const {ast} = transform({
39+
filename: path.join(PROJECT_ROOT, 'App.js'),
40+
src: SRC,
41+
plugins: [],
42+
options: {
43+
dev: true,
44+
enableBabelRuntime: false,
45+
enableBabelRCLookup: false,
46+
experimentalImportSupport,
47+
extendsBabelConfigPath,
48+
globalPrefix: '__metro__',
49+
hot: false,
50+
minify: false,
51+
platform: 'ios',
52+
publicPath: 'test',
53+
projectRoot: PROJECT_ROOT,
54+
},
55+
});
56+
return generate(ast).code;
57+
}
58+
59+
function isLowered(code: string): boolean {
60+
return code.includes('require(') && !/^import\b/m.test(code);
61+
}
62+
63+
describe.each([
64+
['no project Babel config', undefined],
65+
['a project Babel config naming the preset', PROJECT_BABEL_CONFIG],
66+
])('with %s', (_name, extendsBabelConfigPath) => {
67+
test('lowers import/export when experimentalImportSupport is off', () => {
68+
const code = transformToCode({
69+
experimentalImportSupport: false,
70+
extendsBabelConfigPath,
71+
});
72+
73+
expect(isLowered(code)).toBe(true);
74+
});
75+
76+
test('keeps import/export when experimentalImportSupport is on', () => {
77+
// Metro lowers ESM itself in this mode. If the preset lowered it first,
78+
// Metro's plugin would find nothing to do and the option would be a no-op.
79+
const code = transformToCode({
80+
experimentalImportSupport: true,
81+
extendsBabelConfigPath,
82+
});
83+
84+
expect(isLowered(code)).toBe(false);
85+
expect(code).toContain("import foo from './foo'");
86+
});
87+
});

packages/react-native-babel-transformer/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ const transform /*: BabelTransformer['transform'] */ = ({
199199
platform: options.platform,
200200
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
201201
inlinePlatform: options.inlinePlatform,
202+
experimentalImportSupport: options.experimentalImportSupport,
202203
unstable_transformProfile: options.unstable_transformProfile,
203204
},
204205
ast: true,

0 commit comments

Comments
 (0)