@andreas-timm/config 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/package.json +73 -0
- package/skills/bun-config/SKILL.md +78 -0
- package/src/index.ts +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2026, Andreas Timm
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @andreas-timm/config
|
|
2
|
+
|
|
3
|
+
Bun-first utilities for loading layered TOML config files and validating the merged result with Zod.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Load `global.toml`, `local.toml`, or custom TOML file lists in order.
|
|
8
|
+
- Resolve config files relative to a project root, from absolute paths, or from `~`.
|
|
9
|
+
- Deep-merge files with later files overriding earlier values.
|
|
10
|
+
- Inject `root_dir` before schema validation.
|
|
11
|
+
- Report Zod validation issues with the resolved source file paths.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add @andreas-timm/config zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/config.ts
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { load } from "@andreas-timm/config";
|
|
26
|
+
|
|
27
|
+
export const ConfigSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
root_dir: z.string(),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
|
|
33
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
34
|
+
|
|
35
|
+
const rootDir = join(import.meta.dir, "..");
|
|
36
|
+
const configFiles = ["global.toml", "local.toml"];
|
|
37
|
+
|
|
38
|
+
let cached: Promise<Config> | undefined;
|
|
39
|
+
|
|
40
|
+
export function loadConfig(): Promise<Config> {
|
|
41
|
+
cached ??= load(ConfigSchema, rootDir, configFiles);
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resetConfig(): void {
|
|
46
|
+
cached = undefined;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`loadConfig()` caches the promise in module scope, so files are read and parsed only on the first call and concurrent callers share the in-flight load. Call `resetConfig()` to force a reload in tests or after editing a TOML file.
|
|
51
|
+
|
|
52
|
+
For the full setup pattern and usage guidance, see [`skills/bun-config/SKILL.md`](https://github.com/andreas-timm/config-ts/blob/main/skills/bun-config/SKILL.md).
|
|
53
|
+
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import merge from "lodash.merge";
|
|
6
|
+
import { concatMap, from, lastValueFrom } from "rxjs";
|
|
7
|
+
import { reduce } from "rxjs/operators";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
function resolveConfigFilePath(rootDir, fileName) {
|
|
10
|
+
if (fileName.startsWith("/")) {
|
|
11
|
+
return fileName;
|
|
12
|
+
}
|
|
13
|
+
if (fileName.startsWith("~")) {
|
|
14
|
+
const home = homedir();
|
|
15
|
+
if (fileName === "~") {
|
|
16
|
+
return home;
|
|
17
|
+
}
|
|
18
|
+
if (fileName.startsWith("~/") || fileName.startsWith("~\\")) {
|
|
19
|
+
return join(home, fileName.slice(2));
|
|
20
|
+
}
|
|
21
|
+
return join(home, fileName.slice(1));
|
|
22
|
+
}
|
|
23
|
+
return join(rootDir, fileName);
|
|
24
|
+
}
|
|
25
|
+
async function load(configSchema, rootDir, configFiles = ["global.toml", "local.toml"]) {
|
|
26
|
+
const config$ = from(configFiles).pipe(concatMap(async (fileName) => {
|
|
27
|
+
const file = Bun.file(resolveConfigFilePath(rootDir, fileName));
|
|
28
|
+
if (await file.exists()) {
|
|
29
|
+
const text = await file.text();
|
|
30
|
+
return Bun.TOML.parse(text);
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}), reduce((acc, cur) => merge(acc, cur), {}));
|
|
34
|
+
const merged = await lastValueFrom(config$);
|
|
35
|
+
try {
|
|
36
|
+
return configSchema.parse({ root_dir: rootDir, ...merged });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err instanceof z.ZodError) {
|
|
39
|
+
const files = configFiles.map((f) => resolveConfigFilePath(rootDir, f)).join(", ");
|
|
40
|
+
const details = err.issues.map((i) => {
|
|
41
|
+
const path = i.path?.length ? i.path.join(".") : "<root>";
|
|
42
|
+
const expected = "expected" in i && i.expected !== undefined ? `
|
|
43
|
+
expected: ${i.expected}` : "";
|
|
44
|
+
const received = "received" in i && i.received !== undefined ? `
|
|
45
|
+
received: ${i.received}` : "";
|
|
46
|
+
return `- path: ${path}
|
|
47
|
+
code: ${i.code}
|
|
48
|
+
message: ${i.message}${expected}${received}`;
|
|
49
|
+
}).join(`
|
|
50
|
+
`);
|
|
51
|
+
const helpful = `From: ${files}
|
|
52
|
+
` + `Computed root_dir: ${rootDir}
|
|
53
|
+
` + `Schema validation issues:
|
|
54
|
+
${details}`;
|
|
55
|
+
throw new Error(helpful, { cause: err });
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
load
|
|
62
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@andreas-timm/config",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bun-first TOML config loading with Zod validation",
|
|
5
|
+
"license": "BSD-2-Clause",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Andreas Timm",
|
|
8
|
+
"email": "info@andreas-timm.dev"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/andreas-timm/config-ts#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/andreas-timm/config-ts/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bun",
|
|
16
|
+
"config",
|
|
17
|
+
"toml",
|
|
18
|
+
"typescript",
|
|
19
|
+
"zod"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"skills",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/andreas-timm/config-ts.git"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "rm -rf dist && bun build src/index.ts --target bun --format esm --packages external --outfile dist/index.js && tsc -p tsconfig.build.json",
|
|
46
|
+
"lint": "set -x; status=0; biome lint || status=$?; biome check || status=$?; eslint scripts src test || status=$?; tsc --noEmit -p tsconfig.json || status=$?; exit $status",
|
|
47
|
+
"pack": "bun pm pack --ignore-scripts --destination packs",
|
|
48
|
+
"prepack": "bun run build",
|
|
49
|
+
"prepublishOnly": "bun lint && bun test && bun audit --audit-level=moderate",
|
|
50
|
+
"postpack": "bun scripts/postpack.ts",
|
|
51
|
+
"release:publish": "bun scripts/publish.ts"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"lodash.merge": "^4.6.2",
|
|
55
|
+
"rxjs": "^7.8.2"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"zod": "^4.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@biomejs/biome": "^2.4.13",
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@types/bun": "^1.3.13",
|
|
64
|
+
"@types/lodash.merge": "^4.6.9",
|
|
65
|
+
"@types/node": "^25.6.0",
|
|
66
|
+
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
|
67
|
+
"@typescript-eslint/parser": "^8.59.0",
|
|
68
|
+
"globals": "^17.5.0",
|
|
69
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"zod": "^4.3.6"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bun-config
|
|
3
|
+
description: Load layered TOML config files in Bun projects with `@andreas-timm/config`, validated by a Zod schema. Use whenever a Bun project needs typed configuration loading from `global.toml` and `local.toml` or a similar ordered file list with merge and schema validation.
|
|
4
|
+
---
|
|
5
|
+
# Bun Config With `@andreas-timm/config`
|
|
6
|
+
|
|
7
|
+
Use this skill when a Bun project needs typed, layered configuration loaded from TOML files and validated with Zod.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add @andreas-timm/config zod
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## tsconfig.json
|
|
16
|
+
|
|
17
|
+
```jsonc
|
|
18
|
+
{
|
|
19
|
+
"compilerOptions": {
|
|
20
|
+
"types": ["bun"],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@config": ["./src/config.ts"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `types: ["bun"]` is required for `Bun.file` and `Bun.TOML`.
|
|
29
|
+
- The `@config` alias keeps imports stable across the project.
|
|
30
|
+
|
|
31
|
+
## Config Module
|
|
32
|
+
|
|
33
|
+
Create `src/config.ts` that defines the schema and exports `loadConfig`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/config.ts
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import { load } from "@andreas-timm/config";
|
|
40
|
+
|
|
41
|
+
export const ConfigSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
root_dir: z.string(),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
|
|
47
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
48
|
+
|
|
49
|
+
const rootDir = join(import.meta.dir, "..");
|
|
50
|
+
const configFiles = ["global.toml", "local.toml"];
|
|
51
|
+
|
|
52
|
+
let cached: Promise<Config> | undefined;
|
|
53
|
+
|
|
54
|
+
export function loadConfig(): Promise<Config> {
|
|
55
|
+
cached ??= load(ConfigSchema, rootDir, configFiles);
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resetConfig(): void {
|
|
60
|
+
cached = undefined;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { loadConfig } from "@config";
|
|
68
|
+
|
|
69
|
+
const config = await loadConfig();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Behavior
|
|
73
|
+
|
|
74
|
+
- Files are read in order and deep-merged with `lodash.merge`; later files override earlier ones.
|
|
75
|
+
- Missing files are skipped silently.
|
|
76
|
+
- `root_dir` is injected automatically before schema parsing.
|
|
77
|
+
- Validation errors include the resolved file paths and per-issue details.
|
|
78
|
+
- `loadConfig()` caches the promise in module scope, so files are read and parsed only on the first call and concurrent callers share the in-flight load. Call `resetConfig()` to force a reload in tests or after editing a TOML file.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import merge from "lodash.merge";
|
|
4
|
+
import { concatMap, from, lastValueFrom } from "rxjs";
|
|
5
|
+
import { reduce } from "rxjs/operators";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
type ConfigData = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
function resolveConfigFilePath(rootDir: string, fileName: string): string {
|
|
11
|
+
if (fileName.startsWith("/")) {
|
|
12
|
+
return fileName;
|
|
13
|
+
}
|
|
14
|
+
if (fileName.startsWith("~")) {
|
|
15
|
+
const home = homedir();
|
|
16
|
+
if (fileName === "~") {
|
|
17
|
+
return home;
|
|
18
|
+
}
|
|
19
|
+
if (fileName.startsWith("~/") || fileName.startsWith("~\\")) {
|
|
20
|
+
return join(home, fileName.slice(2));
|
|
21
|
+
}
|
|
22
|
+
return join(home, fileName.slice(1));
|
|
23
|
+
}
|
|
24
|
+
return join(rootDir, fileName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function load<Config>(
|
|
28
|
+
configSchema: z.ZodType<Config>,
|
|
29
|
+
rootDir: string,
|
|
30
|
+
configFiles: string[] = ["global.toml", "local.toml"],
|
|
31
|
+
): Promise<Config> {
|
|
32
|
+
const config$ = from(configFiles).pipe(
|
|
33
|
+
concatMap(async (fileName) => {
|
|
34
|
+
const file = Bun.file(resolveConfigFilePath(rootDir, fileName));
|
|
35
|
+
if (await file.exists()) {
|
|
36
|
+
const text = await file.text();
|
|
37
|
+
return Bun.TOML.parse(text) as ConfigData;
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}),
|
|
41
|
+
reduce((acc, cur) => merge(acc, cur), {} as ConfigData),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const merged = await lastValueFrom(config$);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return configSchema.parse({ root_dir: rootDir, ...merged });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof z.ZodError) {
|
|
50
|
+
const files = configFiles
|
|
51
|
+
.map((f) => resolveConfigFilePath(rootDir, f))
|
|
52
|
+
.join(", ");
|
|
53
|
+
const details = err.issues
|
|
54
|
+
.map((i) => {
|
|
55
|
+
const path = i.path?.length ? i.path.join(".") : "<root>";
|
|
56
|
+
const expected =
|
|
57
|
+
"expected" in i && i.expected !== undefined
|
|
58
|
+
? `\n expected: ${i.expected}`
|
|
59
|
+
: "";
|
|
60
|
+
const received =
|
|
61
|
+
"received" in i && i.received !== undefined
|
|
62
|
+
? `\n received: ${i.received}`
|
|
63
|
+
: "";
|
|
64
|
+
return `- path: ${path}\n code: ${i.code}\n message: ${i.message}${expected}${received}`;
|
|
65
|
+
})
|
|
66
|
+
.join("\n");
|
|
67
|
+
// prettier-ignore
|
|
68
|
+
const helpful =
|
|
69
|
+
`From: ${files}\n` +
|
|
70
|
+
`Computed root_dir: ${rootDir}\n` +
|
|
71
|
+
`Schema validation issues:\n${details}`;
|
|
72
|
+
throw new Error(helpful, { cause: err });
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|