@outfitter/config 0.1.0-rc.1

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/README.md ADDED
@@ -0,0 +1,334 @@
1
+ # @outfitter/config
2
+
3
+ XDG-compliant configuration loading with schema validation for Outfitter applications.
4
+
5
+ ## Features
6
+
7
+ - **XDG Base Directory Specification** - Proper paths for config, data, cache, and state
8
+ - **Multi-format support** - TOML, YAML, JSON, and JSON5
9
+ - **Schema validation** - Zod-powered type-safe configuration
10
+ - **Multi-source merging** - Combine defaults, files, env vars, and CLI flags
11
+ - **Deep merge** - Intelligent merging of nested configuration objects
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ bun add @outfitter/config
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { loadConfig, resolveConfig, getConfigDir } from "@outfitter/config";
23
+ import { z } from "zod";
24
+
25
+ // Define your configuration schema
26
+ const AppConfigSchema = z.object({
27
+ apiKey: z.string(),
28
+ timeout: z.number().default(5000),
29
+ features: z.object({
30
+ darkMode: z.boolean().default(false),
31
+ }),
32
+ });
33
+
34
+ // Load from XDG paths (~/.config/myapp/config.toml)
35
+ const result = await loadConfig("myapp", AppConfigSchema);
36
+
37
+ if (result.isOk()) {
38
+ console.log("Config loaded:", result.value);
39
+ } else {
40
+ console.error("Failed:", result.error.message);
41
+ }
42
+ ```
43
+
44
+ ## API Reference
45
+
46
+ ### Configuration Loading
47
+
48
+ #### `loadConfig(appName, schema, options?)`
49
+
50
+ Load configuration from XDG-compliant paths with schema validation.
51
+
52
+ ```typescript
53
+ const result = await loadConfig("myapp", AppConfigSchema);
54
+
55
+ if (result.isOk()) {
56
+ const config = result.value;
57
+ // Type-safe access to your config
58
+ }
59
+ ```
60
+
61
+ **Parameters:**
62
+ - `appName` - Application name for XDG directory lookup
63
+ - `schema` - Zod schema for validation
64
+ - `options.searchPaths` - Custom search paths (overrides XDG defaults)
65
+
66
+ **Search Order:**
67
+ 1. Custom `searchPaths` if provided
68
+ 2. `$XDG_CONFIG_HOME/{appName}/config.{ext}`
69
+ 3. `~/.config/{appName}/config.{ext}`
70
+
71
+ **File Format Preference:** `.toml` > `.yaml` > `.yml` > `.json` > `.json5`
72
+
73
+ **Returns:** `Result<T, NotFoundError | ValidationError | ParseError>`
74
+
75
+ ---
76
+
77
+ #### `resolveConfig(schema, sources)`
78
+
79
+ Merge configuration from multiple sources with precedence rules.
80
+
81
+ ```typescript
82
+ const result = resolveConfig(AppSchema, {
83
+ defaults: { port: 3000, host: "localhost" },
84
+ file: loadedConfig,
85
+ env: { port: parseInt(process.env.PORT!) },
86
+ flags: cliArgs,
87
+ });
88
+ ```
89
+
90
+ **Parameters:**
91
+ - `schema` - Zod schema for validation
92
+ - `sources` - Configuration sources to merge
93
+
94
+ **Returns:** `Result<T, ValidationError | ParseError>`
95
+
96
+ ---
97
+
98
+ #### `parseConfigFile(content, filename)`
99
+
100
+ Parse configuration file content based on extension.
101
+
102
+ ```typescript
103
+ const toml = `
104
+ [server]
105
+ port = 3000
106
+ host = "localhost"
107
+ `;
108
+
109
+ const result = parseConfigFile(toml, "config.toml");
110
+ if (result.isOk()) {
111
+ console.log(result.value.server.port); // 3000
112
+ }
113
+ ```
114
+
115
+ **Parameters:**
116
+ - `content` - Raw file content
117
+ - `filename` - Filename (extension determines parser)
118
+
119
+ **Returns:** `Result<Record<string, unknown>, ParseError>`
120
+
121
+ ---
122
+
123
+ ### XDG Path Helpers
124
+
125
+ #### `getConfigDir(appName)`
126
+
127
+ Get the XDG config directory for an application.
128
+
129
+ ```typescript
130
+ getConfigDir("myapp");
131
+ // With XDG_CONFIG_HOME="/custom": "/custom/myapp"
132
+ // Default: "~/.config/myapp"
133
+ ```
134
+
135
+ #### `getDataDir(appName)`
136
+
137
+ Get the XDG data directory for an application.
138
+
139
+ ```typescript
140
+ getDataDir("myapp");
141
+ // With XDG_DATA_HOME="/custom": "/custom/myapp"
142
+ // Default: "~/.local/share/myapp"
143
+ ```
144
+
145
+ #### `getCacheDir(appName)`
146
+
147
+ Get the XDG cache directory for an application.
148
+
149
+ ```typescript
150
+ getCacheDir("myapp");
151
+ // With XDG_CACHE_HOME="/custom": "/custom/myapp"
152
+ // Default: "~/.cache/myapp"
153
+ ```
154
+
155
+ #### `getStateDir(appName)`
156
+
157
+ Get the XDG state directory for an application.
158
+
159
+ ```typescript
160
+ getStateDir("myapp");
161
+ // With XDG_STATE_HOME="/custom": "/custom/myapp"
162
+ // Default: "~/.local/state/myapp"
163
+ ```
164
+
165
+ ---
166
+
167
+ ### Utilities
168
+
169
+ #### `deepMerge(target, source)`
170
+
171
+ Deep merge two objects with configurable semantics.
172
+
173
+ ```typescript
174
+ const defaults = { server: { port: 3000, host: "localhost" } };
175
+ const overrides = { server: { port: 8080 } };
176
+
177
+ const merged = deepMerge(defaults, overrides);
178
+ // { server: { port: 8080, host: "localhost" } }
179
+ ```
180
+
181
+ **Merge Behavior:**
182
+ - Recursively merges nested plain objects
183
+ - Arrays are replaced (not concatenated)
184
+ - `null` explicitly replaces the target value
185
+ - `undefined` is skipped (does not override)
186
+
187
+ ---
188
+
189
+ ### Types
190
+
191
+ #### `ConfigSources<T>`
192
+
193
+ Configuration sources for multi-layer resolution.
194
+
195
+ ```typescript
196
+ interface ConfigSources<T> {
197
+ defaults?: Partial<T>; // Lowest precedence
198
+ file?: Partial<T>; // From config file
199
+ env?: Partial<T>; // Environment variables
200
+ flags?: Partial<T>; // CLI flags (highest)
201
+ }
202
+ ```
203
+
204
+ #### `LoadConfigOptions`
205
+
206
+ Options for `loadConfig()`.
207
+
208
+ ```typescript
209
+ interface LoadConfigOptions {
210
+ searchPaths?: string[]; // Custom search paths
211
+ }
212
+ ```
213
+
214
+ #### `ParseError`
215
+
216
+ Error thrown when configuration file parsing fails.
217
+
218
+ ```typescript
219
+ class ParseError {
220
+ readonly _tag = "ParseError";
221
+ readonly message: string;
222
+ readonly filename: string;
223
+ readonly line?: number;
224
+ readonly column?: number;
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## XDG Base Directory Specification
231
+
232
+ This package follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) for locating configuration files.
233
+
234
+ | Variable | macOS/Linux Default | Purpose |
235
+ |----------|---------------------|---------|
236
+ | `XDG_CONFIG_HOME` | `~/.config` | User-specific configuration |
237
+ | `XDG_DATA_HOME` | `~/.local/share` | User-specific data files |
238
+ | `XDG_CACHE_HOME` | `~/.cache` | Non-essential cached data |
239
+ | `XDG_STATE_HOME` | `~/.local/state` | Persistent state (logs, history) |
240
+
241
+ ---
242
+
243
+ ## Override Precedence
244
+
245
+ Configuration sources are merged with the following precedence (highest to lowest):
246
+
247
+ ```
248
+ +---------------------------------------+
249
+ | flags (CLI arguments) HIGHEST |
250
+ +---------------------------------------+
251
+ | env (environment variables) |
252
+ +---------------------------------------+
253
+ | file (config file) |
254
+ +---------------------------------------+
255
+ | defaults LOWEST |
256
+ +---------------------------------------+
257
+ ```
258
+
259
+ Higher precedence sources override lower ones. Nested objects are deep-merged.
260
+
261
+ ---
262
+
263
+ ## Supported File Formats
264
+
265
+ | Extension | Parser | Notes |
266
+ |-----------|--------|-------|
267
+ | `.toml` | smol-toml | Preferred for configuration |
268
+ | `.yaml`, `.yml` | yaml | YAML anchors/aliases supported |
269
+ | `.json` | JSON.parse | Strict parsing |
270
+ | `.json5` | json5 | Comments and trailing commas allowed |
271
+
272
+ ---
273
+
274
+ ## Examples
275
+
276
+ ### Loading with Custom Paths
277
+
278
+ ```typescript
279
+ const result = await loadConfig("myapp", AppConfigSchema, {
280
+ searchPaths: ["/etc/myapp", "/opt/myapp/config"],
281
+ });
282
+ ```
283
+
284
+ ### Multi-Source Configuration
285
+
286
+ ```typescript
287
+ import { loadConfig, resolveConfig } from "@outfitter/config";
288
+
289
+ // Load base config from file
290
+ const fileResult = await loadConfig("myapp", RawConfigSchema);
291
+ const fileConfig = fileResult.isOk() ? fileResult.value : {};
292
+
293
+ // Resolve with all sources
294
+ const result = resolveConfig(AppConfigSchema, {
295
+ defaults: {
296
+ server: { port: 3000, host: "localhost" },
297
+ logging: { level: "info" },
298
+ },
299
+ file: fileConfig,
300
+ env: {
301
+ server: { port: parseInt(process.env.PORT || "3000") },
302
+ logging: { level: process.env.LOG_LEVEL },
303
+ },
304
+ flags: {
305
+ logging: { level: cliArgs.verbose ? "debug" : undefined },
306
+ },
307
+ });
308
+ ```
309
+
310
+ ### Error Handling
311
+
312
+ ```typescript
313
+ const result = await loadConfig("myapp", AppConfigSchema);
314
+
315
+ if (result.isErr()) {
316
+ switch (result.error._tag) {
317
+ case "NotFoundError":
318
+ console.log("Config file not found, using defaults");
319
+ break;
320
+ case "ValidationError":
321
+ console.error("Invalid config:", result.error.message);
322
+ break;
323
+ case "ParseError":
324
+ console.error("Parse error in", result.error.filename);
325
+ break;
326
+ }
327
+ }
328
+ ```
329
+
330
+ ---
331
+
332
+ ## License
333
+
334
+ MIT