@meltstudio/config-loader 3.0.1 → 3.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/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
+ - **Schema validation** — optional per-field validation via [Standard Schema](https://github.com/standard-schema/standard-schema) (Zod, Valibot, ArkType, or custom)
64
65
  - **Strict mode** — promote warnings to errors for production safety
65
66
  - **Default values** — static or computed (via functions)
66
67
  - **Multiple files / directory loading** — load from a list of files or an entire directory
@@ -252,6 +253,74 @@ c.array({
252
253
  }); // { name: string; age: number }[]
253
254
  ```
254
255
 
256
+ ## Validation
257
+
258
+ 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.
259
+
260
+ Validation runs **after** type coercion, so validators see the final typed value (e.g., the number `3000`, not the string `"3000"` from an env var).
261
+
262
+ ### With Zod
263
+
264
+ ```typescript
265
+ import c from "@meltstudio/config-loader";
266
+ import { z } from "zod";
267
+
268
+ const config = c
269
+ .schema({
270
+ port: c.number({
271
+ required: true,
272
+ env: "PORT",
273
+ validate: z.number().min(1).max(65535),
274
+ }),
275
+ host: c.string({
276
+ required: true,
277
+ validate: z.string().url(),
278
+ }),
279
+ env: c.string({
280
+ defaultValue: "development",
281
+ validate: z.enum(["development", "staging", "production"]),
282
+ }),
283
+ })
284
+ .load({ env: true, args: false, files: "./config.yaml" });
285
+ ```
286
+
287
+ ### With a custom validator
288
+
289
+ Any object with a `~standard.validate()` method works:
290
+
291
+ ```typescript
292
+ const portValidator = {
293
+ "~standard": {
294
+ version: 1,
295
+ vendor: "my-app",
296
+ validate(value: unknown) {
297
+ if (typeof value === "number" && value >= 1 && value <= 65535) {
298
+ return { value };
299
+ }
300
+ return { issues: [{ message: "must be a valid port (1-65535)" }] };
301
+ },
302
+ },
303
+ };
304
+
305
+ c.number({ required: true, env: "PORT", validate: portValidator });
306
+ ```
307
+
308
+ Validation errors are collected alongside other config errors and thrown as `ConfigLoadError` with `kind: "validation"`:
309
+
310
+ ```typescript
311
+ try {
312
+ const config = c.schema({ ... }).load({ ... });
313
+ } catch (err) {
314
+ if (err instanceof ConfigLoadError) {
315
+ for (const entry of err.errors) {
316
+ if (entry.kind === "validation") {
317
+ console.error(`Validation: ${entry.path} — ${entry.message}`);
318
+ }
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
255
324
  ## Loading Sources
256
325
 
257
326
  ```typescript
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ interface ConfigErrorEntry {
23
23
  /** The source where the error originated (e.g. file path, `"env"`, `"cli"`). */
24
24
  source?: string;
25
25
  /** Classification of the error. */
26
- kind?: "required" | "type_conversion" | "invalid_path" | "invalid_state" | "file_validation" | "null_value" | "strict";
26
+ kind?: "required" | "type_conversion" | "invalid_path" | "invalid_state" | "file_validation" | "null_value" | "strict" | "validation";
27
27
  /** Line number in the config file where the error occurred, if applicable. */
28
28
  line?: number;
29
29
  /** Column number in the config file where the error occurred, if applicable. */
@@ -61,6 +61,7 @@ interface OptionClassParams<T extends OptionKind> {
61
61
  cli: boolean;
62
62
  help: string;
63
63
  defaultValue?: TypedDefaultValue<T>;
64
+ validate?: StandardSchemaV1;
64
65
  }
65
66
  declare class OptionBase<T extends OptionKind = OptionKind> {
66
67
  readonly params: OptionClassParams<T>;
@@ -79,6 +80,8 @@ declare class OptionBase<T extends OptionKind = OptionKind> {
79
80
  } | undefined;
80
81
  } | null;
81
82
  }, envFileResults?: EnvFileResult[], errors?: OptionErrors): ConfigNode | null;
83
+ private resolveValue;
84
+ private runValidation;
82
85
  private resolveFromFileData;
83
86
  checkType(val: Value, path: Path, sourceOfVal: string, errors?: OptionErrors): Value;
84
87
  protected findInObject(obj: ConfigFileData, path: Path, errors?: OptionErrors): Value | ArrayValue;
@@ -149,11 +152,36 @@ interface ConfigFileData extends ConfigFileStructure<ConfigFileData> {
149
152
  type ArrayValue = Array<string | number | boolean | ConfigFileData>;
150
153
  declare class InvalidValue {
151
154
  }
155
+ /**
156
+ * A single issue returned by a Standard Schema validator.
157
+ * Mirrors the Standard Schema v1 spec (https://github.com/standard-schema/standard-schema).
158
+ */
159
+ interface StandardSchemaIssue {
160
+ message: string;
161
+ path?: ReadonlyArray<PropertyKey>;
162
+ }
163
+ /**
164
+ * Minimal Standard Schema v1 interface for value validation.
165
+ * Any object with a `~standard.validate()` method is accepted — this covers
166
+ * Zod 3.24+, Valibot 1.0+, ArkType 2.1+, and custom validators.
167
+ */
168
+ interface StandardSchemaV1<Output = unknown> {
169
+ "~standard": {
170
+ version: 1;
171
+ vendor: string;
172
+ validate(value: unknown): {
173
+ value: Output;
174
+ } | {
175
+ issues: ReadonlyArray<StandardSchemaIssue>;
176
+ };
177
+ };
178
+ }
152
179
 
153
180
  interface ArrayOptionClassParams<T extends OptionTypes> {
154
181
  required: boolean;
155
182
  defaultValue?: SchemaValue<T>[] | (() => SchemaValue<T>[]);
156
183
  item: T;
184
+ validate?: StandardSchemaV1;
157
185
  }
158
186
  declare class ArrayOption<T extends OptionTypes> extends OptionBase<"array"> {
159
187
  item: T;
@@ -165,6 +193,7 @@ declare class ArrayOption<T extends OptionTypes> extends OptionBase<"array"> {
165
193
  interface ObjectOptionClassParams<T extends Node> {
166
194
  required: boolean;
167
195
  item: T;
196
+ validate?: StandardSchemaV1;
168
197
  }
169
198
  declare class ObjectOption<T extends Node = Node> extends OptionBase<"object"> {
170
199
  item: T;
@@ -205,6 +234,8 @@ interface OptionPropsArgs<T> {
205
234
  defaultValue?: T | (() => T);
206
235
  /** Help text shown in CLI `--help` output. */
207
236
  help?: string;
237
+ /** Standard Schema validator run after type coercion. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
238
+ validate?: StandardSchemaV1;
208
239
  }
209
240
  /** Options for configuring an `array` schema field. */
210
241
  interface ArrayOptionPropsArgs<T extends OptionTypes> {
@@ -214,6 +245,8 @@ interface ArrayOptionPropsArgs<T extends OptionTypes> {
214
245
  item: T;
215
246
  /** Static default value or factory function returning one. */
216
247
  defaultValue?: SchemaValue<T>[] | (() => SchemaValue<T>[]);
248
+ /** Standard Schema validator run on the resolved array. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
249
+ validate?: StandardSchemaV1;
217
250
  }
218
251
  /** Options for configuring a nested `object` schema field. */
219
252
  interface ObjectOptionPropsArgs<T extends Node> {
@@ -221,6 +254,8 @@ interface ObjectOptionPropsArgs<T extends Node> {
221
254
  required?: boolean;
222
255
  /** Schema definition for the nested object's shape. */
223
256
  item: T;
257
+ /** Standard Schema validator run on the resolved object. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
258
+ validate?: StandardSchemaV1;
224
259
  }
225
260
  /**
226
261
  * Config-loader entry point. Provides factory functions to define a typed configuration schema.
@@ -244,4 +279,4 @@ declare const option: {
244
279
  schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
245
280
  };
246
281
 
247
- export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, type ExtendedResult, option as default };
282
+ export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, type ExtendedResult, type StandardSchemaV1, option as default };
package/dist/index.js CHANGED
@@ -272,6 +272,22 @@ var OptionBase = class {
272
272
  this.params = params;
273
273
  }
274
274
  getValue(sourceFile, env, args, path2, defaultValues, objectFromArray, envFileResults, errors) {
275
+ const resolved = this.resolveValue(
276
+ sourceFile,
277
+ env,
278
+ args,
279
+ path2,
280
+ defaultValues,
281
+ objectFromArray,
282
+ envFileResults,
283
+ errors
284
+ );
285
+ if (resolved && this.params.validate) {
286
+ this.runValidation(resolved, path2, errors);
287
+ }
288
+ return resolved;
289
+ }
290
+ resolveValue(sourceFile, env, args, path2, defaultValues, objectFromArray, envFileResults, errors) {
275
291
  const ident = path2.join(".");
276
292
  if (this.params.cli && args) {
277
293
  if (ident in args) {
@@ -413,6 +429,27 @@ var OptionBase = class {
413
429
  }
414
430
  return null;
415
431
  }
432
+ runValidation(node, path2, errors) {
433
+ const validator = this.params.validate;
434
+ if (!validator) return;
435
+ const value = node.value;
436
+ if (valueIsInvalid(value)) return;
437
+ const result = validator["~standard"].validate(value);
438
+ if ("issues" in result && result.issues) {
439
+ const ident = path2.join(".");
440
+ const source = node.file ?? node.variableName ?? node.argName ?? node.sourceType;
441
+ for (const issue of result.issues) {
442
+ errors?.errors.push({
443
+ message: `Validation failed for '${ident}': ${issue.message}`,
444
+ path: ident,
445
+ source,
446
+ kind: "validation",
447
+ line: node.line ?? void 0,
448
+ column: node.column ?? void 0
449
+ });
450
+ }
451
+ }
452
+ }
416
453
  resolveFromFileData(data, file, sourceMap, path2, ident, errors) {
417
454
  const val = this.findInObject(data, path2, errors);
418
455
  const loc = lookupLocation(sourceMap, path2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/config-loader",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Melt Studio's tool for loading configurations into a Node.js application.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",