@meltstudio/config-loader 3.2.0 → 3.4.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/README.md CHANGED
@@ -61,6 +61,7 @@ No separate interface to maintain. No `as` casts. The types flow from the schema
61
61
  - **`.env` file support** — load environment variables from `.env` files with automatic line tracking
62
62
  - **Nested objects and arrays** — deeply nested configs with full type safety
63
63
  - **Structured errors** — typed `ConfigLoadError` with per-field error details and warnings
64
+ - **Enum constraints** — restrict values to a fixed set with `oneOf`, with full type narrowing
64
65
  - **Schema validation** — optional per-field validation via [Standard Schema](https://github.com/standard-schema/standard-schema) (Zod, Valibot, ArkType, or custom)
65
66
  - **Strict mode** — promote warnings to errors for production safety
66
67
  - **Default values** — static or computed (via functions)
@@ -253,6 +254,45 @@ c.array({
253
254
  }); // { name: string; age: number }[]
254
255
  ```
255
256
 
257
+ ## Enum Constraints (`oneOf`)
258
+
259
+ Use `oneOf` to restrict a field to a fixed set of allowed values. The check runs after type coercion and before any `validate` schema:
260
+
261
+ ```typescript
262
+ const config = c
263
+ .schema({
264
+ env: c.string({
265
+ env: "NODE_ENV",
266
+ defaultValue: "development",
267
+ oneOf: ["development", "staging", "production"],
268
+ }),
269
+ logLevel: c.number({
270
+ env: "LOG_LEVEL",
271
+ defaultValue: 1,
272
+ oneOf: [0, 1, 2, 3],
273
+ }),
274
+ })
275
+ .load({ env: true, args: false });
276
+ ```
277
+
278
+ If a value is not in the allowed set, a `ConfigLoadError` is thrown with `kind: "validation"`.
279
+
280
+ ### Type Narrowing
281
+
282
+ When `oneOf` is provided, the inferred type is automatically narrowed to the union of the allowed values:
283
+
284
+ ```typescript
285
+ const config = c
286
+ .schema({
287
+ env: c.string({ oneOf: ["dev", "staging", "prod"] }),
288
+ })
289
+ .load({ env: false, args: false });
290
+
291
+ // config.env is typed as "dev" | "staging" | "prod", not string
292
+ ```
293
+
294
+ When used with `cli: true`, the `--help` output automatically lists the allowed values.
295
+
256
296
  ## Validation
257
297
 
258
298
  Add per-field validation using the `validate` option. config-loader accepts any [Standard Schema v1](https://github.com/standard-schema/standard-schema) implementation — including **Zod**, **Valibot**, and **ArkType** — or a custom validator.
package/dist/index.d.ts CHANGED
@@ -61,6 +61,7 @@ interface OptionClassParams<T extends OptionKind> {
61
61
  cli: boolean;
62
62
  help: string;
63
63
  defaultValue?: TypedDefaultValue<T>;
64
+ oneOf?: ReadonlyArray<string | number | boolean>;
64
65
  validate?: StandardSchemaV1;
65
66
  }
66
67
  declare class OptionBase<T extends OptionKind = OptionKind> {
@@ -81,6 +82,7 @@ declare class OptionBase<T extends OptionKind = OptionKind> {
81
82
  } | null;
82
83
  }, envFileResults?: EnvFileResult[], errors?: OptionErrors): ConfigNode | null;
83
84
  private resolveValue;
85
+ private runOneOfCheck;
84
86
  private runValidation;
85
87
  private resolveFromFileData;
86
88
  checkType(val: Value, path: Path, sourceOfVal: string, errors?: OptionErrors): Value;
@@ -101,7 +103,7 @@ declare class ConfigNode {
101
103
  constructor(value: Value | ArrayValue, path: string, sourceType: SourceTypes, file: string | null, variableName: string | null, argName: string | null, line?: number | null, column?: number | null);
102
104
  }
103
105
 
104
- declare class PrimitiveOption<T extends PrimitiveKind = PrimitiveKind> extends OptionBase<T> {
106
+ declare class PrimitiveOption<T extends PrimitiveKind = PrimitiveKind, Narrowed = TypeOfPrimitiveKind<T>> extends OptionBase<T> {
105
107
  }
106
108
 
107
109
  type NodeTree = {
@@ -140,7 +142,7 @@ type TypeOfPrimitiveKind<T extends PrimitiveKind> = T extends "boolean" ? boolea
140
142
  /** Recursively infers the plain TypeScript type from a schema definition. Maps option nodes to their resolved value types. */
141
143
  type SchemaValue<T extends OptionBase | Node> = T extends OptionBase ? T extends ArrayOption<OptionTypes> ? SchemaValue<T["item"]>[] : T extends ObjectOption<infer R> ? {
142
144
  [K in keyof R]: SchemaValue<R[K]>;
143
- } : T extends PrimitiveOption<infer R> ? TypeOfPrimitiveKind<R> : never : T extends Node ? {
145
+ } : T extends PrimitiveOption<infer _R, infer Narrowed> ? Narrowed : never : T extends Node ? {
144
146
  [K in keyof T]: SchemaValue<T[K]>;
145
147
  } : never;
146
148
  type Path = Array<string | number>;
@@ -260,6 +262,8 @@ interface OptionPropsArgs<T> {
260
262
  defaultValue?: T | (() => T);
261
263
  /** Help text shown in CLI `--help` output. */
262
264
  help?: string;
265
+ /** Restrict the value to a fixed set of allowed values. Checked after type coercion, before `validate`. */
266
+ oneOf?: readonly T[];
263
267
  /** Standard Schema validator run after type coercion. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
264
268
  validate?: StandardSchemaV1;
265
269
  }
@@ -283,6 +287,38 @@ interface ObjectOptionPropsArgs<T extends Node> {
283
287
  /** Standard Schema validator run on the resolved object. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
284
288
  validate?: StandardSchemaV1;
285
289
  }
290
+ /**
291
+ * Creates a string configuration option.
292
+ * @param opts - Option configuration (env, cli, required, defaultValue, help, oneOf).
293
+ * @returns A `PrimitiveOption<"string">` for use in a schema.
294
+ * @example
295
+ * c.string({ env: "HOST", defaultValue: "localhost" })
296
+ * c.string({ env: "NODE_ENV", oneOf: ["development", "staging", "production"] })
297
+ */
298
+ declare function string<const V extends readonly string[]>(opts: OptionPropsArgs<string> & {
299
+ oneOf: V;
300
+ }): PrimitiveOption<"string", V[number]>;
301
+ declare function string(opts?: OptionPropsArgs<string>): PrimitiveOption<"string">;
302
+ /**
303
+ * Creates a number configuration option. String values from env/CLI are coerced to numbers.
304
+ * @param opts - Option configuration (env, cli, required, defaultValue, help, oneOf).
305
+ * @returns A `PrimitiveOption<"number">` for use in a schema.
306
+ * @example
307
+ * c.number({ env: "PORT", defaultValue: 3000 })
308
+ * c.number({ env: "LOG_LEVEL", oneOf: [0, 1, 2, 3] })
309
+ */
310
+ declare function number<const V extends readonly number[]>(opts: OptionPropsArgs<number> & {
311
+ oneOf: V;
312
+ }): PrimitiveOption<"number", V[number]>;
313
+ declare function number(opts?: OptionPropsArgs<number>): PrimitiveOption<"number">;
314
+ /**
315
+ * Creates a boolean configuration option. String values `"true"`/`"false"` are coerced.
316
+ * @param opts - Option configuration (env, cli, required, defaultValue, help).
317
+ * @returns A `PrimitiveOption<"boolean">` for use in a schema.
318
+ * @example
319
+ * c.bool({ env: "DEBUG", defaultValue: false })
320
+ */
321
+ declare function bool(opts?: OptionPropsArgs<boolean>): PrimitiveOption<"boolean">;
286
322
  /**
287
323
  * Config-loader entry point. Provides factory functions to define a typed configuration schema.
288
324
  *
@@ -297,9 +333,9 @@ interface ObjectOptionPropsArgs<T extends Node> {
297
333
  * ```
298
334
  */
299
335
  declare const option: {
300
- string: (opts?: OptionPropsArgs<string>) => PrimitiveOption<"string">;
301
- number: (opts?: OptionPropsArgs<number>) => PrimitiveOption<"number">;
302
- bool: (opts?: OptionPropsArgs<boolean>) => PrimitiveOption<"boolean">;
336
+ string: typeof string;
337
+ number: typeof number;
338
+ bool: typeof bool;
303
339
  array: <T extends OptionTypes>(opts: ArrayOptionPropsArgs<T>) => ArrayOption<T>;
304
340
  object: <T extends Node>(opts: ObjectOptionPropsArgs<T>) => ObjectOption<T>;
305
341
  schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
package/dist/index.js CHANGED
@@ -292,6 +292,10 @@ var OptionBase = class {
292
292
  envFileResults,
293
293
  errors
294
294
  );
295
+ if (resolved && this.params.oneOf) {
296
+ const passed = this.runOneOfCheck(resolved, path2, errors);
297
+ if (!passed) return resolved;
298
+ }
295
299
  if (resolved && this.params.validate) {
296
300
  this.runValidation(resolved, path2, errors);
297
301
  }
@@ -455,6 +459,27 @@ var OptionBase = class {
455
459
  }
456
460
  return null;
457
461
  }
462
+ runOneOfCheck(node, path2, errors) {
463
+ const allowed = this.params.oneOf;
464
+ if (!allowed) return true;
465
+ const value = node.value;
466
+ if (valueIsInvalid(value)) return true;
467
+ if (!allowed.includes(value)) {
468
+ const ident = path2.join(".");
469
+ const source = node.file ?? node.variableName ?? node.argName ?? node.sourceType;
470
+ const allowedStr = allowed.map((v) => `'${String(v)}'`).join(", ");
471
+ errors?.errors.push({
472
+ message: `Value '${typeof value === "object" ? JSON.stringify(value) : String(value)}' for '${ident}' is not one of: ${allowedStr}.`,
473
+ path: ident,
474
+ source,
475
+ kind: "validation",
476
+ line: node.line ?? void 0,
477
+ column: node.column ?? void 0
478
+ });
479
+ return false;
480
+ }
481
+ return true;
482
+ }
458
483
  runValidation(node, path2, errors) {
459
484
  const validator = this.params.validate;
460
485
  if (!validator) return;
@@ -654,6 +679,19 @@ var OptionBase = class {
654
679
  var ObjectOption = class extends OptionBase {
655
680
  item;
656
681
  constructor(params) {
682
+ if (!params.item) {
683
+ const hasOptionValues = Object.values(params).some(
684
+ (v) => v instanceof OptionBase
685
+ );
686
+ if (hasOptionValues) {
687
+ throw new Error(
688
+ "Invalid c.object() call: schema fields were passed directly instead of wrapped in { item: { ... } }. Use c.object({ item: { host: c.string() } }) instead of c.object({ host: c.string() })."
689
+ );
690
+ }
691
+ throw new Error(
692
+ "Invalid c.object() call: missing required 'item' property. Use c.object({ item: { host: c.string(), port: c.number() } })."
693
+ );
694
+ }
657
695
  super({
658
696
  kind: "object",
659
697
  env: null,
@@ -1021,7 +1059,12 @@ var Settings = class {
1021
1059
  addArg(node, path2 = []) {
1022
1060
  if (node.params.cli) {
1023
1061
  const ident = path2.join(".");
1024
- this.program.option(`--${ident} <value>`, node.params.help);
1062
+ let help = node.params.help;
1063
+ if (node.params.oneOf) {
1064
+ const allowed = node.params.oneOf.map(String).join(", ");
1065
+ help = help ? `${help} (one of: ${allowed})` : `one of: ${allowed}`;
1066
+ }
1067
+ this.program.option(`--${ident} <value>`, help);
1025
1068
  }
1026
1069
  }
1027
1070
  getValuesFromTree(node) {
@@ -1212,27 +1255,27 @@ var DEFAULTS = {
1212
1255
  cli: false,
1213
1256
  help: ""
1214
1257
  };
1215
- var string = (opts) => {
1258
+ function string(opts) {
1216
1259
  return new PrimitiveOption({
1217
1260
  kind: "string",
1218
1261
  ...DEFAULTS,
1219
1262
  ...opts
1220
1263
  });
1221
- };
1222
- var number = (opts) => {
1264
+ }
1265
+ function number(opts) {
1223
1266
  return new PrimitiveOption({
1224
1267
  kind: "number",
1225
1268
  ...DEFAULTS,
1226
1269
  ...opts
1227
1270
  });
1228
- };
1229
- var bool = (opts) => {
1271
+ }
1272
+ function bool(opts) {
1230
1273
  return new PrimitiveOption({
1231
1274
  kind: "boolean",
1232
1275
  ...DEFAULTS,
1233
1276
  ...opts
1234
1277
  });
1235
- };
1278
+ }
1236
1279
  var array = (opts) => {
1237
1280
  return new ArrayOption({
1238
1281
  ...DEFAULTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/config-loader",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Type-safe configuration loader with full TypeScript inference. Load from YAML, JSON, .env, environment variables, and CLI args.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",