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