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

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-12-27T07:52:28.530Z"
"default": "2025-12-27T08:18:11.303Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

View File

@ -134,7 +134,11 @@
],
"rtl_languages": [
"ar"
]
],
"osr_root": "${OSR_ROOT}"
},
"dev": {
"file_server": "localhost:5000"
},
"i18n": {
"store": "${OSR_ROOT}/i18n-store/store-${LANG}.json",
@ -224,6 +228,23 @@
"sizes_large": "(min-width: 1024px) 1024px, 1024vw",
"sizes_regular": "(min-width: 400px) 400px, 400vw"
}
},
"presets": {
"slow": {
"sizes_medium": "(min-width: 100px) 100px, 100vw",
"sizes_thumbs": "(min-width: 80px) 80px, 80vw",
"sizes_large": "(min-width: 320px) 320px, 320vw"
},
"medium": {
"sizes_medium": "(min-width: 400px) 400px, 400vw",
"sizes_thumbs": "(min-width: 120px) 120px, 120vw",
"sizes_large": "(min-width: 1024px) 1024px, 1024vw"
},
"fast": {
"sizes_medium": "(min-width: 1024px) 1024px, 1024vw",
"sizes_thumbs": "(min-width: 180px) 180px, 180vw",
"sizes_large": "(min-width: 1200px) 1200px, 1200vw"
}
}
}
}

5
app-config.local.json Normal file
View File

@ -0,0 +1,5 @@
{
"core": {
"logging_namespace": "LOCAL_CONFIG_OVERRIDE"
}
}

View File

@ -0,0 +1,11 @@
import { loadConfig } from '../src/app/config-loader.js';
import { I18N_SOURCE_LANGUAGE } from "../src/app/constants.js";
try {
const config = loadConfig(I18N_SOURCE_LANGUAGE);
console.log('LOGGING_NAMESPACE:', config.core.logging_namespace);
console.log('SHOW_GALLERY:', config.features.show_gallery);
} catch (e) {
console.error(e);
}

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
src/app/config.d.ts vendored
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;

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,

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

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
}