@outfitter/config 0.1.0-rc.2 → 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/dist/index.d.ts +44 -2
- package/dist/index.js +95 -17
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -171,6 +171,19 @@ declare const ParseErrorBase: TaggedErrorClass<"ParseError", ParseErrorFields>;
|
|
|
171
171
|
declare class ParseError extends ParseErrorBase {
|
|
172
172
|
readonly category: "validation";
|
|
173
173
|
}
|
|
174
|
+
type CircularExtendsErrorFields = {
|
|
175
|
+
/** Human-readable error message */
|
|
176
|
+
message: string;
|
|
177
|
+
/** The config file paths that form the circular reference */
|
|
178
|
+
chain: string[];
|
|
179
|
+
};
|
|
180
|
+
declare const CircularExtendsErrorBase: TaggedErrorClass<"CircularExtendsError", CircularExtendsErrorFields>;
|
|
181
|
+
/**
|
|
182
|
+
* Error thrown when a circular extends reference is detected.
|
|
183
|
+
*/
|
|
184
|
+
declare class CircularExtendsError extends CircularExtendsErrorBase {
|
|
185
|
+
readonly category: "validation";
|
|
186
|
+
}
|
|
174
187
|
/**
|
|
175
188
|
* Get the XDG config directory for an application.
|
|
176
189
|
*
|
|
@@ -465,5 +478,34 @@ interface LoadConfigOptions {
|
|
|
465
478
|
* });
|
|
466
479
|
* ```
|
|
467
480
|
*/
|
|
468
|
-
declare function loadConfig<T>(appName: string, schema: ZodSchema<T>, options?: LoadConfigOptions): Result<T, InstanceType<typeof NotFoundError> | InstanceType<typeof ValidationError> | InstanceType<typeof ParseError>>;
|
|
469
|
-
|
|
481
|
+
declare function loadConfig<T>(appName: string, schema: ZodSchema<T>, options?: LoadConfigOptions): Result<T, InstanceType<typeof NotFoundError> | InstanceType<typeof ValidationError> | InstanceType<typeof ParseError> | InstanceType<typeof CircularExtendsError>>;
|
|
482
|
+
/**
|
|
483
|
+
* Map environment variables to config object based on prefix.
|
|
484
|
+
*
|
|
485
|
+
* Environment variables are mapped as follows:
|
|
486
|
+
* - `PREFIX_KEY` -> `{ key: value }`
|
|
487
|
+
* - `PREFIX_NESTED__KEY` -> `{ nested: { key: value } }`
|
|
488
|
+
*
|
|
489
|
+
* Only returns values for keys that have matching env vars set.
|
|
490
|
+
* Values are returned as strings; use Zod coercion for type conversion.
|
|
491
|
+
*
|
|
492
|
+
* @param prefix - Environment variable prefix (e.g., "MYAPP")
|
|
493
|
+
* @param schema - Zod schema to determine valid keys (optional, for filtering)
|
|
494
|
+
* @returns Partial config object with mapped values
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* ```typescript
|
|
498
|
+
* // With MYAPP_PORT=8080 and MYAPP_DB__HOST=localhost
|
|
499
|
+
* const schema = z.object({
|
|
500
|
+
* port: z.coerce.number(),
|
|
501
|
+
* db: z.object({ host: z.string() }),
|
|
502
|
+
* });
|
|
503
|
+
*
|
|
504
|
+
* const envConfig = mapEnvToConfig("MYAPP", schema);
|
|
505
|
+
* // { port: "8080", db: { host: "localhost" } }
|
|
506
|
+
*
|
|
507
|
+
* const result = resolveConfig(schema, { env: envConfig });
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
declare function mapEnvToConfig<T>(prefix: string, _schema?: ZodSchema<T>): Partial<T>;
|
|
511
|
+
export { resolveConfig, portSchema, parseEnv, parseConfigFile, optionalBooleanSchema, mapEnvToConfig, loadConfig, getStateDir, getEnvBoolean, getDataDir, getConfigDir, getCacheDir, env, deepMerge, booleanSchema, ParseError, LoadConfigOptions, Env, ConfigSources, CircularExtendsError };
|
package/dist/index.js
CHANGED
|
@@ -8069,7 +8069,7 @@ function getEnvBoolean(key) {
|
|
|
8069
8069
|
// src/index.ts
|
|
8070
8070
|
var import_json5 = __toESM(require_lib(), 1);
|
|
8071
8071
|
import { existsSync, readFileSync } from "node:fs";
|
|
8072
|
-
import { join } from "node:path";
|
|
8072
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8073
8073
|
import {
|
|
8074
8074
|
NotFoundError,
|
|
8075
8075
|
Result,
|
|
@@ -9070,6 +9070,11 @@ var ParseErrorBase = TaggedError("ParseError")();
|
|
|
9070
9070
|
class ParseError extends ParseErrorBase {
|
|
9071
9071
|
category = "validation";
|
|
9072
9072
|
}
|
|
9073
|
+
var CircularExtendsErrorBase = TaggedError("CircularExtendsError")();
|
|
9074
|
+
|
|
9075
|
+
class CircularExtendsError extends CircularExtendsErrorBase {
|
|
9076
|
+
category = "validation";
|
|
9077
|
+
}
|
|
9073
9078
|
function getConfigDir(appName) {
|
|
9074
9079
|
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
|
|
9075
9080
|
const home = process.env["HOME"] ?? "";
|
|
@@ -9240,22 +9245,11 @@ function loadConfig(appName, schema, options) {
|
|
|
9240
9245
|
resourceId: appName
|
|
9241
9246
|
}));
|
|
9242
9247
|
}
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
} catch {
|
|
9247
|
-
return Result.err(new NotFoundError({
|
|
9248
|
-
message: `Failed to read config file: ${configFilePath}`,
|
|
9249
|
-
resourceType: "config",
|
|
9250
|
-
resourceId: configFilePath
|
|
9251
|
-
}));
|
|
9252
|
-
}
|
|
9253
|
-
const filename = configFilePath.split("/").pop() ?? "config";
|
|
9254
|
-
const parseResult = parseConfigFile(content, filename);
|
|
9255
|
-
if (parseResult.isErr()) {
|
|
9256
|
-
return Result.err(parseResult.error);
|
|
9248
|
+
const loadResult = loadConfigFileWithExtends(configFilePath);
|
|
9249
|
+
if (loadResult.isErr()) {
|
|
9250
|
+
return Result.err(loadResult.error);
|
|
9257
9251
|
}
|
|
9258
|
-
const parsed =
|
|
9252
|
+
const parsed = loadResult.unwrap();
|
|
9259
9253
|
const validateResult = schema.safeParse(parsed);
|
|
9260
9254
|
if (!validateResult.success) {
|
|
9261
9255
|
const issues = validateResult.error.issues;
|
|
@@ -9270,12 +9264,95 @@ function loadConfig(appName, schema, options) {
|
|
|
9270
9264
|
}
|
|
9271
9265
|
return Result.ok(validateResult.data);
|
|
9272
9266
|
}
|
|
9267
|
+
function resolveExtendsPath(extendsValue, fromFile) {
|
|
9268
|
+
if (isAbsolute(extendsValue)) {
|
|
9269
|
+
return extendsValue;
|
|
9270
|
+
}
|
|
9271
|
+
return resolve(dirname(fromFile), extendsValue);
|
|
9272
|
+
}
|
|
9273
|
+
function loadConfigFileWithExtends(filePath, visited = new Set) {
|
|
9274
|
+
const normalizedPath = resolve(filePath);
|
|
9275
|
+
if (visited.has(normalizedPath)) {
|
|
9276
|
+
return Result.err(new CircularExtendsError({
|
|
9277
|
+
message: `Circular extends detected: ${[...visited, normalizedPath].join(" -> ")}`,
|
|
9278
|
+
chain: [...visited, normalizedPath]
|
|
9279
|
+
}));
|
|
9280
|
+
}
|
|
9281
|
+
if (!existsSync(filePath)) {
|
|
9282
|
+
return Result.err(new NotFoundError({
|
|
9283
|
+
message: `Config file not found: ${filePath}`,
|
|
9284
|
+
resourceType: "config",
|
|
9285
|
+
resourceId: filePath
|
|
9286
|
+
}));
|
|
9287
|
+
}
|
|
9288
|
+
let content;
|
|
9289
|
+
try {
|
|
9290
|
+
content = readFileSync(filePath, "utf-8");
|
|
9291
|
+
} catch {
|
|
9292
|
+
return Result.err(new NotFoundError({
|
|
9293
|
+
message: `Failed to read config file: ${filePath}`,
|
|
9294
|
+
resourceType: "config",
|
|
9295
|
+
resourceId: filePath
|
|
9296
|
+
}));
|
|
9297
|
+
}
|
|
9298
|
+
const filename = filePath.split("/").pop() ?? "config";
|
|
9299
|
+
const parseResult = parseConfigFile(content, filename);
|
|
9300
|
+
if (parseResult.isErr()) {
|
|
9301
|
+
return Result.err(parseResult.error);
|
|
9302
|
+
}
|
|
9303
|
+
const parsed = parseResult.unwrap();
|
|
9304
|
+
const extendsValue = parsed["extends"];
|
|
9305
|
+
if (extendsValue === undefined) {
|
|
9306
|
+
return Result.ok(parsed);
|
|
9307
|
+
}
|
|
9308
|
+
if (typeof extendsValue !== "string") {
|
|
9309
|
+
return Result.err(new ParseError({
|
|
9310
|
+
message: `Invalid "extends" value in ${filePath}: expected string, got ${typeof extendsValue}`,
|
|
9311
|
+
filename: filePath
|
|
9312
|
+
}));
|
|
9313
|
+
}
|
|
9314
|
+
visited.add(normalizedPath);
|
|
9315
|
+
const extendsPath = resolveExtendsPath(extendsValue, filePath);
|
|
9316
|
+
const baseResult = loadConfigFileWithExtends(extendsPath, visited);
|
|
9317
|
+
if (baseResult.isErr()) {
|
|
9318
|
+
return Result.err(baseResult.error);
|
|
9319
|
+
}
|
|
9320
|
+
const baseConfig = baseResult.unwrap();
|
|
9321
|
+
const { extends: __, ...currentConfig } = parsed;
|
|
9322
|
+
return Result.ok(deepMerge(baseConfig, currentConfig));
|
|
9323
|
+
}
|
|
9324
|
+
function mapEnvToConfig(prefix, _schema) {
|
|
9325
|
+
const result = {};
|
|
9326
|
+
const prefixWithUnderscore = `${prefix}_`;
|
|
9327
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
9328
|
+
if (!key.startsWith(prefixWithUnderscore) || value === undefined) {
|
|
9329
|
+
continue;
|
|
9330
|
+
}
|
|
9331
|
+
const configPath = key.slice(prefixWithUnderscore.length).toLowerCase().split("__");
|
|
9332
|
+
let current = result;
|
|
9333
|
+
for (let i = 0;i < configPath.length - 1; i++) {
|
|
9334
|
+
const segment = configPath[i];
|
|
9335
|
+
if (segment === undefined)
|
|
9336
|
+
continue;
|
|
9337
|
+
if (!(segment in current)) {
|
|
9338
|
+
current[segment] = {};
|
|
9339
|
+
}
|
|
9340
|
+
current = current[segment];
|
|
9341
|
+
}
|
|
9342
|
+
const lastSegment = configPath.at(-1);
|
|
9343
|
+
if (lastSegment !== undefined) {
|
|
9344
|
+
current[lastSegment] = value;
|
|
9345
|
+
}
|
|
9346
|
+
}
|
|
9347
|
+
return result;
|
|
9348
|
+
}
|
|
9273
9349
|
export {
|
|
9274
9350
|
resolveConfig,
|
|
9275
9351
|
portSchema,
|
|
9276
9352
|
parseEnv,
|
|
9277
9353
|
parseConfigFile,
|
|
9278
9354
|
optionalBooleanSchema,
|
|
9355
|
+
mapEnvToConfig,
|
|
9279
9356
|
loadConfig,
|
|
9280
9357
|
getStateDir,
|
|
9281
9358
|
getEnvBoolean,
|
|
@@ -9285,5 +9362,6 @@ export {
|
|
|
9285
9362
|
env,
|
|
9286
9363
|
deepMerge,
|
|
9287
9364
|
booleanSchema,
|
|
9288
|
-
ParseError
|
|
9365
|
+
ParseError,
|
|
9366
|
+
CircularExtendsError
|
|
9289
9367
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/config",
|
|
3
3
|
"description": "XDG-compliant config loading with schema validation for Outfitter",
|
|
4
|
-
"version": "0.1.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"clean": "rm -rf dist"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@outfitter/contracts": "
|
|
31
|
-
"@outfitter/types": "
|
|
30
|
+
"@outfitter/contracts": "0.1.0",
|
|
31
|
+
"@outfitter/types": "0.1.0",
|
|
32
32
|
"zod": "^4.3.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|