feat(02-01): implement deepMerge and overlay-aware loadConfig with tests
- Add deepMerge utility for recursive object merging (arrays replace, not concat) - Extend loadConfig with optional overlayPath parameter - Merge happens before env var expansion and Zod validation - Add 6 deepMerge unit tests and 4 overlay integration tests - Re-export deepMerge from config/index.ts - All 1087 existing tests still pass
This commit is contained in:
+50
-2
@@ -29,9 +29,57 @@ function expandEnvVarsInObject(obj: unknown): unknown {
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function loadConfig(configPath: string): Config {
|
||||
/**
|
||||
* Deep merge two plain objects. Overlay values override base values.
|
||||
* Arrays in overlay replace base arrays (not concatenated).
|
||||
* null in overlay explicitly sets key to null.
|
||||
* Only plain objects recurse; arrays, primitives, and null are overwritten.
|
||||
*/
|
||||
export function deepMerge(
|
||||
base: Record<string, unknown>,
|
||||
overlay: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const result = { ...base };
|
||||
for (const [key, value] of Object.entries(overlay)) {
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null &&
|
||||
!Array.isArray(result[key])
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
result[key] as Record<string, unknown>,
|
||||
value as Record<string, unknown>,
|
||||
);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and validate a Flynn config from YAML file(s).
|
||||
* If overlayPath is provided, the overlay YAML is deep-merged on top of
|
||||
* the base config BEFORE env var expansion and Zod validation.
|
||||
*/
|
||||
export function loadConfig(configPath: string, overlayPath?: string): Config {
|
||||
const rawContent = readFileSync(configPath, 'utf-8');
|
||||
const rawConfig = parse(rawContent);
|
||||
let rawConfig = parse(rawContent);
|
||||
|
||||
if (overlayPath) {
|
||||
const overlayContent = readFileSync(overlayPath, 'utf-8');
|
||||
const overlayConfig = parse(overlayContent);
|
||||
if (overlayConfig && typeof overlayConfig === 'object') {
|
||||
rawConfig = deepMerge(
|
||||
rawConfig as Record<string, unknown>,
|
||||
overlayConfig as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const expandedConfig = expandEnvVarsInObject(rawConfig);
|
||||
return configSchema.parse(expandedConfig);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user