Skip to content

Commit 6d4e378

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, which needs `react-native/babel-preset` to leave ESM intact. `react-native/metro-babel-transformer` only passes the matching `disableImportExportTransform` preset option when it builds the preset config itself, i.e. when the project has no Babel config. Every app created from the template has a `babel.config.js` naming the preset, so the transformer extends that file instead, the preset's CommonJS transform runs regardless, Metro's import plugin then finds nothing to lower, and the Metro option is silently a no-op (react/metro#909). The same lowering is what keeps default and namespace imports eager under `inlineRequires`: Babel emits `_interopRequireDefault(require(...))` at module scope, which the inline-requires plugin does not inline, so only named imports become lazy. Pass `experimentalImportSupport` through the Babel caller and resolve it in the preset as `options.disableImportExportTransform ?? babel.caller(...) ?? false`, the channel #57973 added for `inlinePlatform`. An explicit preset option still wins and the default is unchanged, so builds that do not set the Metro option are byte-identical. `babel-preset-expo` reads the equivalent flag from its caller for the same reason. 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 6d4e378

4 files changed

Lines changed: 214 additions & 1 deletion

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
sourceType: 'module',
44+
});
45+
const code = result?.code;
46+
if (code == null) {
47+
throw new Error('Expected the transform to produce code');
48+
}
49+
return code;
50+
}
51+
52+
function isLowered(code: string): boolean {
53+
return code.includes('require(') && !/^import\b/m.test(code);
54+
}
55+
56+
describe('import/export lowering is skipped when the caller lowers it', () => {
57+
test('lowers by default', () => {
58+
expect(isLowered(transform())).toBe(true);
59+
});
60+
61+
test('keeps ESM when opted out via preset options', () => {
62+
expect(
63+
isLowered(
64+
transform({presetOptions: {disableImportExportTransform: true}}),
65+
),
66+
).toBe(false);
67+
});
68+
69+
test('keeps ESM when the Babel caller lowers imports itself', () => {
70+
// The only channel available when the preset is named in a babel.config.js,
71+
// where Babel supplies no preset options. Metro's transformer sets this from
72+
// its `experimentalImportSupport` transform option.
73+
expect(
74+
isLowered(transform({caller: {experimentalImportSupport: true}})),
75+
).toBe(false);
76+
});
77+
78+
test('lowers when the Babel caller does not lower imports', () => {
79+
expect(
80+
isLowered(transform({caller: {experimentalImportSupport: false}})),
81+
).toBe(true);
82+
});
83+
84+
test('preset options take precedence over the caller', () => {
85+
expect(
86+
isLowered(
87+
transform({
88+
presetOptions: {disableImportExportTransform: false},
89+
caller: {experimentalImportSupport: true},
90+
}),
91+
),
92+
).toBe(true);
93+
});
94+
});

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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 fs = require('node:fs');
15+
const os = require('node:os');
16+
const path = require('node:path');
17+
18+
const PROJECT_ROOT = path.sep === '/' ? '/my/project' : 'C:\\my\\project';
19+
const PRESET_PATH = require.resolve('@react-native/babel-preset');
20+
const SRC = "import foo from './foo';\nexport const bar = foo;";
21+
22+
// The transformer memoizes its resolved Babel config in a module-level
23+
// closure, so a fresh module instance is required for every distinct config.
24+
beforeEach(() => {
25+
jest.resetModules();
26+
});
27+
28+
let tmpDir: string;
29+
let babelConfigPath: string;
30+
31+
beforeAll(() => {
32+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-babel-transformer-'));
33+
babelConfigPath = path.join(tmpDir, 'babel.config.js');
34+
// A project Babel config that names the preset with no options - the shape
35+
// of the app template. The preset then only learns about Metro's transform
36+
// options through the Babel caller.
37+
fs.writeFileSync(
38+
babelConfigPath,
39+
`module.exports = {presets: [${JSON.stringify(PRESET_PATH)}]};\n`,
40+
);
41+
});
42+
43+
afterAll(() => {
44+
fs.rmSync(tmpDir, {recursive: true, force: true});
45+
});
46+
47+
function transformToCode({
48+
experimentalImportSupport,
49+
extendsBabelConfigPath,
50+
}: {
51+
experimentalImportSupport: boolean,
52+
extendsBabelConfigPath?: string,
53+
}): string {
54+
const {transform} = require('../index.js');
55+
const {ast} = transform({
56+
filename: path.join(PROJECT_ROOT, 'App.js'),
57+
src: SRC,
58+
plugins: [],
59+
options: {
60+
dev: true,
61+
enableBabelRuntime: false,
62+
enableBabelRCLookup: false,
63+
experimentalImportSupport,
64+
extendsBabelConfigPath,
65+
globalPrefix: '__metro__',
66+
hot: false,
67+
minify: false,
68+
platform: 'ios',
69+
publicPath: 'test',
70+
projectRoot: PROJECT_ROOT,
71+
},
72+
});
73+
return generate(ast).code;
74+
}
75+
76+
function isLowered(code: string): boolean {
77+
return code.includes('require(') && !/^import\b/m.test(code);
78+
}
79+
80+
describe.each([
81+
['no project Babel config', () => undefined],
82+
['a project Babel config naming the preset', () => babelConfigPath],
83+
])('with %s', (_name, getExtendsPath) => {
84+
test('lowers import/export when experimentalImportSupport is off', () => {
85+
const code = transformToCode({
86+
experimentalImportSupport: false,
87+
extendsBabelConfigPath: getExtendsPath(),
88+
});
89+
90+
expect(isLowered(code)).toBe(true);
91+
});
92+
93+
test('keeps import/export when experimentalImportSupport is on', () => {
94+
// Metro lowers ESM itself in this mode. If the preset lowered it first,
95+
// Metro's plugin would find nothing to do and the option would be a no-op.
96+
const code = transformToCode({
97+
experimentalImportSupport: true,
98+
extendsBabelConfigPath: getExtendsPath(),
99+
});
100+
101+
expect(isLowered(code)).toBe(false);
102+
expect(code).toContain("import foo from './foo'");
103+
});
104+
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ const transform /*: BabelTransformer['transform'] */ = ({
199199
platform: options.platform,
200200
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
201201
inlinePlatform: options.inlinePlatform,
202+
// Lets the preset skip its own import/export lowering when Metro will do
203+
// it, even when the preset is named in a project Babel config and so
204+
// receives no `disableImportExportTransform` option from us.
205+
experimentalImportSupport: options.experimentalImportSupport,
202206
unstable_transformProfile: options.unstable_transformProfile,
203207
},
204208
ast: true,

0 commit comments

Comments
 (0)