1// Copyright 2021-present 650 Industries (Expo). All rights reserved. 2 3import chalk from 'chalk'; 4import Debug from 'debug'; 5import type { BabelTransformer, BabelTransformerArgs } from 'metro-babel-transformer'; 6import resolveFrom from 'resolve-from'; 7 8import { generateFunctionMap } from './generateFunctionMap'; 9import { getBabelConfig } from './getBabelConfig'; 10 11const debug = Debug('expo:metro:exotic-babel-transformer'); 12 13let babelCore: typeof import('@babel/core') | undefined; 14 15function getBabelCoreFromProject(projectRoot: string) { 16 if (babelCore) return babelCore; 17 babelCore = require(resolveFrom(projectRoot, '@babel/core')); 18 return babelCore!; 19} 20 21let babelParser: typeof import('@babel/parser') | undefined; 22 23function getBabelParserFromProject(projectRoot: string) { 24 if (babelParser) return babelParser; 25 babelParser = require(resolveFrom(projectRoot, '@babel/parser')); 26 return babelParser!; 27} 28 29function sucrase( 30 args: BabelTransformerArgs, 31 { 32 transforms, 33 }: { 34 transforms: string[]; 35 } 36): Partial<ReturnType<BabelTransformer['transform']>> { 37 const { 38 src, 39 filename, 40 options: { dev }, 41 } = args; 42 const { transform } = require('sucrase'); 43 44 const results = transform(src, { 45 filePath: filename, 46 production: !dev, 47 transforms, 48 }); 49 50 return { 51 code: results.code, 52 functionMap: null, 53 }; 54} 55 56const getExpensiveSucraseTransforms = (filename: string) => [ 57 'jsx', 58 'imports', 59 /\.tsx?$/.test(filename) ? 'typescript' : 'flow', 60]; 61 62function parseAst(projectRoot: string, sourceCode: string) { 63 const babylon = getBabelParserFromProject(projectRoot); 64 65 return babylon.parse(sourceCode, { 66 sourceType: 'unambiguous', 67 }); 68} 69 70export type Rule = { 71 warn?: boolean; 72 type?: 'module' | 'app'; 73 name?: string; 74 test: ((args: BabelTransformerArgs) => boolean) | RegExp; 75 transform: BabelTransformer['transform']; 76}; 77 78/** Create a transformer that emulates Webpack's loader system. */ 79export function createMultiRuleTransformer({ 80 getRuleType, 81 rules, 82}: { 83 getRuleType: (args: BabelTransformerArgs) => string; 84 rules: Rule[]; 85}): BabelTransformer['transform'] { 86 // const warnings: string[] = []; 87 return function transform(args: BabelTransformerArgs) { 88 const { filename, options } = args; 89 const OLD_BABEL_ENV = process.env.BABEL_ENV; 90 process.env.BABEL_ENV = options?.dev ? 'development' : process.env.BABEL_ENV || 'production'; 91 92 try { 93 const ruleType = getRuleType(args); 94 95 for (const rule of rules) { 96 // optimization for checking node modules 97 if (rule.type && rule.type !== ruleType) { 98 continue; 99 } 100 101 const isMatched = 102 typeof rule.test === 'function' ? rule.test(args) : rule.test.test(args.filename); 103 if (isMatched) { 104 const results = rule.transform(args); 105 // @ts-ignore: Add extra property for testing 106 results._ruleName = rule.name; 107 // Perform a basic parse if none exists, this enables us to control the output, but only if it changed. 108 if (results.code && !results.ast) { 109 // Parse AST with babel otherwise Metro transformer will throw away the returned results. 110 results.ast = parseAst(options?.projectRoot, results.code); 111 } 112 113 // TODO: Suboptimal warnings 114 // if (rule.warn) { 115 // const matchName = 116 // filename.match(/node_modules\/((:?@[\w\d-]+\/[\w\d-]+)|(:?[\w\d-]+))\/?/)?.[1] ?? 117 // filename; 118 // if (matchName && !warnings.includes(matchName)) { 119 // warnings.push(matchName); 120 // console.warn(chalk.yellow.bold`warn `, matchName); 121 // console.warn( 122 // chalk.yellow`untranspiled module is potentially causing bundler slowdown, using modules that support commonjs will make your dev server much faster.` 123 // ); 124 // } 125 // } 126 127 return results; 128 } 129 } 130 throw new Error('no loader rule to handle file: ' + filename); 131 } finally { 132 if (OLD_BABEL_ENV) { 133 process.env.BABEL_ENV = OLD_BABEL_ENV; 134 } 135 } 136 }; 137} 138 139function app(args: BabelTransformerArgs) { 140 debug('app:', args.filename); 141 142 const { filename, options, src, plugins } = args; 143 const babelConfig = { 144 // ES modules require sourceType='module' but OSS may not always want that 145 sourceType: 'unambiguous', 146 ...getBabelConfig(filename, options, plugins), 147 // Variables that are exposed to the user's babel preset. 148 caller: { 149 name: 'metro', 150 151 platform: options.platform, 152 }, 153 ast: true, 154 }; 155 156 // Surface a warning function so babel linters can be used. 157 Object.defineProperty(babelConfig.caller, 'onWarning', { 158 enumerable: false, 159 writable: false, 160 value: (babelConfig.caller.onWarning = function (msg: any) { 161 // Format the file path first so users know where the warning came from. 162 console.warn(chalk.bold.yellow`warn ` + args.filename); 163 console.warn(msg); 164 }), 165 }); 166 167 const { parseSync, transformFromAstSync } = getBabelCoreFromProject(options.projectRoot); 168 const sourceAst = parseSync(src, babelConfig); 169 170 // Should never happen. 171 if (!sourceAst) return { ast: null }; 172 173 const result = transformFromAstSync(sourceAst, src, babelConfig); 174 175 // TODO: Disable by default 176 const functionMap = generateFunctionMap(sourceAst, { filename }); 177 // The result from `transformFromAstSync` can be null (if the file is ignored) 178 if (!result) { 179 return { ast: null, functionMap }; 180 } 181 182 return { ast: result.ast, functionMap }; 183} 184 185export const loaders: Record<string, (args: BabelTransformerArgs) => any> = { 186 // Perform the standard, and most expensive transpilation sequence. 187 app, 188 189 // Transpile react-native with sucrase. 190 reactNativeModule(args) { 191 // Special file needs full transpilation. 192 if (args.filename.includes('react-native/Libraries/Events/EventPolyfill.js')) { 193 // Match React Native modules which use non-standard flow features, convert them using babel (most expensive). 194 return app(args); 195 } 196 197 debug('rn:', args.filename); 198 return sucrase(args, { 199 transforms: ['jsx', 'flow', 'imports'], 200 }); 201 }, 202 203 // Transpile expo modules with sucrase. 204 expoModule(args) { 205 debug('expo:', args.filename); 206 // TODO: Fix all expo packages 207 return sucrase(args, { 208 transforms: [ 209 'imports', 210 // TODO: fix expo-processing, expo/vector-icons 211 /(expo-processing|expo\/vector-icons)/.test(args.filename) && 'jsx', 212 // TODO: fix expo-asset-utils 213 /(expo-asset-utils)/.test(args.filename) && 'flow', 214 ].filter(Boolean) as string[], 215 }); 216 }, 217 218 // Transpile known community modules with the most expensive sucrase 219 untranspiledModule(args) { 220 debug('known issues:', args.filename); 221 return sucrase(args, { 222 transforms: getExpensiveSucraseTransforms(args.filename), 223 }); 224 }, 225 226 // Pass all modules through without transpiling them. 227 passthroughModule(args) { 228 const { filename, options, src } = args; 229 debug('passthrough:', filename); 230 231 // Perform a basic ast parse, this doesn't matter since the worker will parse and ignore anyways. 232 const ast = parseAst(options.projectRoot, src); 233 234 // TODO: Disable by default 235 const functionMap = generateFunctionMap(ast, { filename }); 236 237 return { 238 code: src, 239 functionMap, 240 ast, 241 }; 242 }, 243}; 244