From 146e1917c904b5bd1c0b44e6c86e64d88e33de45 Mon Sep 17 00:00:00 2001 From: Lubos Date: Thu, 11 Sep 2025 15:57:39 +0800 Subject: [PATCH] fix: initial release --- src/dotenv.ts | 16 ++---- src/loader.ts | 94 +++++++++--------------------------- src/types.ts | 24 +++------ src/update.ts | 37 +++----------- src/watch.ts | 30 ++---------- test/dotenv.test.ts | 4 +- test/fixture/.config/test.ts | 5 +- test/loader.test.ts | 15 ++---- test/update.test.ts | 8 +-- 9 files changed, 52 insertions(+), 181 deletions(-) diff --git a/src/dotenv.ts b/src/dotenv.ts index a2f4b1f..7755ad3 100644 --- a/src/dotenv.ts +++ b/src/dotenv.ts @@ -105,11 +105,7 @@ export async function loadDotenv(options: DotenvOptions): Promise { } // Based on https://github.com/motdotla/dotenv-expand -function interpolate( - target: Record, - source: Record = {}, - parse = (v: any) => v, -) { +function interpolate(target: Record, source: Record = {}, parse = (v: any) => v) { function getValue(key: string) { // Source value 'wins' over target value return source[key] === undefined ? target[key] : source[key]; @@ -137,11 +133,7 @@ function interpolate( // Avoid recursion if (parents.includes(key)) { - console.warn( - `Please avoid recursive environment variables ( loop: ${parents.join( - " > ", - )} > ${key} )`, - ); + console.warn(`Please avoid recursive environment variables ( loop: ${parents.join(" > ")} > ${key} )`); return ""; } @@ -151,9 +143,7 @@ function interpolate( value = interpolate(value, [...parents, key]); } - return value === undefined - ? newValue - : newValue.replace(replacePart, value); + return value === undefined ? newValue : newValue.replace(replacePart, value); }, value), ); } diff --git a/src/loader.ts b/src/loader.ts index 92ee3d3..7f8e5f8 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -58,9 +58,7 @@ export async function loadConfig< options.cwd = resolve(process.cwd(), options.cwd || "."); options.name = options.name || "config"; options.envName = options.envName ?? process.env.NODE_ENV; - options.configFile = - options.configFile ?? - (options.name === "config" ? "config" : `${options.name}.config`); + options.configFile = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); options.rcFile = options.rcFile ?? `.${options.name}rc`; if (options.extend !== false) { options.extend = { @@ -173,11 +171,7 @@ export async function loadConfig< const keys = ( Array.isArray(options.packageJson) ? options.packageJson - : [ - typeof options.packageJson === "string" - ? options.packageJson - : options.name, - ] + : [typeof options.packageJson === "string" ? options.packageJson : options.name] ).filter((t) => t && typeof t === "string"); const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {}); const values = keys.map((key) => pkgJsonFile?.[key]); @@ -189,19 +183,11 @@ export async function loadConfig< // TODO: #253 change order from defaults to overrides in next major version for (const key in rawConfigs) { const value = rawConfigs[key as ConfigSource]; - configs[key as ConfigSource] = await (typeof value === "function" - ? value({ configs, rawConfigs }) - : value); + configs[key as ConfigSource] = await (typeof value === "function" ? value({ configs, rawConfigs }) : value); } // Combine sources - r.config = _merger( - configs.overrides, - configs.main, - configs.rc, - configs.packageJson, - configs.defaultConfig, - ) as T; + r.config = _merger(configs.overrides, configs.main, configs.rc, configs.packageJson, configs.defaultConfig) as T; // Allow extending if (options.extend) { @@ -252,10 +238,10 @@ export async function loadConfig< return r; } -async function extendConfig< - T extends UserInputConfig = UserInputConfig, - MT extends ConfigLayerMeta = ConfigLayerMeta, ->(config: InputConfig, options: LoadConfigOptions) { +async function extendConfig( + config: InputConfig, + options: LoadConfigOptions, +) { (config as any)._layers = config._layers || []; if (!options.extend) { return; @@ -265,9 +251,7 @@ async function extendConfig< keys = [keys]; } const extendSources: Array< - | string - | [string, SourceOptions?] - | { source: string; options?: SourceOptions } + string | [string, SourceOptions?] | { source: string; options?: SourceOptions } > = []; for (const key of keys) { const value = config[key]; @@ -278,11 +262,7 @@ async function extendConfig< for (let extendSource of extendSources) { const originalExtendSource = extendSource; let sourceOptions: SourceOptions = {}; - if ( - typeof extendSource === "object" && - extendSource !== null && - "source" in extendSource - ) { + if (typeof extendSource === "object" && extendSource !== null && "source" in extendSource) { sourceOptions = extendSource.options || {}; extendSource = extendSource.source; } @@ -293,20 +273,14 @@ async function extendConfig< if (typeof extendSource !== "string") { // TODO: Use error in next major versions - console.warn( - `Cannot extend config from \`${JSON.stringify( - originalExtendSource, - )}\` in ${options.cwd}`, - ); + console.warn(`Cannot extend config from \`${JSON.stringify(originalExtendSource)}\` in ${options.cwd}`); continue; } const _config = await resolveConfig(extendSource, options, sourceOptions); if (!_config.config) { // TODO: Use error in next major versions - console.warn( - `Cannot extend config from \`${extendSource}\` in ${options.cwd}`, - ); + console.warn(`Cannot extend config from \`${extendSource}\` in ${options.cwd}`); continue; } await extendConfig(_config.config, { @@ -323,23 +297,12 @@ async function extendConfig< } // TODO: Either expose from giget directly or redirect all non file:// protocols to giget -const GIGET_PREFIXES = [ - "gh:", - "github:", - "gitlab:", - "bitbucket:", - "https://", - "http://", -]; +const GIGET_PREFIXES = ["gh:", "github:", "gitlab:", "bitbucket:", "https://", "http://"]; // https://github.com/dword-design/package-name-regex -const NPM_PACKAGE_RE = - /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/; +const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/; -async function resolveConfig< - T extends UserInputConfig = UserInputConfig, - MT extends ConfigLayerMeta = ConfigLayerMeta, ->( +async function resolveConfig( source: string, options: LoadConfigOptions, sourceOptions: SourceOptions = {}, @@ -356,18 +319,11 @@ async function resolveConfig< const _merger = options.merger || defu; // Download giget URIs and resolve to local path - const customProviderKeys = Object.keys( - sourceOptions.giget?.providers || {}, - ).map((key) => `${key}:`); + const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`); const gigetPrefixes = - customProviderKeys.length > 0 - ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] - : GIGET_PREFIXES; - - if ( - options.giget !== false && - gigetPrefixes.some((prefix) => source.startsWith(prefix)) - ) { + customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES; + + if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) { const { downloadTemplate } = await import("giget"); const { digest } = await import("ohash"); @@ -427,10 +383,7 @@ async function resolveConfig< res.configFile = tryResolve(resolve(cwd, source), options) || - tryResolve( - resolve(cwd, ".config", source.replace(/\.config$/, "")), - options, - ) || + tryResolve(resolve(cwd, ".config", source.replace(/\.config$/, "")), options) || tryResolve(resolve(cwd, ".config", source), options) || source; @@ -442,8 +395,7 @@ async function resolveConfig< const configFileExt = extname(res.configFile!) || ""; if (configFileExt in ASYNC_LOADERS) { - const asyncLoader = - await ASYNC_LOADERS[configFileExt as keyof typeof ASYNC_LOADERS](); + const asyncLoader = await ASYNC_LOADERS[configFileExt as keyof typeof ASYNC_LOADERS](); const contents = await readFile(res.configFile!, "utf8"); res.config = asyncLoader(contents); } else { @@ -452,9 +404,7 @@ async function resolveConfig< })) as T; } if (typeof res.config === "function") { - res.config = await ( - res.config as (ctx?: ConfigFunctionContext) => Promise - )(options.context); + res.config = await (res.config as (ctx?: ConfigFunctionContext) => Promise)(options.context); } // Extend env specific config diff --git a/src/types.ts b/src/types.ts index 8213c14..922487a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,20 +81,13 @@ export interface ResolvedConfig< _configFile?: string; } -export type ConfigSource = - | "overrides" - | "main" - | "rc" - | "packageJson" - | "defaultConfig"; +export type ConfigSource = "overrides" | "main" | "rc" | "packageJson" | "defaultConfig"; export interface ConfigFunctionContext { [key: string]: any; } -export interface ResolvableConfigContext< - T extends UserInputConfig = UserInputConfig, -> { +export interface ResolvableConfigContext { configs: Record; rawConfigs: Record | null | undefined>; } @@ -135,11 +128,7 @@ export interface LoadConfigOptions< resolve?: ( id: string, options: LoadConfigOptions, - ) => - | null - | undefined - | ResolvedConfig - | Promise | undefined | null>; + ) => null | undefined | ResolvedConfig | Promise | undefined | null>; jiti?: Jiti; jitiOptions?: JitiOptions; @@ -157,10 +146,9 @@ export interface LoadConfigOptions< configFileRequired?: boolean; } -export type DefineConfig< - T extends UserInputConfig = UserInputConfig, - MT extends ConfigLayerMeta = ConfigLayerMeta, -> = (input: InputConfig) => InputConfig; +export type DefineConfig = ( + input: InputConfig, +) => InputConfig; export function createDefineConfig< T extends UserInputConfig = UserInputConfig, diff --git a/src/update.ts b/src/update.ts index 7d67555..d6afe73 100644 --- a/src/update.ts +++ b/src/update.ts @@ -9,39 +9,24 @@ const UPDATABLE_EXTS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"] as const; /** * @experimental Update a config file or create a new one. */ -export async function updateConfig( - opts: UpdateConfigOptions, -): Promise { +export async function updateConfig(opts: UpdateConfigOptions): Promise { const { parseModule } = await import("magicast"); // Try to find an existing config file let configFile = tryResolve(`./${opts.configFile}`, opts.cwd, SUPPORTED_EXTENSIONS) || - tryResolve( - `./.config/${opts.configFile}`, - opts.cwd, - SUPPORTED_EXTENSIONS, - ) || - tryResolve( - `./.config/${opts.configFile.split(".")[0]}`, - opts.cwd, - SUPPORTED_EXTENSIONS, - ); + tryResolve(`./.config/${opts.configFile}`, opts.cwd, SUPPORTED_EXTENSIONS) || + tryResolve(`./.config/${opts.configFile.split(".")[0]}`, opts.cwd, SUPPORTED_EXTENSIONS); // If not found let created = false; if (!configFile) { - configFile = join( - opts.cwd, - opts.configFile + (opts.createExtension || ".ts"), - ); - const createResult = - (await opts.onCreate?.({ configFile: configFile })) ?? true; + configFile = join(opts.cwd, opts.configFile + (opts.createExtension || ".ts")); + const createResult = (await opts.onCreate?.({ configFile: configFile })) ?? true; if (!createResult) { throw new Error("Config file creation aborted."); } - const content = - typeof createResult === "string" ? createResult : `export default {}\n`; + const content = typeof createResult === "string" ? createResult : `export default {}\n`; await mkdir(dirname(configFile), { recursive: true }); await writeFile(configFile, content, "utf8"); created = true; @@ -62,10 +47,7 @@ export async function updateConfig( if (!defaultExport) { throw new Error("Default export is missing in the config file!"); } - const configObj = - defaultExport.$type === "function-call" - ? defaultExport.$args[0] - : defaultExport; + const configObj = defaultExport.$type === "function-call" ? defaultExport.$args[0] : defaultExport; await opts.onUpdate?.(configObj); @@ -99,10 +81,7 @@ export interface UpdateConfigResult { type MaybePromise = T | Promise; -type MagicAstOptions = Exclude< - Parameters<(typeof import("magicast"))["parseModule"]>[1], - undefined ->; +type MagicAstOptions = Exclude[1], undefined>; export interface UpdateConfigOptions { /** diff --git a/src/watch.ts b/src/watch.ts index 8843137..783b016 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -2,12 +2,7 @@ import type { ChokidarOptions } from "chokidar"; import { debounce } from "perfect-debounce"; import { resolve } from "pathe"; import type { diff } from "ohash/utils"; -import type { - UserInputConfig, - ConfigLayerMeta, - ResolvedConfig, - LoadConfigOptions, -} from "./types"; +import type { UserInputConfig, ConfigLayerMeta, ResolvedConfig, LoadConfigOptions } from "./types"; import { SUPPORTED_EXTENSIONS, loadConfig } from "./loader"; type DiffEntries = ReturnType; @@ -27,10 +22,7 @@ export interface WatchConfigOptions< chokidarOptions?: ChokidarOptions; debounce?: false | number; - onWatch?: (event: { - type: "created" | "updated" | "removed"; - path: string; - }) => void | Promise; + onWatch?: (event: { type: "created" | "updated" | "removed"; path: string }) => void | Promise; acceptHMR?: (context: { getDiff: () => DiffEntries; @@ -58,9 +50,7 @@ export async function watchConfig< let config = await loadConfig(options); const configName = options.name || "config"; - const configFileName = - options.configFile ?? - (options.name === "config" ? "config" : `${options.name}.config`); + const configFileName = options.configFile ?? (options.name === "config" ? "config" : `${options.name}.config`); const watchingFiles = [ ...new Set( (config.layers || []) @@ -69,21 +59,11 @@ export async function watchConfig< ...SUPPORTED_EXTENSIONS.flatMap((ext) => [ resolve(l.cwd!, configFileName + ext), resolve(l.cwd!, ".config", configFileName + ext), - resolve( - l.cwd!, - ".config", - configFileName.replace(/\.config$/, "") + ext, - ), + resolve(l.cwd!, ".config", configFileName.replace(/\.config$/, "") + ext), ]), l.source && resolve(l.cwd!, l.source), // TODO: Support watching rc from home and workspace - options.rcFile && - resolve( - l.cwd!, - typeof options.rcFile === "string" - ? options.rcFile - : `.${configName}rc`, - ), + options.rcFile && resolve(l.cwd!, typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`), options.packageJson && resolve(l.cwd!, "package.json"), ]) .filter(Boolean), diff --git a/test/dotenv.test.ts b/test/dotenv.test.ts index c4d6742..767d956 100644 --- a/test/dotenv.test.ts +++ b/test/dotenv.test.ts @@ -4,9 +4,7 @@ import { join, normalize } from "pathe"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { setupDotenv } from "../src"; -const tmpDir = normalize( - fileURLToPath(new URL(".tmp-dotenv", import.meta.url)), -); +const tmpDir = normalize(fileURLToPath(new URL(".tmp-dotenv", import.meta.url))); const r = (path: string) => join(tmpDir, path); describe("update config file", () => { diff --git a/test/fixture/.config/test.ts b/test/fixture/.config/test.ts index f4b5d51..42f2334 100644 --- a/test/fixture/.config/test.ts +++ b/test/fixture/.config/test.ts @@ -1,9 +1,6 @@ export default { theme: "./theme", - extends: [ - ["c12-npm-test"], - ["gh:unjs/c12/test/fixture/_github#main", { giget: {} }], - ], + extends: [["c12-npm-test"], ["gh:unjs/c12/test/fixture/_github#main", { giget: {} }]], $test: { extends: ["./test.config.dev"], envConfig: true, diff --git a/test/loader.test.ts b/test/loader.test.ts index 752474c..a1bdb7d 100644 --- a/test/loader.test.ts +++ b/test/loader.test.ts @@ -4,10 +4,8 @@ import { normalize } from "pathe"; import type { ConfigLayer, ConfigLayerMeta, UserInputConfig } from "../src"; import { loadConfig } from "../src"; -const r = (path: string) => - normalize(fileURLToPath(new URL(path, import.meta.url))); -const transformPaths = (object: object) => - JSON.parse(JSON.stringify(object).replaceAll(r("."), "/")); +const r = (path: string) => normalize(fileURLToPath(new URL(path, import.meta.url))); +const transformPaths = (object: object) => JSON.parse(JSON.stringify(object).replaceAll(r("."), "/")); describe("loader", () => { it("load fixture config", async () => { @@ -306,14 +304,9 @@ describe("loader", () => { expect(resolvedConfigKeys).not.toContain("$meta"); expect(resolvedConfigKeys).not.toContain("$test"); - const transformdLayers = transformPaths(layers!) as ConfigLayer< - UserInputConfig, - ConfigLayerMeta - >[]; + const transformdLayers = transformPaths(layers!) as ConfigLayer[]; - const configLayer = transformdLayers.find( - (layer) => layer.configFile === "test.config", - )!; + const configLayer = transformdLayers.find((layer) => layer.configFile === "test.config")!; expect(Object.keys(configLayer.config!)).toContain("$test"); const baseLayerConfig = transformdLayers.find( diff --git a/test/update.test.ts b/test/update.test.ts index 4cb9608..11ac891 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -5,8 +5,7 @@ import { updateConfig } from "../src/update"; import { readFile, rm, mkdir, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; -const r = (path: string) => - normalize(fileURLToPath(new URL(path, import.meta.url))); +const r = (path: string) => normalize(fileURLToPath(new URL(path, import.meta.url))); describe("update config file", () => { const tmpDir = r("./.tmp"); @@ -42,10 +41,7 @@ describe("update config file", () => { it("update existing in .config folder", async () => { const tmpDotConfig = r("./.tmp/.config"); await mkdir(tmpDotConfig, { recursive: true }); - await writeFile( - r("./.tmp/.config/foobar.ts"), - "export default { test: true }", - ); + await writeFile(r("./.tmp/.config/foobar.ts"), "export default { test: true }"); const res = await updateConfig({ cwd: tmpDir, configFile: "foobar.config",