Bun-only configuration store with layered sources, traces, and writable TOML settings.
import { createConfig } from "@flatina/confkit";
type AppConfig = {
theme: {
mode: "light" | "dark";
};
server: {
port: number;
};
};
const config = await createConfig<AppConfig>()
.useDefaults({
theme: { mode: "light" },
server: { port: 3000 },
})
.useTomlFile("../app.toml", { name: "parent", required: false })
.useTomlFile("app.toml", {
name: "workspace",
required: false,
watch: true,
})
.useWritableTomlFile("~/.config/app/settings.toml", {
name: "user",
create: true,
defaultWrite: true,
})
.useEnv({
map: {
APP_PORT: "server.port",
},
})
.useArgv({
map: {
port: "server.port",
},
})
.validate((value) => {
if (typeof value.server.port !== "number") {
throw new Error("server.port must be a number");
}
})
.load();
await config.set("theme.mode", "dark");
console.log(config.getTrace("theme.mode"));- Sources are merged in registration order.
- Later sources override earlier sources at the same path.
- Objects are deep-merged; arrays are replaced.
- Writable TOML files preserve values, not comments or formatting.
mapoptions work as env/argv allowlists.
const unsubscribe = config.onChange((next, previous) => {
if (next.value.theme.mode !== previous.value.theme.mode) {
applyTheme(next.value.theme.mode);
}
});
await config.set("theme.mode", "dark");- Call
unsubscribe()only for shorter-lived scopes. - Call
dispose()when discarding a config that useswatch; it closes file watchers and clears listeners.
await config.patch({
theme: {
mode: "dark",
},
});
await config.unset("theme.mode");
await config.reload();set,patch,unset, andreloademit only when the effective config changes.- Env and argv sources can shadow saved settings for the current run.