require('dotenv').config(); /** * Expo SDK 56 replaces global fetch with expo/fetch unless this is set at transform time. * dotenv loads `.env` above; babel needs this here so release bundles inline the value. */ process.env.EXPO_PUBLIC_USE_RN_FETCH = process.env.EXPO_PUBLIC_USE_RN_FETCH ?? '1'; const {expoInlineEnvVars} = require('babel-preset-expo/build/plugins/inline-env-vars'); /** * Custom plugin that prints a file name when it's being processed by babel. * Disabled by default. To enable, set DEBUG_BABEL_TRACE=true in the environment. */ function traceTransformer() { return { visitor: { Program(path, state) { console.log('🔧 Transforming file:', state.filename); }, }, }; } const isTestEnv = process.env.BABEL_ENV === 'test' || process.env.NODE_ENV === 'test'; // One config for both remaining callers: `@callstack/repack` (native builds, app source via its // loaders, allowlisted node_modules via babel-swc-loader) and `babel-jest` (tests, coverage). const config = { presets: [require('@react-native/babel-preset')], plugins: [ // This is needed due to a react-native bug: https://github.com/facebook/react-native/issues/29084#issuecomment-1030732709 // It is included in @react-native/babel-preset but needs to be before plugin-proposal-class-properties or FlatList will break '@babel/plugin-transform-flow-strip-types', // Inline EXPO_PUBLIC_* env vars into release bundles (e.g. EXPO_PUBLIC_USE_RN_FETCH to keep RN fetch). expoInlineEnvVars, ['@babel/plugin-proposal-class-properties', {loose: true}], ['@babel/plugin-proposal-private-methods', {loose: true}], ['@babel/plugin-proposal-private-property-in-object', {loose: true}], /* Fullstory */ '@fullstory/react-native', [ '@fullstory/babel-plugin-annotate-react', { native: true, }, ], // Import alias for native devices [ 'module-resolver', { extensions: [ '.native.js', '.native.jsx', '.native.ts', '.native.tsx', '.js', '.jsx', '.ts', '.tsx', '.ios.js', '.ios.jsx', '.ios.ts', '.ios.tsx', '.android.js', '.android.jsx', '.android.ts', '.android.tx', ], alias: { '@assets': './assets', '@components': './src/components', '@hooks': './src/hooks', '@libs': './src/libs', '@navigation': './src/libs/Navigation', '@pages': './src/pages', '@prompts': './prompts', '@styles': './src/styles', // This path is provide alias for files like `ONYXKEYS` and `CONST`. '@src': './src', '@userActions': './src/libs/actions', '@github': './.github', '@selectors': './src/selectors', }, }, ], '@babel/plugin-transform-export-namespace-from', // The worklets babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/ // Skipped under Jest (isTestEnv): reanimated and worklets are fully mocked in jest/setup.ts, so the // plugin's worklet transform is wasted work - and its output is not cached, so a heavy nested // Babel transform re-runs on every test run. ...(isTestEnv ? [] : [ [ 'react-native-worklets/plugin', { bundleMode: true, // In Bundle Mode, an import that a worklet captures in its closure becomes a "remote function" // and throws when called synchronously on a Worklet Runtime. The `react-native-live-markdown` // ExpensiMark parser runs on the LiveMarkdownRuntime and directly imports `ExpensiMark`, // `unescapeText` (from `expensify-common/utils`) and `decode` (from `html-entities`) into its // worklets, so those imports must be forwarded to avoid the "Tried to synchronously call a // Remote Function" crash. Their transitive dependencies (expensify-common's `Str`, `punycode`, // `awesome-phonenumber`, html-entities' internals, ...) do NOT need forwarding: the bundler puts // the whole `require()` graph onto the Worklet Runtime, so they resolve locally there. // `relativePaths` only needs to cover live-markdown's own `./rangeUtils` (expensify-common and // html-entities resolve to CommonJS, where `relativePaths` forwarding never fires). // See https://docs.swmansion.com/react-native-worklets/docs/bundleMode/importForwarding importForwarding: { moduleNames: ['expensify-common', 'expensify-common/utils', 'html-entities'], relativePaths: ['@expensify/react-native-live-markdown'], }, }, ], ]), ], env: { production: { plugins: [['transform-remove-console', {exclude: ['error', 'warn']}]], }, test: { plugins: ['@babel/plugin-transform-dynamic-import'], }, }, }; if (process.env.DEBUG_BABEL_TRACE) { config.plugins.push(traceTransformer); } /* * We use and react-native-performance to capture/monitor stats * By default is disabled in production as it adds small overhead * When CAPTURE_METRICS is set we're explicitly saying that we want to capture metrics * To enable the for release builds we add these aliases */ if (process.env.CAPTURE_METRICS === 'true') { const path = require('path'); const profilingRenderer = path.resolve(__dirname, './node_modules/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-profiling'); config.plugins.push([ 'module-resolver', { root: ['./'], alias: { 'ReactNativeRenderer-prod': profilingRenderer, 'scheduler/tracing': 'scheduler/tracing-profiling', }, }, 'extra-alias', ]); } // Re.Pack runs babel only on allowlisted node_modules (babel-swc-loader): app-source aliases don't // apply there, and worklets are transformed by worklets-loader instead, so drop the alias plugin // and the worklet options. const repack = { ...config, plugins: config.plugins .filter((plugin) => !(Array.isArray(plugin) && plugin[0] === 'module-resolver' && plugin[2] !== 'extra-alias')) .map((plugin) => (Array.isArray(plugin) && plugin[0] === 'react-native-worklets/plugin' ? 'react-native-worklets/plugin' : plugin)), }; module.exports = (api) => { if (process.env.DEBUG_BABEL_CONFIG === 'true') { console.debug('babel.config.js'); console.debug(' - api.version:', api.version); console.debug(' - api.env:', api.env()); console.debug(' - process.env.NODE_ENV:', process.env.NODE_ENV); console.debug(' - process.env.BABEL_ENV:', process.env.BABEL_ENV); } // For native builds the caller is "@callstack/repack", for tests "babel-jest". // The web build and Storybook (Rsbuild) don't call into this file at all, and anything else // (an editor plugin, a stray script) still gets no config rather than an unintended one. const runningIn = api.caller((args = {}) => args.name); if (process.env.DEBUG_BABEL_CONFIG === 'true') { console.debug(' - running in: ', runningIn); } if (runningIn === '@callstack/repack') { return repack; } return runningIn === 'babel-jest' ? config : {}; };