This commit is contained in:
babayaga
2025-12-27 09:25:38 +01:00
parent 37c7a78d1e
commit 48458746a7
10 changed files with 167 additions and 39 deletions
+75 -11
View File
@@ -1,6 +1,8 @@
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";
@@ -8,17 +10,44 @@ import type { AppConfig } from "./config.schema.js";
import { I18N_SOURCE_LANGUAGE } from "./constants.js"
const DEFAULT_CONFIG_PATH = path.resolve("./app-config.json");
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,
configPath: string = DEFAULT_CONFIG_PATH
libraryPath: string = LIBRARY_CONFIG_PATH
): AppConfig {
let rawContent: string;
// 1. Load Library Config (Defaults)
let rawLibraryContent: string;
try {
rawContent = fs.readFileSync(configPath, 'utf-8');
rawLibraryContent = fs.readFileSync(libraryPath, 'utf-8');
} catch (error) {
throw new Error(`Failed to read config file at ${configPath}: ${error}`);
throw new Error(`Failed to read library config file at ${libraryPath}: ${error}`);
}
const variables = {
@@ -26,18 +55,53 @@ export function loadConfig(
...process.env
};
const substitutedContent = substitute(false, rawContent, variables);
let parsedConfig: unknown;
const substitutedLibraryContent = substitute(false, rawLibraryContent, variables);
let libraryConfig: any;
try {
parsedConfig = JSON.parse(substitutedContent);
libraryConfig = JSON.parse(substitutedLibraryContent);
} catch (error) {
throw new Error(`Failed to parse config JSON after substitution: ${error}`);
throw new Error(`Failed to parse library config JSON: ${error}`);
}
const result = appConfigSchema.safeParse(parsedConfig);
// 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}`);
}
+19
View File
@@ -11,6 +11,7 @@ export interface AppConfig {
shopify: Shopify;
pages: Pages;
core: Core;
dev: Dev;
i18n: I18N;
products: Products;
retail: Retail;
@@ -50,6 +51,7 @@ export interface Core {
languages: string[];
languages_prod: string[];
rtl_languages: string[];
osr_root: string;
}
export interface Defaults {
@@ -58,6 +60,10 @@ export interface Defaults {
contact: string;
}
export interface Dev {
file_server: string;
}
export interface Ecommerce {
brand: string;
currencySymbol: string;
@@ -98,6 +104,7 @@ export interface NavigationButton {
export interface Optimization {
image_settings: ImageSettings;
presets: Presets;
}
export interface ImageSettings {
@@ -113,6 +120,18 @@ export interface Gallery {
sizes_regular: string;
}
export interface Presets {
slow: Fast;
medium: Fast;
fast: Fast;
}
export interface Fast {
sizes_medium: string;
sizes_thumbs: string;
sizes_large: string;
}
export interface Osrl {
env: string;
env_dev: string;
+21 -2
View File
@@ -51,7 +51,12 @@ export const coreSchema = z.object({
translate_content: z.boolean(),
languages: z.array(z.string()),
languages_prod: z.array(z.string()),
rtl_languages: z.array(z.string())
rtl_languages: z.array(z.string()),
osr_root: z.string()
});
export const devSchema = z.object({
file_server: z.string()
});
export const i18NSchema = z.object({
@@ -120,6 +125,12 @@ export const gallerySchema = z.object({
sizes_regular: z.string()
});
export const fastSchema = z.object({
sizes_medium: z.string(),
sizes_thumbs: z.string(),
sizes_large: z.string()
});
export const blogSchema = z.object({
store: z.string()
});
@@ -161,6 +172,12 @@ export const imageSettingsSchema = z.object({
lightbox: gallerySchema
});
export const presetsSchema = z.object({
slow: fastSchema,
medium: fastSchema,
fast: fastSchema
});
export const homeSchema = z.object({
hero: z.string(),
_blog: blogSchema
@@ -171,7 +188,8 @@ export const pagesSchema = z.object({
});
export const optimizationSchema = z.object({
image_settings: imageSettingsSchema
image_settings: imageSettingsSchema,
presets: presetsSchema
});
export const appConfigSchema = z.object({
@@ -187,6 +205,7 @@ export const appConfigSchema = z.object({
shopify: shopifySchema,
pages: pagesSchema,
core: coreSchema,
dev: devSchema,
i18n: i18NSchema,
products: productsSchema,
retail: retailSchema,
+2 -2
View File
@@ -11,8 +11,8 @@ export { I18N_SOURCE_LANGUAGE }
// Load config
const config = loadConfig(I18N_SOURCE_LANGUAGE)
export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
export const FILE_SERVER_DEV = 'localhost:5000'
export const OSR_ROOT = () => path.resolve(resolve(config.core.osr_root))
export const FILE_SERVER_DEV = config.dev.file_server
export const LOGGING_NAMESPACE = config.core.logging_namespace
export const TRANSLATE_CONTENT = config.core.translate_content
+10 -21
View File
@@ -24,26 +24,15 @@ const imagesSchema = z.object({
type Images = z.infer<typeof imagesSchema>;
export const IMAGE_PRESET: Images =
import { loadConfig } from './config-loader.js'
import { I18N_SOURCE_LANGUAGE } from "./constants.js"
// Load config
const config = loadConfig(I18N_SOURCE_LANGUAGE)
export const IMAGE_PRESET: Images =
{
[E_BROADBAND_SPEED.SLOW]: {
// For 2g connections: smaller image widths help performance. (Middle East & Africa)
sizes_medium: "(min-width: 100px) 100px, 100vw",
sizes_thumbs: "(min-width: 80px) 80px, 80vw",
sizes_large: "(min-width: 320px) 320px, 320vw",
},
[E_BROADBAND_SPEED.MEDIUM]:
{
// For 3g connections: a moderate size image for a balance of quality and speed.
sizes_medium: "(min-width: 400px) 400px, 400vw",
sizes_thumbs: "(min-width: 120px) 120px, 120vw",
sizes_large: "(min-width: 1024px) 1024px, 1024vw",
},
[E_BROADBAND_SPEED.FAST]:
{
// For 4g connections: larger images for high-resolution displays.
sizes_medium: "(min-width: 1024px) 1024px, 1024vw",
sizes_thumbs: "(min-width: 180px) 180px, 180vw",
sizes_large: "(min-width: 1200px) 1200px, 1200vw"
}
[E_BROADBAND_SPEED.SLOW]: config.optimization.presets.slow,
[E_BROADBAND_SPEED.MEDIUM]: config.optimization.presets.medium,
[E_BROADBAND_SPEED.FAST]: config.optimization.presets.fast
}