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/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__/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/__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..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. @@ -156,16 +173,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..669b11f01a 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -20,15 +20,19 @@ 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, 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), + >, ... }; @@ -57,12 +61,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'; @@ -281,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); @@ -394,7 +396,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 +438,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/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; 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"