1import { ConfigAPI, PluginItem, TransformOptions } from '@babel/core';
2
3import { lazyImports } from './lazyImports';
4
5type BabelPresetExpoPlatformOptions = {
6  useTransformReactJSXExperimental?: boolean;
7  disableImportExportTransform?: boolean;
8  // Defaults to undefined, set to something truthy to disable `@babel/plugin-transform-react-jsx-self` and `@babel/plugin-transform-react-jsx-source`.
9  withDevTools?: boolean;
10  // Defaults to undefined, set to `true` to disable `@babel/plugin-transform-flow-strip-types`
11  disableFlowStripTypesTransform?: boolean;
12  // Defaults to undefined, set to `false` to disable `@babel/plugin-transform-runtime`
13  enableBabelRuntime?: boolean;
14  // Defaults to `'default'`, can also use `'hermes-canary'`
15  unstable_transformProfile?: 'default' | 'hermes-canary';
16};
17
18export type BabelPresetExpoOptions = {
19  lazyImports?: boolean;
20  reanimated?: boolean;
21  jsxRuntime?: 'classic' | 'automatic';
22  jsxImportSource?: string;
23  web?: BabelPresetExpoPlatformOptions;
24  native?: BabelPresetExpoPlatformOptions;
25};
26
27function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}): TransformOptions {
28  const { web = {}, native = {}, reanimated } = options;
29
30  const bundler = api.caller(getBundler);
31  const isWebpack = bundler === 'webpack';
32  let platform = api.caller((caller) => (caller as any)?.platform);
33
34  // If the `platform` prop is not defined then this must be a custom config that isn't
35  // defining a platform in the babel-loader. Currently this may happen with Next.js + Expo web.
36  if (!platform && isWebpack) {
37    platform = 'web';
38  }
39
40  const platformOptions: BabelPresetExpoPlatformOptions =
41    platform === 'web'
42      ? {
43          // Only disable import/export transform when Webpack is used because
44          // Metro does not support tree-shaking.
45          disableImportExportTransform: isWebpack,
46          ...web,
47        }
48      : { disableImportExportTransform: false, ...native };
49
50  // Note that if `options.lazyImports` is not set (i.e., `null` or `undefined`),
51  // `metro-react-native-babel-preset` will handle it.
52  const lazyImportsOption = options?.lazyImports;
53
54  const extraPlugins: PluginItem[] = [
55    // `metro-react-native-babel-preset` configures this plugin with `{ loose: true }`, which breaks all
56    // getters and setters in spread objects. We need to add this plugin ourself without that option.
57    // @see https://github.com/expo/expo/pull/11960#issuecomment-887796455
58    [require.resolve('@babel/plugin-proposal-object-rest-spread'), { loose: false }],
59  ];
60
61  // Set true to disable `@babel/plugin-transform-react-jsx`
62  // we override this logic outside of the metro preset so we can add support for
63  // React 17 automatic JSX transformations.
64  // If the logic for `useTransformReactJSXExperimental` ever changes in `metro-react-native-babel-preset`
65  // then this block should be updated to reflect those changes.
66  if (!platformOptions.useTransformReactJSXExperimental) {
67    extraPlugins.push([
68      require('@babel/plugin-transform-react-jsx'),
69      {
70        // Defaults to `automatic`, pass in `classic` to disable auto JSX transformations.
71        runtime: (options && options.jsxRuntime) || 'automatic',
72        ...(options &&
73          options.jsxRuntime !== 'classic' && {
74            importSource: (options && options.jsxImportSource) || 'react',
75          }),
76      },
77    ]);
78    // Purposefully not adding the deprecated packages:
79    // `@babel/plugin-transform-react-jsx-self` and `@babel/plugin-transform-react-jsx-source`
80    // back to the preset.
81  }
82
83  const aliasPlugin = getAliasPlugin();
84  if (aliasPlugin) {
85    extraPlugins.push(aliasPlugin);
86  }
87
88  if (platform === 'web') {
89    extraPlugins.push(require.resolve('babel-plugin-react-native-web'));
90  }
91
92  return {
93    presets: [
94      [
95        // We use `require` here instead of directly using the package name because we want to
96        // specifically use the `metro-react-native-babel-preset` installed by this package (ex:
97        // `babel-preset-expo/node_modules/`). This way the preset will not change unintentionally.
98        // Reference: https://github.com/expo/expo/pull/4685#discussion_r307143920
99        require('metro-react-native-babel-preset'),
100        {
101          // Defaults to undefined, set to something truthy to disable `@babel/plugin-transform-react-jsx-self` and `@babel/plugin-transform-react-jsx-source`.
102          withDevTools: platformOptions.withDevTools,
103          // Defaults to undefined, set to `true` to disable `@babel/plugin-transform-flow-strip-types`
104          disableFlowStripTypesTransform: platformOptions.disableFlowStripTypesTransform,
105          // Defaults to undefined, set to `false` to disable `@babel/plugin-transform-runtime`
106          enableBabelRuntime: platformOptions.enableBabelRuntime,
107          // Defaults to `'default'`, can also use `'hermes-canary'`
108          unstable_transformProfile: platformOptions.unstable_transformProfile,
109          // Set true to disable `@babel/plugin-transform-react-jsx` and
110          // the deprecated packages `@babel/plugin-transform-react-jsx-self`, and `@babel/plugin-transform-react-jsx-source`.
111          //
112          // Otherwise, you'll sometime get errors like the following (starting in Expo SDK 43, React Native 64, React 17):
113          //
114          // TransformError App.js: /path/to/App.js: Duplicate __self prop found. You are most likely using the deprecated transform-react-jsx-self Babel plugin.
115          // Both __source and __self are automatically set when using the automatic jsxRuntime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.
116          useTransformReactJSXExperimental: true,
117
118          disableImportExportTransform: platformOptions.disableImportExportTransform,
119          lazyImportExportTransform:
120            lazyImportsOption === true
121              ? (importModuleSpecifier: string) => {
122                  // Do not lazy-initialize packages that are local imports (similar to `lazy: true`
123                  // behavior) or are in the blacklist.
124                  return !(
125                    importModuleSpecifier.includes('./') || lazyImports.has(importModuleSpecifier)
126                  );
127                }
128              : // Pass the option directly to `metro-react-native-babel-preset`, which in turn
129                // passes it to `babel-plugin-transform-modules-commonjs`
130                lazyImportsOption,
131        },
132      ],
133    ],
134
135    plugins: [
136      ...extraPlugins,
137      // TODO: Remove
138      [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
139      require.resolve('@babel/plugin-proposal-export-namespace-from'),
140      // Automatically add `react-native-reanimated/plugin` when the package is installed.
141      // TODO: Move to be a customTransformOption.
142      hasModule('react-native-reanimated') &&
143        reanimated !== false && [require.resolve('react-native-reanimated/plugin')],
144    ].filter(Boolean) as PluginItem[],
145  };
146}
147
148function getAliasPlugin(): PluginItem | null {
149  if (!hasModule('@expo/vector-icons')) {
150    return null;
151  }
152  return [
153    require.resolve('babel-plugin-module-resolver'),
154    {
155      alias: {
156        'react-native-vector-icons': '@expo/vector-icons',
157      },
158    },
159  ];
160}
161
162function hasModule(name: string): boolean {
163  try {
164    return !!require.resolve(name);
165  } catch (error: any) {
166    if (error.code === 'MODULE_NOT_FOUND' && error.message.includes(name)) {
167      return false;
168    }
169    throw error;
170  }
171}
172
173/** Determine which bundler is being used. */
174function getBundler(caller: any) {
175  if (!caller) return null;
176  if (caller.bundler) return caller.bundler;
177  if (
178    // Known tools that use `webpack`-mode via `babel-loader`: `@expo/webpack-config`, Next.js <10
179    caller.name === 'babel-loader' ||
180    // NextJS 11 uses this custom caller name.
181    caller.name === 'next-babel-turbo-loader'
182  ) {
183    return 'webpack';
184  }
185
186  // Assume anything else is Metro.
187  return 'metro';
188}
189
190export default babelPresetExpo;
191module.exports = babelPresetExpo;
192