Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Legacy RC file in the user's home directory
7. Config from `package.json`
8. Default config passed by options
9. Extended config layers

## Options

Expand All @@ -102,7 +104,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`

Expand Down
6 changes: 4 additions & 2 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ 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)
rcSources.push(rc9.read({ name: options.rcFile, dir: homedir() }));
}
rawConfigs.rc = _merger({} as T, ...rcSources);
}
Expand Down
88 changes: 88 additions & 0 deletions test/global-rc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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("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\nshared=config\n");
await writeFile(resolve(home, ".testrc"), "legacy=true\nshared=home\n");
const { config } = await load();
expect(config).toMatchObject({ xdg: true, legacy: true, shared: "config" });
});
});
Loading