@optique/config 1.0.0-dev.1758 → 1.0.0-dev.1772

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.cjs CHANGED
@@ -281,6 +281,11 @@ function createConfigContext(options) {
281
281
  * @param options Binding options including context, key, and default.
282
282
  * @returns A new parser with config fallback behavior.
283
283
  * @throws {TypeError} If `key` is not a property key or function.
284
+ * @throws {Error} If the inner parser's {@link Parser.validateValue} hook
285
+ * throws while re-validating a fallback value (a
286
+ * config-sourced value or the configured `default`) —
287
+ * the hook can run even when no CLI tokens are parsed
288
+ * (see issue #414).
284
289
  * @since 0.10.0
285
290
  *
286
291
  * @example
@@ -370,7 +375,7 @@ function bindConfig(parser, options) {
370
375
  },
371
376
  complete: (state, exec) => {
372
377
  if (isConfigBindState(state) && state.hasCliValue) return parser.complete(state.cliState, exec);
373
- return (0, __optique_core_mode_dispatch.wrapForMode)(parser.$mode, getConfigOrDefault(state, options));
378
+ return getConfigOrDefault(state, options, parser.$mode, parser);
374
379
  },
375
380
  suggest: (context, prefix) => {
376
381
  const innerState = getSuggestInnerState(context.state);
@@ -398,22 +403,30 @@ function bindConfig(parser, options) {
398
403
  configurable: true,
399
404
  enumerable: false
400
405
  });
406
+ if (typeof parser.validateValue === "function") Object.defineProperty(boundParser, "validateValue", {
407
+ value: parser.validateValue.bind(parser),
408
+ configurable: true,
409
+ enumerable: false
410
+ });
401
411
  (0, __optique_core_parser.defineInheritedAnnotationParser)(boundParser);
402
412
  const dependencyMetadata = (0, __optique_core_parser.composeWrappedSourceMetadata)(parser.dependencyMetadata, (sourceMetadata) => ({
403
413
  ...sourceMetadata,
404
- getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => ({
405
- success: true,
406
- value: options.default
407
- }) : void 0,
414
+ getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => {
415
+ if (typeof parser.validateValue === "function") return parser.validateValue(options.default);
416
+ return {
417
+ success: true,
418
+ value: options.default
419
+ };
420
+ } : void 0,
408
421
  extractSourceValue: (state) => {
409
422
  if (!isConfigBindState(state)) {
410
- if (sourceMetadata.preservesSourceValue) return getConfigSourceValue(state, options, state, sourceMetadata.extractSourceValue);
423
+ if (sourceMetadata.preservesSourceValue) return getConfigSourceValue(state, options, state, sourceMetadata.extractSourceValue, parser);
411
424
  return sourceMetadata.extractSourceValue(state);
412
425
  }
413
426
  if (state.hasCliValue) return sourceMetadata.extractSourceValue(state.cliState);
414
427
  const fallbackState = state.cliState ?? state;
415
428
  if (!sourceMetadata.preservesSourceValue) return sourceMetadata.extractSourceValue(fallbackState);
416
- return getConfigSourceValue(state, options, fallbackState, sourceMetadata.extractSourceValue);
429
+ return getConfigSourceValue(state, options, fallbackState, sourceMetadata.extractSourceValue, parser);
417
430
  }
418
431
  }));
419
432
  if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
@@ -428,9 +441,24 @@ function bindConfig(parser, options) {
428
441
  * Checks both annotations (for top-level parsers) and the active config
429
442
  * registry (for parsers nested inside object() when used with context-aware
430
443
  * runners).
444
+ *
445
+ * When `innerParser.validateValue` is available, the returned fallback
446
+ * value is routed through it so that the inner CLI parser's constraints
447
+ * (regex patterns, numeric bounds, choices, etc.) are enforced on
448
+ * config-sourced values and configured defaults (see issue #414). If
449
+ * `innerParser` is absent or does not implement `validateValue` (for
450
+ * example, the inner parser is wrapped in `map()`), the value is
451
+ * returned unchanged to preserve existing behavior.
452
+ *
431
453
  * @throws {TypeError} If the key callback returns a Promise or thenable.
454
+ * @throws {Error} Propagates errors thrown by
455
+ * `innerParser.validateValue()` (via
456
+ * {@link validateFallbackValue}) while revalidating a
457
+ * config-sourced value or the configured `default`
458
+ * against the inner CLI parser's constraints (see
459
+ * issue #414).
432
460
  */
433
- function getConfigOrDefault(state, options) {
461
+ function getConfigOrDefault(state, options, mode, innerParser) {
434
462
  const annotations = (0, __optique_core_annotations.getAnnotations)(state);
435
463
  const contextId = options.context.id;
436
464
  const annotationValue = annotations?.[contextId];
@@ -445,18 +473,32 @@ function getConfigOrDefault(state, options) {
445
473
  configValue = options.key(configData, configMeta);
446
474
  if (configValue != null && (typeof configValue === "object" || typeof configValue === "function") && "then" in configValue && typeof configValue.then === "function") throw new TypeError("The key callback must return a synchronous value, but got a thenable.");
447
475
  } else configValue = configData[options.key];
448
- if (configValue !== void 0) return {
449
- success: true,
450
- value: configValue
451
- };
452
- if (options.default !== void 0) return {
453
- success: true,
454
- value: options.default
455
- };
456
- return {
476
+ if (configValue !== void 0) return validateFallbackValue(mode, innerParser, configValue);
477
+ if (options.default !== void 0) return validateFallbackValue(mode, innerParser, options.default);
478
+ return (0, __optique_core_mode_dispatch.wrapForMode)(mode, {
457
479
  success: false,
458
480
  error: __optique_core_message.message`Missing required configuration value.`
459
- };
481
+ });
482
+ }
483
+ /**
484
+ * Routes a (successful) fallback value through the inner parser's
485
+ * `validateValue()` hook, or returns the value unchanged when no
486
+ * validator is available. See {@link getConfigOrDefault} for context.
487
+ *
488
+ * @throws {Error} Propagates errors thrown by
489
+ * `innerParser.validateValue()` while revalidating the
490
+ * fallback value against the inner CLI parser's
491
+ * constraints (see issue #414). When the hook returns
492
+ * a failed {@link Result} the failure is propagated
493
+ * through the return value; only an actual exception
494
+ * thrown by the hook escapes through this path.
495
+ */
496
+ function validateFallbackValue(mode, innerParser, value) {
497
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return (0, __optique_core_mode_dispatch.wrapForMode)(mode, {
498
+ success: true,
499
+ value
500
+ });
501
+ return innerParser.validateValue(value);
460
502
  }
461
503
  /**
462
504
  * Resolves a config-backed dependency source with fallback priority.
@@ -466,27 +508,46 @@ function getConfigOrDefault(state, options) {
466
508
  * back to `options.default` and finally delegates to the wrapped parser's
467
509
  * source extractor.
468
510
  *
511
+ * When `innerParser` exposes `validateValue`, the returned fallback is
512
+ * routed through it so that the inner CLI parser's constraints are
513
+ * enforced on config-sourced source values and configured defaults
514
+ * (see issue #414). This helper is only invoked from the
515
+ * `preservesSourceValue: true` branch in {@link bindConfig}, so the
516
+ * source value type is guaranteed to equal `TValue`.
517
+ *
469
518
  * @param state The wrapper state, which may carry config annotations.
470
519
  * @param options The binding options with lookup and default settings.
471
520
  * @param innerState The unwrapped inner state for delegated extraction.
472
521
  * @param extractInnerSourceValue The wrapped parser's source extractor.
522
+ * @param innerParser The wrapped parser, used to revalidate fallback values.
473
523
  * @returns The resolved source value, an async source value, or `undefined`.
474
524
  * @throws {TypeError} If {@link getConfigOrDefault} rejects a thenable-returning
475
525
  * key callback.
526
+ * @throws {Error} Propagates errors thrown by
527
+ * `innerParser.validateValue()` (via
528
+ * {@link getConfigOrDefault} / {@link validateFallbackValue})
529
+ * while revalidating a config-sourced value or the
530
+ * configured `default` against the inner CLI parser's
531
+ * constraints (see issue #414).
476
532
  */
477
- function getConfigSourceValue(state, options, innerState, extractInnerSourceValue) {
533
+ function getConfigSourceValue(state, options, innerState, extractInnerSourceValue, innerParser) {
478
534
  const annotations = (0, __optique_core_annotations.getAnnotations)(state);
479
535
  const contextId = options.context.id;
480
536
  const annotationValue = annotations?.[contextId];
481
537
  const configData = annotationValue?.data ?? getActiveConfig(contextId);
538
+ const validateFallback = (parsed) => {
539
+ if (!parsed.success) return parsed;
540
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return parsed;
541
+ return innerParser.validateValue(parsed.value);
542
+ };
482
543
  if (configData !== void 0 && configData !== null) {
483
- const resolved = getConfigOrDefault(state, options);
484
- if (resolved.success) return resolved;
544
+ const resolved = getConfigOrDefault(state, options, "sync", void 0);
545
+ if (resolved.success) return validateFallback(resolved);
485
546
  }
486
- if (options.default !== void 0) return {
547
+ if (options.default !== void 0) return validateFallback({
487
548
  success: true,
488
549
  value: options.default
489
- };
550
+ });
490
551
  return extractInnerSourceValue(innerState);
491
552
  }
492
553
 
package/dist/index.d.cts CHANGED
@@ -223,6 +223,11 @@ interface BindConfigOptions<T, TValue, TConfigMeta = ConfigMeta> {
223
223
  * @param options Binding options including context, key, and default.
224
224
  * @returns A new parser with config fallback behavior.
225
225
  * @throws {TypeError} If `key` is not a property key or function.
226
+ * @throws {Error} If the inner parser's {@link Parser.validateValue} hook
227
+ * throws while re-validating a fallback value (a
228
+ * config-sourced value or the configured `default`) —
229
+ * the hook can run even when no CLI tokens are parsed
230
+ * (see issue #414).
226
231
  * @since 0.10.0
227
232
  *
228
233
  * @example
package/dist/index.d.ts CHANGED
@@ -223,6 +223,11 @@ interface BindConfigOptions<T, TValue, TConfigMeta = ConfigMeta> {
223
223
  * @param options Binding options including context, key, and default.
224
224
  * @returns A new parser with config fallback behavior.
225
225
  * @throws {TypeError} If `key` is not a property key or function.
226
+ * @throws {Error} If the inner parser's {@link Parser.validateValue} hook
227
+ * throws while re-validating a fallback value (a
228
+ * config-sourced value or the configured `default`) —
229
+ * the hook can run even when no CLI tokens are parsed
230
+ * (see issue #414).
226
231
  * @since 0.10.0
227
232
  *
228
233
  * @example
package/dist/index.js CHANGED
@@ -258,6 +258,11 @@ function createConfigContext(options) {
258
258
  * @param options Binding options including context, key, and default.
259
259
  * @returns A new parser with config fallback behavior.
260
260
  * @throws {TypeError} If `key` is not a property key or function.
261
+ * @throws {Error} If the inner parser's {@link Parser.validateValue} hook
262
+ * throws while re-validating a fallback value (a
263
+ * config-sourced value or the configured `default`) —
264
+ * the hook can run even when no CLI tokens are parsed
265
+ * (see issue #414).
261
266
  * @since 0.10.0
262
267
  *
263
268
  * @example
@@ -347,7 +352,7 @@ function bindConfig(parser, options) {
347
352
  },
348
353
  complete: (state, exec) => {
349
354
  if (isConfigBindState(state) && state.hasCliValue) return parser.complete(state.cliState, exec);
350
- return wrapForMode(parser.$mode, getConfigOrDefault(state, options));
355
+ return getConfigOrDefault(state, options, parser.$mode, parser);
351
356
  },
352
357
  suggest: (context, prefix) => {
353
358
  const innerState = getSuggestInnerState(context.state);
@@ -375,22 +380,30 @@ function bindConfig(parser, options) {
375
380
  configurable: true,
376
381
  enumerable: false
377
382
  });
383
+ if (typeof parser.validateValue === "function") Object.defineProperty(boundParser, "validateValue", {
384
+ value: parser.validateValue.bind(parser),
385
+ configurable: true,
386
+ enumerable: false
387
+ });
378
388
  defineInheritedAnnotationParser(boundParser);
379
389
  const dependencyMetadata = composeWrappedSourceMetadata(parser.dependencyMetadata, (sourceMetadata) => ({
380
390
  ...sourceMetadata,
381
- getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => ({
382
- success: true,
383
- value: options.default
384
- }) : void 0,
391
+ getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => {
392
+ if (typeof parser.validateValue === "function") return parser.validateValue(options.default);
393
+ return {
394
+ success: true,
395
+ value: options.default
396
+ };
397
+ } : void 0,
385
398
  extractSourceValue: (state) => {
386
399
  if (!isConfigBindState(state)) {
387
- if (sourceMetadata.preservesSourceValue) return getConfigSourceValue(state, options, state, sourceMetadata.extractSourceValue);
400
+ if (sourceMetadata.preservesSourceValue) return getConfigSourceValue(state, options, state, sourceMetadata.extractSourceValue, parser);
388
401
  return sourceMetadata.extractSourceValue(state);
389
402
  }
390
403
  if (state.hasCliValue) return sourceMetadata.extractSourceValue(state.cliState);
391
404
  const fallbackState = state.cliState ?? state;
392
405
  if (!sourceMetadata.preservesSourceValue) return sourceMetadata.extractSourceValue(fallbackState);
393
- return getConfigSourceValue(state, options, fallbackState, sourceMetadata.extractSourceValue);
406
+ return getConfigSourceValue(state, options, fallbackState, sourceMetadata.extractSourceValue, parser);
394
407
  }
395
408
  }));
396
409
  if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
@@ -405,9 +418,24 @@ function bindConfig(parser, options) {
405
418
  * Checks both annotations (for top-level parsers) and the active config
406
419
  * registry (for parsers nested inside object() when used with context-aware
407
420
  * runners).
421
+ *
422
+ * When `innerParser.validateValue` is available, the returned fallback
423
+ * value is routed through it so that the inner CLI parser's constraints
424
+ * (regex patterns, numeric bounds, choices, etc.) are enforced on
425
+ * config-sourced values and configured defaults (see issue #414). If
426
+ * `innerParser` is absent or does not implement `validateValue` (for
427
+ * example, the inner parser is wrapped in `map()`), the value is
428
+ * returned unchanged to preserve existing behavior.
429
+ *
408
430
  * @throws {TypeError} If the key callback returns a Promise or thenable.
431
+ * @throws {Error} Propagates errors thrown by
432
+ * `innerParser.validateValue()` (via
433
+ * {@link validateFallbackValue}) while revalidating a
434
+ * config-sourced value or the configured `default`
435
+ * against the inner CLI parser's constraints (see
436
+ * issue #414).
409
437
  */
410
- function getConfigOrDefault(state, options) {
438
+ function getConfigOrDefault(state, options, mode, innerParser) {
411
439
  const annotations = getAnnotations(state);
412
440
  const contextId = options.context.id;
413
441
  const annotationValue = annotations?.[contextId];
@@ -422,18 +450,32 @@ function getConfigOrDefault(state, options) {
422
450
  configValue = options.key(configData, configMeta);
423
451
  if (configValue != null && (typeof configValue === "object" || typeof configValue === "function") && "then" in configValue && typeof configValue.then === "function") throw new TypeError("The key callback must return a synchronous value, but got a thenable.");
424
452
  } else configValue = configData[options.key];
425
- if (configValue !== void 0) return {
426
- success: true,
427
- value: configValue
428
- };
429
- if (options.default !== void 0) return {
430
- success: true,
431
- value: options.default
432
- };
433
- return {
453
+ if (configValue !== void 0) return validateFallbackValue(mode, innerParser, configValue);
454
+ if (options.default !== void 0) return validateFallbackValue(mode, innerParser, options.default);
455
+ return wrapForMode(mode, {
434
456
  success: false,
435
457
  error: message`Missing required configuration value.`
436
- };
458
+ });
459
+ }
460
+ /**
461
+ * Routes a (successful) fallback value through the inner parser's
462
+ * `validateValue()` hook, or returns the value unchanged when no
463
+ * validator is available. See {@link getConfigOrDefault} for context.
464
+ *
465
+ * @throws {Error} Propagates errors thrown by
466
+ * `innerParser.validateValue()` while revalidating the
467
+ * fallback value against the inner CLI parser's
468
+ * constraints (see issue #414). When the hook returns
469
+ * a failed {@link Result} the failure is propagated
470
+ * through the return value; only an actual exception
471
+ * thrown by the hook escapes through this path.
472
+ */
473
+ function validateFallbackValue(mode, innerParser, value) {
474
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return wrapForMode(mode, {
475
+ success: true,
476
+ value
477
+ });
478
+ return innerParser.validateValue(value);
437
479
  }
438
480
  /**
439
481
  * Resolves a config-backed dependency source with fallback priority.
@@ -443,27 +485,46 @@ function getConfigOrDefault(state, options) {
443
485
  * back to `options.default` and finally delegates to the wrapped parser's
444
486
  * source extractor.
445
487
  *
488
+ * When `innerParser` exposes `validateValue`, the returned fallback is
489
+ * routed through it so that the inner CLI parser's constraints are
490
+ * enforced on config-sourced source values and configured defaults
491
+ * (see issue #414). This helper is only invoked from the
492
+ * `preservesSourceValue: true` branch in {@link bindConfig}, so the
493
+ * source value type is guaranteed to equal `TValue`.
494
+ *
446
495
  * @param state The wrapper state, which may carry config annotations.
447
496
  * @param options The binding options with lookup and default settings.
448
497
  * @param innerState The unwrapped inner state for delegated extraction.
449
498
  * @param extractInnerSourceValue The wrapped parser's source extractor.
499
+ * @param innerParser The wrapped parser, used to revalidate fallback values.
450
500
  * @returns The resolved source value, an async source value, or `undefined`.
451
501
  * @throws {TypeError} If {@link getConfigOrDefault} rejects a thenable-returning
452
502
  * key callback.
503
+ * @throws {Error} Propagates errors thrown by
504
+ * `innerParser.validateValue()` (via
505
+ * {@link getConfigOrDefault} / {@link validateFallbackValue})
506
+ * while revalidating a config-sourced value or the
507
+ * configured `default` against the inner CLI parser's
508
+ * constraints (see issue #414).
453
509
  */
454
- function getConfigSourceValue(state, options, innerState, extractInnerSourceValue) {
510
+ function getConfigSourceValue(state, options, innerState, extractInnerSourceValue, innerParser) {
455
511
  const annotations = getAnnotations(state);
456
512
  const contextId = options.context.id;
457
513
  const annotationValue = annotations?.[contextId];
458
514
  const configData = annotationValue?.data ?? getActiveConfig(contextId);
515
+ const validateFallback = (parsed) => {
516
+ if (!parsed.success) return parsed;
517
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return parsed;
518
+ return innerParser.validateValue(parsed.value);
519
+ };
459
520
  if (configData !== void 0 && configData !== null) {
460
- const resolved = getConfigOrDefault(state, options);
461
- if (resolved.success) return resolved;
521
+ const resolved = getConfigOrDefault(state, options, "sync", void 0);
522
+ if (resolved.success) return validateFallback(resolved);
462
523
  }
463
- if (options.default !== void 0) return {
524
+ if (options.default !== void 0) return validateFallback({
464
525
  success: true,
465
526
  value: options.default
466
- };
527
+ });
467
528
  return extractInnerSourceValue(innerState);
468
529
  }
469
530
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/config",
3
- "version": "1.0.0-dev.1758+0d05b449",
3
+ "version": "1.0.0-dev.1772+a1288062",
4
4
  "description": "Configuration file support for Optique with Standard Schema validation",
5
5
  "keywords": [
6
6
  "CLI",
@@ -59,7 +59,7 @@
59
59
  "@standard-schema/spec": "^1.1.0"
60
60
  },
61
61
  "dependencies": {
62
- "@optique/core": "1.0.0-dev.1758+0d05b449"
62
+ "@optique/core": "1.0.0-dev.1772+a1288062"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@standard-schema/spec": "^1.1.0",
@@ -67,7 +67,7 @@
67
67
  "tsdown": "^0.13.0",
68
68
  "typescript": "^5.8.3",
69
69
  "zod": "^3.25.0 || ^4.0.0",
70
- "@optique/env": "1.0.0-dev.1758+0d05b449"
70
+ "@optique/env": "1.0.0-dev.1772+a1288062"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsdown",