├── .prettierrc ├── .testrc ├── test ├── fixture │ ├── .testrc │ ├── array │ │ ├── .testrc │ │ └── test.config.ts │ ├── .env │ ├── .env.local │ ├── jsx │ │ ├── index.js │ │ └── src │ │ │ └── index.jsx │ ├── not-a-folder.ts │ ├── .gitignore │ ├── _github │ │ ├── test.config.ts │ │ └── package.json │ ├── node_modules │ │ └── c12-npm-test │ │ │ ├── test.config.ts │ │ │ └── package.json │ ├── package.json │ ├── theme │ │ └── .config │ │ │ └── test.config.json5 │ ├── test.config.dev.ts │ ├── .base │ │ └── test.config.jsonc │ └── .config │ │ └── test.ts ├── types.ts ├── update.test.ts ├── dotenv.test.ts └── loader.test.ts ├── renovate.json ├── .gitignore ├── src ├── index.ts ├── update.ts ├── types.ts ├── watch.ts ├── dotenv.ts └── loader.ts ├── vitest.config.ts ├── .editorconfig ├── eslint.config.mjs ├── playground ├── load.ts └── watch.ts ├── tsconfig.json ├── .github └── workflows │ ├── autofix.yml │ └── ci.yml ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.testrc: -------------------------------------------------------------------------------- 1 | testConfig=true 2 | -------------------------------------------------------------------------------- /test/fixture/.testrc: -------------------------------------------------------------------------------- 1 | rcFile=true 2 | -------------------------------------------------------------------------------- /test/fixture/array/.testrc: -------------------------------------------------------------------------------- 1 | rcFile=true 2 | -------------------------------------------------------------------------------- /test/fixture/.env: -------------------------------------------------------------------------------- 1 | dotenv=true 2 | dotenvOverride=.env 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>unjs/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/.env.local: -------------------------------------------------------------------------------- 1 | dotenvLocal=true 2 | dotenvOverride=.env.local 3 | -------------------------------------------------------------------------------- /test/fixture/jsx/index.js: -------------------------------------------------------------------------------- 1 | import { hello } from "./src"; 2 | 3 | hello(); 4 | -------------------------------------------------------------------------------- /test/fixture/not-a-folder.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | not_a_folder: true, 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixture/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | node_modules/.c12 3 | node_modules/.cache 4 | -------------------------------------------------------------------------------- /test/fixture/_github/test.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | githubLayer: true, 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixture/jsx/src/index.jsx: -------------------------------------------------------------------------------- 1 | export function hello() { 2 | console.log("hello world"); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/node_modules/c12-npm-test/test.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | npmConfig: true 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | *.log* 4 | .DS_Store 5 | coverage 6 | dist 7 | types 8 | .tmp* 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dotenv"; 2 | export * from "./loader"; 3 | export * from "./types"; 4 | export * from "./watch"; 5 | -------------------------------------------------------------------------------- /test/fixture/_github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c12-github-layer", 3 | "version": "0.0.0", 4 | "exports": { 5 | ".": "./test.config.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/node_modules/c12-npm-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c12-npm-test", 3 | "version": "0.0.0", 4 | "exports": { 5 | ".": "./test.config.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixture", 3 | "c12": { 4 | "packageJSON": true 5 | }, 6 | "c12-alt": { 7 | "packageJSON2": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/theme/.config/test.config.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: "../.base", 3 | colors: { 4 | primary: "theme_primary", 5 | secondary: "theme_secondary", 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/test.config.dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | devConfig: true, 3 | dotenv: process.env.dotenv, 4 | dotenvLocal: process.env.dotenvLocal, 5 | dotenvOverride: process.env.dotenvOverride, 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixture/array/test.config.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | a: "boo", 4 | b: "foo", 5 | }, 6 | { 7 | a: "boo", 8 | b: "foo", 9 | }, 10 | { 11 | a: "boo", 12 | b: "foo", 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ["text", "clover", "json"], 7 | include: ["src/**/*.ts"], 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,*.yml,*.cjson}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /test/fixture/.base/test.config.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // jsonc can support comments! 3 | "$meta": { 4 | "name": "base", 5 | "version": "1.0.0", 6 | }, 7 | "baseConfig": true, 8 | "colors": { 9 | "primary": "base_primary", 10 | "text": "base_text", 11 | }, 12 | "array": ["b"], 13 | "$env": { 14 | "test": { "baseEnvConfig": true }, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import unjs from "eslint-config-unjs"; 2 | 3 | // https://github.com/unjs/eslint-config 4 | export default unjs({ 5 | ignores: [], 6 | rules: { 7 | "unicorn/prevent-abbreviations": 0, 8 | "@typescript-eslint/no-non-null-assertion": 0, 9 | }, 10 | markdown: { 11 | rules: { 12 | "unicorn/no-anonymous-default-export": 0, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /playground/load.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { loadConfig } from "../src"; 3 | 4 | const r = (path: string) => fileURLToPath(new URL(path, import.meta.url)); 5 | 6 | async function main() { 7 | const fixtureDir = r("../test/fixture"); 8 | const config = await loadConfig({ cwd: fixtureDir, dotenv: true }); 9 | console.log(config); 10 | } 11 | 12 | // eslint-disable-next-line unicorn/prefer-top-level-await 13 | main().catch(console.error); 14 | -------------------------------------------------------------------------------- /test/fixture/.config/test.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | theme: "./theme", 3 | extends: [ 4 | ["c12-npm-test"], 5 | ["gh:unjs/c12/test/fixture/_github#main", { giget: {} }], 6 | "./not-a-folder.ts", 7 | ], 8 | $test: { 9 | extends: ["./test.config.dev"], 10 | envConfig: true, 11 | }, 12 | colors: { 13 | primary: "user_primary", 14 | }, 15 | configFile: true, 16 | overridden: false, 17 | enableDefault: true, 18 | // foo: "bar", 19 | // x: "123", 20 | array: ["a"], 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "Preserve", 5 | "resolveJsonModule": true, 6 | "esModuleInterop": false, 7 | "allowSyntheticDefaultImports": true, 8 | "skipLibCheck": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "strict": true, 12 | "verbatimModuleSyntax": true, 13 | "isolatedModules": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitOverride": true, 16 | "noEmit": true, 17 | "jsx": "preserve", 18 | "jsxImportSource": "react" 19 | }, 20 | "include": ["src", "test"] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # needed to securely identify the workflow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["main"] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | autofix: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - run: npm i -fg corepack && corepack enable 17 | - uses: actions/setup-node@v6 18 | with: 19 | node-version: 22 20 | cache: "pnpm" 21 | - run: pnpm install 22 | - run: pnpm lint:fix 23 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 24 | with: 25 | commit-message: "chore: apply automated updates" 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v6 19 | - run: npm i -fg corepack && corepack enable 20 | - uses: actions/setup-node@v6 21 | with: 22 | node-version: 22 23 | cache: "pnpm" 24 | - run: pnpm install 25 | - run: pnpm lint 26 | if: ${{ matrix.os != 'windows-latest' }} 27 | - run: pnpm build 28 | if: ${{ matrix.os != 'windows-latest' }} 29 | - run: pnpm test:types 30 | if: ${{ matrix.os != 'windows-latest' }} 31 | - run: pnpm vitest --coverage 32 | - uses: codecov/codecov-action@v5 33 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { expectTypeOf } from "expect-type"; 2 | import { loadConfig, createDefineConfig } from "../src"; 3 | 4 | interface MyConfig { 5 | foo: string; 6 | } 7 | 8 | interface MyMeta { 9 | metaFoo: string; 10 | } 11 | 12 | const defineMyConfig = createDefineConfig(); 13 | 14 | const userConfig = defineMyConfig({ 15 | foo: "bar", 16 | $meta: { 17 | metaFoo: "bar", 18 | }, 19 | $development: { 20 | foo: "bar", 21 | }, 22 | }); 23 | 24 | expectTypeOf(userConfig.$production!.foo).toEqualTypeOf(); 25 | expectTypeOf(userConfig.$meta!.metaFoo).toEqualTypeOf(); 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | async function main() { 29 | const config = await loadConfig({}); 30 | expectTypeOf(config.config!.foo).toEqualTypeOf(); 31 | expectTypeOf(config.meta!.metaFoo).toEqualTypeOf(); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Pooya Parsa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /playground/watch.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { watchConfig } from "../src"; 3 | 4 | const r = (path: string) => fileURLToPath(new URL(path, import.meta.url)); 5 | 6 | async function main() { 7 | const fixtureDir = r("../test/fixture"); 8 | const config = await watchConfig({ 9 | cwd: fixtureDir, 10 | dotenv: true, 11 | packageJson: ["c12", "c12-alt"], 12 | globalRc: true, 13 | envName: "test", 14 | extend: { 15 | extendKey: ["theme", "extends"], 16 | }, 17 | onWatch: (event) => { 18 | console.log("[watcher]", event.type, event.path); 19 | }, 20 | acceptHMR({ getDiff }) { 21 | const diff = getDiff(); 22 | if (diff.length === 0) { 23 | console.log("No config changed detected!"); 24 | return true; // No changes! 25 | } 26 | }, 27 | onUpdate({ getDiff }) { 28 | const diff = getDiff(); 29 | console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n")); 30 | }, 31 | }); 32 | console.log("watching config files:", config.watchingFiles); 33 | console.log("initial config", config.config); 34 | } 35 | 36 | // eslint-disable-next-line unicorn/prefer-top-level-await 37 | main().catch(console.error); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c12", 3 | "version": "3.3.3", 4 | "description": "Smart Config Loader", 5 | "repository": "unjs/c12", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.mts", 12 | "default": "./dist/index.mjs" 13 | }, 14 | "./update": { 15 | "types": "./dist/update.d.mts", 16 | "default": "./dist/update.mjs" 17 | } 18 | }, 19 | "types": "./dist/index.d.mts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "obuild", 25 | "dev": "vitest dev", 26 | "lint": "eslint . && prettier -c src test", 27 | "lint:fix": "automd && eslint . --fix && prettier -w src test", 28 | "prepack": "unbuild", 29 | "release": "pnpm build && pnpm test && changelogen --release --push --publish", 30 | "test": "pnpm lint && vitest run --coverage && pnpm test:types", 31 | "test:types": "tsc --noEmit" 32 | }, 33 | "dependencies": { 34 | "chokidar": "^5.0.0", 35 | "confbox": "^0.2.2", 36 | "defu": "^6.1.4", 37 | "dotenv": "^17.2.3", 38 | "exsolve": "^1.0.8", 39 | "giget": "^2.0.0", 40 | "jiti": "^2.6.1", 41 | "ohash": "^2.0.11", 42 | "pathe": "^2.0.3", 43 | "perfect-debounce": "^2.0.0", 44 | "pkg-types": "^2.3.0", 45 | "rc9": "^2.1.2" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^25.0.2", 49 | "@vitest/coverage-v8": "^4.0.15", 50 | "automd": "^0.4.2", 51 | "changelogen": "^0.6.2", 52 | "eslint": "^9.39.2", 53 | "eslint-config-unjs": "^0.5.0", 54 | "expect-type": "^1.3.0", 55 | "magicast": "^0.5.1", 56 | "obuild": "^0.4.8", 57 | "prettier": "^3.7.4", 58 | "typescript": "^5.9.3", 59 | "vitest": "^4.0.15" 60 | }, 61 | "peerDependencies": { 62 | "magicast": "*" 63 | }, 64 | "peerDependenciesMeta": { 65 | "magicast": { 66 | "optional": true 67 | } 68 | }, 69 | "packageManager": "pnpm@10.26.0" 70 | } 71 | -------------------------------------------------------------------------------- /test/update.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { expect, it, describe, beforeAll } from "vitest"; 3 | import { normalize } from "pathe"; 4 | import { updateConfig } from "../src/update"; 5 | import { readFile, rm, mkdir, writeFile } from "node:fs/promises"; 6 | import { existsSync } from "node:fs"; 7 | 8 | const r = (path: string) => 9 | normalize(fileURLToPath(new URL(path, import.meta.url))); 10 | 11 | describe("update config file", () => { 12 | const tmpDir = r("./.tmp"); 13 | beforeAll(async () => { 14 | await rm(tmpDir, { recursive: true }).catch(() => {}); 15 | }); 16 | it("create new config", async () => { 17 | let onCreateFile; 18 | const res = await updateConfig({ 19 | cwd: tmpDir, 20 | configFile: "foo.config", 21 | onCreate: ({ configFile }) => { 22 | onCreateFile = configFile; 23 | return "export default { test: true }"; 24 | }, 25 | onUpdate: (config) => { 26 | config.test2 = false; 27 | }, 28 | }); 29 | expect(res.created).toBe(true); 30 | expect(res.configFile).toBe(r("./.tmp/foo.config.ts")); 31 | expect(onCreateFile).toBe(r("./.tmp/foo.config.ts")); 32 | 33 | expect(existsSync(r("./.tmp/foo.config.ts"))).toBe(true); 34 | const contents = await readFile(r("./.tmp/foo.config.ts"), "utf8"); 35 | expect(contents).toMatchInlineSnapshot(` 36 | "export default { 37 | test: true, 38 | test2: false 39 | };" 40 | `); 41 | }); 42 | it("update existing in .config folder", async () => { 43 | const tmpDotConfig = r("./.tmp/.config"); 44 | await mkdir(tmpDotConfig, { recursive: true }); 45 | await writeFile( 46 | r("./.tmp/.config/foobar.ts"), 47 | "export default { test: true }", 48 | ); 49 | const res = await updateConfig({ 50 | cwd: tmpDir, 51 | configFile: "foobar.config", 52 | onCreate: () => { 53 | return "export default { test: true }"; 54 | }, 55 | onUpdate: (config) => { 56 | config.test2 = false; 57 | }, 58 | }); 59 | expect(res.created).toBe(false); 60 | expect(res.configFile).toBe(r("./.tmp/.config/foobar.ts")); 61 | 62 | expect(existsSync(r("./.tmp/.config/foobar.ts"))).toBe(true); 63 | const contents = await readFile(r("./.tmp/.config/foobar.ts"), "utf8"); 64 | expect(contents).toMatchInlineSnapshot(` 65 | "export default { 66 | test: true, 67 | test2: false 68 | };" 69 | `); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/dotenv.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { beforeEach, expect, it, describe, afterAll } from "vitest"; 3 | import { join, normalize } from "pathe"; 4 | import { mkdir, rm, unlink, writeFile } from "node:fs/promises"; 5 | import { setupDotenv } from "../src"; 6 | 7 | const tmpDir = normalize( 8 | fileURLToPath(new URL(".tmp-dotenv", import.meta.url)), 9 | ); 10 | const r = (path: string) => join(tmpDir, path); 11 | 12 | const cwdEnvFileName = ".env.12345"; 13 | const cwdEnvPath = join(process.cwd(), cwdEnvFileName); 14 | 15 | describe("update config file", () => { 16 | beforeEach(async () => { 17 | await rm(tmpDir, { recursive: true, force: true }); 18 | await mkdir(tmpDir, { recursive: true }); 19 | }); 20 | afterAll(async () => { 21 | await rm(tmpDir, { recursive: true, force: true }); 22 | await unlink(cwdEnvPath).catch(console.error); 23 | }); 24 | it("should read .env file into process.env", async () => { 25 | await setupDotenv({ cwd: tmpDir }); 26 | expect(process.env.dotenv).toBeUndefined(); 27 | 28 | await writeFile(r(".env"), "dotenv=123"); 29 | await setupDotenv({ cwd: tmpDir }); 30 | expect(process.env.dotenv).toBe("123"); 31 | 32 | await writeFile(r(".env"), "dotenv=456"); 33 | await setupDotenv({ cwd: tmpDir }); 34 | expect(process.env.dotenv).toBe("456"); 35 | }); 36 | it("should not override OS environment values", async () => { 37 | process.env.override = "os"; 38 | 39 | await writeFile(r(".env"), "override=123"); 40 | await setupDotenv({ cwd: tmpDir }); 41 | expect(process.env.override).toBe("os"); 42 | 43 | await writeFile(r(".env"), "override=456"); 44 | await setupDotenv({ cwd: tmpDir }); 45 | expect(process.env.override).toBe("os"); 46 | }); 47 | 48 | it("should load envs files with the correct priorities", async () => { 49 | await writeFile(r(".my-env"), "foo=bar"); 50 | await setupDotenv({ cwd: tmpDir, fileName: ".my-env" }); 51 | expect(process.env.foo).toBe("bar"); 52 | 53 | await writeFile(r(".my-env"), "fizz=buzz"); 54 | await writeFile(r(".my-env"), "api_key=12345678"); 55 | await writeFile(r(".my-env.local"), "fizz=buzz_local"); 56 | await setupDotenv({ cwd: tmpDir, fileName: [".my-env", ".my-env.local"] }); 57 | expect(process.env.api_key).toBe("12345678"); 58 | expect(process.env.fizz).toBe("buzz_local"); 59 | }); 60 | 61 | it("should default to `process.cwd()` when `options.cwd` is not provided", async () => { 62 | await writeFile(cwdEnvPath, "humpty=dumpty"); 63 | 64 | await setupDotenv({ fileName: [cwdEnvFileName] }); 65 | 66 | expect(process.env.humpty).toBe("dumpty"); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/update.ts: -------------------------------------------------------------------------------- 1 | import { resolveModulePath } from "exsolve"; 2 | import { SUPPORTED_EXTENSIONS } from "./loader"; 3 | import { join, normalize } from "pathe"; 4 | import { readFile, writeFile, mkdir } from "node:fs/promises"; 5 | import { dirname, extname } from "node:path"; 6 | 7 | const UPDATABLE_EXTS = [".js", ".ts", ".mjs", ".cjs", ".mts", ".cts"] as const; 8 | 9 | /** 10 | * @experimental Update a config file or create a new one. 11 | */ 12 | export async function updateConfig( 13 | opts: UpdateConfigOptions, 14 | ): Promise { 15 | const { parseModule } = await import("magicast"); 16 | 17 | // Try to find an existing config file 18 | let configFile = 19 | tryResolve(`./${opts.configFile}`, opts.cwd, SUPPORTED_EXTENSIONS) || 20 | tryResolve( 21 | `./.config/${opts.configFile}`, 22 | opts.cwd, 23 | SUPPORTED_EXTENSIONS, 24 | ) || 25 | tryResolve( 26 | `./.config/${opts.configFile.split(".")[0]}`, 27 | opts.cwd, 28 | SUPPORTED_EXTENSIONS, 29 | ); 30 | 31 | // If not found 32 | let created = false; 33 | if (!configFile) { 34 | configFile = join( 35 | opts.cwd, 36 | opts.configFile + (opts.createExtension || ".ts"), 37 | ); 38 | const createResult = 39 | (await opts.onCreate?.({ configFile: configFile })) ?? true; 40 | if (!createResult) { 41 | throw new Error("Config file creation aborted."); 42 | } 43 | const content = 44 | typeof createResult === "string" ? createResult : `export default {}\n`; 45 | await mkdir(dirname(configFile), { recursive: true }); 46 | await writeFile(configFile, content, "utf8"); 47 | created = true; 48 | } 49 | 50 | // Make sure extension is editable 51 | const ext = extname(configFile); 52 | if (!UPDATABLE_EXTS.includes(ext as any)) { 53 | throw new Error( 54 | `Unsupported config file extension: ${ext} (${configFile}) (supported: ${UPDATABLE_EXTS.join(", ")})`, 55 | ); 56 | } 57 | 58 | const contents = await readFile(configFile, "utf8"); 59 | const _module = parseModule(contents, opts.magicast); 60 | 61 | const defaultExport = _module.exports.default; 62 | if (!defaultExport) { 63 | throw new Error("Default export is missing in the config file!"); 64 | } 65 | const configObj = 66 | defaultExport.$type === "function-call" 67 | ? defaultExport.$args[0] 68 | : defaultExport; 69 | 70 | await opts.onUpdate?.(configObj); 71 | 72 | await writeFile(configFile, _module.generate().code); 73 | 74 | return { 75 | configFile, 76 | created, 77 | }; 78 | } 79 | 80 | // --- Internal --- 81 | 82 | function tryResolve(path: string, cwd: string, extensions: string[]) { 83 | const res = resolveModulePath(path, { 84 | try: true, 85 | from: join(cwd, "/"), 86 | extensions, 87 | suffixes: ["", "/index"], 88 | cache: false, 89 | }); 90 | return res ? normalize(res) : undefined; 91 | } 92 | 93 | // --- Types --- 94 | 95 | export interface UpdateConfigResult { 96 | configFile?: string; 97 | created?: boolean; 98 | } 99 | 100 | type MaybePromise = T | Promise; 101 | 102 | type MagicAstOptions = Exclude< 103 | Parameters<(typeof import("magicast"))["parseModule"]>[1], 104 | undefined 105 | >; 106 | 107 | export interface UpdateConfigOptions { 108 | /** 109 | * Current working directory 110 | */ 111 | cwd: string; 112 | 113 | /** 114 | * Config file name 115 | */ 116 | configFile: string; 117 | 118 | /** 119 | * Extension used for new config file. 120 | */ 121 | createExtension?: string; 122 | 123 | /** 124 | * Magicast options 125 | */ 126 | magicast?: MagicAstOptions; 127 | 128 | /** 129 | * Update function. 130 | */ 131 | onUpdate?: (config: any) => MaybePromise; 132 | 133 | /** 134 | * Handle default config creation. 135 | * 136 | * Tip: you can use this option as a hook to prompt users about config creation. 137 | * 138 | * Context object: 139 | * - path: determined full path to the config file 140 | * 141 | * Returns types: 142 | * - string: custom config template 143 | * - true: write the template 144 | * - false: abort the operation 145 | */ 146 | onCreate?: (ctx: { configFile: string }) => MaybePromise; 147 | } 148 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Jiti, JitiOptions } from "jiti"; 2 | import type { DownloadTemplateOptions } from "giget"; 3 | import type { DotenvOptions } from "./dotenv"; 4 | 5 | export interface ConfigLayerMeta { 6 | name?: string; 7 | [key: string]: any; 8 | } 9 | 10 | export type UserInputConfig = Record; 11 | 12 | export interface C12InputConfig< 13 | T extends UserInputConfig = UserInputConfig, 14 | MT extends ConfigLayerMeta = ConfigLayerMeta, 15 | > { 16 | $test?: T; 17 | $development?: T; 18 | $production?: T; 19 | $env?: Record; 20 | $meta?: MT; 21 | } 22 | 23 | export type InputConfig< 24 | T extends UserInputConfig = UserInputConfig, 25 | MT extends ConfigLayerMeta = ConfigLayerMeta, 26 | > = C12InputConfig & T; 27 | 28 | export interface SourceOptions< 29 | T extends UserInputConfig = UserInputConfig, 30 | MT extends ConfigLayerMeta = ConfigLayerMeta, 31 | > { 32 | /** Custom meta for layer */ 33 | meta?: MT; 34 | 35 | /** Layer config overrides */ 36 | overrides?: T; 37 | 38 | [key: string]: any; 39 | 40 | /** 41 | * Options for cloning remote sources 42 | * 43 | * @see https://giget.unjs.io 44 | */ 45 | giget?: DownloadTemplateOptions; 46 | 47 | /** 48 | * Install dependencies after cloning 49 | * 50 | * @see https://nypm.unjs.io 51 | */ 52 | install?: boolean; 53 | 54 | /** 55 | * Token for cloning private sources 56 | * 57 | * @see https://giget.unjs.io#providing-token-for-private-repositories 58 | */ 59 | auth?: string; 60 | } 61 | 62 | export interface ConfigLayer< 63 | T extends UserInputConfig = UserInputConfig, 64 | MT extends ConfigLayerMeta = ConfigLayerMeta, 65 | > { 66 | config: T | null; 67 | source?: string; 68 | sourceOptions?: SourceOptions; 69 | meta?: MT; 70 | cwd?: string; 71 | configFile?: string; 72 | } 73 | 74 | export interface ResolvedConfig< 75 | T extends UserInputConfig = UserInputConfig, 76 | MT extends ConfigLayerMeta = ConfigLayerMeta, 77 | > extends ConfigLayer { 78 | config: T; 79 | layers?: ConfigLayer[]; 80 | cwd?: string; 81 | _configFile?: string; 82 | } 83 | 84 | export type ConfigSource = 85 | | "overrides" 86 | | "main" 87 | | "rc" 88 | | "packageJson" 89 | | "defaultConfig"; 90 | 91 | export interface ConfigFunctionContext { 92 | [key: string]: any; 93 | } 94 | 95 | export interface ResolvableConfigContext< 96 | T extends UserInputConfig = UserInputConfig, 97 | > { 98 | configs: Record; 99 | rawConfigs: Record | null | undefined>; 100 | } 101 | 102 | type MaybePromise = T | Promise; 103 | export type ResolvableConfig = 104 | | MaybePromise 105 | | ((ctx: ResolvableConfigContext) => MaybePromise); 106 | 107 | export interface LoadConfigOptions< 108 | T extends UserInputConfig = UserInputConfig, 109 | MT extends ConfigLayerMeta = ConfigLayerMeta, 110 | > { 111 | name?: string; 112 | cwd?: string; 113 | 114 | configFile?: string; 115 | 116 | rcFile?: false | string; 117 | globalRc?: boolean; 118 | 119 | dotenv?: boolean | DotenvOptions; 120 | 121 | envName?: string | false; 122 | 123 | packageJson?: boolean | string | string[]; 124 | 125 | defaults?: T; 126 | 127 | defaultConfig?: ResolvableConfig; 128 | overrides?: ResolvableConfig; 129 | 130 | omit$Keys?: boolean; 131 | 132 | /** Context passed to config functions */ 133 | context?: ConfigFunctionContext; 134 | 135 | resolve?: ( 136 | id: string, 137 | options: LoadConfigOptions, 138 | ) => 139 | | null 140 | | undefined 141 | | ResolvedConfig 142 | | Promise | undefined | null>; 143 | 144 | jiti?: Jiti; 145 | jitiOptions?: JitiOptions; 146 | 147 | giget?: false | DownloadTemplateOptions; 148 | 149 | merger?: (...sources: Array) => T; 150 | 151 | extend?: 152 | | false 153 | | { 154 | extendKey?: string | string[]; 155 | }; 156 | 157 | configFileRequired?: boolean; 158 | } 159 | 160 | export type DefineConfig< 161 | T extends UserInputConfig = UserInputConfig, 162 | MT extends ConfigLayerMeta = ConfigLayerMeta, 163 | > = (input: InputConfig) => InputConfig; 164 | 165 | export function createDefineConfig< 166 | T extends UserInputConfig = UserInputConfig, 167 | MT extends ConfigLayerMeta = ConfigLayerMeta, 168 | >(): DefineConfig { 169 | return (input: InputConfig) => input; 170 | } 171 | -------------------------------------------------------------------------------- /src/watch.ts: -------------------------------------------------------------------------------- 1 | import type { ChokidarOptions } from "chokidar"; 2 | import { debounce } from "perfect-debounce"; 3 | import { resolve } from "pathe"; 4 | import type { diff } from "ohash/utils"; 5 | import type { 6 | UserInputConfig, 7 | ConfigLayerMeta, 8 | ResolvedConfig, 9 | LoadConfigOptions, 10 | } from "./types"; 11 | import { SUPPORTED_EXTENSIONS, loadConfig } from "./loader"; 12 | 13 | type DiffEntries = ReturnType; 14 | 15 | export type ConfigWatcher< 16 | T extends UserInputConfig = UserInputConfig, 17 | MT extends ConfigLayerMeta = ConfigLayerMeta, 18 | > = ResolvedConfig & { 19 | watchingFiles: string[]; 20 | unwatch: () => Promise; 21 | }; 22 | 23 | export interface WatchConfigOptions< 24 | T extends UserInputConfig = UserInputConfig, 25 | MT extends ConfigLayerMeta = ConfigLayerMeta, 26 | > extends LoadConfigOptions { 27 | chokidarOptions?: ChokidarOptions; 28 | debounce?: false | number; 29 | 30 | onWatch?: (event: { 31 | type: "created" | "updated" | "removed"; 32 | path: string; 33 | }) => void | Promise; 34 | 35 | acceptHMR?: (context: { 36 | getDiff: () => DiffEntries; 37 | newConfig: ResolvedConfig; 38 | oldConfig: ResolvedConfig; 39 | }) => void | boolean | Promise; 40 | 41 | onUpdate?: (context: { 42 | getDiff: () => ReturnType; 43 | newConfig: ResolvedConfig; 44 | oldConfig: ResolvedConfig; 45 | }) => void | Promise; 46 | } 47 | 48 | const eventMap = { 49 | add: "created", 50 | change: "updated", 51 | unlink: "removed", 52 | } as const; 53 | 54 | export async function watchConfig< 55 | T extends UserInputConfig = UserInputConfig, 56 | MT extends ConfigLayerMeta = ConfigLayerMeta, 57 | >(options: WatchConfigOptions): Promise> { 58 | let config = await loadConfig(options); 59 | 60 | const configName = options.name || "config"; 61 | const configFileName = 62 | options.configFile ?? 63 | (options.name === "config" ? "config" : `${options.name}.config`); 64 | const watchingFiles = [ 65 | ...new Set( 66 | (config.layers || []) 67 | .filter((l) => l.cwd) 68 | .flatMap((l) => [ 69 | ...SUPPORTED_EXTENSIONS.flatMap((ext) => [ 70 | resolve(l.cwd!, configFileName + ext), 71 | resolve(l.cwd!, ".config", configFileName + ext), 72 | resolve( 73 | l.cwd!, 74 | ".config", 75 | configFileName.replace(/\.config$/, "") + ext, 76 | ), 77 | ]), 78 | l.source && resolve(l.cwd!, l.source), 79 | // TODO: Support watching rc from home and workspace 80 | options.rcFile && 81 | resolve( 82 | l.cwd!, 83 | typeof options.rcFile === "string" 84 | ? options.rcFile 85 | : `.${configName}rc`, 86 | ), 87 | options.packageJson && resolve(l.cwd!, "package.json"), 88 | ]) 89 | .filter(Boolean), 90 | ), 91 | ] as string[]; 92 | 93 | const watch = await import("chokidar").then((r) => r.watch || r.default || r); 94 | const { diff } = await import("ohash/utils"); 95 | const _fswatcher = watch(watchingFiles, { 96 | ignoreInitial: true, 97 | ...options.chokidarOptions, 98 | }); 99 | 100 | const onChange = async (event: string, path: string) => { 101 | const type = eventMap[event as keyof typeof eventMap]; 102 | if (!type) { 103 | return; 104 | } 105 | if (options.onWatch) { 106 | await options.onWatch({ 107 | type, 108 | path, 109 | }); 110 | } 111 | const oldConfig = config; 112 | try { 113 | config = await loadConfig(options); 114 | } catch (error) { 115 | console.warn(`Failed to load config ${path}\n${error}`); 116 | return; 117 | } 118 | const changeCtx = { 119 | newConfig: config, 120 | oldConfig, 121 | getDiff: () => diff(oldConfig.config, config.config), 122 | }; 123 | if (options.acceptHMR) { 124 | const changeHandled = await options.acceptHMR(changeCtx); 125 | if (changeHandled) { 126 | return; 127 | } 128 | } 129 | if (options.onUpdate) { 130 | await options.onUpdate(changeCtx); 131 | } 132 | }; 133 | 134 | if (options.debounce === false) { 135 | _fswatcher.on("all", onChange); 136 | } else { 137 | _fswatcher.on("all", debounce(onChange, options.debounce ?? 100)); 138 | } 139 | 140 | const utils: Partial> = { 141 | watchingFiles, 142 | unwatch: async () => { 143 | await _fswatcher.close(); 144 | }, 145 | }; 146 | 147 | return new Proxy>(utils as ConfigWatcher, { 148 | get(_, prop) { 149 | if (prop in utils) { 150 | return utils[prop as keyof typeof utils]; 151 | } 152 | return config[prop as keyof ResolvedConfig]; 153 | }, 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /src/dotenv.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp, statSync } from "node:fs"; 2 | import { resolve } from "pathe"; 3 | import * as dotenv from "dotenv"; 4 | 5 | export interface DotenvOptions { 6 | /** 7 | * The project root directory (either absolute or relative to the current working directory). 8 | * 9 | * Defaults to `options.cwd` in `loadConfig` context, or `process.cwd()` when used as standalone. 10 | */ 11 | cwd?: string; 12 | 13 | /** 14 | * What file or files to look in for environment variables (either absolute or relative 15 | * to the current working directory). For example, `.env`. 16 | * With the array type, the order enforce the env loading priority (last one overrides). 17 | */ 18 | fileName?: string | string[]; 19 | 20 | /** 21 | * Whether to interpolate variables within .env. 22 | * 23 | * @example 24 | * ```env 25 | * BASE_DIR="/test" 26 | * # resolves to "/test/further" 27 | * ANOTHER_DIR="${BASE_DIR}/further" 28 | * ``` 29 | */ 30 | interpolate?: boolean; 31 | 32 | /** 33 | * An object describing environment variables (key, value pairs). 34 | */ 35 | env?: NodeJS.ProcessEnv; 36 | } 37 | 38 | export type Env = typeof process.env; 39 | 40 | /** 41 | * Load and interpolate environment variables into `process.env`. 42 | * If you need more control (or access to the values), consider using `loadDotenv` instead 43 | * 44 | */ 45 | export async function setupDotenv(options: DotenvOptions): Promise { 46 | const targetEnvironment = options.env ?? process.env; 47 | 48 | // Load env 49 | const environment = await loadDotenv({ 50 | cwd: options.cwd, 51 | fileName: options.fileName ?? ".env", 52 | env: targetEnvironment, 53 | interpolate: options.interpolate ?? true, 54 | }); 55 | 56 | const dotenvVars = getDotEnvVars(targetEnvironment); 57 | 58 | // Fill process.env 59 | for (const key in environment) { 60 | // Skip private variables 61 | if (key.startsWith("_")) { 62 | continue; 63 | } 64 | // Override if variables are not already set or come from `.env` 65 | if (targetEnvironment[key] === undefined || dotenvVars.has(key)) { 66 | targetEnvironment[key] = environment[key]; 67 | } 68 | } 69 | 70 | return environment; 71 | } 72 | 73 | /** Load environment variables into an object. */ 74 | export async function loadDotenv(options: DotenvOptions): Promise { 75 | const environment = Object.create(null); 76 | 77 | const cwd = resolve(options.cwd || "."); 78 | const _fileName = options.fileName || ".env"; 79 | const dotenvFiles = typeof _fileName === "string" ? [_fileName] : _fileName; 80 | 81 | const dotenvVars = getDotEnvVars(options.env || {}); 82 | 83 | // Apply process.env 84 | Object.assign(environment, options.env); 85 | 86 | for (const file of dotenvFiles) { 87 | const dotenvFile = resolve(cwd, file); 88 | if (!statSync(dotenvFile, { throwIfNoEntry: false })?.isFile()) { 89 | continue; 90 | } 91 | const parsed = dotenv.parse(await fsp.readFile(dotenvFile, "utf8")); 92 | for (const key in parsed) { 93 | if (key in environment && !dotenvVars.has(key)) { 94 | continue; // Do not override existing env variables 95 | } 96 | environment[key] = parsed[key]; 97 | dotenvVars.add(key); 98 | } 99 | } 100 | 101 | // Interpolate env 102 | if (options.interpolate) { 103 | interpolate(environment); 104 | } 105 | 106 | return environment; 107 | } 108 | 109 | // Based on https://github.com/motdotla/dotenv-expand 110 | function interpolate( 111 | target: Record, 112 | source: Record = {}, 113 | parse = (v: any) => v, 114 | ) { 115 | function getValue(key: string) { 116 | // Source value 'wins' over target value 117 | return source[key] === undefined ? target[key] : source[key]; 118 | } 119 | 120 | function interpolate(value: unknown, parents: string[] = []): any { 121 | if (typeof value !== "string") { 122 | return value; 123 | } 124 | const matches: string[] = value.match(/(.?\${?(?:[\w:]+)?}?)/g) || []; 125 | return parse( 126 | // eslint-disable-next-line unicorn/no-array-reduce 127 | matches.reduce((newValue, match) => { 128 | const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || []; 129 | const prefix = parts[1]; 130 | 131 | let value, replacePart: string; 132 | 133 | if (prefix === "\\") { 134 | replacePart = parts[0] || ""; 135 | value = replacePart.replace(String.raw`\$`, "$"); 136 | } else { 137 | const key = parts[2]; 138 | replacePart = (parts[0] || "").slice(prefix.length); 139 | 140 | // Avoid recursion 141 | if (parents.includes(key)) { 142 | console.warn( 143 | `Please avoid recursive environment variables ( loop: ${parents.join( 144 | " > ", 145 | )} > ${key} )`, 146 | ); 147 | return ""; 148 | } 149 | 150 | value = getValue(key); 151 | 152 | // Resolve recursive interpolations 153 | value = interpolate(value, [...parents, key]); 154 | } 155 | 156 | return value === undefined 157 | ? newValue 158 | : newValue.replace(replacePart, value); 159 | }, value), 160 | ); 161 | } 162 | 163 | for (const key in target) { 164 | target[key] = interpolate(getValue(key)); 165 | } 166 | } 167 | 168 | // Internal: Keep track of which variables that are set by dotenv 169 | 170 | declare global { 171 | var __c12_dotenv_vars__: Map, Set>; 172 | } 173 | 174 | function getDotEnvVars(targetEnvironment: Record) { 175 | const globalRegistry = (globalThis.__c12_dotenv_vars__ ||= new Map()); 176 | if (!globalRegistry.has(targetEnvironment)) { 177 | globalRegistry.set(targetEnvironment, new Set()); 178 | } 179 | return globalRegistry.get(targetEnvironment)!; 180 | } 181 | -------------------------------------------------------------------------------- /test/loader.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { expect, it, describe } from "vitest"; 3 | import { normalize } from "pathe"; 4 | import type { ConfigLayer, ConfigLayerMeta, UserInputConfig } from "../src"; 5 | import { loadConfig } from "../src"; 6 | 7 | const r = (path: string) => 8 | normalize(fileURLToPath(new URL(path, import.meta.url))); 9 | const transformPaths = (object: object) => 10 | JSON.parse(JSON.stringify(object).replaceAll(r("."), "/")); 11 | 12 | describe("loader", () => { 13 | it("load fixture config", async () => { 14 | type UserConfig = Partial<{ 15 | virtual: boolean; 16 | overridden: boolean; 17 | enableDefault: boolean; 18 | defaultConfig: boolean; 19 | extends: string[]; 20 | }>; 21 | const { config, layers } = await loadConfig({ 22 | cwd: r("./fixture"), 23 | name: "test", 24 | dotenv: { 25 | cwd: r("./fixture"), // TODO: fix types 26 | fileName: [".env", ".env.local"], 27 | }, 28 | packageJson: ["c12", "c12-alt"], 29 | globalRc: true, 30 | envName: "test", 31 | extend: { 32 | extendKey: ["theme", "extends"], 33 | }, 34 | resolve: (id) => { 35 | if (id === "virtual") { 36 | return { config: { virtual: true } }; 37 | } 38 | }, 39 | overrides: { 40 | overridden: true, 41 | }, 42 | defaults: { 43 | defaultConfig: true, 44 | }, 45 | defaultConfig: ({ configs }) => { 46 | if (configs?.main?.enableDefault) { 47 | return Promise.resolve({ 48 | extends: ["virtual"], 49 | }); 50 | } 51 | return {}; 52 | }, 53 | }); 54 | 55 | expect(transformPaths(config!)).toMatchInlineSnapshot(` 56 | { 57 | "$env": { 58 | "test": { 59 | "baseEnvConfig": true, 60 | }, 61 | }, 62 | "$test": { 63 | "envConfig": true, 64 | "extends": [ 65 | "./test.config.dev", 66 | ], 67 | }, 68 | "array": [ 69 | "a", 70 | "b", 71 | ], 72 | "baseConfig": true, 73 | "baseEnvConfig": true, 74 | "colors": { 75 | "primary": "user_primary", 76 | "secondary": "theme_secondary", 77 | "text": "base_text", 78 | }, 79 | "configFile": true, 80 | "defaultConfig": true, 81 | "devConfig": true, 82 | "dotenv": "true", 83 | "dotenvLocal": "true", 84 | "dotenvOverride": ".env.local", 85 | "enableDefault": true, 86 | "envConfig": true, 87 | "githubLayer": true, 88 | "not_a_folder": true, 89 | "npmConfig": true, 90 | "overridden": true, 91 | "packageJSON": true, 92 | "packageJSON2": true, 93 | "rcFile": true, 94 | "testConfig": true, 95 | "virtual": true, 96 | } 97 | `); 98 | 99 | expect(transformPaths(layers!)).toMatchInlineSnapshot(` 100 | [ 101 | { 102 | "config": { 103 | "overridden": true, 104 | }, 105 | }, 106 | { 107 | "config": { 108 | "$test": { 109 | "envConfig": true, 110 | "extends": [ 111 | "./test.config.dev", 112 | ], 113 | }, 114 | "array": [ 115 | "a", 116 | ], 117 | "colors": { 118 | "primary": "user_primary", 119 | }, 120 | "configFile": true, 121 | "enableDefault": true, 122 | "envConfig": true, 123 | "extends": [ 124 | "./test.config.dev", 125 | [ 126 | "c12-npm-test", 127 | ], 128 | [ 129 | "gh:unjs/c12/test/fixture/_github#main", 130 | { 131 | "giget": {}, 132 | }, 133 | ], 134 | "./not-a-folder.ts", 135 | ], 136 | "overridden": false, 137 | "theme": "./theme", 138 | }, 139 | "configFile": "test.config", 140 | "cwd": "/fixture", 141 | }, 142 | { 143 | "config": { 144 | "rcFile": true, 145 | "testConfig": true, 146 | }, 147 | "configFile": ".testrc", 148 | }, 149 | { 150 | "config": { 151 | "packageJSON": true, 152 | "packageJSON2": true, 153 | }, 154 | "configFile": "package.json", 155 | }, 156 | { 157 | "_configFile": "/fixture/theme/.config/test.config.json5", 158 | "config": { 159 | "colors": { 160 | "primary": "theme_primary", 161 | "secondary": "theme_secondary", 162 | }, 163 | }, 164 | "configFile": "/fixture/theme/.config/test.config.json5", 165 | "cwd": "/fixture/theme", 166 | "meta": {}, 167 | "source": "test.config", 168 | "sourceOptions": {}, 169 | }, 170 | { 171 | "_configFile": "/fixture/.base/test.config.jsonc", 172 | "config": { 173 | "$env": { 174 | "test": { 175 | "baseEnvConfig": true, 176 | }, 177 | }, 178 | "array": [ 179 | "b", 180 | ], 181 | "baseConfig": true, 182 | "baseEnvConfig": true, 183 | "colors": { 184 | "primary": "base_primary", 185 | "text": "base_text", 186 | }, 187 | }, 188 | "configFile": "/fixture/.base/test.config.jsonc", 189 | "cwd": "/fixture/.base", 190 | "meta": { 191 | "name": "base", 192 | "version": "1.0.0", 193 | }, 194 | "source": "test.config", 195 | "sourceOptions": {}, 196 | }, 197 | { 198 | "_configFile": "/fixture/test.config.dev.ts", 199 | "config": { 200 | "devConfig": true, 201 | "dotenv": "true", 202 | "dotenvLocal": "true", 203 | "dotenvOverride": ".env.local", 204 | }, 205 | "configFile": "/fixture/test.config.dev.ts", 206 | "cwd": "/fixture", 207 | "meta": {}, 208 | "source": "./test.config.dev", 209 | "sourceOptions": {}, 210 | }, 211 | { 212 | "_configFile": "/fixture/node_modules/c12-npm-test/test.config.ts", 213 | "config": { 214 | "npmConfig": true, 215 | }, 216 | "configFile": "/fixture/node_modules/c12-npm-test/test.config.ts", 217 | "cwd": "/fixture/node_modules/c12-npm-test", 218 | "meta": {}, 219 | "source": "/fixture/node_modules/c12-npm-test/test.config.ts", 220 | "sourceOptions": {}, 221 | }, 222 | { 223 | "_configFile": "/fixture/node_modules/.c12/gh_unjs_c12_vsPD2sVEDo/test.config.ts", 224 | "config": { 225 | "githubLayer": true, 226 | }, 227 | "configFile": "/fixture/node_modules/.c12/gh_unjs_c12_vsPD2sVEDo/test.config.ts", 228 | "cwd": "/fixture/node_modules/.c12/gh_unjs_c12_vsPD2sVEDo", 229 | "meta": {}, 230 | "source": "test.config", 231 | "sourceOptions": { 232 | "giget": {}, 233 | }, 234 | }, 235 | { 236 | "_configFile": "/fixture/not-a-folder.ts", 237 | "config": { 238 | "not_a_folder": true, 239 | }, 240 | "configFile": "/fixture/not-a-folder.ts", 241 | "cwd": "/fixture", 242 | "meta": {}, 243 | "source": "./not-a-folder.ts", 244 | "sourceOptions": {}, 245 | }, 246 | { 247 | "config": { 248 | "virtual": true, 249 | }, 250 | }, 251 | ] 252 | `); 253 | }); 254 | 255 | it("extend from git repo", async () => { 256 | const { config } = await loadConfig({ 257 | name: "test", 258 | cwd: r("./fixture/new_dir"), 259 | overrides: { 260 | extends: ["github:unjs/c12/test/fixture"], 261 | }, 262 | }); 263 | const { config: nonExtendingConfig } = await loadConfig({ 264 | name: "test", 265 | cwd: r("./fixture/new_dir"), 266 | giget: false, 267 | overrides: { 268 | extends: ["github:unjs/c12/test/fixture"], 269 | }, 270 | }); 271 | 272 | expect(transformPaths(config!)).toMatchInlineSnapshot(` 273 | { 274 | "$test": { 275 | "envConfig": true, 276 | "extends": [ 277 | "./test.config.dev", 278 | ], 279 | }, 280 | "array": [ 281 | "a", 282 | ], 283 | "colors": { 284 | "primary": "user_primary", 285 | }, 286 | "configFile": true, 287 | "devConfig": true, 288 | "dotenv": "true", 289 | "dotenvLocal": "true", 290 | "dotenvOverride": ".env.local", 291 | "enableDefault": true, 292 | "envConfig": true, 293 | "githubLayer": true, 294 | "not_a_folder": true, 295 | "npmConfig": true, 296 | "overridden": false, 297 | "theme": "./theme", 298 | } 299 | `); 300 | 301 | expect(transformPaths(nonExtendingConfig!)).toMatchInlineSnapshot(` 302 | {} 303 | `); 304 | }); 305 | 306 | it("omit$Keys", async () => { 307 | const { config, layers } = await loadConfig({ 308 | name: "test", 309 | cwd: r("./fixture"), 310 | envName: "test", 311 | omit$Keys: true, 312 | extend: { 313 | extendKey: ["theme", "extends"], 314 | }, 315 | }); 316 | 317 | const resolvedConfigKeys = Object.keys(config!); 318 | 319 | expect(resolvedConfigKeys).not.toContain("$env"); 320 | expect(resolvedConfigKeys).not.toContain("$meta"); 321 | expect(resolvedConfigKeys).not.toContain("$test"); 322 | 323 | const transformdLayers = transformPaths(layers!) as ConfigLayer< 324 | UserInputConfig, 325 | ConfigLayerMeta 326 | >[]; 327 | 328 | const configLayer = transformdLayers.find( 329 | (layer) => layer.configFile === "test.config", 330 | )!; 331 | expect(Object.keys(configLayer.config!)).toContain("$test"); 332 | 333 | const baseLayerConfig = transformdLayers.find( 334 | (layer) => layer.configFile === "/fixture/.base/test.config.jsonc", 335 | )!; 336 | expect(Object.keys(baseLayerConfig.config!)).toContain("$env"); 337 | }); 338 | 339 | it("no config loaded and configFileRequired is default setting", async () => { 340 | await expect( 341 | loadConfig({ 342 | configFile: "CUSTOM", 343 | }), 344 | ).resolves.not.toThrowError(); 345 | }); 346 | 347 | it("no config loaded and configFileRequired is true", async () => { 348 | await expect( 349 | loadConfig({ 350 | configFile: "CUSTOM", 351 | configFileRequired: true, 352 | }), 353 | ).rejects.toThrowError("Required config (CUSTOM) cannot be resolved."); 354 | }); 355 | 356 | it("loads arrays exported from config without merging", async () => { 357 | const loaded = await loadConfig({ 358 | name: "test", 359 | cwd: r("./fixture/array"), 360 | }); 361 | expect(loaded.configFile).toBe(r("./fixture/array/test.config.ts")); 362 | expect(loaded._configFile).toEqual(loaded.configFile); 363 | expect(loaded.config).toEqual([ 364 | { a: "boo", b: "foo" }, 365 | { a: "boo", b: "foo" }, 366 | { a: "boo", b: "foo" }, 367 | ]); 368 | expect(loaded.layers![0].config).toEqual(loaded.config); 369 | expect(loaded.layers![1]).toEqual({ 370 | config: { 371 | rcFile: true, 372 | }, 373 | configFile: ".testrc", 374 | }); 375 | }); 376 | 377 | it("try reproduce error with index.js on root importing jsx/tsx", async () => { 378 | await loadConfig({ 379 | name: "test", 380 | cwd: r("./fixture/jsx"), 381 | }); 382 | }); 383 | }); 384 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚙️ c12 2 | 3 | 4 | 5 | [![npm version](https://img.shields.io/npm/v/c12?color=yellow)](https://npmjs.com/package/c12) 6 | [![npm downloads](https://img.shields.io/npm/dm/c12?color=yellow)](https://npm.chart.dev/c12) 7 | [![codecov](https://img.shields.io/codecov/c/gh/unjs/c12?color=yellow)](https://codecov.io/gh/unjs/c12) 8 | 9 | 10 | 11 | c12 (pronounced as /siːtwelv/, like c-twelve) is a smart configuration loader. 12 | 13 | ## ✅ Features 14 | 15 | - `.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts` `.json` config loader with [unjs/jiti](https://jiti.unjs.io) 16 | - `.jsonc`, `.json5`, `.yaml`, `.yml`, `.toml` config loader with [unjs/confbox](https://confbox.unjs.io) 17 | - `.config/` directory support ([config dir proposal](https://github.com/pi0/config-dir)) 18 | - `.rc` config support with [unjs/rc9](https://github.com/unjs/rc9) 19 | - `.env` support with [dotenv](https://www.npmjs.com/package/dotenv) 20 | - Multiple sources merged with [unjs/defu](https://github.com/unjs/defu) 21 | - Reads config from the nearest `package.json` file 22 | - [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources 23 | - Overwrite with [environment-specific configuration](#environment-specific-configuration) 24 | - Config watcher with auto-reload and HMR support 25 | - Create or update configuration files with [magicast](https://github.com/unjs/magicast) 26 | 27 | ## 🦴 Used by 28 | 29 | - [Hey API](https://github.com/hey-api/openapi-ts) 30 | - [Kysely](https://github.com/kysely-org/kysely-ctl) 31 | - [Nitro](https://nitro.build/) 32 | - [Nuxt](https://nuxt.com/) 33 | - [Prisma](https://github.com/prisma/prisma) 34 | - [Trigger.dev](https://github.com/triggerdotdev/trigger.dev) 35 | - [UnJS](https://github.com/unjs) 36 | - [WXT](https://github.com/wxt-dev/wxt) 37 | 38 | ## Usage 39 | 40 | Install package: 41 | 42 | ```sh 43 | # ✨ Auto-detect 44 | npx nypm install c12 45 | ``` 46 | 47 | Import: 48 | 49 | ```js 50 | // ESM import 51 | import { loadConfig, watchConfig } from "c12"; 52 | 53 | // or using dynamic import 54 | const { loadConfig, watchConfig } = await import("c12"); 55 | ``` 56 | 57 | Load configuration: 58 | 59 | ```js 60 | // Get loaded config 61 | const { config } = await loadConfig({}); 62 | 63 | // Get resolved config and extended layers 64 | const { config, configFile, layers } = await loadConfig({}); 65 | ``` 66 | 67 | ## Loading priority 68 | 69 | c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by below order: 70 | 71 | 1. Config overrides passed by options 72 | 2. Config file in CWD 73 | 3. RC file in CWD 74 | 4. Global RC file in the user's home directory 75 | 5. Config from `package.json` 76 | 6. Default config passed by options 77 | 7. Extended config layers 78 | 79 | ## Options 80 | 81 | ### `cwd` 82 | 83 | Resolve configuration from this working directory. The default is `process.cwd()` 84 | 85 | ### `name` 86 | 87 | Configuration base name. The default is `config`. 88 | 89 | ### `configFile` 90 | 91 | Configuration file name without extension. Default is generated from `name` (f.e., if `name` is `foo`, the config file will be => `foo.config`). 92 | 93 | ### `rcFile` 94 | 95 | RC Config file name. Default is generated from `name` (name=foo => `.foorc`). 96 | 97 | Set to `false` to disable loading RC config. 98 | 99 | ### `globalRc` 100 | 101 | Load RC config from the workspace directory and the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. 102 | 103 | ### `dotenv` 104 | 105 | Loads `.env` file when `true` or an options object is passed. It is disabled by default. 106 | 107 | Supports loading multiple files that extend eachother in left-to-right order when a `fileName`s array of relative/absolute paths is passed in the options object. 108 | 109 | **Example:** 110 | 111 | ```ini 112 | # .env 113 | CONNECTION_POOL_MAX="10" 114 | DATABASE_URL="<...rds...>" 115 | ``` 116 | 117 | ```ini 118 | # .env.local 119 | DATABASE_URL="<...localhost...>" 120 | ``` 121 | 122 | ```js 123 | export default { 124 | connectionPoolMax: process.env.CONNECTION_POOL_MAX, 125 | databaseURL: process.env.DATABASE_URL, 126 | }; 127 | ``` 128 | 129 | ```ts 130 | import { loadConfig } from "c12"; 131 | 132 | const config = await loadConfig({ 133 | dotenv: { 134 | fileName: [".env", ".env.local"], 135 | }, 136 | }); 137 | 138 | console.log(config.config.connectionPoolMax); // "10" 139 | console.log(config.config.databaseURL); // "<...localhost...>" 140 | ``` 141 | 142 | ### `packageJson` 143 | 144 | Loads config from nearest `package.json` file. It is disabled by default. 145 | 146 | If `true` value is passed, c12 uses `name` field from `package.json`. 147 | 148 | You can also pass either a string or an array of strings as a value to use those fields. 149 | 150 | ### `defaults` 151 | 152 | Specify default configuration. It has the **lowest** priority and is applied **after extending** config. 153 | 154 | ### `defaultConfig` 155 | 156 | Specify default configuration. It is applied **before** extending config. 157 | 158 | ### `overrides` 159 | 160 | Specify override configuration. It has the **highest** priority and is applied **before extending** config. 161 | 162 | ### `omit$Keys` 163 | 164 | Exclude environment-specific and built-in keys start with `$` in the resolved config. The default is `false`. 165 | 166 | ### `jiti` 167 | 168 | Custom [unjs/jiti](https://github.com/unjs/jiti) instance used to import configuration files. 169 | 170 | ### `jitiOptions` 171 | 172 | Custom [unjs/jiti](https://github.com/unjs/jiti) options to import configuration files. 173 | 174 | ### `giget` 175 | 176 | Options passed to [unjs/giget](https://github.com/unjs/giget) when extending layer from git source. 177 | 178 | ### `merger` 179 | 180 | Custom options merger function. Default is [defu](https://github.com/unjs/defu). 181 | 182 | **Note:** Custom merge function should deeply merge options with arguments high -> low priority. 183 | 184 | ### `envName` 185 | 186 | Environment name used for [environment specific configuration](#environment-specific-configuration). 187 | 188 | The default is `process.env.NODE_ENV`. You can set `envName` to `false` or an empty string to disable the feature. 189 | 190 | ### `context` 191 | 192 | Context object passed to dynamic config functions. 193 | 194 | ### `resolve` 195 | 196 | You can define a custom function that resolves the config. 197 | 198 | ### `configFileRequired` 199 | 200 | If this option is set to `true`, loader fails if the main config file does not exists. 201 | 202 | ## Extending configuration 203 | 204 | If resolved config contains a `extends` key, it will be used to extend the configuration. 205 | 206 | Extending can be nested and each layer can extend from one base or more. 207 | 208 | The final config is merged result of extended options and user options with [unjs/defu](https://github.com/unjs/defu). 209 | 210 | Each item in extends is a string that can be either an absolute or relative path to the current config file pointing to a config file for extending or the directory containing the config file. 211 | If it starts with either `github:`, `gitlab:`, `bitbucket:`, or `https:`, c12 automatically clones it. 212 | 213 | For custom merging strategies, you can directly access each layer with `layers` property. 214 | 215 | **Example:** 216 | 217 | ```js 218 | // config.ts 219 | export default { 220 | colors: { 221 | primary: "user_primary", 222 | }, 223 | extends: ["./theme"], 224 | }; 225 | ``` 226 | 227 | ```js 228 | // config.dev.ts 229 | export default { 230 | dev: true, 231 | }; 232 | ``` 233 | 234 | ```js 235 | // theme/config.ts 236 | export default { 237 | extends: "../base", 238 | colors: { 239 | primary: "theme_primary", 240 | secondary: "theme_secondary", 241 | }, 242 | }; 243 | ``` 244 | 245 | ```js 246 | // base/config.ts 247 | export default { 248 | colors: { 249 | primary: "base_primary", 250 | text: "base_text", 251 | }, 252 | }; 253 | ``` 254 | 255 | The loaded configuration would look like this: 256 | 257 | ```js 258 | const config = { 259 | dev: true, 260 | colors: { 261 | primary: "user_primary", 262 | secondary: "theme_secondary", 263 | text: "base_text", 264 | }, 265 | }; 266 | ``` 267 | 268 | Layers: 269 | 270 | ```js 271 | [ 272 | { 273 | config: { 274 | /* theme config */ 275 | }, 276 | configFile: "/path/to/theme/config.ts", 277 | cwd: "/path/to/theme ", 278 | }, 279 | { 280 | config: { 281 | /* base config */ 282 | }, 283 | configFile: "/path/to/base/config.ts", 284 | cwd: "/path/to/base", 285 | }, 286 | { 287 | config: { 288 | /* dev config */ 289 | }, 290 | configFile: "/path/to/config.dev.ts", 291 | cwd: "/path/", 292 | }, 293 | ]; 294 | ``` 295 | 296 | ## Extending config layer from remote sources 297 | 298 | You can also extend configuration from remote sources such as npm or github. 299 | 300 | In the repo, there should be a `config.ts` (or `config.{name}.ts`) file to be considered as a valid config layer. 301 | 302 | **Example:** Extend from a github repository 303 | 304 | ```js 305 | // config.ts 306 | export default { 307 | extends: "gh:user/repo", 308 | }; 309 | ``` 310 | 311 | **Example:** Extend from a github repository with branch and subpath 312 | 313 | ```js 314 | // config.ts 315 | export default { 316 | extends: "gh:user/repo/theme#dev", 317 | }; 318 | ``` 319 | 320 | **Example:** Extend a private repository and install dependencies: 321 | 322 | ```js 323 | // config.ts 324 | export default { 325 | extends: ["gh:user/repo", { auth: process.env.GITHUB_TOKEN, install: true }], 326 | }; 327 | ``` 328 | 329 | You can pass more options to `giget: {}` in layer config or disable it by setting it to `false`. 330 | 331 | Refer to [unjs/giget](https://giget.unjs.io) for more information. 332 | 333 | ## Environment-specific configuration 334 | 335 | Users can define environment-specific configuration using these config keys: 336 | 337 | - `$test: {...}` 338 | - `$development: {...}` 339 | - `$production: {...}` 340 | - `$env: { [env]: {...} }` 341 | 342 | c12 tries to match [`envName`](#envname) and override environment config if specified. 343 | 344 | **Note:** Environment will be applied when extending each configuration layer. This way layers can provide environment-specific configuration. 345 | 346 | **Example:** 347 | 348 | ```js 349 | export default { 350 | // Default configuration 351 | logLevel: "info", 352 | 353 | // Environment overrides 354 | $test: { logLevel: "silent" }, 355 | $development: { logLevel: "warning" }, 356 | $production: { logLevel: "error" }, 357 | $env: { 358 | staging: { logLevel: "debug" }, 359 | }, 360 | }; 361 | ``` 362 | 363 | ## Watching configuration 364 | 365 | you can use `watchConfig` instead of `loadConfig` to load config and watch for changes, add and removals in all expected configuration paths and auto reload with new config. 366 | 367 | ### Lifecycle hooks 368 | 369 | - `onWatch`: This function is always called when config is updated, added, or removed before attempting to reload the config. 370 | - `acceptHMR`: By implementing this function, you can compare old and new functions and return `true` if a full reload is not needed. 371 | - `onUpdate`: This function is always called after the new config is updated. If `acceptHMR` returns true, it will be skipped. 372 | 373 | ```ts 374 | import { watchConfig } from "c12"; 375 | 376 | const config = watchConfig({ 377 | cwd: ".", 378 | // chokidarOptions: {}, // Default is { ignoreInitial: true } 379 | // debounce: 200 // Default is 100. You can set it to false to disable debounced watcher 380 | onWatch: (event) => { 381 | console.log("[watcher]", event.type, event.path); 382 | }, 383 | acceptHMR({ oldConfig, newConfig, getDiff }) { 384 | const diff = getDiff(); 385 | if (diff.length === 0) { 386 | console.log("No config changed detected!"); 387 | return true; // No changes! 388 | } 389 | }, 390 | onUpdate({ oldConfig, newConfig, getDiff }) { 391 | const diff = getDiff(); 392 | console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n")); 393 | }, 394 | }); 395 | 396 | console.log("watching config files:", config.watchingFiles); 397 | console.log("initial config", config.config); 398 | 399 | // Stop watcher when not needed anymore 400 | // await config.unwatch(); 401 | ``` 402 | 403 | ## Updating config 404 | 405 | > [!NOTE] 406 | > This feature is experimental 407 | 408 | Update or create a new configuration files. 409 | 410 | Add `magicast` peer dependency: 411 | 412 | ```sh 413 | # ✨ Auto-detect 414 | npx nypm install -D magicast 415 | ``` 416 | 417 | Import util from `c12/update` 418 | 419 | ```js 420 | const { configFile, created } = await updateConfig({ 421 | cwd: ".", 422 | configFile: "foo.config", 423 | onCreate: ({ configFile }) => { 424 | // You can prompt user if wants to create a new config file and return false to cancel 425 | console.log(`Creating new config file in ${configFile}...`); 426 | return "export default { test: true }"; 427 | }, 428 | onUpdate: (config) => { 429 | // You can update the config contents just like an object 430 | config.test2 = false; 431 | }, 432 | }); 433 | 434 | console.log(`Config file ${created ? "created" : "updated"} in ${configFile}`); 435 | ``` 436 | 437 | ## Configuration functions 438 | 439 | You can use a function to define your configuration dynamically based on context. 440 | 441 | ```ts 442 | // config.ts 443 | export default (ctx) => { 444 | return { 445 | apiUrl: ctx?.dev ? "http://localhost:3000" : "https://api.example.com", 446 | }; 447 | }; 448 | ``` 449 | 450 | ```ts 451 | // Usage 452 | import { loadConfig } from "c12"; 453 | 454 | const config = await loadConfig({ 455 | context: { dev: true }, 456 | }); 457 | ``` 458 | 459 | ## Contribution 460 | 461 |
462 | Local development 463 | 464 | - Clone this repository 465 | - Install the latest LTS version of [Node.js](https://nodejs.org/en/) 466 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 467 | - Install dependencies using `pnpm install` 468 | - Run tests using `pnpm dev` or `pnpm test` 469 | 470 |
471 | 472 | 473 | 474 | ## License 475 | 476 | 477 | 478 | Published under the [MIT](https://github.com/unjs/c12/blob/main/LICENSE) license. 479 | Made by [@pi0](https://github.com/pi0) and [community](https://github.com/unjs/c12/graphs/contributors) 💛 480 |

481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | --- 490 | 491 | _🤖 auto updated with [automd](https://automd.unjs.io)_ 492 | 493 | 494 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { readFile, rm } from "node:fs/promises"; 3 | import { pathToFileURL } from "node:url"; 4 | import { homedir } from "node:os"; 5 | import { resolve, extname, dirname, basename, join, normalize } from "pathe"; 6 | import { resolveModulePath } from "exsolve"; 7 | import { createJiti } from "jiti"; 8 | import * as rc9 from "rc9"; 9 | import { defu } from "defu"; 10 | import { findWorkspaceDir, readPackageJSON } from "pkg-types"; 11 | import { setupDotenv } from "./dotenv"; 12 | 13 | import type { 14 | UserInputConfig, 15 | ConfigLayerMeta, 16 | LoadConfigOptions, 17 | ResolvedConfig, 18 | ResolvableConfig, 19 | ConfigLayer, 20 | SourceOptions, 21 | InputConfig, 22 | ConfigSource, 23 | ConfigFunctionContext, 24 | } from "./types"; 25 | 26 | const _normalize = (p?: string) => p?.replace(/\\/g, "/"); 27 | 28 | const ASYNC_LOADERS = { 29 | ".yaml": () => import("confbox/yaml").then((r) => r.parseYAML), 30 | ".yml": () => import("confbox/yaml").then((r) => r.parseYAML), 31 | ".jsonc": () => import("confbox/jsonc").then((r) => r.parseJSONC), 32 | ".json5": () => import("confbox/json5").then((r) => r.parseJSON5), 33 | ".toml": () => import("confbox/toml").then((r) => r.parseTOML), 34 | } as const; 35 | 36 | export const SUPPORTED_EXTENSIONS = Object.freeze([ 37 | // with jiti 38 | ".js", 39 | ".ts", 40 | ".mjs", 41 | ".cjs", 42 | ".mts", 43 | ".cts", 44 | ".json", 45 | // with confbox 46 | ".jsonc", 47 | ".json5", 48 | ".yaml", 49 | ".yml", 50 | ".toml", 51 | ]) as unknown as string[]; 52 | 53 | export async function loadConfig< 54 | T extends UserInputConfig = UserInputConfig, 55 | MT extends ConfigLayerMeta = ConfigLayerMeta, 56 | >(options: LoadConfigOptions): Promise> { 57 | // Normalize options 58 | options.cwd = resolve(process.cwd(), options.cwd || "."); 59 | options.name = options.name || "config"; 60 | options.envName = options.envName ?? process.env.NODE_ENV; 61 | options.configFile = 62 | options.configFile ?? 63 | (options.name === "config" ? "config" : `${options.name}.config`); 64 | options.rcFile = options.rcFile ?? `.${options.name}rc`; 65 | if (options.extend !== false) { 66 | options.extend = { 67 | extendKey: "extends", 68 | ...options.extend, 69 | }; 70 | } 71 | 72 | // Custom merger 73 | const _merger = options.merger || defu; 74 | 75 | // Create jiti instance 76 | options.jiti = 77 | options.jiti || 78 | createJiti(join(options.cwd, options.configFile), { 79 | interopDefault: true, 80 | moduleCache: false, 81 | extensions: [...SUPPORTED_EXTENSIONS], 82 | ...options.jitiOptions, 83 | }); 84 | 85 | // Create context 86 | const r: ResolvedConfig = { 87 | config: {} as any, 88 | cwd: options.cwd, 89 | configFile: resolve(options.cwd, options.configFile), 90 | layers: [], 91 | _configFile: undefined, 92 | }; 93 | 94 | // prettier-ignore 95 | const rawConfigs: Record< 96 | ConfigSource, 97 | ResolvableConfig | null | undefined 98 | > = { 99 | overrides: options.overrides, 100 | main: undefined, 101 | rc: undefined, 102 | packageJson: undefined, 103 | defaultConfig: options.defaultConfig, 104 | }; 105 | 106 | // Load dotenv 107 | if (options.dotenv) { 108 | await setupDotenv({ 109 | cwd: options.cwd, 110 | ...(options.dotenv === true ? {} : options.dotenv), 111 | }); 112 | } 113 | 114 | // Load main config file 115 | const _mainConfig = await resolveConfig(".", options); 116 | if (_mainConfig.configFile) { 117 | rawConfigs.main = _mainConfig.config; 118 | r.configFile = _mainConfig.configFile; 119 | r._configFile = _mainConfig._configFile; 120 | } 121 | 122 | if (_mainConfig.meta) { 123 | r.meta = _mainConfig.meta; 124 | } 125 | 126 | // Load rc files 127 | if (options.rcFile) { 128 | const rcSources: T[] = []; 129 | // 1. cwd 130 | rcSources.push(rc9.read({ name: options.rcFile, dir: options.cwd })); 131 | if (options.globalRc) { 132 | // 2. workspace 133 | const workspaceDir = await findWorkspaceDir(options.cwd).catch(() => {}); 134 | if (workspaceDir) { 135 | rcSources.push(rc9.read({ name: options.rcFile, dir: workspaceDir })); 136 | } 137 | // 3. user home 138 | rcSources.push(rc9.readUser({ name: options.rcFile, dir: options.cwd })); 139 | } 140 | rawConfigs.rc = _merger({} as T, ...rcSources); 141 | } 142 | 143 | // Load config from package.json 144 | if (options.packageJson) { 145 | const keys = ( 146 | Array.isArray(options.packageJson) 147 | ? options.packageJson 148 | : [ 149 | typeof options.packageJson === "string" 150 | ? options.packageJson 151 | : options.name, 152 | ] 153 | ).filter((t) => t && typeof t === "string"); 154 | const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {}); 155 | const values = keys.map((key) => pkgJsonFile?.[key]); 156 | rawConfigs.packageJson = _merger({} as T, ...values); 157 | } 158 | 159 | // Resolve config sources 160 | const configs = {} as Record; 161 | // TODO: #253 change order from defaults to overrides in next major version 162 | for (const key in rawConfigs) { 163 | const value = rawConfigs[key as ConfigSource]; 164 | configs[key as ConfigSource] = await (typeof value === "function" 165 | ? value({ configs, rawConfigs }) 166 | : value); 167 | } 168 | 169 | if (Array.isArray(configs.main)) { 170 | // If the main config exports an array, use it directly without merging or extending 171 | r.config = configs.main; 172 | } else { 173 | // Combine sources 174 | r.config = _merger( 175 | configs.overrides, 176 | configs.main, 177 | configs.rc, 178 | configs.packageJson, 179 | configs.defaultConfig, 180 | ) as T; 181 | 182 | // Allow extending 183 | if (options.extend) { 184 | await extendConfig(r.config, options); 185 | r.layers = r.config._layers; 186 | delete r.config._layers; 187 | r.config = _merger(r.config, ...r.layers!.map((e) => e.config)) as T; 188 | } 189 | } 190 | 191 | // Preserve unmerged sources as layers 192 | const baseLayers: ConfigLayer[] = [ 193 | configs.overrides && { 194 | config: configs.overrides, 195 | configFile: undefined, 196 | cwd: undefined, 197 | }, 198 | { config: configs.main, configFile: options.configFile, cwd: options.cwd }, 199 | configs.rc && { config: configs.rc, configFile: options.rcFile }, 200 | configs.packageJson && { 201 | config: configs.packageJson, 202 | configFile: "package.json", 203 | }, 204 | ].filter((l) => l && l.config) as ConfigLayer[]; 205 | 206 | r.layers = [...baseLayers, ...r.layers!]; 207 | 208 | // Apply defaults 209 | if (options.defaults) { 210 | r.config = _merger(r.config, options.defaults) as T; 211 | } 212 | 213 | // Remove environment-specific and built-in keys start with $ 214 | if (options.omit$Keys) { 215 | for (const key in r.config) { 216 | if (key.startsWith("$")) { 217 | delete r.config[key]; 218 | } 219 | } 220 | } 221 | 222 | // Fail if no config loaded 223 | if (options.configFileRequired && !r._configFile) { 224 | throw new Error(`Required config (${r.configFile}) cannot be resolved.`); 225 | } 226 | 227 | // Return resolved config 228 | return r; 229 | } 230 | 231 | async function extendConfig< 232 | T extends UserInputConfig = UserInputConfig, 233 | MT extends ConfigLayerMeta = ConfigLayerMeta, 234 | >(config: InputConfig, options: LoadConfigOptions) { 235 | (config as any)._layers = config._layers || []; 236 | if (!options.extend) { 237 | return; 238 | } 239 | let keys = options.extend.extendKey; 240 | if (typeof keys === "string") { 241 | keys = [keys]; 242 | } 243 | const extendSources = []; 244 | for (const key of keys as string[]) { 245 | extendSources.push( 246 | ...(Array.isArray(config[key]) ? config[key] : [config[key]]).filter( 247 | Boolean, 248 | ), 249 | ); 250 | delete config[key]; 251 | } 252 | for (let extendSource of extendSources) { 253 | const originalExtendSource = extendSource; 254 | let sourceOptions = {}; 255 | if (extendSource.source) { 256 | sourceOptions = extendSource.options || {}; 257 | extendSource = extendSource.source; 258 | } 259 | if (Array.isArray(extendSource)) { 260 | sourceOptions = extendSource[1] || {}; 261 | extendSource = extendSource[0]; 262 | } 263 | if (typeof extendSource !== "string") { 264 | // TODO: Use error in next major versions 265 | 266 | console.warn( 267 | `Cannot extend config from \`${JSON.stringify( 268 | originalExtendSource, 269 | )}\` in ${options.cwd}`, 270 | ); 271 | continue; 272 | } 273 | const _config = await resolveConfig(extendSource, options, sourceOptions); 274 | if (!_config.config) { 275 | // TODO: Use error in next major versions 276 | 277 | console.warn( 278 | `Cannot extend config from \`${extendSource}\` in ${options.cwd}`, 279 | ); 280 | continue; 281 | } 282 | await extendConfig(_config.config, { ...options, cwd: _config.cwd }); 283 | config._layers.push(_config); 284 | if (_config.config._layers) { 285 | config._layers.push(..._config.config._layers); 286 | delete _config.config._layers; 287 | } 288 | } 289 | } 290 | 291 | // TODO: Either expose from giget directly or redirect all non file:// protocols to giget 292 | const GIGET_PREFIXES = [ 293 | "gh:", 294 | "github:", 295 | "gitlab:", 296 | "bitbucket:", 297 | "https://", 298 | "http://", 299 | ]; 300 | 301 | // https://github.com/dword-design/package-name-regex 302 | const NPM_PACKAGE_RE = 303 | /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/; 304 | 305 | async function resolveConfig< 306 | T extends UserInputConfig = UserInputConfig, 307 | MT extends ConfigLayerMeta = ConfigLayerMeta, 308 | >( 309 | source: string, 310 | options: LoadConfigOptions, 311 | sourceOptions: SourceOptions = {}, 312 | ): Promise> { 313 | // Custom user resolver 314 | if (options.resolve) { 315 | const res = await options.resolve(source, options); 316 | if (res) { 317 | return res; 318 | } 319 | } 320 | 321 | // Custom merger 322 | const _merger = options.merger || defu; 323 | 324 | // Download giget URIs and resolve to local path 325 | const customProviderKeys = Object.keys( 326 | sourceOptions.giget?.providers || {}, 327 | ).map((key) => `${key}:`); 328 | const gigetPrefixes = 329 | customProviderKeys.length > 0 330 | ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] 331 | : GIGET_PREFIXES; 332 | 333 | if ( 334 | options.giget !== false && 335 | gigetPrefixes.some((prefix) => source.startsWith(prefix)) 336 | ) { 337 | const { downloadTemplate } = await import("giget"); 338 | const { digest } = await import("ohash"); 339 | 340 | const cloneName = 341 | source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + 342 | "_" + 343 | digest(source).slice(0, 10).replace(/[-_]/g, ""); 344 | 345 | let cloneDir: string; 346 | 347 | const localNodeModules = resolve(options.cwd!, "node_modules"); 348 | 349 | const parentDir = dirname(options.cwd!); 350 | if (basename(parentDir) === ".c12") { 351 | cloneDir = join(parentDir, cloneName); 352 | } else if (existsSync(localNodeModules)) { 353 | cloneDir = join(localNodeModules, ".c12", cloneName); 354 | } else { 355 | cloneDir = process.env.XDG_CACHE_HOME 356 | ? resolve(process.env.XDG_CACHE_HOME, "c12", cloneName) 357 | : resolve(homedir(), ".cache/c12", cloneName); 358 | } 359 | 360 | if (existsSync(cloneDir) && !sourceOptions.install) { 361 | await rm(cloneDir, { recursive: true }); 362 | } 363 | const cloned = await downloadTemplate(source, { 364 | dir: cloneDir, 365 | install: sourceOptions.install, 366 | force: sourceOptions.install, 367 | auth: sourceOptions.auth, 368 | ...options.giget, 369 | ...sourceOptions.giget, 370 | }); 371 | source = cloned.dir; 372 | } 373 | 374 | // Try resolving as npm package 375 | if (NPM_PACKAGE_RE.test(source)) { 376 | source = tryResolve(source, options) || source; 377 | } 378 | 379 | // Import from local fs 380 | const ext = extname(source); 381 | const isDir = !ext || ext === basename(source); /* #71 */ 382 | const cwd = resolve(options.cwd!, isDir ? source : dirname(source)); 383 | if (isDir) { 384 | source = options.configFile!; 385 | } 386 | const res: ResolvedConfig = { 387 | config: undefined as unknown as T, 388 | configFile: undefined, 389 | cwd, 390 | source, 391 | sourceOptions, 392 | }; 393 | 394 | res.configFile = 395 | tryResolve(resolve(cwd, source), options) || 396 | tryResolve( 397 | resolve(cwd, ".config", source.replace(/\.config$/, "")), 398 | options, 399 | ) || 400 | tryResolve(resolve(cwd, ".config", source), options) || 401 | source; 402 | 403 | if (!existsSync(res.configFile!)) { 404 | return res; 405 | } 406 | 407 | res._configFile = res.configFile; 408 | 409 | const configFileExt = extname(res.configFile!) || ""; 410 | if (configFileExt in ASYNC_LOADERS) { 411 | const asyncLoader = 412 | await ASYNC_LOADERS[configFileExt as keyof typeof ASYNC_LOADERS](); 413 | const contents = await readFile(res.configFile!, "utf8"); 414 | res.config = asyncLoader(contents); 415 | } else { 416 | res.config = (await options.jiti!.import(res.configFile!, { 417 | default: true, 418 | })) as T; 419 | } 420 | if (typeof res.config === "function") { 421 | res.config = await ( 422 | res.config as (ctx?: ConfigFunctionContext) => Promise 423 | )(options.context); 424 | } 425 | 426 | // Extend env specific config 427 | if (options.envName) { 428 | const envConfig = { 429 | ...res.config!["$" + options.envName], 430 | ...res.config!.$env?.[options.envName], 431 | }; 432 | if (Object.keys(envConfig).length > 0) { 433 | res.config = _merger(envConfig, res.config); 434 | } 435 | } 436 | 437 | // Meta 438 | res.meta = defu(res.sourceOptions!.meta, res.config!.$meta) as MT; 439 | delete res.config!.$meta; 440 | 441 | // Overrides 442 | if (res.sourceOptions!.overrides) { 443 | res.config = _merger(res.sourceOptions!.overrides, res.config) as T; 444 | } 445 | 446 | // Always windows paths 447 | res.configFile = _normalize(res.configFile); 448 | res.source = _normalize(res.source); 449 | 450 | return res; 451 | } 452 | 453 | // --- internal --- 454 | 455 | function tryResolve(id: string, options: LoadConfigOptions) { 456 | const res = resolveModulePath(id, { 457 | try: true, 458 | from: pathToFileURL(join(options.cwd || ".", options.configFile || "/")), 459 | suffixes: ["", "/index"], 460 | extensions: SUPPORTED_EXTENSIONS, 461 | cache: false, 462 | }); 463 | return res ? normalize(res) : undefined; 464 | } 465 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## v3.3.3 6 | 7 | [compare changes](https://github.com/unjs/c12/compare/v3.3.2...v3.3.3) 8 | 9 | ### 📦 Build 10 | 11 | - Update chokidar to 5.x ([0dcf338](https://github.com/unjs/c12/commit/0dcf338)) 12 | 13 | ### 🏡 Chore 14 | 15 | - Update dev deps ([78ca00b](https://github.com/unjs/c12/commit/78ca00b)) 16 | 17 | ### ❤️ Contributors 18 | 19 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 20 | 21 | ## v3.3.2 22 | 23 | [compare changes](https://github.com/unjs/c12/compare/v3.3.1...v3.3.2) 24 | 25 | ### 📖 Documentation 26 | 27 | - Fix typo in `globalRc` option ([#281](https://github.com/unjs/c12/pull/281)) 28 | 29 | ### 📦 Build 30 | 31 | - Switch to obuild (rolldown) ([9addbb1](https://github.com/unjs/c12/commit/9addbb1)) 32 | - Relax `magicast` peer dependency range ([eae6be1](https://github.com/unjs/c12/commit/eae6be1)) 33 | 34 | ### 🏡 Chore 35 | 36 | - Update dev deps ([26fe8fe](https://github.com/unjs/c12/commit/26fe8fe)) 37 | - Update deps ([16fd49d](https://github.com/unjs/c12/commit/16fd49d)) 38 | - Update deps ([120c0e6](https://github.com/unjs/c12/commit/120c0e6)) 39 | - Update scripts ([543b39c](https://github.com/unjs/c12/commit/543b39c)) 40 | 41 | ### ❤️ Contributors 42 | 43 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 44 | - Jan T. Sott 45 | 46 | ## v3.3.1 47 | 48 | [compare changes](https://github.com/unjs/c12/compare/v3.3.0...v3.3.1) 49 | 50 | ### 🩹 Fixes 51 | 52 | - Extend with explicit extensions only ([#268](https://github.com/unjs/c12/pull/268), [#276](https://github.com/unjs/c12/pull/276)) 53 | 54 | ### 🏡 Chore 55 | 56 | - Update lockfile ([7dc6386](https://github.com/unjs/c12/commit/7dc6386)) 57 | - Update fixture ([128f2f6](https://github.com/unjs/c12/commit/128f2f6)) 58 | 59 | ### ❤️ Contributors 60 | 61 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 62 | - Ígor Jacaúna ([@igorjacauna](https://github.com/igorjacauna)) 63 | 64 | ## v3.3.0 65 | 66 | [compare changes](https://github.com/unjs/c12/compare/v3.2.0...v3.3.0) 67 | 68 | ### 🚀 Enhancements 69 | 70 | - Support loading config with array exports ([#272](https://github.com/unjs/c12/pull/272)) 71 | - Allow extends without extension ([#268](https://github.com/unjs/c12/pull/268)) 72 | 73 | ### 🩹 Fixes 74 | 75 | - **loadDotenv:** `cwd` is optional ([#273](https://github.com/unjs/c12/pull/273)) 76 | 77 | ### 📖 Documentation 78 | 79 | - Improve `dotenv` section with multiple files example ([#270](https://github.com/unjs/c12/pull/270)) 80 | 81 | ### 🏡 Chore 82 | 83 | - Update readme ([5f803d6](https://github.com/unjs/c12/commit/5f803d6)) 84 | - Sort a-z ([40207d2](https://github.com/unjs/c12/commit/40207d2)) 85 | - Update lockfile ([2cc4f5e](https://github.com/unjs/c12/commit/2cc4f5e)) 86 | - Update lockfile ([38e04db](https://github.com/unjs/c12/commit/38e04db)) 87 | 88 | ### ✅ Tests 89 | 90 | - Add missing await ([63e5b5e](https://github.com/unjs/c12/commit/63e5b5e)) 91 | - Update snapshot ([bc671c4](https://github.com/unjs/c12/commit/bc671c4)) 92 | 93 | ### ❤️ Contributors 94 | 95 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 96 | - Devbro1 97 | - Carson ([@carson2222](https://github.com/carson2222)) 98 | - Igal Klebanov 99 | 100 | ## v3.2.0 101 | 102 | [compare changes](https://github.com/unjs/c12/compare/v3.1.0...v3.2.0) 103 | 104 | ### 🚀 Enhancements 105 | 106 | - Support scoped env files ([#256](https://github.com/unjs/c12/pull/256)) 107 | - Support `context` for function config ([#258](https://github.com/unjs/c12/pull/258)) 108 | - Support `configFileRequired` ([#241](https://github.com/unjs/c12/pull/241)) 109 | 110 | ### 🏡 Chore 111 | 112 | - **readme:** Add prisma to the "used by" list ([#255](https://github.com/unjs/c12/pull/255)) 113 | - Update deps ([bb49bc2](https://github.com/unjs/c12/commit/bb49bc2)) 114 | - Update dotenv to v17 ([9044e56](https://github.com/unjs/c12/commit/9044e56)) 115 | - Simplify readme ([61fdae7](https://github.com/unjs/c12/commit/61fdae7)) 116 | - Simplify docs ([e896c6a](https://github.com/unjs/c12/commit/e896c6a)) 117 | 118 | ### ✅ Tests 119 | 120 | - Update snapshot ([c4db5e5](https://github.com/unjs/c12/commit/c4db5e5)) 121 | 122 | ### ❤️ Contributors 123 | 124 | - Kanon ([@ysknsid25](https://github.com/ysknsid25)) 125 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 126 | - SilverSnow <1664816390@qq.com> 127 | - Giorgio Boa ([@gioboa](https://github.com/gioboa)) 128 | - Alberto Schiabel ([@jkomyno](https://github.com/jkomyno)) 129 | 130 | ## v3.1.0 131 | 132 | [compare changes](https://github.com/unjs/c12/compare/v3.0.4...v3.1.0) 133 | 134 | ### 🚀 Enhancements 135 | 136 | - Pass raw configs to function sources ([#253](https://github.com/unjs/c12/pull/253)) 137 | 138 | ### 📖 Documentation 139 | 140 | - Update nitro link ([#248](https://github.com/unjs/c12/pull/248)) 141 | 142 | ### 🏡 Chore 143 | 144 | - Update deps ([998de19](https://github.com/unjs/c12/commit/998de19)) 145 | - Lint ([2a6a251](https://github.com/unjs/c12/commit/2a6a251)) 146 | 147 | ### ❤️ Contributors 148 | 149 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 150 | - @beer ([@iiio2](https://github.com/iiio2)) 151 | 152 | ## v3.0.4 153 | 154 | [compare changes](https://github.com/unjs/c12/compare/v3.0.3...v3.0.4) 155 | 156 | ### 🩹 Fixes 157 | 158 | - Handle watcher reload errors ([#247](https://github.com/unjs/c12/pull/247)) 159 | 160 | ### 🏡 Chore 161 | 162 | - Update deps ([1891b2e](https://github.com/unjs/c12/commit/1891b2e)) 163 | 164 | ### ❤️ Contributors 165 | 166 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 167 | 168 | ## v3.0.3 169 | 170 | [compare changes](https://github.com/unjs/c12/compare/v3.0.2...v3.0.3) 171 | 172 | ### 🩹 Fixes 173 | 174 | - Check if `.env/` is a directory before accessing ([#238](https://github.com/unjs/c12/pull/238)) 175 | - Update dotenv assigned env variables on subsequent calls ([#243](https://github.com/unjs/c12/pull/243)) 176 | 177 | ### 📖 Documentation 178 | 179 | - Add `resolve` option ([#240](https://github.com/unjs/c12/pull/240)) 180 | - Remove unsupported `configFile: false` ([#239](https://github.com/unjs/c12/pull/239)) 181 | 182 | ### 🏡 Chore 183 | 184 | - Update deps ([b5b3e43](https://github.com/unjs/c12/commit/b5b3e43)) 185 | - Update confbox to 0.2x ([cbbf9a5](https://github.com/unjs/c12/commit/cbbf9a5)) 186 | 187 | ### ✅ Tests 188 | 189 | - Only include `src` for coverage report ([#242](https://github.com/unjs/c12/pull/242)) 190 | 191 | ### ❤️ Contributors 192 | 193 | - Kricsleo ([@kricsleo](https://github.com/kricsleo)) 194 | - Kanon ([@ysknsid25](https://github.com/ysknsid25)) 195 | - Daniel Roe ([@danielroe](https://github.com/danielroe)) 196 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 197 | 198 | ## v3.0.2 199 | 200 | [compare changes](https://github.com/unjs/c12/compare/v3.0.1...v3.0.2) 201 | 202 | ### 🏡 Chore 203 | 204 | - Update exsolve to 1.0.0 ([937bfe4](https://github.com/unjs/c12/commit/937bfe4)) 205 | - Update deps ([0156c8d](https://github.com/unjs/c12/commit/0156c8d)) 206 | 207 | ### ❤️ Contributors 208 | 209 | - Pooya Parsa ([@pi0](https://github.com/pi0)) 210 | 211 | ## v3.0.1 212 | 213 | [compare changes](https://github.com/unjs/c12/compare/v3.0.0...v3.0.1) 214 | 215 | ### 🩹 Fixes 216 | 217 | - Fix windows related resolve issues ([#235](https://github.com/unjs/c12/pull/235)) 218 | 219 | ### ❤️ Contributors 220 | 221 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 222 | 223 | ## v3.0.0 224 | 225 | [compare changes](https://github.com/unjs/c12/compare/v2.0.4...v3.0.0) 226 | 227 | ### 🩹 Fixes 228 | 229 | - Allow custom giget provider ([#207](https://github.com/unjs/c12/pull/207)) 230 | 231 | ### 💅 Refactors 232 | 233 | - Migrate from `mlly` to `exsolve` for module resolution ([822af14](https://github.com/unjs/c12/commit/822af14)) 234 | - Fully migrate to exsolve for module resolution ([146899e](https://github.com/unjs/c12/commit/146899e)) 235 | 236 | ### 📦 Build 237 | 238 | - ⚠️ Esm-only ([d53d0a2](https://github.com/unjs/c12/commit/d53d0a2)) 239 | 240 | ### 🏡 Chore 241 | 242 | - Update deps ([83da8d6](https://github.com/unjs/c12/commit/83da8d6)) 243 | - Update pkg-types to v2 ([6607dcf](https://github.com/unjs/c12/commit/6607dcf)) 244 | - Update lock file ([e180e22](https://github.com/unjs/c12/commit/e180e22)) 245 | - Update giget to v2 ([89d16b3](https://github.com/unjs/c12/commit/89d16b3)) 246 | 247 | #### ⚠️ Breaking Changes 248 | 249 | - ⚠️ Esm-only ([d53d0a2](https://github.com/unjs/c12/commit/d53d0a2)) 250 | 251 | ### ❤️ Contributors 252 | 253 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 254 | - Ayax 255 | 256 | ## v2.0.4 257 | 258 | [compare changes](https://github.com/unjs/c12/compare/v2.0.3...v2.0.4) 259 | 260 | ### 📦 Build 261 | 262 | - Fix typo in exports ([82f560c](https://github.com/unjs/c12/commit/82f560c)) 263 | 264 | ### ❤️ Contributors 265 | 266 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 267 | 268 | ## v2.0.3 269 | 270 | [compare changes](https://github.com/unjs/c12/compare/v2.0.2...v2.0.3) 271 | 272 | ### 💅 Refactors 273 | 274 | - Upgrade to ohash v2 ([#230](https://github.com/unjs/c12/pull/230)) 275 | 276 | ### 📦 Build 277 | 278 | - Update `exports` ([acee667](https://github.com/unjs/c12/commit/acee667)) 279 | 280 | ### 🏡 Chore 281 | 282 | - Update deps ([f5badac](https://github.com/unjs/c12/commit/f5badac)) 283 | 284 | ### ❤️ Contributors 285 | 286 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 287 | 288 | ## v2.0.2 289 | 290 | [compare changes](https://github.com/unjs/c12/compare/v2.0.1...v2.0.2) 291 | 292 | ### 🩹 Fixes 293 | 294 | - Preserve `meta` for main config ([#227](https://github.com/unjs/c12/pull/227)) 295 | 296 | ### 📖 Documentation 297 | 298 | - Add kysely-ctl to readme ([#225](https://github.com/unjs/c12/pull/225)) 299 | 300 | ### 🏡 Chore 301 | 302 | - Update readme ([bc5a6b6](https://github.com/unjs/c12/commit/bc5a6b6)) 303 | - Update deps ([0d4778b](https://github.com/unjs/c12/commit/0d4778b)) 304 | - Fix ci ([38a0e95](https://github.com/unjs/c12/commit/38a0e95)) 305 | - Update pathe to 2.x ([cc72a9a](https://github.com/unjs/c12/commit/cc72a9a)) 306 | 307 | ### ❤️ Contributors 308 | 309 | - Daniel Roe ([@danielroe](http://github.com/danielroe)) 310 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 311 | - Igal Klebanov 312 | 313 | ## v2.0.1 314 | 315 | [compare changes](https://github.com/unjs/c12/compare/v2.0.0...v2.0.1) 316 | 317 | ### 🩹 Fixes 318 | 319 | - Update to jiti 2.3 ([1420b72](https://github.com/unjs/c12/commit/1420b72)) 320 | 321 | ### 🏡 Chore 322 | 323 | - Update deps ([40b510e](https://github.com/unjs/c12/commit/40b510e)) 324 | 325 | ### ❤️ Contributors 326 | 327 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 328 | 329 | ## v2.0.0 330 | 331 | [compare changes](https://github.com/unjs/c12/compare/v2.0.0-beta.3...v2.0.0) 332 | 333 | ### 🏡 Chore 334 | 335 | - Update release script ([7cfd90f](https://github.com/unjs/c12/commit/7cfd90f)) 336 | - Update deps ([6639a4b](https://github.com/unjs/c12/commit/6639a4b)) 337 | 338 | ### ❤️ Contributors 339 | 340 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 341 | 342 | ## v2.0.0-beta.3 343 | 344 | [compare changes](https://github.com/unjs/c12/compare/v2.0.0-beta.2...v2.0.0-beta.3) 345 | 346 | ### 💅 Refactors 347 | 348 | - Update to jiti 2.0.0 ([c2cc1e9](https://github.com/unjs/c12/commit/c2cc1e9)) 349 | 350 | ### 🏡 Chore 351 | 352 | - Update deps ([24e8c47](https://github.com/unjs/c12/commit/24e8c47)) 353 | - Update chokidar to v4 ([77aa9a2](https://github.com/unjs/c12/commit/77aa9a2)) 354 | 355 | ### ❤️ Contributors 356 | 357 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 358 | 359 | ## v2.0.0-beta.2 360 | 361 | [compare changes](https://github.com/unjs/c12/compare/v2.0.0-beta.1...v2.0.0-beta.2) 362 | 363 | ### 🚀 Enhancements 364 | 365 | - Allow disabling remote extend with `giget: false` ([#181](https://github.com/unjs/c12/pull/181)) 366 | - Support update existing `.config/[name].[ext]` config ([#169](https://github.com/unjs/c12/pull/169)) 367 | 368 | ### 🩹 Fixes 369 | 370 | - **updateConfig:** Properly resolve config relative to cwd ([#188](https://github.com/unjs/c12/pull/188)) 371 | 372 | ### 🏡 Chore 373 | 374 | - Update deps ([6d22a97](https://github.com/unjs/c12/commit/6d22a97)) 375 | 376 | ### ❤️ Contributors 377 | 378 | - Yizack Rangel ([@Yizack](http://github.com/Yizack)) 379 | - Samuel Braun 380 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 381 | 382 | ## v2.0.0-beta.1 383 | 384 | [compare changes](https://github.com/unjs/c12/compare/v1.11.1...v2.0.0-beta.1) 385 | 386 | ### 🚀 Enhancements 387 | 388 | - Upgrade to jiti v2 beta ([#172](https://github.com/unjs/c12/pull/172)) 389 | 390 | ### 🏡 Chore 391 | 392 | - Aadd hey-api to list of users ([#171](https://github.com/unjs/c12/pull/171)) 393 | - Update release script for beta ([0127b2d](https://github.com/unjs/c12/commit/0127b2d)) 394 | - Stricter tsconfig ([e930e6b](https://github.com/unjs/c12/commit/e930e6b)) 395 | - Update deps ([da3595c](https://github.com/unjs/c12/commit/da3595c)) 396 | 397 | ### ❤️ Contributors 398 | 399 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 400 | - Lubos ([@mrlubos](http://github.com/mrlubos)) 401 | 402 | ## v1.11.1 403 | 404 | [compare changes](https://github.com/unjs/c12/compare/v1.11.0...v1.11.1) 405 | 406 | ### 🩹 Fixes 407 | 408 | - **update:** Await on `onUpdate` ([6b37c98](https://github.com/unjs/c12/commit/6b37c98)) 409 | - **update:** Respect falsy value of `onCreate` ([cc4e991](https://github.com/unjs/c12/commit/cc4e991)) 410 | - **update:** Use relative path to resolve config ([8b58b25](https://github.com/unjs/c12/commit/8b58b25)) 411 | 412 | ### ❤️ Contributors 413 | 414 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 415 | 416 | ## v1.11.0 417 | 418 | [compare changes](https://github.com/unjs/c12/compare/v1.10.0...v1.11.0) 419 | 420 | ### 🚀 Enhancements 421 | 422 | - Resolvable configs ([#159](https://github.com/unjs/c12/pull/159)) 423 | - Custom merger to replace built-in defu ([#160](https://github.com/unjs/c12/pull/160)) 424 | - Config update util ([#162](https://github.com/unjs/c12/pull/162)) 425 | 426 | ### 🩹 Fixes 427 | 428 | - **loadConfig:** `config` is not nullable ([#161](https://github.com/unjs/c12/pull/161)) 429 | 430 | ### 💅 Refactors 431 | 432 | - Internally use named sources ([#158](https://github.com/unjs/c12/pull/158)) 433 | 434 | ### 🏡 Chore 435 | 436 | - Update dependencies ([3105900](https://github.com/unjs/c12/commit/3105900)) 437 | - Update to eslint v9 ([ddbb78c](https://github.com/unjs/c12/commit/ddbb78c)) 438 | - Lint ([06f21a1](https://github.com/unjs/c12/commit/06f21a1)) 439 | - Apply automated updates ([fa0fda1](https://github.com/unjs/c12/commit/fa0fda1)) 440 | - Fix typo (overridden) ([09bb378](https://github.com/unjs/c12/commit/09bb378)) 441 | - Update snapshot ([8b64427](https://github.com/unjs/c12/commit/8b64427)) 442 | - Update test ([764f4ac](https://github.com/unjs/c12/commit/764f4ac)) 443 | 444 | ### ❤️ Contributors 445 | 446 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 447 | 448 | ## v1.10.0 449 | 450 | [compare changes](https://github.com/unjs/c12/compare/v1.9.0...v1.10.0) 451 | 452 | ### 🚀 Enhancements 453 | 454 | - Support `auth` shortcut for layer config ([#142](https://github.com/unjs/c12/pull/142)) 455 | 456 | ### 🏡 Chore 457 | 458 | - Update automd ([a5834c7](https://github.com/unjs/c12/commit/a5834c7)) 459 | - Update ci ([b970591](https://github.com/unjs/c12/commit/b970591)) 460 | - Apply automated updates ([db37eaa](https://github.com/unjs/c12/commit/db37eaa)) 461 | 462 | ### ❤️ Contributors 463 | 464 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 465 | 466 | ## v1.9.0 467 | 468 | [compare changes](https://github.com/unjs/c12/compare/v1.8.0...v1.9.0) 469 | 470 | ### 🚀 Enhancements 471 | 472 | - Use confbox ([#140](https://github.com/unjs/c12/pull/140)) 473 | 474 | ### 🔥 Performance 475 | 476 | - Lazy load `chokidar` ([a8b3a1d](https://github.com/unjs/c12/commit/a8b3a1d)) 477 | 478 | ### 🩹 Fixes 479 | 480 | - Deep merge rc sources with defu ([#139](https://github.com/unjs/c12/pull/139)) 481 | - **watcher:** Watch `.config` and all supported extensions ([94c8181](https://github.com/unjs/c12/commit/94c8181)) 482 | 483 | ### ❤️ Contributors 484 | 485 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 486 | - Sébastien Chopin ([@Atinux](http://github.com/Atinux)) 487 | 488 | ## v1.8.0 489 | 490 | [compare changes](https://github.com/unjs/c12/compare/v1.7.0...v1.8.0) 491 | 492 | ### 🚀 Enhancements 493 | 494 | - Support `.config` dir ([#136](https://github.com/unjs/c12/pull/136)) 495 | 496 | ### 🩹 Fixes 497 | 498 | - Use default export of `json5` for parsing ([#135](https://github.com/unjs/c12/pull/135)) 499 | 500 | ### 🏡 Chore 501 | 502 | - Add used by section ([9e998a8](https://github.com/unjs/c12/commit/9e998a8)) 503 | - Use automd ([b114398](https://github.com/unjs/c12/commit/b114398)) 504 | 505 | ### ✅ Tests 506 | 507 | - Refactor to use named configs ([329b6f8](https://github.com/unjs/c12/commit/329b6f8)) 508 | - Update tests ([593619a](https://github.com/unjs/c12/commit/593619a)) 509 | 510 | ### ❤️ Contributors 511 | 512 | - Sadegh Barati ([@sadeghbarati](http://github.com/sadeghbarati)) 513 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 514 | 515 | ## v1.7.0 516 | 517 | [compare changes](https://github.com/unjs/c12/compare/v1.6.1...v1.7.0) 518 | 519 | ### 🚀 Enhancements 520 | 521 | - `.jsonc` config support ([#132](https://github.com/unjs/c12/pull/132)) 522 | - Json5 support ([#133](https://github.com/unjs/c12/pull/133)) 523 | 524 | ### ❤️ Contributors 525 | 526 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 527 | 528 | ## v1.6.1 529 | 530 | [compare changes](https://github.com/unjs/c12/compare/v1.6.0...v1.6.1) 531 | 532 | ### 🩹 Fixes 533 | 534 | - Preserve cloned dir if `install` option provided ([81e2891](https://github.com/unjs/c12/commit/81e2891)) 535 | 536 | ### ❤️ Contributors 537 | 538 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 539 | 540 | ## v1.6.0 541 | 542 | [compare changes](https://github.com/unjs/c12/compare/v1.5.1...v1.6.0) 543 | 544 | ### 🚀 Enhancements 545 | 546 | - Option to omit $ keys from resolved config ([#100](https://github.com/unjs/c12/pull/100)) 547 | - Support `install` for source options ([#126](https://github.com/unjs/c12/pull/126)) 548 | 549 | ### 🩹 Fixes 550 | 551 | - Normalize windows backslash for `configFile` and `source` ([#48](https://github.com/unjs/c12/pull/48)) 552 | - Clone sub layers into `node_modules/.c12` ([#125](https://github.com/unjs/c12/pull/125)) 553 | - Handle `http://` prefixes with giget as well ([6c09735](https://github.com/unjs/c12/commit/6c09735)) 554 | 555 | ### 📖 Documentation 556 | 557 | - Add package pronunciation ([#118](https://github.com/unjs/c12/pull/118)) 558 | 559 | ### 🏡 Chore 560 | 561 | - Update docs ([54ed82b](https://github.com/unjs/c12/commit/54ed82b)) 562 | - Update lockfile and vitest ([fecad1a](https://github.com/unjs/c12/commit/fecad1a)) 563 | 564 | ### ❤️ Contributors 565 | 566 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 567 | - Lo ([@LoTwT](http://github.com/LoTwT)) 568 | - Alex Kozack 569 | - Nozomu Ikuta 570 | 571 | ## v1.5.1 572 | 573 | [compare changes](https://github.com/unjs/c12/compare/v1.4.2...v1.5.1) 574 | 575 | ### 🚀 Enhancements 576 | 577 | - Improve extending github layers ([#109](https://github.com/unjs/c12/pull/109)) 578 | - Allow setting giget clone options ([#112](https://github.com/unjs/c12/pull/112)) 579 | 580 | ### 🏡 Chore 581 | 582 | - Update dependencies ([1f2ab64](https://github.com/unjs/c12/commit/1f2ab64)) 583 | - Update release script ([6c21f09](https://github.com/unjs/c12/commit/6c21f09)) 584 | 585 | ### ✅ Tests 586 | 587 | - Update gh fixture to main ([a8b73c2](https://github.com/unjs/c12/commit/a8b73c2)) 588 | 589 | ### 🎨 Styles 590 | 591 | - Lint with prettier v3 ([7940e9b](https://github.com/unjs/c12/commit/7940e9b)) 592 | 593 | ### ❤️ Contributors 594 | 595 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 596 | 597 | ## v1.4.2 598 | 599 | [compare changes](https://github.com/unjs/c12/compare/v1.4.1...v1.4.2) 600 | 601 | 602 | ### 🩹 Fixes 603 | 604 | - Allow extends dir to start with dot ([#71](https://github.com/unjs/c12/pull/71)) 605 | 606 | ### 📖 Documentation 607 | 608 | - Fix typo for `configFile` ([#83](https://github.com/unjs/c12/pull/83)) 609 | 610 | ### 🏡 Chore 611 | 612 | - **release:** V1.4.1 ([2b87193](https://github.com/unjs/c12/commit/2b87193)) 613 | - Update dependencies ([309454a](https://github.com/unjs/c12/commit/309454a)) 614 | - Lint project ([a102400](https://github.com/unjs/c12/commit/a102400)) 615 | - Lint ([e19a6ff](https://github.com/unjs/c12/commit/e19a6ff)) 616 | 617 | ### ❤️ Contributors 618 | 619 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 620 | - Rijk Van Zanten ([@rijkvanzanten](http://github.com/rijkvanzanten)) 621 | 622 | ## v1.4.1 623 | 624 | [compare changes](https://github.com/unjs/c12/compare/v1.4.0...v1.4.1) 625 | 626 | 627 | ### 🩹 Fixes 628 | 629 | - **watchConfig:** Handle custom config names ([eedd141](https://github.com/unjs/c12/commit/eedd141)) 630 | 631 | ### ❤️ Contributors 632 | 633 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 634 | 635 | ## v1.4.0 636 | 637 | [compare changes](https://github.com/unjs/c12/compare/v1.3.0...v1.4.0) 638 | 639 | 640 | ### 🚀 Enhancements 641 | 642 | - `watchConfig` utility ([#77](https://github.com/unjs/c12/pull/77)) 643 | - **watchConfig:** Support hmr ([#78](https://github.com/unjs/c12/pull/78)) 644 | 645 | ### 📖 Documentation 646 | 647 | - Fix small grammer issues ([5f2b3a1](https://github.com/unjs/c12/commit/5f2b3a1)) 648 | 649 | ### ❤️ Contributors 650 | 651 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 652 | 653 | ## v1.3.0 654 | 655 | [compare changes](https://github.com/unjs/c12/compare/v1.2.0...v1.3.0) 656 | 657 | 658 | ### 🚀 Enhancements 659 | 660 | - Generic types support ([#64](https://github.com/unjs/c12/pull/64)) 661 | 662 | ### 🩹 Fixes 663 | 664 | - Use `rm` instead of `rmdir` for recursive remove ([#69](https://github.com/unjs/c12/pull/69)) 665 | 666 | ### 🏡 Chore 667 | 668 | - **readme:** Update badges ([ff08ce2](https://github.com/unjs/c12/commit/ff08ce2)) 669 | - **readme:** Add emoji ([9df0498](https://github.com/unjs/c12/commit/9df0498)) 670 | - Update to pnpm 8 ([ecec1f2](https://github.com/unjs/c12/commit/ecec1f2)) 671 | 672 | ### ❤️ Contributors 673 | 674 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 675 | - Daniel Roe 676 | - Sébastien Chopin ([@Atinux](http://github.com/Atinux)) 677 | 678 | ## v1.2.0 679 | 680 | [compare changes](https://github.com/unjs/c12/compare/v1.1.2...v1.2.0) 681 | 682 | 683 | ### 🚀 Enhancements 684 | 685 | - Load config from `package.json` ([#52](https://github.com/unjs/c12/pull/52)) 686 | - Environment specific configuration ([#61](https://github.com/unjs/c12/pull/61)) 687 | - Layer meta and source options ([#62](https://github.com/unjs/c12/pull/62)) 688 | - `envName` config ([4a0227d](https://github.com/unjs/c12/commit/4a0227d)) 689 | 690 | ### 🩹 Fixes 691 | 692 | - Allow extending from npm packages with subpath ([#54](https://github.com/unjs/c12/pull/54)) 693 | 694 | ### 📖 Documentation 695 | 696 | - Fix grammer and typos ([3e8436c](https://github.com/unjs/c12/commit/3e8436c)) 697 | - Don't mention unsupported usage ([ea7ac6e](https://github.com/unjs/c12/commit/ea7ac6e)) 698 | 699 | ### 🏡 Chore 700 | 701 | - Update badge ([b0c78e2](https://github.com/unjs/c12/commit/b0c78e2)) 702 | - Update readme ([8480e41](https://github.com/unjs/c12/commit/8480e41)) 703 | - Update mlly ([cf6ef84](https://github.com/unjs/c12/commit/cf6ef84)) 704 | 705 | ### ✅ Tests 706 | 707 | - Update test for env extends ([f363687](https://github.com/unjs/c12/commit/f363687)) 708 | - Update snapshot ([071180f](https://github.com/unjs/c12/commit/071180f)) 709 | 710 | ### ❤️ Contributors 711 | 712 | - Pooya Parsa ([@pi0](http://github.com/pi0)) 713 | - Christian Preston ([@cpreston321](http://github.com/cpreston321)) 714 | - Guillaume Chau ([@Akryum](http://github.com/Akryum)) 715 | 716 | ## v1.1.2 717 | 718 | [compare changes](https://github.com/unjs/c12/compare/v1.1.1...v1.1.2) 719 | 720 | 721 | ### 🏡 Chore 722 | 723 | - Update dependencies ([efac912](https://github.com/unjs/c12/commit/efac912)) 724 | 725 | ### ❤️ Contributors 726 | 727 | - Pooya Parsa 728 | 729 | ## v1.1.1 730 | 731 | [compare changes](https://github.com/unjs/c12/compare/v1.1.0...v1.1.1) 732 | 733 | 734 | ### 🏡 Chore 735 | 736 | - Update mlly ([b085c9b](https://github.com/unjs/c12/commit/b085c9b)) 737 | 738 | ### 🎨 Styles 739 | 740 | - Format with prettier ([f66ddd6](https://github.com/unjs/c12/commit/f66ddd6)) 741 | 742 | ### ❤️ Contributors 743 | 744 | - Pooya Parsa 745 | 746 | ## [1.1.0](https://github.com/unjs/c12/compare/v1.0.1...v1.1.0) (2022-12-06) 747 | 748 | 749 | ### Features 750 | 751 | * use giget to clone github urls ([4c7590a](https://github.com/unjs/c12/commit/4c7590ab94c667acd45fb1df05026d49b89431bc)) 752 | 753 | 754 | ### Bug Fixes 755 | 756 | * remove tmp dir to clone ([020e0b0](https://github.com/unjs/c12/commit/020e0b0ede67d02ce6953402201d3913c237dd1c)) 757 | 758 | ### [1.0.1](https://github.com/unjs/c12/compare/v1.0.0...v1.0.1) (2022-11-15) 759 | 760 | ## [1.0.0](https://github.com/unjs/c12/compare/v0.2.13...v1.0.0) (2022-11-15) 761 | 762 | ### [0.2.13](https://github.com/unjs/c12/compare/v0.2.12...v0.2.13) (2022-09-19) 763 | 764 | ### [0.2.12](https://github.com/unjs/c12/compare/v0.2.11...v0.2.12) (2022-09-14) 765 | 766 | 767 | ### Features 768 | 769 | * `defaultConfig` to be applied before extending ([1c4e898](https://github.com/unjs/c12/commit/1c4e8984e9ecacdeedfe5e2a98e5cb3991e94462)) 770 | 771 | ### [0.2.11](https://github.com/unjs/c12/compare/v0.2.10...v0.2.11) (2022-09-06) 772 | 773 | 774 | ### Features 775 | 776 | * custom `jiti` and `jitiOptions` ([bfd1be5](https://github.com/unjs/c12/commit/bfd1be5a21556eff57c75c1a9d6bece109823923)) 777 | * support loading rc from workspace dir in global mode ([7365a9c](https://github.com/unjs/c12/commit/7365a9cea52c6f9b7d266338bf8096c87c24b5ce)) 778 | 779 | 780 | ### Bug Fixes 781 | 782 | * `jitiOptions` is optional ([457a045](https://github.com/unjs/c12/commit/457a045683bb491bf9a42d035135b3dc7afce07b)) 783 | * validate sources value to be string ([#32](https://github.com/unjs/c12/issues/32)) ([f97c850](https://github.com/unjs/c12/commit/f97c850e81f2049e74194b76b82c82974a775141)) 784 | 785 | ### [0.2.10](https://github.com/unjs/c12/compare/v0.2.9...v0.2.10) (2022-09-01) 786 | 787 | 788 | ### Features 789 | 790 | * allow extending from multiple keys ([33cb210](https://github.com/unjs/c12/commit/33cb21032ec9d06baac4c69fc0dbf174b89b8944)), closes [#24](https://github.com/unjs/c12/issues/24) 791 | 792 | ### [0.2.9](https://github.com/unjs/c12/compare/v0.2.8...v0.2.9) (2022-08-04) 793 | 794 | 795 | ### Features 796 | 797 | * use native esm resolution where possible ([#26](https://github.com/unjs/c12/issues/26)) ([9744621](https://github.com/unjs/c12/commit/97446215b1069b5f8dec68528e0cfcecdd9f5659)) 798 | 799 | ### [0.2.8](https://github.com/unjs/c12/compare/v0.2.7...v0.2.8) (2022-06-29) 800 | 801 | 802 | ### Features 803 | 804 | * try resolving paths as npm package ([7c48947](https://github.com/unjs/c12/commit/7c48947754bce2f881d153eb3c490f2940814c80)) 805 | 806 | 807 | ### Bug Fixes 808 | 809 | * warn when extend layers cannot be resolved ([f6506e8](https://github.com/unjs/c12/commit/f6506e814520716908944be0d2845b489feac353)) 810 | 811 | ### [0.2.7](https://github.com/unjs/c12/compare/v0.2.6...v0.2.7) (2022-04-20) 812 | 813 | 814 | ### Bug Fixes 815 | 816 | * check resolved config file existence before loading ([dda579d](https://github.com/unjs/c12/commit/dda579d26467bb8e3f2964de2f76057cc48edbcf)) 817 | 818 | ### [0.2.6](https://github.com/unjs/c12/compare/v0.2.5...v0.2.6) (2022-04-20) 819 | 820 | 821 | ### Bug Fixes 822 | 823 | * only ignore `MODULE_NOT_FOUND` when message contains configFile ([e067a56](https://github.com/unjs/c12/commit/e067a56e5d47bf396b6b8abd898fff249a1037fd)) 824 | 825 | ### [0.2.5](https://github.com/unjs/c12/compare/v0.2.4...v0.2.5) (2022-04-07) 826 | 827 | ### [0.2.4](https://github.com/unjs/c12/compare/v0.2.3...v0.2.4) (2022-03-21) 828 | 829 | 830 | ### Bug Fixes 831 | 832 | * avoid double merging of layers ([367cbf8](https://github.com/unjs/c12/commit/367cbf876bad7c389a462409c1cb1481c7b61804)), closes [nuxt/framework#3800](https://github.com/nuxt/framework/issues/3800) 833 | 834 | ### [0.2.3](https://github.com/unjs/c12/compare/v0.2.2...v0.2.3) (2022-03-18) 835 | 836 | 837 | ### Bug Fixes 838 | 839 | * don't strip empty config files ([#8](https://github.com/unjs/c12/issues/8)) ([67bb1ee](https://github.com/unjs/c12/commit/67bb1ee1d36a0668ccf259a42148002b20ff0c25)) 840 | 841 | ### [0.2.2](https://github.com/unjs/c12/compare/v0.2.0...v0.2.2) (2022-03-16) 842 | 843 | ## [0.2.0](https://github.com/unjs/c12/compare/v0.1.4...v0.2.0) (2022-03-16) 844 | 845 | 846 | ### ⚠ BREAKING CHANGES 847 | 848 | * preserve all merging sources 849 | 850 | ### Features 851 | 852 | * preserve all merging sources ([7a69480](https://github.com/unjs/c12/commit/7a694809c6b21c22fada40256373aced9d56d706)) 853 | 854 | ### [0.1.4](https://github.com/unjs/c12/compare/v0.1.3...v0.1.4) (2022-03-07) 855 | 856 | 857 | ### Bug Fixes 858 | 859 | * disable `requireCache` ([#6](https://github.com/unjs/c12/issues/6)) ([1a6f7d3](https://github.com/unjs/c12/commit/1a6f7d368b643bcebfa38d160c3c31dd7339ae65)) 860 | 861 | ### [0.1.3](https://github.com/unjs/c12/compare/v0.1.2...v0.1.3) (2022-02-10) 862 | 863 | 864 | ### Bug Fixes 865 | 866 | * apply defaults after extending ([c86024c](https://github.com/unjs/c12/commit/c86024cdc13708b837e5da717fde91ed1bbf6e9a)) 867 | 868 | ### [0.1.2](https://github.com/unjs/c12/compare/v0.1.1...v0.1.2) (2022-02-10) 869 | 870 | 871 | ### Features 872 | 873 | * extend options ([a76db4d](https://github.com/unjs/c12/commit/a76db4d6c363e0af7e7249f225f036117a750738)) 874 | * support custom resolver ([bd9997b](https://github.com/unjs/c12/commit/bd9997b3e897a9312d4c1bf0862db641d1e5f18f)) 875 | 876 | ### 0.1.1 (2022-01-31) 877 | 878 | 879 | ### Features 880 | 881 | * basic extends support ([#1](https://github.com/unjs/c12/issues/1)) ([ef199fc](https://github.com/unjs/c12/commit/ef199fcdbcfbff85f4a434ffc70aa1fb065c9a9f)) 882 | * extends support with remote repo ([17ef358](https://github.com/unjs/c12/commit/17ef3586c5b844d7a52e44508d05dbb92618f8fa)) 883 | * nested extend support ([4885487](https://github.com/unjs/c12/commit/48854874d9121724961b4275e96675706a86c465)) 884 | 885 | 886 | ### Bug Fixes 887 | 888 | * escape unsupported chars from tmpdir ([fd04922](https://github.com/unjs/c12/commit/fd04922c40a9893e7e98e06d3be650674b5c6508)) 889 | * temp directory initialization ([3aaf5db](https://github.com/unjs/c12/commit/3aaf5dbf57ceb34704de02c4756f0ac50281c6d1)) 890 | --------------------------------------------------------------------------------