site-library/src/app/config-loader.ts
2025-12-27 16:02:48 +01:00

109 lines
4.1 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { substitute } from "@polymech/commons/variables";
import { appConfigSchema } from "./config.schema.js";
import type { AppConfig } from "./config.schema.js";
const I18N_SOURCE_LANGUAGE = 'en';
const LIBRARY_CONFIG_PATH = path.resolve("./app-config.json");
const USER_CONFIG_DEFAULT_PATH = path.resolve("./app-config.local.json");
function deepMerge(target: any, source: any): any {
if (typeof source !== 'object' || source === null) {
return source; // Primitives or null overwrite
}
if (Array.isArray(source)) {
return source; // Arrays overwrite
}
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
return source; // Target is not mergeable object, overwrite
}
const result = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const val = source[key];
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
result[key] = deepMerge(result[key], val);
} else {
result[key] = val;
}
}
}
return result;
}
export function loadConfig(
locale: string = I18N_SOURCE_LANGUAGE,
libraryPath: string = LIBRARY_CONFIG_PATH
): AppConfig {
// 1. Load Library Config (Defaults)
let rawLibraryContent: string;
try {
rawLibraryContent = fs.readFileSync(libraryPath, 'utf-8');
} catch (error) {
throw new Error(`Failed to read library config file at ${libraryPath}: ${error}`);
}
const variables = {
LANG: locale
};
const substitutedLibraryContent = substitute(false, rawLibraryContent, variables);
let libraryConfig: any;
try {
libraryConfig = JSON.parse(substitutedLibraryContent);
} catch (error) {
throw new Error(`Failed to parse library config JSON: ${error}`);
}
// 2. Parse CLI Arguments
// We assume the caller might want to pass args, or we just grab process.argv
// We cast to any because yargs returns a complex type
const argv: any = yargs(hideBin(process.argv)).parseSync ? yargs(hideBin(process.argv)).parseSync() : (yargs(hideBin(process.argv)) as any).argv;
// 3. Determine User Config Path
// Check for --config <path>
const userConfigPath = argv.config ? path.resolve(argv.config) : USER_CONFIG_DEFAULT_PATH;
// 4. Load User Config (if exists)
let userConfig: any = {};
if (fs.existsSync(userConfigPath)) {
try {
const rawUserContent = fs.readFileSync(userConfigPath, 'utf-8');
const substitutedUserContent = substitute(false, rawUserContent, variables);
userConfig = JSON.parse(substitutedUserContent);
} catch (error) {
console.warn(`Failed to load or parse user config at ${userConfigPath}: ${error}`);
}
}
// 5. Merge: Library <- User <- CLI
// Note: yargs parses --config as part of argv, but also other flags like --core.logging_namespace
// We filter out specific known CLI-only flags if needed, but config schema validation will drop unknown keys anyway?
// Actually zod 'strip' is default in safeParse? No, usually it passes through unless strict().
// We should probably rely on valid keys overwriting.
// CLI args often come with standard keys like '$0', '_' which we might want to exclude if we blindly merge.
// However, deepMerge will add them.
// Ideally we would only merge keys that exist in the schema, but dynamic is fine for now.
let mergedConfig = deepMerge(libraryConfig, userConfig);
mergedConfig = deepMerge(mergedConfig, argv);
// 6. Validate
const result = appConfigSchema.safeParse(mergedConfig);
if (!result.success) {
// Pretty print error if possible or just message
throw new Error(`Config validation failed: ${result.error.message}`);
}
return result.data;
}