From 18c9e30b192f69d8e9cb711c911eaa1d3baa8e09 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 11 Jun 2024 21:28:52 +0200 Subject: [PATCH] feat: custom merger to replace built-in defu (#160) --- README.md | 64 +++++++++++++++++++++++++++++++++++---------------- src/loader.ts | 20 ++++++++++------ src/types.ts | 2 ++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 140b134..3fc294b 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,12 @@ Custom [unjs/jiti](https://github.com/unjs/jiti) options to import configuration Options passed to [unjs/giget](https://github.com/unjs/giget) when extending layer from git source. +### `merger` + +Custom options merger function. Default is [defu](https://github.com/unjs/defu). + +**Note:** Custom merge function should deeply merge options with arguments high -> low priority. + ### `envName` Environment name used for [environment specific configuration](#environment-specific-configuration). @@ -216,10 +222,10 @@ export default { // base/config.ts export default { colors: { - primary: 'base_primary', - text: 'base_text' - } -} + primary: "base_primary", + text: "base_text", + }, +}; ``` The loaded configuration would look like this: @@ -228,21 +234,39 @@ The loaded configuration would look like this: const config = { dev: true, colors: { - primary: 'user_primary', - secondary: 'theme_secondary', - text: 'base_text' - } -} + primary: "user_primary", + secondary: "theme_secondary", + text: "base_text", + }, +}; ``` Layers: ```js [ - { config: { /* theme config */ }, configFile: "/path/to/theme/config.ts", cwd: "/path/to/theme " }, - { config: { /* base config */ }, configFile: "/path/to/base/config.ts", cwd: "/path/to/base" }, - { config: { /* dev config */ }, configFile: "/path/to/config.dev.ts", cwd: "/path/" }, -] + { + config: { + /* theme config */ + }, + configFile: "/path/to/theme/config.ts", + cwd: "/path/to/theme ", + }, + { + config: { + /* base config */ + }, + configFile: "/path/to/base/config.ts", + cwd: "/path/to/base", + }, + { + config: { + /* dev config */ + }, + configFile: "/path/to/config.dev.ts", + cwd: "/path/", + }, +]; ``` ## Extending config layer from remote sources @@ -300,16 +324,16 @@ c12 tries to match [`envName`](#envname) and override environment config if spec ```js export default { // Default configuration - logLevel: 'info', + logLevel: "info", // Environment overrides - $test: { logLevel: 'silent' }, - $development: { logLevel: 'warning' }, - $production: { logLevel: 'error' }, + $test: { logLevel: "silent" }, + $development: { logLevel: "warning" }, + $production: { logLevel: "error" }, $env: { - staging: { logLevel: 'debug' } - } -} + staging: { logLevel: "debug" }, + }, +}; ``` ## Watching configuration diff --git a/src/loader.ts b/src/loader.ts index 5b33381..1e172a9 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -67,6 +67,9 @@ export async function loadConfig< }; } + // Custom merger + const _merger = options.merger || defu; + // Create jiti instance options.jiti = options.jiti || @@ -126,7 +129,7 @@ export async function loadConfig< // 3. user home rcSources.push(rc9.readUser({ name: options.rcFile, dir: options.cwd })); } - _configs.rc = defu({} as T, ...rcSources); + _configs.rc = _merger({} as T, ...rcSources); } // Load config from package.json @@ -142,7 +145,7 @@ export async function loadConfig< ).filter((t) => t && typeof t === "string"); const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {}); const values = keys.map((key) => pkgJsonFile?.[key]); - _configs.packageJson = defu({} as T, ...values); + _configs.packageJson = _merger({} as T, ...values); } // Resolve config sources @@ -155,7 +158,7 @@ export async function loadConfig< } // Combine sources - r.config = defu( + r.config = _merger( configs.overrides, configs.main, configs.rc, @@ -168,7 +171,7 @@ export async function loadConfig< await extendConfig(r.config, options); r.layers = r.config._layers; delete r.config._layers; - r.config = defu(r.config, ...r.layers!.map((e) => e.config)) as T; + r.config = _merger(r.config, ...r.layers!.map((e) => e.config)) as T; } // Preserve unmerged sources as layers @@ -190,7 +193,7 @@ export async function loadConfig< // Apply defaults if (options.defaults) { - r.config = defu(r.config, options.defaults) as T; + r.config = _merger(r.config, options.defaults) as T; } // Remove environment-specific and built-in keys start with $ @@ -296,6 +299,9 @@ async function resolveConfig< } } + // Custom merger + const _merger = options.merger || defu; + // Download giget URIs and resolve to local path if (GIGET_PREFIXES.some((prefix) => source.startsWith(prefix))) { const { downloadTemplate } = await import("giget"); @@ -393,7 +399,7 @@ async function resolveConfig< ...res.config!.$env?.[options.envName], }; if (Object.keys(envConfig).length > 0) { - res.config = defu(envConfig, res.config); + res.config = _merger(envConfig, res.config); } } @@ -403,7 +409,7 @@ async function resolveConfig< // Overrides if (res.sourceOptions!.overrides) { - res.config = defu(res.sourceOptions!.overrides, res.config) as T; + res.config = _merger(res.sourceOptions!.overrides, res.config) as T; } // Always windows paths diff --git a/src/types.ts b/src/types.ts index 574a638..59444bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -133,6 +133,8 @@ export interface LoadConfigOptions< giget?: DownloadTemplateOptions; + merger?: (...sources: Array) => T; + extend?: | false | {