diff --git a/packages/config-loader/src/sources/MergedConfigSource.test.ts b/packages/config-loader/src/sources/MergedConfigSource.test.ts index e8a3b8b565..0eb3903df7 100644 --- a/packages/config-loader/src/sources/MergedConfigSource.test.ts +++ b/packages/config-loader/src/sources/MergedConfigSource.test.ts @@ -17,8 +17,9 @@ import { MergedConfigSource } from './MergedConfigSource'; import { MutableConfigSource } from './MutableConfigSource'; import { isResolved, readAll, simpleSource } from './__testUtils__/testUtils'; +import { ConfigSource } from './types'; -describe('MergeConfigSource', () => { +describe('MergedConfigSource', () => { it('should forward from a single source', async () => { const source = simpleSource([{ a: 1 }, { a: 2 }, { a: 3 }]); const merged = MergedConfigSource.from([source]); @@ -104,4 +105,47 @@ describe('MergeConfigSource', () => { done: true, }); }); + + it('should be flattened', async () => { + const sym = Symbol.for( + '@backstage/config-loader#MergedConfigSource.sources', + ); + const sourceA: ConfigSource = { + async *readConfigData() { + yield { data: [] }; + }, + }; + const sourceD: ConfigSource = { + async *readConfigData() { + yield { data: [] }; + }, + }; + const sourceB: ConfigSource = { + async *readConfigData() { + yield { data: [] }; + }, + }; + const sourceC: ConfigSource = { + async *readConfigData() { + yield { data: [] }; + }, + }; + + const sourceAB = MergedConfigSource.from([sourceA, sourceB]); + const sourceABC = MergedConfigSource.from([sourceAB, sourceC]); + const sourceABCD = MergedConfigSource.from([sourceABC, sourceD]); + + expect((sourceAB as any)[sym]).toEqual([sourceA, sourceB]); + expect((sourceABC as any)[sym]).toEqual([sourceA, sourceB, sourceC]); + expect((sourceABCD as any)[sym]).toEqual([ + sourceA, + sourceB, + sourceC, + sourceD, + ]); + + await expect(readAll(sourceAB)).resolves.toEqual([[]]); + await expect(readAll(sourceABC)).resolves.toEqual([[]]); + await expect(readAll(sourceABCD)).resolves.toEqual([[]]); + }); }); diff --git a/packages/config-loader/src/sources/MergedConfigSource.ts b/packages/config-loader/src/sources/MergedConfigSource.ts index 9bf5b20b38..206290b383 100644 --- a/packages/config-loader/src/sources/MergedConfigSource.ts +++ b/packages/config-loader/src/sources/MergedConfigSource.ts @@ -21,13 +21,29 @@ import { ReadConfigDataOptions, } from './types'; +const sourcesSymbol = Symbol.for( + '@backstage/config-loader#MergedConfigSource.sources', +); + export class MergedConfigSource implements ConfigSource { + // An optimization to flatten nested merged sources to avid unnecessary microtasks + static #flattenSources(sources: ConfigSource[]): ConfigSource[] { + return sources.flatMap(source => { + if (sourcesSymbol in source) { + return this.#flattenSources(source[sourcesSymbol] as ConfigSource[]); + } + return source; + }); + } + static from(sources: ConfigSource[]): ConfigSource { - return new MergedConfigSource(sources); + return new MergedConfigSource(this.#flattenSources(sources)); } private constructor(private readonly sources: ConfigSource[]) {} + [sourcesSymbol] = this.sources; + async *readConfigData( options?: ReadConfigDataOptions, ): AsyncConfigSourceIterator {