From a3d075cbdc64584abae0939f195efe3c0c5a8f98 Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Mon, 29 Jun 2026 02:44:56 -0700 Subject: [PATCH 1/2] Remove support for deprecated YAML and `.es6` config files (#1752) Summary: These have been deprecated (with a runtime warning) and removal planned for a year now. I believe they're little used in the wild and inherently very limited. Expo has already dropped support. Removing YAML support lets us drop a dependency also. Changelog: ``` - **[Breaking]**: Remove support for YAML and `.es6` config files ``` Differential Revision: D109987702 --- packages/metro-config/package.json | 3 +-- .../src/__fixtures__/yaml-extensionless | 2 -- .../src/__tests__/loadConfig-test.js | 10 ---------- packages/metro-config/src/loadConfig.js | 18 ++++++------------ yarn.lock | 5 ----- 5 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 packages/metro-config/src/__fixtures__/yaml-extensionless diff --git a/packages/metro-config/package.json b/packages/metro-config/package.json index 2eed694b50..551d89e3ef 100644 --- a/packages/metro-config/package.json +++ b/packages/metro-config/package.json @@ -25,8 +25,7 @@ "metro": "0.85.0", "metro-cache": "0.85.0", "metro-core": "0.85.0", - "metro-runtime": "0.85.0", - "yaml": "^2.6.1" + "metro-runtime": "0.85.0" }, "devDependencies": { "@types/connect": "^3.4.35", diff --git a/packages/metro-config/src/__fixtures__/yaml-extensionless b/packages/metro-config/src/__fixtures__/yaml-extensionless deleted file mode 100644 index 130a47fd1a..0000000000 --- a/packages/metro-config/src/__fixtures__/yaml-extensionless +++ /dev/null @@ -1,2 +0,0 @@ -# Use cacheVersion as a dummy free text field to check we've read the config -cacheVersion: yaml-extensionless diff --git a/packages/metro-config/src/__tests__/loadConfig-test.js b/packages/metro-config/src/__tests__/loadConfig-test.js index 78656dc777..404bb2d2a6 100644 --- a/packages/metro-config/src/__tests__/loadConfig-test.js +++ b/packages/metro-config/src/__tests__/loadConfig-test.js @@ -156,16 +156,6 @@ describe('loadConfig', () => { ); }); - test('supports loading YAML (deprecated)', async () => { - const result = await loadConfig({ - config: path.resolve(FIXTURES, 'yaml-extensionless'), - }); - expect(console.warn).toHaveBeenCalledWith( - 'YAML config is deprecated, please migrate to JavaScript config (e.g. metro.config.js)', - ); - expect(result.cacheVersion).toEqual('yaml-extensionless'); - }); - describe('given a search directory', () => { const HOME = process.platform === 'win32' ? 'C:\\Home' : '/home'; const mockHomeDir = jest.fn().mockReturnValue(HOME); diff --git a/packages/metro-config/src/loadConfig.js b/packages/metro-config/src/loadConfig.js index 72d73a8561..7060e45ec4 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -20,7 +20,6 @@ import {homedir} from 'os'; import * as path from 'path'; // eslint-disable-next-line no-restricted-imports import {pathToFileURL} from 'url'; -import {parse as parseYaml} from 'yaml'; type ResolveConfigResult = { filepath: string, @@ -57,12 +56,8 @@ const SEARCH_PLACES = [ 'package.json', ]; -const JS_EXTENSIONS = new Set([ - ...SEARCH_JS_EXTS, - '.es6', // Deprecated -]); +const JS_EXTENSIONS = new Set(SEARCH_JS_EXTS); const TS_EXTENSIONS = new Set(SEARCH_TS_EXTS); -const YAML_EXTENSIONS = new Set(['.yml', '.yaml', '']); // Deprecated const PACKAGE_JSON = path.sep + 'package.json'; const PACKAGE_JSON_PROP_NAME = 'metro'; @@ -394,7 +389,7 @@ async function loadConfig( export async function loadConfigFile( absolutePath: string, ): Promise { - // Config should be JSON, CommonJS, ESM or YAML (deprecated) + // Config should be JSON, CommonJS, or ESM let config: unknown; const extension = path.extname(absolutePath); @@ -436,15 +431,14 @@ export async function loadConfigFile( throw error; } } - } else if (YAML_EXTENSIONS.has(extension)) { - console.warn( - 'YAML config is deprecated, please migrate to JavaScript config (e.g. metro.config.js)', + } else if (extension === '.yaml' || extension === '.yml') { + throw new Error( + 'YAML config is no longer supported, please migrate to JavaScript config (e.g. metro.config.js)', ); - config = parseYaml(fs.readFileSync(absolutePath, 'utf8')); } else { throw new Error( `Unsupported config file extension: ${extension}. ` + - `Supported extensions are ${[...JS_EXTENSIONS, ...TS_EXTENSIONS, ...YAML_EXTENSIONS].map(ext => (ext === '' ? 'none' : `${ext}`)).join()})}.`, + `Supported extensions are ${[...JS_EXTENSIONS, ...TS_EXTENSIONS].map(ext => (ext === '' ? 'none' : `${ext}`)).join()})}.`, ); } diff --git a/yarn.lock b/yarn.lock index a41a00ccca..cbf5c21e30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5913,11 +5913,6 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" - integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" From 2b7355df26d7c9b4ee43a4f1ddd3cbfb82008b1e Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Mon, 29 Jun 2026 02:44:56 -0700 Subject: [PATCH 2/2] Support array-exporting config files (RFC) Summary: Back in https://github.com/react/metro/pull/1580 ("future?"), I floated the idea of a Metro config sugar using an array-valued export. This implements that, on top of the ESM support and TS support added over the last few months (but not announced yet). One hesitation - I'm wary of having too many ways to do the same thing, so this is part RFC. However, IMO, this is a sufficient improvement to warrant a bit of churn, it's purely additive for now but ought to replace `mergeConfig` in the long term. The [recommended Metro config for RN](https://reactnative.dev/docs/metro) is currently: ```js // metro.config.js const { getDefaultConfig, mergeConfig, } = require('react-native/metro-config'); /** * Metro configuration * https://metrobundler.dev/docs/configuration * * type {import('metro-config').MetroConfig} */ const config = {}; module.exports = mergeConfig(getDefaultConfig(__dirname), config); ``` This could be: ```typescript // metro.config.mts /** * Metro configuration * https://metrobundler.dev/docs/configuration */ import type {MetroConfig} from 'metro'; export default [ await import('react-native/metro-config'), defaults => ({ // Your config here. }) ] satisfies MetroConfig; ``` That's fully type checked and extensible. ## Benefits - No need to pass in the project root into framework defaults (`__dirname`/`import.meta.dirname`) - because the config loader already infers that into `projectRoot` based on the location of the config file, and downstream config functions are given fully-hydrated configs, including `projectRoot`. (If it needs to be overridden, it can be via the first entry) - No need to import `getDefaultConfig` or `mergeConfig` - and (in future?) no need for RN to (re-)export them. ## Libraries For libraries that need to modify Metro config, currently the popular pattern is a decorator: ```js // metro.config.js const { getDefaultConfig, mergeConfig, } = require('react-native/metro-config'); const { wrapWithReanimatedMetroConfig, } = require('react-native-reanimated/metro-config'); /** * Metro configuration * https://metrobundler.dev/docs/configuration * * type {import('metro-config').MetroConfig} */ const config = mergeConfig(getDefaultConfig(__dirname), {}); module.exports = wrapWithReanimatedMetroConfig(config); ``` This could be: ```typescript // metro.config.mts /** * Metro configuration * https://metrobundler.dev/docs/configuration */ import type {MetroConfig} from 'metro'; export default [ await import('react-native/metro-config'), defaults => ({ // Your config here. }), await import('react-native-reanimated/metro-config'), // Reanimated would need to support this by exporting a default function. ] satisfies MetroConfig; ``` ## Why now? We just bumped minimum Node.js for Metro/RN to 22.13 - that means TypeScript is supported everywhere. To minimise churn, I think ideally we announce the new recommended format all at once with the next RN release - ESM + TS (+ array form). Differential Revision: D109989571 --- docs/Configuration.md | 52 ++++++++++--------- .../__fixtures__/merge-array.metro.config.js | 27 ++++++++++ .../src/__tests__/loadConfig-test.js | 17 ++++++ packages/metro-config/src/loadConfig.js | 13 +++-- packages/metro-config/src/types.js | 10 +++- packages/metro-config/types/loadConfig.d.ts | 13 +++-- packages/metro-config/types/types.d.ts | 12 ++++- 7 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 packages/metro-config/src/__fixtures__/merge-array.metro.config.js diff --git a/docs/Configuration.md b/docs/Configuration.md index b3ad39d5a9..e1e4ede15d 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -773,15 +773,9 @@ The default value is `['hg.update']`. ## Merging Configurations -Using the `metro-config` package it is possible to merge multiple configurations together. +If a config file exports an *array*, the first entry will be merged into Metro's defaults, and each subsequent entry into the previous merged result. -| Method | Description | -| --------------------------------------- | ----------------------------------------------------------------------------------- | -| `mergeConfig(...configs): MergedConfig` | Returns the merged configuration of two or more configuration objects or functions. | - -`configs` may be any combination of (promises resolving to) configuration objects or functions. Functions are called with the merged config of all configs to the left, which may be useful for complex merges with the previous config. - -If any arguments are promises or async functions, `mergeConfig` will return a `Promise`, otherwise it will return the merged config synchronously. +Entries may be any combination of (promises resolving to) configuration objects or functions. Functions are called with the merged config of all configs to the left, which may be useful for complex merges with the previous config. :::note @@ -792,23 +786,31 @@ This allows overriding and removing default config parameters such as `platforms #### Merging Example + ```typescript // metro.config.ts -import type {ConfigT} from 'metro-config'; -import {mergeConfig} from 'metro-config'; - -export default (defaults: ConfigT) => - mergeConfig( - defaults, - // Function form: extends the default additionalExts - config => ({ - watcher: {additionalExts: [...config.watcher.additionalExts, 'mts', 'cts']}, - }), - // Plain object form - {transformer: {minifierPath: 'metro-minify-terser'}}, - // Function form: additionalExts already includes 'mts' and 'cts' from above - config => ({ - watcher: {additionalExts: [...config.watcher.additionalExts, 'css']}, - }), - ); +import type {MetroConfig} from 'metro-config'; + +export default [ + // Function form: extends the default additionalExts + config => ({ + watcher: {additionalExts: [...config.watcher.additionalExts, 'mts', 'cts']}, + }), + // Plain object form + {transformer: {minifierPath: 'metro-minify-terser'}}, + // Function form: additionalExts already includes 'mts' and 'cts' from above + config => ({ + watcher: {additionalExts: [...config.watcher.additionalExts, 'css']}, + }), +] satisfies MetroConfig; ``` + +#### The `mergeConfig` API + +Array configs use `metro-config`'s `mergeConfig` under the hood, which you may also use directly. + +| Method | Description | +| --------------------------------------- | ----------------------------------------------------------------------------------- | +| `mergeConfig(...configs): MergedConfig` | Returns the merged configuration of two or more configuration objects or functions. | + +If any arguments are promises or async functions, `mergeConfig` will return a `Promise`, otherwise it will return the merged config synchronously. diff --git a/packages/metro-config/src/__fixtures__/merge-array.metro.config.js b/packages/metro-config/src/__fixtures__/merge-array.metro.config.js new file mode 100644 index 0000000000..c2ba39fc70 --- /dev/null +++ b/packages/metro-config/src/__fixtures__/merge-array.metro.config.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +/*:: +import type {MetroConfig} from '../types'; +*/ + +module.exports = [ + // defaults are implicit + previous => ({ + resolver: { + sourceExts: ['before', ...previous.resolver.sourceExts], + }, + }), + previous => ({ + resolver: { + sourceExts: [...previous.resolver.sourceExts, 'after'], + }, + }), +] /*:: as MetroConfig */; diff --git a/packages/metro-config/src/__tests__/loadConfig-test.js b/packages/metro-config/src/__tests__/loadConfig-test.js index 404bb2d2a6..279c99b831 100644 --- a/packages/metro-config/src/__tests__/loadConfig-test.js +++ b/packages/metro-config/src/__tests__/loadConfig-test.js @@ -98,6 +98,23 @@ describe('loadConfig', () => { }); }); + test('array valued exports merge', async () => { + const defaultConfigOverrides = { + resolver: { + sourceExts: ['override'], + }, + }; + const config = path.resolve( + __dirname, + '../__fixtures__/merge-array.metro.config.js', + ); + const result = await loadConfig({config}, defaultConfigOverrides); + expect(result.projectRoot).toEqual(path.dirname(config)); + expect(result.resolver).toMatchObject({ + sourceExts: ['before', 'override', 'after'], + }); + }); + test('can load the config from a path pointing to a directory', async () => { // We don't actually use the specified file in this test but it needs to // resolve to a real file on the file system. diff --git a/packages/metro-config/src/loadConfig.js b/packages/metro-config/src/loadConfig.js index 7060e45ec4..669b11f01a 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -25,9 +25,14 @@ type ResolveConfigResult = { filepath: string, isEmpty: boolean, config: - | ((baseConfig: ConfigT) => Promise) - | ((baseConfig: ConfigT) => ConfigT) - | InputConfigT, + | ((baseConfig: ConfigT) => Promise) + | ((baseConfig: ConfigT) => InputConfigT) + | InputConfigT + | ReadonlyArray< + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise), + >, ... }; @@ -276,6 +281,8 @@ async function loadMetroConfigFromDisk( const resultedConfig = await configModule(defaultConfig); return mergeConfig(defaultConfig, resultedConfig); + } else if (Array.isArray(configModule)) { + return mergeConfig(defaultConfig, ...configModule); } return mergeConfig(defaultConfig, configModule); diff --git a/packages/metro-config/src/types.js b/packages/metro-config/src/types.js index 8d4ca510c8..b1810b6e57 100644 --- a/packages/metro-config/src/types.js +++ b/packages/metro-config/src/types.js @@ -263,7 +263,15 @@ export type InputConfigT = Partial< >, >; -export type MetroConfig = InputConfigT; +export type MetroConfig = + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise) + | ReadonlyArray< + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise), + >; export type ConfigT = Readonly< MetalConfigT & { diff --git a/packages/metro-config/types/loadConfig.d.ts b/packages/metro-config/types/loadConfig.d.ts index 1278b6a1f7..f4ec6bc78c 100644 --- a/packages/metro-config/types/loadConfig.d.ts +++ b/packages/metro-config/types/loadConfig.d.ts @@ -6,7 +6,7 @@ * * @noformat * @oncall react_native - * @generated SignedSource<<766965f89c595a34edf84abd019f9b92>> + * @generated SignedSource<<31c1727bd4ec31822cebd8243fbf3fe4>> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-config/src/loadConfig.js @@ -21,9 +21,14 @@ type ResolveConfigResult = { filepath: string; isEmpty: boolean; config: - | ((baseConfig: ConfigT) => Promise) - | ((baseConfig: ConfigT) => ConfigT) - | InputConfigT; + | ((baseConfig: ConfigT) => Promise) + | ((baseConfig: ConfigT) => InputConfigT) + | InputConfigT + | ReadonlyArray< + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise) + >; }; declare function resolveConfig( filePath?: string, diff --git a/packages/metro-config/types/types.d.ts b/packages/metro-config/types/types.d.ts index 8cb30693e0..b0b7574077 100644 --- a/packages/metro-config/types/types.d.ts +++ b/packages/metro-config/types/types.d.ts @@ -6,7 +6,7 @@ * * @noformat * @oncall react_native - * @generated SignedSource<<926fc453e7c2af496911a003ca20e556>> + * @generated SignedSource<<294ace0b3b28919393688be198af72c3>> * * This file was translated from Flow by scripts/generateTypeScriptDefinitions.js * Original file: packages/metro-config/src/types.js @@ -251,7 +251,15 @@ export type InputConfigT = Partial< } > >; -export type MetroConfig = InputConfigT; +export type MetroConfig = + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise) + | ReadonlyArray< + | InputConfigT + | ((baseConfig: ConfigT) => InputConfigT) + | ((baseConfig: ConfigT) => Promise) + >; export type ConfigT = Readonly< MetalConfigT & { cacheStores: CacheStoresConfigT;