Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

'use strict';

// $FlowExpectedError[untyped-import] - Preset is untyped
const preset = require('../index');
const babel = require('@babel/core');

const FILENAME = '/app/src/App.js';
const SRC = "import foo from './foo';\nexport const bar = foo;";

type PresetOptions = {
disableImportExportTransform?: boolean,
};

type CallerOptions = {
experimentalImportSupport?: boolean,
};

function transform({
presetOptions = {},
caller = {},
}: {
presetOptions?: PresetOptions,
caller?: CallerOptions,
} = {}): string {
const result = babel.transformSync(SRC, {
babelrc: false,
caller: {name: 'test', ...caller},
compact: false,
configFile: false,
filename: FILENAME,
presets: [[preset, {dev: false, ...presetOptions}]],
sourceMaps: false,
});
const code = result?.code;
if (code == null) {
throw new Error('Expected the transform to produce code');
}
return code;
}

function isLowered(code: string): boolean {
return code.includes('require(') && !/^import\b/m.test(code);
}

describe('import/export lowering is skipped when the caller lowers it', () => {
test('lowers by default', () => {
expect(isLowered(transform())).toBe(true);
});

test('keeps ESM when opted out via preset options', () => {
expect(
isLowered(
transform({presetOptions: {disableImportExportTransform: true}}),
),
).toBe(false);
});

test('keeps ESM when the Babel caller lowers imports itself', () => {
// The only channel available when the preset is named in a babel.config.js,
// where Babel supplies no preset options. Metro's transformer sets this from
// its `experimentalImportSupport` transform option.
expect(
isLowered(transform({caller: {experimentalImportSupport: true}})),
).toBe(false);
});

test('lowers when the Babel caller does not lower imports', () => {
expect(
isLowered(transform({caller: {experimentalImportSupport: false}})),
).toBe(true);
});

test('preset options take precedence over the caller', () => {
expect(
isLowered(
transform({
presetOptions: {disableImportExportTransform: false},
caller: {experimentalImportSupport: true},
}),
),
).toBe(true);
});
});
13 changes: 12 additions & 1 deletion packages/react-native-babel-preset/src/configs/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ function getInlinePlatform(caller) {
return caller?.inlinePlatform ?? false;
}

// Boolean, whether the caller lowers `import`/`export` itself (Metro's
// `experimentalImportSupport`). When it does, the preset must leave ESM intact.
function getExperimentalImportSupport(caller) {
return caller?.experimentalImportSupport ?? false;
}

// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
const loose = true;

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

const disableImportExportTransform =
options.disableImportExportTransform ??
babel?.caller(getExperimentalImportSupport) ??
false;

// Hermes V1 uses more optimised transform profiles. There is currently no
// difference between stable and canary, but canary may in future be used to
// test features in pre-prod Hermes V1 versions.
Expand Down Expand Up @@ -151,7 +162,7 @@ const getPreset = (src, options, babel) => {
extraPlugins.push([require('@react-native/babel-plugin-codegen')]);
}

if (!options.disableImportExportTransform) {
if (!disableImportExportTransform) {
extraPlugins.push(
[require('@babel/plugin-proposal-export-default-from')],
[
Expand Down
Original file line number Diff line number Diff line change
@@ -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
*/

'use strict';

// A project Babel config that names the preset with no options - the shape of
// the app template. The preset then only learns about Metro's transform
// options through the Babel caller.
module.exports = {
presets: [require.resolve('@react-native/babel-preset')],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

'use strict';

const generate = require('@babel/generator').default;
const path = require('node:path');

const PROJECT_ROOT = path.sep === '/' ? '/my/project' : 'C:\\my\\project';
const PROJECT_BABEL_CONFIG = path.join(
__dirname,
'__fixtures__',
'babel.config.js',
);
const SRC = "import foo from './foo';\nexport const bar = foo;";

// The transformer memoizes its resolved Babel config in a module-level
// closure, so a fresh module instance is required for every distinct config.
beforeEach(() => {
jest.resetModules();
});

function transformToCode({
experimentalImportSupport,
extendsBabelConfigPath,
}: {
experimentalImportSupport: boolean,
extendsBabelConfigPath?: string,
}): string {
const {transform} = require('../index.js');
const {ast} = transform({
filename: path.join(PROJECT_ROOT, 'App.js'),
src: SRC,
plugins: [],
options: {
dev: true,
enableBabelRuntime: false,
enableBabelRCLookup: false,
experimentalImportSupport,
extendsBabelConfigPath,
globalPrefix: '__metro__',
hot: false,
minify: false,
platform: 'ios',
publicPath: 'test',
projectRoot: PROJECT_ROOT,
},
});
return generate(ast).code;
}

function isLowered(code: string): boolean {
return code.includes('require(') && !/^import\b/m.test(code);
}

describe.each([
['no project Babel config', undefined],
['a project Babel config naming the preset', PROJECT_BABEL_CONFIG],
])('with %s', (_name, extendsBabelConfigPath) => {
test('lowers import/export when experimentalImportSupport is off', () => {
const code = transformToCode({
experimentalImportSupport: false,
extendsBabelConfigPath,
});

expect(isLowered(code)).toBe(true);
});

test('keeps import/export when experimentalImportSupport is on', () => {
// Metro lowers ESM itself in this mode. If the preset lowered it first,
// Metro's plugin would find nothing to do and the option would be a no-op.
const code = transformToCode({
experimentalImportSupport: true,
extendsBabelConfigPath,
});

expect(isLowered(code)).toBe(false);
expect(code).toContain("import foo from './foo'");
});
});
1 change: 1 addition & 0 deletions packages/react-native-babel-transformer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ const transform /*: BabelTransformer['transform'] */ = ({
platform: options.platform,
// $FlowFixMe[prop-missing] Remove suppression after next Metro release
inlinePlatform: options.inlinePlatform,
experimentalImportSupport: options.experimentalImportSupport,
unstable_transformProfile: options.unstable_transformProfile,
},
ast: true,
Expand Down