From 7e728c532be59ebd54b7bbf13e09ec60b9981b32 Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Sun, 28 Jun 2026 11:03:27 -0700 Subject: [PATCH] Fix mergeConfig dropping state when a config in the chain is async When a config (function returning a Promise) appears mid-chain, mergeConfig handed off to mergeConfigAsync with two bugs: - It passed reversedConfigs.toReversed() as a single argument instead of spreading it into the rest parameter, so the remaining configs were wrapped in an extra array and merged as junk (e.g. an index key '0'). - It passed the unresolved promise as the base, discarding currentConfig (the base merged with every config to the left of the async one). Spread the remaining configs and fold currentConfig into the resolved async config so prior merges and trailing overrides are both preserved. Adds a regression test (async config function followed by an override) that fails before and passes after. --- .../metro-config/src/__tests__/mergeConfig-test.js | 14 ++++++++++++++ packages/metro-config/src/loadConfig.js | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/metro-config/src/__tests__/mergeConfig-test.js b/packages/metro-config/src/__tests__/mergeConfig-test.js index 569d3541b3..4a9c78a4e5 100644 --- a/packages/metro-config/src/__tests__/mergeConfig-test.js +++ b/packages/metro-config/src/__tests__/mergeConfig-test.js @@ -29,6 +29,20 @@ describe('mergeConfig', () => { }); }); + test('applies trailing overrides after an async config function', async () => { + const base: InputConfigT = {server: {port: 8081}}; + const asyncOverride = (): Promise => + Promise.resolve({transformer: {assetPlugins: ['async-plugin']}}); + const trailing: InputConfigT = {resolver: {sourceExts: ['ts']}}; + + const result = await mergeConfig(base, asyncOverride, trailing); + + // The base and every config in the chain must survive the async branch. + expect(result.server?.port).toBe(8081); + expect(result.transformer?.assetPlugins).toEqual(['async-plugin']); + expect(result.resolver?.sourceExts).toEqual(['ts']); + }); + describe('server.tls merging', () => { describe('override IS applied when tls is false or object', () => { test('override tls: object replaces base tls: false', () => { diff --git a/packages/metro-config/src/loadConfig.js b/packages/metro-config/src/loadConfig.js index 72d73a8561..def7dc089e 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -244,7 +244,10 @@ function mergeConfig< typeof next === 'function' ? next(currentConfig) : next; if (nextConfig instanceof Promise) { // $FlowFixMe[incompatible-type] Not clear why Flow doesn't like this - return mergeConfigAsync(nextConfig, reversedConfigs.toReversed()); + return mergeConfigAsync( + nextConfig.then(resolved => mergeConfigObjects(currentConfig, resolved)), + ...reversedConfigs.toReversed(), + ); } currentConfig = mergeConfigObjects(currentConfig, nextConfig) as T; }