From 1293ad1048557b33265f7f0a0cef9433dc97fc8d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 11 Aug 2026 12:48:08 +0000 Subject: [PATCH 1/4] feat: read global rc from xdg config dir --- README.md | 4 +- src/loader.ts | 8 +++- test/global-rc.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 test/global-rc.test.ts diff --git a/README.md b/README.md index 6dc8819e..5d032238 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,9 @@ Set to `false` to disable loading RC config. ### `globalRc` -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. +Load RC config from the workspace directory, the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) and the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. + +When the same key is set in more than one of these, the workspace wins over the user config directory, which wins over the home directory. ### `dotenv` diff --git a/src/loader.ts b/src/loader.ts index 4354e500..cce6a1ed 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -124,8 +124,12 @@ export async function loadConfig< if (workspaceDir) { rcSources.push(rc9.read({ name: options.rcFile, dir: workspaceDir })); } - // 3. user home - rcSources.push(rc9.readUser({ name: options.rcFile, dir: options.cwd })); + // 3. user config dir ($XDG_CONFIG_HOME or ~/.config) + rcSources.push(rc9.readUserConfig({ name: options.rcFile })); + // 4. user home (legacy, skipped when $XDG_CONFIG_HOME makes it the same dir) + if (!process.env.XDG_CONFIG_HOME) { + rcSources.push(rc9.readUser({ name: options.rcFile })); + } } rawConfigs.rc = _merger({} as T, ...rcSources); } diff --git a/test/global-rc.test.ts b/test/global-rc.test.ts new file mode 100644 index 00000000..5ffecaea --- /dev/null +++ b/test/global-rc.test.ts @@ -0,0 +1,89 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { resolve } from "pathe"; +import { loadConfig } from "../src/index.ts"; + +describe("global rc", () => { + let dir: string; + let home: string; + let cwd: string; + const env = { + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, + }; + + const restoreEnv = () => { + for (const [key, value] of Object.entries(env)) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }; + + beforeEach(async () => { + dir = await mkdtemp(resolve(tmpdir(), "c12-global-rc-")); + home = resolve(dir, "home"); + cwd = resolve(dir, "project"); + await mkdir(resolve(home, ".config"), { recursive: true }); + await mkdir(cwd, { recursive: true }); + process.env.HOME = home; + process.env.USERPROFILE = home; + delete process.env.XDG_CONFIG_HOME; + }); + + afterEach(async () => { + restoreEnv(); + await rm(dir, { recursive: true, force: true }); + }); + + const load = () => loadConfig({ cwd, name: "test", globalRc: true, packageJson: false }); + + it("reads the legacy home rc file", async () => { + await writeFile(resolve(home, ".testrc"), "legacy=true\nshared=home\n"); + const { config } = await load(); + expect(config).toMatchObject({ legacy: true, shared: "home" }); + }); + + it("reads the rc file from ~/.config", async () => { + await writeFile(resolve(home, ".config/.testrc"), "xdg=true\nshared=config\n"); + const { config } = await load(); + expect(config).toMatchObject({ xdg: true, shared: "config" }); + }); + + it("prefers ~/.config over the legacy home rc file", async () => { + await writeFile(resolve(home, ".testrc"), "legacy=true\nshared=home\n"); + await writeFile(resolve(home, ".config/.testrc"), "xdg=true\nshared=config\n"); + const { config } = await load(); + expect(config).toMatchObject({ legacy: true, xdg: true, shared: "config" }); + }); + + it("still reads the legacy home rc file when ~/.config rc is empty", async () => { + await writeFile(resolve(home, ".config/.testrc"), ""); + await writeFile(resolve(home, ".testrc"), "legacy=true\nshared=home\n"); + const { config } = await load(); + expect(config).toMatchObject({ legacy: true, shared: "home" }); + }); + + it("prefers the project rc file over both", async () => { + await writeFile(resolve(home, ".testrc"), "shared=home\n"); + await writeFile(resolve(home, ".config/.testrc"), "shared=config\n"); + await writeFile(resolve(cwd, ".testrc"), "shared=project\n"); + const { config } = await load(); + expect(config).toMatchObject({ shared: "project" }); + }); + + it("reads a single location when XDG_CONFIG_HOME is set", async () => { + const xdg = resolve(dir, "xdg"); + await mkdir(xdg, { recursive: true }); + process.env.XDG_CONFIG_HOME = xdg; + await writeFile(resolve(xdg, ".testrc"), "xdg=true\nlist[]=a\n"); + await writeFile(resolve(home, ".testrc"), "legacy=true\n"); + const { config } = await load(); + expect(config).toMatchObject({ xdg: true, list: ["a"] }); + expect(config).not.toHaveProperty("legacy"); + }); +}); From 29fcba94c8661557b1feb9797b915bb6e7db46c0 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 11 Aug 2026 13:55:51 +0000 Subject: [PATCH 2/4] docs: list full global rc order in loading priority --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5d032238..66a4c6bd 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,12 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo 1. Config overrides passed by options 2. Config file in CWD 3. RC file in CWD -4. Global RC file in the user's home directory -5. Config from `package.json` -6. Default config passed by options -7. Extended config layers +4. RC file in the workspace directory +5. RC file in the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) +6. RC file in the user's home directory +7. Config from `package.json` +8. Default config passed by options +9. Extended config layers ## Options From 7d5a0fed184af596939509990724196ab5d6ceba Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 11 Aug 2026 16:50:53 +0000 Subject: [PATCH 3/4] docs: note home rc is skipped when xdg_config_home is set --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66a4c6bd..ceb6a762 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo 3. RC file in CWD 4. RC file in the workspace directory 5. RC file in the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) -6. RC file in the user's home directory +6. Legacy RC file in the user's home directory, when `$XDG_CONFIG_HOME` is not set 7. Config from `package.json` 8. Default config passed by options 9. Extended config layers @@ -104,7 +104,7 @@ Set to `false` to disable loading RC config. ### `globalRc` -Load RC config from the workspace directory, the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) and the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. +Load RC config from the workspace directory, the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) and, when `$XDG_CONFIG_HOME` is not set, the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. When the same key is set in more than one of these, the workspace wins over the user config directory, which wins over the home directory. From dfd43c744c743fb1a74766eb305486848f17996e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 13 Aug 2026 06:17:39 +0100 Subject: [PATCH 4/4] fix: always read legacy config --- README.md | 4 ++-- src/loader.ts | 6 ++---- test/global-rc.test.ts | 9 ++++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ceb6a762..8266c527 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo 3. RC file in CWD 4. RC file in the workspace directory 5. RC file in the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) -6. Legacy RC file in the user's home directory, when `$XDG_CONFIG_HOME` is not set +6. Legacy RC file in the user's home directory 7. Config from `package.json` 8. Default config passed by options 9. Extended config layers @@ -104,7 +104,7 @@ Set to `false` to disable loading RC config. ### `globalRc` -Load RC config from the workspace directory, the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) and, when `$XDG_CONFIG_HOME` is not set, the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. +Load RC config from the workspace directory, the user's config directory (`$XDG_CONFIG_HOME` or `~/.config`) and the user's home directory. Only enabled when `rcFile` is provided. Set to `false` to disable this functionality. When the same key is set in more than one of these, the workspace wins over the user config directory, which wins over the home directory. diff --git a/src/loader.ts b/src/loader.ts index cce6a1ed..3654d984 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -126,10 +126,8 @@ export async function loadConfig< } // 3. user config dir ($XDG_CONFIG_HOME or ~/.config) rcSources.push(rc9.readUserConfig({ name: options.rcFile })); - // 4. user home (legacy, skipped when $XDG_CONFIG_HOME makes it the same dir) - if (!process.env.XDG_CONFIG_HOME) { - rcSources.push(rc9.readUser({ name: options.rcFile })); - } + // 4. user home (legacy) + rcSources.push(rc9.read({ name: options.rcFile, dir: homedir() })); } rawConfigs.rc = _merger({} as T, ...rcSources); } diff --git a/test/global-rc.test.ts b/test/global-rc.test.ts index 5ffecaea..09024808 100644 --- a/test/global-rc.test.ts +++ b/test/global-rc.test.ts @@ -76,14 +76,13 @@ describe("global rc", () => { expect(config).toMatchObject({ shared: "project" }); }); - it("reads a single location when XDG_CONFIG_HOME is set", async () => { + it("still reads the legacy home rc file when XDG_CONFIG_HOME is set", async () => { const xdg = resolve(dir, "xdg"); await mkdir(xdg, { recursive: true }); process.env.XDG_CONFIG_HOME = xdg; - await writeFile(resolve(xdg, ".testrc"), "xdg=true\nlist[]=a\n"); - await writeFile(resolve(home, ".testrc"), "legacy=true\n"); + await writeFile(resolve(xdg, ".testrc"), "xdg=true\nshared=config\n"); + await writeFile(resolve(home, ".testrc"), "legacy=true\nshared=home\n"); const { config } = await load(); - expect(config).toMatchObject({ xdg: true, list: ["a"] }); - expect(config).not.toHaveProperty("legacy"); + expect(config).toMatchObject({ xdg: true, legacy: true, shared: "config" }); }); });