UNPKG

@expo/metro-config

Version:

A Metro config for running React Native projects with the Metro bundler

391 lines 21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EXPO_DEBUG = exports.INTERNAL_CALLSITES_REGEX = exports.internal_supervisingTransformerPath = exports.unstable_transformerPath = void 0; exports.createStableModuleIdFactory = createStableModuleIdFactory; exports.getDefaultConfig = getDefaultConfig; // Copyright 2023-present 650 Industries (Expo). All rights reserved. const config_1 = require("@expo/config"); const paths_1 = require("@expo/config/paths"); const metro_cache_1 = require("@expo/metro/metro-cache"); const exclusionList_1 = __importDefault(require("@expo/metro/metro-config/defaults/exclusionList")); const chalk_1 = __importDefault(require("chalk")); const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const resolve_from_1 = __importDefault(require("resolve-from")); const binary_file_store_1 = require("./binary-file-store"); const customizeFrame_1 = require("./customizeFrame"); Object.defineProperty(exports, "INTERNAL_CALLSITES_REGEX", { enumerable: true, get: function () { return customizeFrame_1.INTERNAL_CALLSITES_REGEX; } }); const env_1 = require("./env"); const getModulesPaths_1 = require("./getModulesPaths"); const getWatchFolders_1 = require("./getWatchFolders"); const rewriteRequestUrl_1 = require("./rewriteRequestUrl"); const sideEffects_1 = require("./serializer/sideEffects"); const withExpoSerializers_1 = require("./serializer/withExpoSerializers"); const postcss_1 = require("./transform-worker/postcss"); const filePath_1 = require("./utils/filePath"); const getPkgVersion_1 = require("./utils/getPkgVersion"); const setOnReadonly_1 = require("./utils/setOnReadonly"); const debug = require('debug')('expo:metro:config'); let hasWarnedAboutReactNative = false; function getReactNativeHostPackage(platform) { // NOTE(@kitten): Duplicated as `getSupportPackageForPlatform` in expo-modules-autolinking switch (platform) { case 'tvos': return 'react-native-tvos'; case 'macos': return 'react-native-macos'; default: return 'react-native'; } } function getReactNativeHostPath(projectRoot, platform) { const hostPackage = getReactNativeHostPackage(platform); // TODO(@kitten): Switch to `@expo/require-utils` / assess manual require.resolve return path_1.default.dirname(resolve_from_1.default.silent(projectRoot, `${hostPackage}/package.json`) ?? resolve_from_1.default.silent(projectRoot, 'react-native/package.json') ?? 'react-native/package.json'); } // Patch Metro's graph to support always parsing certain modules. This enables // things like Tailwind CSS which update based on their own heuristics. function patchMetroGraphToSupportUncachedModules() { const { Graph, } = require('@expo/metro/metro/DeltaBundler/Graph'); const original_traverseDependencies = Graph.prototype .traverseDependencies; if (!original_traverseDependencies.__patched) { original_traverseDependencies.__patched = true; // eslint-disable-next-line no-inner-declarations function traverseDependencies(paths, options) { this.dependencies.forEach((dependency) => { // Find any dependencies that have been marked as `skipCache` and ensure they are invalidated. // `skipCache` is set when a CSS module is found by PostCSS. if ( // TODO(@kitten): MixedOutput needs to be upcast, but `data` isn't defined in `JSFile`? dependency.output.find((file) => file.data.css?.skipCache) && !paths.includes(dependency.path)) { // Ensure we invalidate the `unstable_transformResultKey` (input hash) so the module isn't removed in // the Graph._processModule method. (0, setOnReadonly_1.setOnReadonly)(dependency, 'unstable_transformResultKey', dependency.unstable_transformResultKey + '.'); // Add the path to the list of modified paths so it gets run through the transformer again, // this will ensure it is passed to PostCSS -> Tailwind. paths.push(dependency.path); } }); // Invoke the original method with the new paths to ensure the standard behavior is preserved. return original_traverseDependencies.call(this, paths, options); } // Ensure we don't patch the method twice. Graph.prototype.traverseDependencies = traverseDependencies; traverseDependencies.__patched = true; } } function createNumericModuleIdFactory() { const fileToIdMap = new Map(); let nextId = 0; return (modulePath) => { let id = fileToIdMap.get(modulePath); if (typeof id !== 'number') { id = nextId++; fileToIdMap.set(modulePath, id); } return id; }; } function memoize(fn) { const cache = new Map(); return ((...args) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }); } function asMetroConfigInput(config) { return config; } function createStableModuleIdFactory(root) { const getModulePath = (modulePath, scope) => { // NOTE: Metro allows this but it can lead to confusing errors when dynamic requires cannot be resolved, e.g. `module 456 cannot be found`. if (modulePath == null) { return 'MODULE_NOT_FOUND'; } else if ((0, sideEffects_1.isVirtualModule)(modulePath)) { // Virtual modules should be stable. return modulePath; } else if (path_1.default.isAbsolute(modulePath)) { return (0, filePath_1.toPosixPath)(path_1.default.relative(root, modulePath)) + scope; } else { return (0, filePath_1.toPosixPath)(modulePath) + scope; } }; const memoizedGetModulePath = memoize(getModulePath); // This is an absolute file path. // TODO: We may want a hashed version for production builds in the future. return (modulePath, context) => { const env = context?.environment ?? 'client'; if (env === 'client') { // Only need scope for server bundles where multiple dimensions could run simultaneously. // @ts-expect-error: we patch this to support being a string. return memoizedGetModulePath(modulePath, ''); } // Helps find missing parts to the patch. if (!context?.platform) { // context = { platform: 'web' }; throw new Error('createStableModuleIdFactory: `context.platform` is required'); } // Only need scope for server bundles where multiple dimensions could run simultaneously. const scope = env !== 'client' ? `?platform=${context?.platform}&env=${env}` : ''; // @ts-expect-error: we patch this to support being a string. return memoizedGetModulePath(modulePath, scope); }; } function getDefaultConfig(projectRoot, { mode, isCSSEnabled = true, unstable_beforeAssetSerializationPlugins } = {}) { const { getDefaultConfig: getDefaultMetroConfig, mergeConfig, } = require('@expo/metro/metro-config'); if (isCSSEnabled) { patchMetroGraphToSupportUncachedModules(); } const reactNativePath = path_1.default.dirname(resolve_from_1.default.silent(projectRoot, 'react-native/package.json') ?? 'react-native/package.json'); if (reactNativePath === 'react-native' && !hasWarnedAboutReactNative) { hasWarnedAboutReactNative = true; console.log(chalk_1.default.yellow(`\u203A Could not resolve react-native! Is it installed and a project dependency?`)); } const sourceExtsConfig = { isTS: true, isReact: true, isModern: true }; const sourceExts = (0, paths_1.getBareExtensions)([], sourceExtsConfig); // Add support for cjs (without platform extensions). sourceExts.push('cjs'); const reanimatedVersion = (0, getPkgVersion_1.getPkgVersion)(projectRoot, 'react-native-reanimated'); const workletsVersion = (0, getPkgVersion_1.getPkgVersion)(projectRoot, 'react-native-worklets'); const babelRuntimeVersion = (0, getPkgVersion_1.getPkgVersion)(projectRoot, '@babel/runtime'); let sassVersion = null; if (isCSSEnabled) { sassVersion = (0, getPkgVersion_1.getPkgVersion)(projectRoot, 'sass'); // Enable SCSS by default so we can provide a better error message // when sass isn't installed. sourceExts.push('scss', 'sass', 'css'); } let pkg; try { pkg = (0, config_1.getPackageJson)(projectRoot); } catch (error) { if (error && error.name === 'ConfigError') { console.log(chalk_1.default.yellow(`\u203A Could not find a package.json at the project root! ("${projectRoot}")`)); } else { throw error; } } const watchFolders = (0, getWatchFolders_1.getWatchFolders)(projectRoot); const nodeModulesPaths = (0, getModulesPaths_1.getModulesPaths)(projectRoot); if (env_1.env.EXPO_DEBUG) { console.log(); console.log(`Expo Metro config:`); try { console.log(`- Version: ${require('../package.json').version}`); } catch { } console.log(`- Extensions: ${sourceExts.join(', ')}`); console.log(`- React Native: ${reactNativePath}`); console.log(`- Watch Folders: ${watchFolders.join(', ')}`); console.log(`- Node Module Paths: ${nodeModulesPaths.join(', ')}`); console.log(`- Sass: ${sassVersion}`); console.log(`- Reanimated: ${reanimatedVersion}`); console.log(`- Worklets: ${workletsVersion}`); console.log(`- Babel Runtime: ${babelRuntimeVersion}`); console.log(); } const metroDefaultValues = getDefaultMetroConfig.getDefaultValues(projectRoot); const cacheStore = new binary_file_store_1.FileStore({ root: path_1.default.join(os_1.default.tmpdir(), 'metro-cache'), }); const serverRoot = (0, paths_1.getMetroServerRoot)(projectRoot); const routerPackageRoot = resolve_from_1.default.silent(projectRoot, 'expo-router'); const expoMetroConfig = asMetroConfigInput({ reporter: { // Remove the default reporter which metro always resolves to be the react-native-community/cli reporter. // This prints a giant React logo which is less accessible to users on smaller terminals. update() { /*noop*/ }, }, watchFolders, resolver: { unstable_conditionsByPlatform: { ios: ['react-native'], android: ['react-native'], tvos: ['react-native'], macos: ['react-native'], // This is removed for server platforms. web: ['browser'], }, resolverMainFields: ['react-native', 'browser', 'main'], platforms: ['ios', 'android', 'tvos', 'macos'], assetExts: metroDefaultValues.resolver.assetExts .concat( // Add default support for `expo-image` file types. ['heic', 'avif'], // Add default support for `expo-sqlite` file types. ['db']) .filter((assetExt) => !sourceExts.includes(assetExt)), sourceExts, nodeModulesPaths, blockList: [ // .expo/types contains generated declaration files which are not and should not be processed by Metro. // This prevents unwanted fast refresh on the declaration files changes. // NOTE(@kitten): `exclusionList` automatically adds Metro's default values (0, exclusionList_1.default)(['.expo/types', '.expo/web/cache']), // NOTE(@kitten): @expo/metro-file-map allows us to exclude project-relative directories, since the // pattern is reapplied to normal paths during the Node crawling phase /^(?:android[\\/]app[\\/]build|android[\\/]\.gradle|ios[\\/]Pods)$/, ], }, cacheStores: [cacheStore], watcher: { // strip starting dot from env files. We only support watching development variants of env files as production is inlined using a different system. additionalExts: ['env', 'local', 'development'], }, serializer: { isThirdPartyModule(module) { // Block virtual modules from appearing in the source maps. if ((0, sideEffects_1.isVirtualModule)(module.path)) return true; // Generally block node modules if (/(?:^|[/\\])node_modules[/\\]/.test(module.path)) { // Allow the expo-router/entry and expo/AppEntry modules to be considered first party so the root of the app appears in the trace. return !module.path.match(/[/\\](expo-router[/\\]entry|expo[/\\]AppEntry)/); } return false; }, createModuleIdFactory: env_1.env.EXPO_USE_METRO_REQUIRE ? createStableModuleIdFactory.bind(null, serverRoot) : createNumericModuleIdFactory, getModulesRunBeforeMainModule: () => { const preModules = [ // NOTE(@kitten): `getModulesRunBeforeMainModule` is deprecated, but still partially expected // We instead add the canonical path, but don't expect or enforce Metro to re-order modules require.resolve(path_1.default.join(getReactNativeHostPath(projectRoot), 'Libraries/Core/InitializeCore')), ]; const stdRuntime = resolve_from_1.default.silent(projectRoot, 'expo/src/winter/index.ts'); if (stdRuntime) { preModules.push(stdRuntime); } else { debug('"expo/src/winter" not found, this may cause issues'); } // We need to shift this to be the first module so web Fast Refresh works as expected. // This will only be applied if the module is installed and imported somewhere in the bundle already. const metroRuntime = getExpoMetroRuntimeOptional(projectRoot); if (metroRuntime) { preModules.push(metroRuntime); } else { debug('"@expo/metro-runtime" not found, this may cause issues'); } return preModules; }, getPolyfills: ({ platform }) => { // Do nothing for nullish platforms. if (!platform) { return []; } return require(path_1.default.join(getReactNativeHostPath(projectRoot, platform), 'rn-get-polyfills'))(); }, }, server: { rewriteRequestUrl: (0, rewriteRequestUrl_1.getRewriteRequestUrl)(projectRoot), port: Number(env_1.env.RCT_METRO_PORT) || 8081, // NOTE(EvanBacon): Moves the server root down to the monorepo root. // This enables proper monorepo support for web. unstable_serverRoot: serverRoot, }, symbolicator: { customizeFrame: (0, customizeFrame_1.getDefaultCustomizeFrame)(), }, transformerPath: require.resolve('./transform-worker/transform-worker'), // NOTE: All of these values are used in the cache key. They should not contain any absolute paths. transformer: { unstable_workerThreads: true, // Custom: These are passed to `getCacheKey` and ensure invalidation when the version changes. unstable_renameRequire: false, _expoRouterPath: routerPackageRoot ? path_1.default.relative(serverRoot, routerPackageRoot) : undefined, postcssHash: (0, postcss_1.getPostcssConfigHash)(projectRoot), browserslistHash: pkg?.browserslist ? (0, metro_cache_1.stableHash)(JSON.stringify(pkg?.browserslist)).toString('hex') : null, sassVersion, // Ensure invalidation when the version changes due to the Reanimated and Worklets Babel plugins. reanimatedVersion, workletsVersion, // Ensure invalidation when using identical projects in monorepos _expoRelativeProjectRoot: path_1.default.relative(serverRoot, projectRoot), // `require.context` support unstable_allowRequireContext: true, allowOptionalDependencies: true, babelTransformerPath: require.resolve('./babel-transformer'), // Only apply expo internal asyncRequireModulePath when `expo` is installed // This must be a module request, rather than an absolute path to keep the cache clean asyncRequireModulePath: getExpoOptional(projectRoot, 'internal/async-require-module') ? 'expo/internal/async-require-module' : metroDefaultValues.transformer.asyncRequireModulePath, assetRegistryPath: '@react-native/assets-registry/registry', // Determines the minimum version of `@babel/runtime`, so we default it to the project's installed version of `@babel/runtime` enableBabelRuntime: babelRuntimeVersion ?? undefined, // Allows additional babelrc lookups (mostly unused). The default of `undefined` enables the project's custom Babel config without enabling babelrc/configFile discovery enableBabelRCLookup: undefined, // hermesParser: true, getTransformOptions: async () => ({ transform: { experimentalImportSupport: true, inlineRequires: false, }, }), }, }); // Merge in the default config from Metro here, even though loadConfig uses it as defaults. // This is a convenience for getDefaultConfig use in metro.config.js, e.g. to modify assetExts. const metroConfig = mergeConfig( // NOTE(@kitten): We neither want ConfigT/MetroConfig here, which is mostly marked as readonly, // nor InputConfigT which is inexact and partial. Instead, we want an exact type combination of // the default config and Expo's config metroDefaultValues, expoMetroConfig); // NOTE(@kitten): `useWatchman` is currently defaulting to `false` // However, in standard Metro, it defaults to `true`, and we must override this to `null` to disable the slow "native find" codepath // See: https://github.com/facebook/metro/blob/b9c243f/packages/metro-file-map/src/index.js#L326 // See: https://github.com/facebook/metro/blob/b9c243f/packages/metro/src/node-haste/DependencyGraph/createFileMap.js#L109 metroConfig.resolver.useWatchman = null; return (0, withExpoSerializers_1.withExpoSerializers)(metroConfig, { unstable_beforeAssetSerializationPlugins }); } /** Use to access the Expo Metro transformer path */ exports.unstable_transformerPath = require.resolve('./transform-worker/transform-worker'); exports.internal_supervisingTransformerPath = require.resolve('./transform-worker/supervising-transform-worker'); // re-export for legacy cases. exports.EXPO_DEBUG = env_1.env.EXPO_DEBUG; function getExpoOptional(projectRoot, subModule = 'package.json') { return resolve_from_1.default.silent(projectRoot, `expo/${subModule}`); } function getExpoMetroRuntimeOptional(projectRoot) { const EXPO_METRO_RUNTIME = '@expo/metro-runtime'; const metroRuntime = resolve_from_1.default.silent(projectRoot, EXPO_METRO_RUNTIME); if (metroRuntime) { return metroRuntime; } // NOTE(@kitten): While `@expo/metro-runtime` is a peer, auto-installing this peer is valid and expected // When it's auto-installed it may not be hoisted or not accessible from the project root, so we need to // try to also resolve it via `expo-router`, where it's a required peer const baseExpoRouter = resolve_from_1.default.silent(projectRoot, 'expo-router/package.json'); if (baseExpoRouter) { return resolve_from_1.default.silent(baseExpoRouter, EXPO_METRO_RUNTIME); } // When expo-router isn't installed, however, we instead try to resolve it from `expo`, where it's an // optional peer dependency const baseExpo = getExpoOptional(projectRoot); return baseExpo ? resolve_from_1.default.silent(baseExpo, EXPO_METRO_RUNTIME) : undefined; } //# sourceMappingURL=ExpoMetroConfig.js.map