@optique/core 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.
@@ -131,6 +131,21 @@ function inheritAnnotations(source, target) {
131
131
  return cloned;
132
132
  }
133
133
  /**
134
+ * Returns whether an annotations record carries at least one own symbol key.
135
+ *
136
+ * An annotations object with no own symbol keys is treated as a no-op by the
137
+ * injection pipeline: it should behave identically to omitting the
138
+ * `annotations` option entirely. `null` and `undefined` are accepted for
139
+ * call-site convenience and always return `false`.
140
+ *
141
+ * @param annotations The annotations record to check.
142
+ * @returns `true` when the record has at least one own symbol key.
143
+ * @internal
144
+ */
145
+ function hasMeaningfulAnnotations(annotations) {
146
+ return annotations != null && Object.getOwnPropertySymbols(annotations).length > 0;
147
+ }
148
+ /**
134
149
  * Injects annotations into parser state while preserving state shape.
135
150
  *
136
151
  * - Primitive, null, and undefined states are wrapped with internal metadata.
@@ -138,6 +153,8 @@ function inheritAnnotations(source, target) {
138
153
  * - Plain object states are shallow-cloned with annotations attached.
139
154
  * - Built-in object states (Date/Map/Set/RegExp) are cloned by constructor.
140
155
  * - Other non-plain object states are cloned via prototype/descriptors.
156
+ * - If the `annotations` record has no own symbol keys, the state is
157
+ * returned unchanged; an empty annotations object is a no-op.
141
158
  *
142
159
  * @param state The parser state to annotate.
143
160
  * @param annotations The annotations to inject.
@@ -145,6 +162,7 @@ function inheritAnnotations(source, target) {
145
162
  * @internal
146
163
  */
147
164
  function injectAnnotations(state, annotations) {
165
+ if (!hasMeaningfulAnnotations(annotations)) return state;
148
166
  if (state == null || typeof state !== "object") {
149
167
  const wrapper = {};
150
168
  Object.defineProperties(wrapper, {
@@ -243,6 +261,7 @@ exports.annotationStateValueKey = annotationStateValueKey;
243
261
  exports.annotationWrapperKey = annotationWrapperKey;
244
262
  exports.firstPassAnnotationKey = firstPassAnnotationKey;
245
263
  exports.getAnnotations = getAnnotations;
264
+ exports.hasMeaningfulAnnotations = hasMeaningfulAnnotations;
246
265
  exports.inheritAnnotations = inheritAnnotations;
247
266
  exports.injectAnnotations = injectAnnotations;
248
267
  exports.isInjectedAnnotationWrapper = isInjectedAnnotationWrapper;
@@ -100,6 +100,19 @@ declare function annotateFreshArray<T>(source: unknown, target: readonly T[]): r
100
100
  * @internal
101
101
  */
102
102
  declare function inheritAnnotations<T>(source: unknown, target: T): T;
103
+ /**
104
+ * Returns whether an annotations record carries at least one own symbol key.
105
+ *
106
+ * An annotations object with no own symbol keys is treated as a no-op by the
107
+ * injection pipeline: it should behave identically to omitting the
108
+ * `annotations` option entirely. `null` and `undefined` are accepted for
109
+ * call-site convenience and always return `false`.
110
+ *
111
+ * @param annotations The annotations record to check.
112
+ * @returns `true` when the record has at least one own symbol key.
113
+ * @internal
114
+ */
115
+ declare function hasMeaningfulAnnotations(annotations: Annotations | null | undefined): annotations is Annotations;
103
116
  /**
104
117
  * Injects annotations into parser state while preserving state shape.
105
118
  *
@@ -108,6 +121,8 @@ declare function inheritAnnotations<T>(source: unknown, target: T): T;
108
121
  * - Plain object states are shallow-cloned with annotations attached.
109
122
  * - Built-in object states (Date/Map/Set/RegExp) are cloned by constructor.
110
123
  * - Other non-plain object states are cloned via prototype/descriptors.
124
+ * - If the `annotations` record has no own symbol keys, the state is
125
+ * returned unchanged; an empty annotations object is a no-op.
111
126
  *
112
127
  * @param state The parser state to annotate.
113
128
  * @param annotations The annotations to inject.
@@ -134,4 +149,4 @@ declare function unwrapInjectedAnnotationWrapper<T>(value: T): T;
134
149
  */
135
150
  declare function isInjectedAnnotationWrapper(value: unknown): boolean;
136
151
  //#endregion
137
- export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
152
+ export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, hasMeaningfulAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -100,6 +100,19 @@ declare function annotateFreshArray<T>(source: unknown, target: readonly T[]): r
100
100
  * @internal
101
101
  */
102
102
  declare function inheritAnnotations<T>(source: unknown, target: T): T;
103
+ /**
104
+ * Returns whether an annotations record carries at least one own symbol key.
105
+ *
106
+ * An annotations object with no own symbol keys is treated as a no-op by the
107
+ * injection pipeline: it should behave identically to omitting the
108
+ * `annotations` option entirely. `null` and `undefined` are accepted for
109
+ * call-site convenience and always return `false`.
110
+ *
111
+ * @param annotations The annotations record to check.
112
+ * @returns `true` when the record has at least one own symbol key.
113
+ * @internal
114
+ */
115
+ declare function hasMeaningfulAnnotations(annotations: Annotations | null | undefined): annotations is Annotations;
103
116
  /**
104
117
  * Injects annotations into parser state while preserving state shape.
105
118
  *
@@ -108,6 +121,8 @@ declare function inheritAnnotations<T>(source: unknown, target: T): T;
108
121
  * - Plain object states are shallow-cloned with annotations attached.
109
122
  * - Built-in object states (Date/Map/Set/RegExp) are cloned by constructor.
110
123
  * - Other non-plain object states are cloned via prototype/descriptors.
124
+ * - If the `annotations` record has no own symbol keys, the state is
125
+ * returned unchanged; an empty annotations object is a no-op.
111
126
  *
112
127
  * @param state The parser state to annotate.
113
128
  * @param annotations The annotations to inject.
@@ -134,4 +149,4 @@ declare function unwrapInjectedAnnotationWrapper<T>(value: T): T;
134
149
  */
135
150
  declare function isInjectedAnnotationWrapper(value: unknown): boolean;
136
151
  //#endregion
137
- export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
152
+ export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, hasMeaningfulAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -130,6 +130,21 @@ function inheritAnnotations(source, target) {
130
130
  return cloned;
131
131
  }
132
132
  /**
133
+ * Returns whether an annotations record carries at least one own symbol key.
134
+ *
135
+ * An annotations object with no own symbol keys is treated as a no-op by the
136
+ * injection pipeline: it should behave identically to omitting the
137
+ * `annotations` option entirely. `null` and `undefined` are accepted for
138
+ * call-site convenience and always return `false`.
139
+ *
140
+ * @param annotations The annotations record to check.
141
+ * @returns `true` when the record has at least one own symbol key.
142
+ * @internal
143
+ */
144
+ function hasMeaningfulAnnotations(annotations) {
145
+ return annotations != null && Object.getOwnPropertySymbols(annotations).length > 0;
146
+ }
147
+ /**
133
148
  * Injects annotations into parser state while preserving state shape.
134
149
  *
135
150
  * - Primitive, null, and undefined states are wrapped with internal metadata.
@@ -137,6 +152,8 @@ function inheritAnnotations(source, target) {
137
152
  * - Plain object states are shallow-cloned with annotations attached.
138
153
  * - Built-in object states (Date/Map/Set/RegExp) are cloned by constructor.
139
154
  * - Other non-plain object states are cloned via prototype/descriptors.
155
+ * - If the `annotations` record has no own symbol keys, the state is
156
+ * returned unchanged; an empty annotations object is a no-op.
140
157
  *
141
158
  * @param state The parser state to annotate.
142
159
  * @param annotations The annotations to inject.
@@ -144,6 +161,7 @@ function inheritAnnotations(source, target) {
144
161
  * @internal
145
162
  */
146
163
  function injectAnnotations(state, annotations) {
164
+ if (!hasMeaningfulAnnotations(annotations)) return state;
147
165
  if (state == null || typeof state !== "object") {
148
166
  const wrapper = {};
149
167
  Object.defineProperties(wrapper, {
@@ -236,4 +254,4 @@ function isInjectedAnnotationWrapper(value) {
236
254
  }
237
255
 
238
256
  //#endregion
239
- export { annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
257
+ export { annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, firstPassAnnotationKey, getAnnotations, hasMeaningfulAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -3795,6 +3795,11 @@ function group(label, parser, options = {}) {
3795
3795
  configurable: true,
3796
3796
  enumerable: false
3797
3797
  });
3798
+ if (typeof parser.validateValue === "function") Object.defineProperty(groupParser, "validateValue", {
3799
+ value: parser.validateValue.bind(parser),
3800
+ configurable: true,
3801
+ enumerable: false
3802
+ });
3798
3803
  return groupParser;
3799
3804
  }
3800
3805
  /**
@@ -3795,6 +3795,11 @@ function group(label, parser, options = {}) {
3795
3795
  configurable: true,
3796
3796
  enumerable: false
3797
3797
  });
3798
+ if (typeof parser.validateValue === "function") Object.defineProperty(groupParser, "validateValue", {
3799
+ value: parser.validateValue.bind(parser),
3800
+ configurable: true,
3801
+ enumerable: false
3802
+ });
3798
3803
  return groupParser;
3799
3804
  }
3800
3805
  /**
@@ -306,6 +306,20 @@ function optional(parser) {
306
306
  enumerable: false
307
307
  });
308
308
  }
309
+ if (typeof parser.validateValue === "function") {
310
+ const innerValidate = parser.validateValue.bind(parser);
311
+ Object.defineProperty(optionalParser, "validateValue", {
312
+ value(v) {
313
+ if (v === void 0) return require_mode_dispatch.wrapForMode(parser.$mode, {
314
+ success: true,
315
+ value: v
316
+ });
317
+ return innerValidate(v);
318
+ },
319
+ configurable: true,
320
+ enumerable: false
321
+ });
322
+ }
309
323
  if (parser.dependencyMetadata != null) {
310
324
  const composed = require_dependency_metadata.composeDependencyMetadata(parser.dependencyMetadata, "optional");
311
325
  if (composed != null) optionalParser.dependencyMetadata = composed;
@@ -504,6 +518,16 @@ function withDefault(parser, defaultValue, options) {
504
518
  enumerable: false
505
519
  });
506
520
  }
521
+ if (typeof parser.validateValue === "function") {
522
+ const innerValidate = parser.validateValue.bind(parser);
523
+ Object.defineProperty(withDefaultParser, "validateValue", {
524
+ value(v) {
525
+ return innerValidate(v);
526
+ },
527
+ configurable: true,
528
+ enumerable: false
529
+ });
530
+ }
507
531
  if (parser.dependencyMetadata != null) {
508
532
  const composed = require_dependency_metadata.composeDependencyMetadata(parser.dependencyMetadata, "withDefault", { defaultValue: () => {
509
533
  let v;
@@ -649,6 +673,7 @@ function map(parser, transform) {
649
673
  }
650
674
  };
651
675
  delete mappedParser.normalizeValue;
676
+ delete mappedParser.validateValue;
652
677
  if ("placeholder" in parser) Object.defineProperty(mappedParser, "placeholder", {
653
678
  get() {
654
679
  try {
@@ -1152,6 +1177,72 @@ function multiple(parser, options = {}) {
1152
1177
  enumerable: false
1153
1178
  });
1154
1179
  }
1180
+ {
1181
+ const innerValidate = typeof parser.validateValue === "function" ? parser.validateValue.bind(parser) : void 0;
1182
+ const validateArity = (values) => {
1183
+ if (values.length < min) {
1184
+ const customMessage = options.errors?.tooFew;
1185
+ return {
1186
+ success: false,
1187
+ error: customMessage ? typeof customMessage === "function" ? customMessage(min, values.length) : customMessage : require_message.message`Expected at least ${require_message.text(min.toLocaleString("en"))} values, but got only ${require_message.text(values.length.toLocaleString("en"))}.`
1188
+ };
1189
+ }
1190
+ if (values.length > max) {
1191
+ const customMessage = options.errors?.tooMany;
1192
+ return {
1193
+ success: false,
1194
+ error: customMessage ? typeof customMessage === "function" ? customMessage(max, values.length) : customMessage : require_message.message`Expected at most ${require_message.text(max.toLocaleString("en"))} values, but got ${require_message.text(values.length.toLocaleString("en"))}.`
1195
+ };
1196
+ }
1197
+ return {
1198
+ success: true,
1199
+ value: values
1200
+ };
1201
+ };
1202
+ Object.defineProperty(resultParser, "validateValue", {
1203
+ value(values) {
1204
+ if (!Array.isArray(values)) {
1205
+ const actualType = values === null ? "null" : typeof values;
1206
+ return require_mode_dispatch.wrapForMode(parser.$mode, {
1207
+ success: false,
1208
+ error: require_message.message`Expected an array of values, but received ${actualType}.`
1209
+ });
1210
+ }
1211
+ const arity = validateArity(values);
1212
+ if (!arity.success) return require_mode_dispatch.wrapForMode(parser.$mode, arity);
1213
+ if (innerValidate == null) return require_mode_dispatch.wrapForMode(parser.$mode, arity);
1214
+ return require_mode_dispatch.dispatchByMode(parser.$mode, () => {
1215
+ let changed = false;
1216
+ const normalized = [];
1217
+ for (const v of values) {
1218
+ const r = innerValidate(v);
1219
+ if (!r.success) return r;
1220
+ normalized.push(r.value);
1221
+ if (r.value !== v) changed = true;
1222
+ }
1223
+ return {
1224
+ success: true,
1225
+ value: changed ? normalized : values
1226
+ };
1227
+ }, async () => {
1228
+ let changed = false;
1229
+ const normalized = [];
1230
+ for (const v of values) {
1231
+ const r = await innerValidate(v);
1232
+ if (!r.success) return r;
1233
+ normalized.push(r.value);
1234
+ if (r.value !== v) changed = true;
1235
+ }
1236
+ return {
1237
+ success: true,
1238
+ value: changed ? normalized : values
1239
+ };
1240
+ });
1241
+ },
1242
+ configurable: true,
1243
+ enumerable: false
1244
+ });
1245
+ }
1155
1246
  if (parser.dependencyMetadata?.source != null) {
1156
1247
  const innerSource = parser.dependencyMetadata.source;
1157
1248
  Object.defineProperty(resultParser, "dependencyMetadata", {
@@ -1279,6 +1370,11 @@ function nonEmpty(parser) {
1279
1370
  configurable: true,
1280
1371
  enumerable: false
1281
1372
  });
1373
+ if (typeof parser.validateValue === "function") Object.defineProperty(nonEmptyParser, "validateValue", {
1374
+ value: parser.validateValue.bind(parser),
1375
+ configurable: true,
1376
+ enumerable: false
1377
+ });
1282
1378
  return nonEmptyParser;
1283
1379
  }
1284
1380
 
package/dist/modifiers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { annotateFreshArray, annotationKey, getAnnotations, inheritAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./annotations.js";
2
2
  import { formatMessage, message, text } from "./message.js";
3
- import { dispatchByMode, dispatchIterableByMode, mapModeValue } from "./mode-dispatch.js";
3
+ import { dispatchByMode, dispatchIterableByMode, mapModeValue, wrapForMode } from "./mode-dispatch.js";
4
4
  import { composeDependencyMetadata } from "./dependency-metadata.js";
5
5
  import { defineInheritedAnnotationParser, defineSourceBindingOnlyAnnotationCompletionParser, unmatchedNonCliDependencySourceStateMarker } from "./parser.js";
6
6
 
@@ -306,6 +306,20 @@ function optional(parser) {
306
306
  enumerable: false
307
307
  });
308
308
  }
309
+ if (typeof parser.validateValue === "function") {
310
+ const innerValidate = parser.validateValue.bind(parser);
311
+ Object.defineProperty(optionalParser, "validateValue", {
312
+ value(v) {
313
+ if (v === void 0) return wrapForMode(parser.$mode, {
314
+ success: true,
315
+ value: v
316
+ });
317
+ return innerValidate(v);
318
+ },
319
+ configurable: true,
320
+ enumerable: false
321
+ });
322
+ }
309
323
  if (parser.dependencyMetadata != null) {
310
324
  const composed = composeDependencyMetadata(parser.dependencyMetadata, "optional");
311
325
  if (composed != null) optionalParser.dependencyMetadata = composed;
@@ -504,6 +518,16 @@ function withDefault(parser, defaultValue, options) {
504
518
  enumerable: false
505
519
  });
506
520
  }
521
+ if (typeof parser.validateValue === "function") {
522
+ const innerValidate = parser.validateValue.bind(parser);
523
+ Object.defineProperty(withDefaultParser, "validateValue", {
524
+ value(v) {
525
+ return innerValidate(v);
526
+ },
527
+ configurable: true,
528
+ enumerable: false
529
+ });
530
+ }
507
531
  if (parser.dependencyMetadata != null) {
508
532
  const composed = composeDependencyMetadata(parser.dependencyMetadata, "withDefault", { defaultValue: () => {
509
533
  let v;
@@ -649,6 +673,7 @@ function map(parser, transform) {
649
673
  }
650
674
  };
651
675
  delete mappedParser.normalizeValue;
676
+ delete mappedParser.validateValue;
652
677
  if ("placeholder" in parser) Object.defineProperty(mappedParser, "placeholder", {
653
678
  get() {
654
679
  try {
@@ -1152,6 +1177,72 @@ function multiple(parser, options = {}) {
1152
1177
  enumerable: false
1153
1178
  });
1154
1179
  }
1180
+ {
1181
+ const innerValidate = typeof parser.validateValue === "function" ? parser.validateValue.bind(parser) : void 0;
1182
+ const validateArity = (values) => {
1183
+ if (values.length < min) {
1184
+ const customMessage = options.errors?.tooFew;
1185
+ return {
1186
+ success: false,
1187
+ error: customMessage ? typeof customMessage === "function" ? customMessage(min, values.length) : customMessage : message`Expected at least ${text(min.toLocaleString("en"))} values, but got only ${text(values.length.toLocaleString("en"))}.`
1188
+ };
1189
+ }
1190
+ if (values.length > max) {
1191
+ const customMessage = options.errors?.tooMany;
1192
+ return {
1193
+ success: false,
1194
+ error: customMessage ? typeof customMessage === "function" ? customMessage(max, values.length) : customMessage : message`Expected at most ${text(max.toLocaleString("en"))} values, but got ${text(values.length.toLocaleString("en"))}.`
1195
+ };
1196
+ }
1197
+ return {
1198
+ success: true,
1199
+ value: values
1200
+ };
1201
+ };
1202
+ Object.defineProperty(resultParser, "validateValue", {
1203
+ value(values) {
1204
+ if (!Array.isArray(values)) {
1205
+ const actualType = values === null ? "null" : typeof values;
1206
+ return wrapForMode(parser.$mode, {
1207
+ success: false,
1208
+ error: message`Expected an array of values, but received ${actualType}.`
1209
+ });
1210
+ }
1211
+ const arity = validateArity(values);
1212
+ if (!arity.success) return wrapForMode(parser.$mode, arity);
1213
+ if (innerValidate == null) return wrapForMode(parser.$mode, arity);
1214
+ return dispatchByMode(parser.$mode, () => {
1215
+ let changed = false;
1216
+ const normalized = [];
1217
+ for (const v of values) {
1218
+ const r = innerValidate(v);
1219
+ if (!r.success) return r;
1220
+ normalized.push(r.value);
1221
+ if (r.value !== v) changed = true;
1222
+ }
1223
+ return {
1224
+ success: true,
1225
+ value: changed ? normalized : values
1226
+ };
1227
+ }, async () => {
1228
+ let changed = false;
1229
+ const normalized = [];
1230
+ for (const v of values) {
1231
+ const r = await innerValidate(v);
1232
+ if (!r.success) return r;
1233
+ normalized.push(r.value);
1234
+ if (r.value !== v) changed = true;
1235
+ }
1236
+ return {
1237
+ success: true,
1238
+ value: changed ? normalized : values
1239
+ };
1240
+ });
1241
+ },
1242
+ configurable: true,
1243
+ enumerable: false
1244
+ });
1245
+ }
1155
1246
  if (parser.dependencyMetadata?.source != null) {
1156
1247
  const innerSource = parser.dependencyMetadata.source;
1157
1248
  Object.defineProperty(resultParser, "dependencyMetadata", {
@@ -1279,6 +1370,11 @@ function nonEmpty(parser) {
1279
1370
  configurable: true,
1280
1371
  enumerable: false
1281
1372
  });
1373
+ if (typeof parser.validateValue === "function") Object.defineProperty(nonEmptyParser, "validateValue", {
1374
+ value: parser.validateValue.bind(parser),
1375
+ configurable: true,
1376
+ enumerable: false
1377
+ });
1282
1378
  return nonEmptyParser;
1283
1379
  }
1284
1380
 
package/dist/parser.cjs CHANGED
@@ -68,7 +68,7 @@ function createParserContext(frame, exec) {
68
68
  }
69
69
  function injectAnnotationsIntoState(state, options) {
70
70
  const annotations = options?.annotations;
71
- if (annotations == null) return state;
71
+ if (!require_annotations.hasMeaningfulAnnotations(annotations)) return state;
72
72
  return require_annotations.injectAnnotations(state, annotations);
73
73
  }
74
74
  /**
@@ -97,7 +97,7 @@ function injectAnnotationsIntoState(state, options) {
97
97
  */
98
98
  function parseSync(parser, args, options) {
99
99
  const initialState = injectAnnotationsIntoState(parser.initialState, options);
100
- const shouldUnwrapAnnotatedValue = options?.annotations != null || require_annotations.isInjectedAnnotationWrapper(parser.initialState);
100
+ const shouldUnwrapAnnotatedValue = require_annotations.hasMeaningfulAnnotations(options?.annotations) || require_annotations.isInjectedAnnotationWrapper(parser.initialState);
101
101
  const exec = {
102
102
  usage: parser.usage,
103
103
  phase: "parse",
@@ -169,7 +169,7 @@ function isBufferUnchanged(previous, current) {
169
169
  */
170
170
  async function parseAsync(parser, args, options) {
171
171
  const initialState = injectAnnotationsIntoState(parser.initialState, options);
172
- const shouldUnwrapAnnotatedValue = options?.annotations != null || require_annotations.isInjectedAnnotationWrapper(parser.initialState);
172
+ const shouldUnwrapAnnotatedValue = require_annotations.hasMeaningfulAnnotations(options?.annotations) || require_annotations.isInjectedAnnotationWrapper(parser.initialState);
173
173
  const exec = {
174
174
  usage: parser.usage,
175
175
  phase: "parse",
package/dist/parser.d.cts CHANGED
@@ -262,6 +262,50 @@ interface Parser<M extends Mode = "sync", TValue = unknown, TState = unknown> {
262
262
  * @since 1.0.0
263
263
  */
264
264
  normalizeValue?(value: TValue): TValue;
265
+ /**
266
+ * Optionally re-validates a value as if it had been parsed from CLI
267
+ * input, surfacing any constraint violations from the underlying value
268
+ * parser (e.g., regex patterns, numeric bounds, `choice()` values).
269
+ *
270
+ * Wrappers like `bindEnv()` and `bindConfig()` call this on fallback
271
+ * values — environment variables parsed by a looser env parser,
272
+ * configured defaults, and values loaded from config files — so that
273
+ * those values obey the same validation semantics as CLI input.
274
+ * Without it, parser constraints can be silently bypassed through
275
+ * fallback paths.
276
+ *
277
+ * Built-in primitive parsers ({@link option}, {@link argument})
278
+ * implement this method by round-tripping the value through the inner
279
+ * {@link ValueParser.format} and {@link ValueParser.parse} calls: the
280
+ * value is serialized back to a string and re-parsed, which re-runs
281
+ * every constraint check. Combinator wrappers ({@link optional},
282
+ * {@link withDefault}) forward this method from their inner parser.
283
+ * {@link map} intentionally does *not* forward it because the mapping
284
+ * function is one-way: the mapped output type no longer corresponds
285
+ * to the inner parser's constraints. Exclusive combinators
286
+ * ({@link or}, `longestMatch()`) and multi-source combinators
287
+ * (`merge()`, `concat()`) intentionally do not implement this method
288
+ * because the active branch or key ownership is unknown at validation
289
+ * time.
290
+ *
291
+ * Implementations must wrap any *exception* thrown by `format()` in
292
+ * `try`/`catch` and return the original value as a successful
293
+ * {@link ValueParserResult}. This specifically protects
294
+ * dependency-derived parsers whose factory cannot run without the
295
+ * current dependency value, and custom value parsers whose `format()`
296
+ * intentionally throws for unsupported inputs. Values that
297
+ * `format()` successfully serializes to a string are always re-parsed,
298
+ * and any resulting parse failure is propagated — they represent the
299
+ * bug class this method exists to surface.
300
+ *
301
+ * @param value The candidate value to validate.
302
+ * @returns A {@link ValueParserResult} indicating success (with the
303
+ * possibly-canonicalized value) or failure (with an error
304
+ * message). In async mode, returns a `Promise` resolving to
305
+ * the result.
306
+ * @since 1.0.0
307
+ */
308
+ validateValue?(value: TValue): ModeValue<M, ValueParserResult<TValue>>;
265
309
  /**
266
310
  * Internal dependency metadata describing this parser's dependency
267
311
  * capabilities. Used by the dependency runtime to resolve dependencies
package/dist/parser.d.ts CHANGED
@@ -262,6 +262,50 @@ interface Parser<M extends Mode = "sync", TValue = unknown, TState = unknown> {
262
262
  * @since 1.0.0
263
263
  */
264
264
  normalizeValue?(value: TValue): TValue;
265
+ /**
266
+ * Optionally re-validates a value as if it had been parsed from CLI
267
+ * input, surfacing any constraint violations from the underlying value
268
+ * parser (e.g., regex patterns, numeric bounds, `choice()` values).
269
+ *
270
+ * Wrappers like `bindEnv()` and `bindConfig()` call this on fallback
271
+ * values — environment variables parsed by a looser env parser,
272
+ * configured defaults, and values loaded from config files — so that
273
+ * those values obey the same validation semantics as CLI input.
274
+ * Without it, parser constraints can be silently bypassed through
275
+ * fallback paths.
276
+ *
277
+ * Built-in primitive parsers ({@link option}, {@link argument})
278
+ * implement this method by round-tripping the value through the inner
279
+ * {@link ValueParser.format} and {@link ValueParser.parse} calls: the
280
+ * value is serialized back to a string and re-parsed, which re-runs
281
+ * every constraint check. Combinator wrappers ({@link optional},
282
+ * {@link withDefault}) forward this method from their inner parser.
283
+ * {@link map} intentionally does *not* forward it because the mapping
284
+ * function is one-way: the mapped output type no longer corresponds
285
+ * to the inner parser's constraints. Exclusive combinators
286
+ * ({@link or}, `longestMatch()`) and multi-source combinators
287
+ * (`merge()`, `concat()`) intentionally do not implement this method
288
+ * because the active branch or key ownership is unknown at validation
289
+ * time.
290
+ *
291
+ * Implementations must wrap any *exception* thrown by `format()` in
292
+ * `try`/`catch` and return the original value as a successful
293
+ * {@link ValueParserResult}. This specifically protects
294
+ * dependency-derived parsers whose factory cannot run without the
295
+ * current dependency value, and custom value parsers whose `format()`
296
+ * intentionally throws for unsupported inputs. Values that
297
+ * `format()` successfully serializes to a string are always re-parsed,
298
+ * and any resulting parse failure is propagated — they represent the
299
+ * bug class this method exists to surface.
300
+ *
301
+ * @param value The candidate value to validate.
302
+ * @returns A {@link ValueParserResult} indicating success (with the
303
+ * possibly-canonicalized value) or failure (with an error
304
+ * message). In async mode, returns a `Promise` resolving to
305
+ * the result.
306
+ * @since 1.0.0
307
+ */
308
+ validateValue?(value: TValue): ModeValue<M, ValueParserResult<TValue>>;
265
309
  /**
266
310
  * Internal dependency metadata describing this parser's dependency
267
311
  * capabilities. Used by the dependency runtime to resolve dependencies
package/dist/parser.js CHANGED
@@ -1,4 +1,4 @@
1
- import { injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./annotations.js";
1
+ import { hasMeaningfulAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./annotations.js";
2
2
  import { cloneMessage, message } from "./message.js";
3
3
  import { cloneUsage, normalizeUsage } from "./usage.js";
4
4
  import { cloneDocEntry, isDocEntryHidden } from "./doc.js";
@@ -68,7 +68,7 @@ function createParserContext(frame, exec) {
68
68
  }
69
69
  function injectAnnotationsIntoState(state, options) {
70
70
  const annotations = options?.annotations;
71
- if (annotations == null) return state;
71
+ if (!hasMeaningfulAnnotations(annotations)) return state;
72
72
  return injectAnnotations(state, annotations);
73
73
  }
74
74
  /**
@@ -97,7 +97,7 @@ function injectAnnotationsIntoState(state, options) {
97
97
  */
98
98
  function parseSync(parser, args, options) {
99
99
  const initialState = injectAnnotationsIntoState(parser.initialState, options);
100
- const shouldUnwrapAnnotatedValue = options?.annotations != null || isInjectedAnnotationWrapper(parser.initialState);
100
+ const shouldUnwrapAnnotatedValue = hasMeaningfulAnnotations(options?.annotations) || isInjectedAnnotationWrapper(parser.initialState);
101
101
  const exec = {
102
102
  usage: parser.usage,
103
103
  phase: "parse",
@@ -169,7 +169,7 @@ function isBufferUnchanged(previous, current) {
169
169
  */
170
170
  async function parseAsync(parser, args, options) {
171
171
  const initialState = injectAnnotationsIntoState(parser.initialState, options);
172
- const shouldUnwrapAnnotatedValue = options?.annotations != null || isInjectedAnnotationWrapper(parser.initialState);
172
+ const shouldUnwrapAnnotatedValue = hasMeaningfulAnnotations(options?.annotations) || isInjectedAnnotationWrapper(parser.initialState);
173
173
  const exec = {
174
174
  usage: parser.usage,
175
175
  phase: "parse",
@@ -438,6 +438,7 @@ function option(...args) {
438
438
  const isAsync = mode === "async";
439
439
  const syncValueParser = valueParser;
440
440
  const dependencyMetadata = valueParser != null ? require_dependency_metadata.extractDependencyMetadata(valueParser) : void 0;
441
+ const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : require_message.message`${require_message.optionNames(optionNames$1)}: ${error}`;
441
442
  const result = {
442
443
  $mode: mode,
443
444
  $valueType: [],
@@ -619,7 +620,6 @@ function option(...args) {
619
620
  };
620
621
  },
621
622
  complete(state, exec) {
622
- const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : require_message.message`${require_message.optionNames(optionNames$1)}: ${error}`;
623
623
  const missing = valueParser == null ? {
624
624
  success: true,
625
625
  value: false
@@ -693,6 +693,50 @@ function option(...args) {
693
693
  enumerable: false
694
694
  });
695
695
  }
696
+ if (valueParser == null) Object.defineProperty(result, "validateValue", {
697
+ value(v) {
698
+ if (typeof v !== "boolean") {
699
+ const actualType = v === null ? "null" : typeof v;
700
+ return require_mode_dispatch.wrapForMode(mode, {
701
+ success: false,
702
+ error: formatInvalidValueError(require_message.message`Expected a boolean value, but received ${actualType}.`)
703
+ });
704
+ }
705
+ return require_mode_dispatch.wrapForMode(mode, {
706
+ success: true,
707
+ value: v
708
+ });
709
+ },
710
+ configurable: true,
711
+ enumerable: false
712
+ });
713
+ else if (!require_dependency.isDerivedValueParser(valueParser)) {
714
+ const vp = valueParser;
715
+ const wrapParseResult = (parsed) => parsed.success ? parsed : {
716
+ success: false,
717
+ error: formatInvalidValueError(parsed.error)
718
+ };
719
+ Object.defineProperty(result, "validateValue", {
720
+ value(v) {
721
+ let stringified;
722
+ try {
723
+ stringified = vp.format(v);
724
+ } catch {
725
+ return require_mode_dispatch.wrapForMode(mode, {
726
+ success: true,
727
+ value: v
728
+ });
729
+ }
730
+ if (typeof stringified !== "string") return require_mode_dispatch.wrapForMode(mode, {
731
+ success: true,
732
+ value: v
733
+ });
734
+ return require_mode_dispatch.dispatchByMode(mode, () => wrapParseResult(vp.parse(stringified)), async () => wrapParseResult(await vp.parse(stringified)));
735
+ },
736
+ configurable: true,
737
+ enumerable: false
738
+ });
739
+ }
696
740
  if (valueParser != null) Object.defineProperty(result, "placeholder", {
697
741
  get() {
698
742
  try {
@@ -934,6 +978,7 @@ function argument(valueParser, options = {}) {
934
978
  const isAsync = valueParser.$mode === "async";
935
979
  const syncValueParser = valueParser;
936
980
  const dependencyMetadata = require_dependency_metadata.extractDependencyMetadata(valueParser);
981
+ const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : require_message.message`${require_message.metavar(valueParser.metavar)}: ${error}`;
937
982
  const optionPattern = /^--?[a-z0-9-]+$/i;
938
983
  const term = {
939
984
  type: "argument",
@@ -1007,7 +1052,6 @@ function argument(valueParser, options = {}) {
1007
1052
  });
1008
1053
  },
1009
1054
  complete(state, exec) {
1010
- const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : require_message.message`${require_message.metavar(valueParser.metavar)}: ${error}`;
1011
1055
  const missing = {
1012
1056
  success: false,
1013
1057
  error: options.errors?.endOfInput ?? require_message.message`Expected a ${require_message.metavar(valueParser.metavar)}, but too few arguments.`
@@ -1075,6 +1119,34 @@ function argument(valueParser, options = {}) {
1075
1119
  enumerable: false
1076
1120
  });
1077
1121
  }
1122
+ if (!require_dependency.isDerivedValueParser(valueParser)) {
1123
+ const vp = valueParser;
1124
+ const vpMode = valueParser.$mode;
1125
+ const wrapParseResult = (parsed) => parsed.success ? parsed : {
1126
+ success: false,
1127
+ error: formatInvalidValueError(parsed.error)
1128
+ };
1129
+ Object.defineProperty(result, "validateValue", {
1130
+ value(v) {
1131
+ let stringified;
1132
+ try {
1133
+ stringified = vp.format(v);
1134
+ } catch {
1135
+ return require_mode_dispatch.wrapForMode(vpMode, {
1136
+ success: true,
1137
+ value: v
1138
+ });
1139
+ }
1140
+ if (typeof stringified !== "string") return require_mode_dispatch.wrapForMode(vpMode, {
1141
+ success: true,
1142
+ value: v
1143
+ });
1144
+ return require_mode_dispatch.dispatchByMode(vpMode, () => wrapParseResult(syncValueParser.parse(stringified)), async () => wrapParseResult(await vp.parse(stringified)));
1145
+ },
1146
+ configurable: true,
1147
+ enumerable: false
1148
+ });
1149
+ }
1078
1150
  Object.defineProperty(result, "placeholder", {
1079
1151
  get() {
1080
1152
  try {
@@ -1316,6 +1388,11 @@ function command(name, parser, options = {}) {
1316
1388
  configurable: true,
1317
1389
  enumerable: false
1318
1390
  });
1391
+ if (typeof parser.validateValue === "function") Object.defineProperty(result, "validateValue", {
1392
+ value: parser.validateValue.bind(parser),
1393
+ configurable: true,
1394
+ enumerable: false
1395
+ });
1319
1396
  return result;
1320
1397
  }
1321
1398
  /**
@@ -3,7 +3,7 @@ import { message, metavar, optionName, optionNames, text, valueSet } from "./mes
3
3
  import { getDefaultValuesFunction, getDependencyIds, getSnapshottedDefaultDependencyValues, isDerivedValueParser, suggestWithDependency } from "./dependency.js";
4
4
  import { validateCommandNames, validateOptionNames } from "./validate.js";
5
5
  import { extractOptionNames, isDocHidden, isSuggestionHidden } from "./usage.js";
6
- import { dispatchByMode, dispatchIterableByMode } from "./mode-dispatch.js";
6
+ import { dispatchByMode, dispatchIterableByMode, wrapForMode } from "./mode-dispatch.js";
7
7
  import { extractDependencyMetadata } from "./dependency-metadata.js";
8
8
  import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
9
9
  import { extractLeadingCommandNames } from "./usage-internals.js";
@@ -438,6 +438,7 @@ function option(...args) {
438
438
  const isAsync = mode === "async";
439
439
  const syncValueParser = valueParser;
440
440
  const dependencyMetadata = valueParser != null ? extractDependencyMetadata(valueParser) : void 0;
441
+ const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : message`${optionNames(optionNames$1)}: ${error}`;
441
442
  const result = {
442
443
  $mode: mode,
443
444
  $valueType: [],
@@ -619,7 +620,6 @@ function option(...args) {
619
620
  };
620
621
  },
621
622
  complete(state, exec) {
622
- const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : message`${optionNames(optionNames$1)}: ${error}`;
623
623
  const missing = valueParser == null ? {
624
624
  success: true,
625
625
  value: false
@@ -693,6 +693,50 @@ function option(...args) {
693
693
  enumerable: false
694
694
  });
695
695
  }
696
+ if (valueParser == null) Object.defineProperty(result, "validateValue", {
697
+ value(v) {
698
+ if (typeof v !== "boolean") {
699
+ const actualType = v === null ? "null" : typeof v;
700
+ return wrapForMode(mode, {
701
+ success: false,
702
+ error: formatInvalidValueError(message`Expected a boolean value, but received ${actualType}.`)
703
+ });
704
+ }
705
+ return wrapForMode(mode, {
706
+ success: true,
707
+ value: v
708
+ });
709
+ },
710
+ configurable: true,
711
+ enumerable: false
712
+ });
713
+ else if (!isDerivedValueParser(valueParser)) {
714
+ const vp = valueParser;
715
+ const wrapParseResult = (parsed) => parsed.success ? parsed : {
716
+ success: false,
717
+ error: formatInvalidValueError(parsed.error)
718
+ };
719
+ Object.defineProperty(result, "validateValue", {
720
+ value(v) {
721
+ let stringified;
722
+ try {
723
+ stringified = vp.format(v);
724
+ } catch {
725
+ return wrapForMode(mode, {
726
+ success: true,
727
+ value: v
728
+ });
729
+ }
730
+ if (typeof stringified !== "string") return wrapForMode(mode, {
731
+ success: true,
732
+ value: v
733
+ });
734
+ return dispatchByMode(mode, () => wrapParseResult(vp.parse(stringified)), async () => wrapParseResult(await vp.parse(stringified)));
735
+ },
736
+ configurable: true,
737
+ enumerable: false
738
+ });
739
+ }
696
740
  if (valueParser != null) Object.defineProperty(result, "placeholder", {
697
741
  get() {
698
742
  try {
@@ -934,6 +978,7 @@ function argument(valueParser, options = {}) {
934
978
  const isAsync = valueParser.$mode === "async";
935
979
  const syncValueParser = valueParser;
936
980
  const dependencyMetadata = extractDependencyMetadata(valueParser);
981
+ const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : message`${metavar(valueParser.metavar)}: ${error}`;
937
982
  const optionPattern = /^--?[a-z0-9-]+$/i;
938
983
  const term = {
939
984
  type: "argument",
@@ -1007,7 +1052,6 @@ function argument(valueParser, options = {}) {
1007
1052
  });
1008
1053
  },
1009
1054
  complete(state, exec) {
1010
- const formatInvalidValueError = (error) => options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(error) : options.errors.invalidValue : message`${metavar(valueParser.metavar)}: ${error}`;
1011
1055
  const missing = {
1012
1056
  success: false,
1013
1057
  error: options.errors?.endOfInput ?? message`Expected a ${metavar(valueParser.metavar)}, but too few arguments.`
@@ -1075,6 +1119,34 @@ function argument(valueParser, options = {}) {
1075
1119
  enumerable: false
1076
1120
  });
1077
1121
  }
1122
+ if (!isDerivedValueParser(valueParser)) {
1123
+ const vp = valueParser;
1124
+ const vpMode = valueParser.$mode;
1125
+ const wrapParseResult = (parsed) => parsed.success ? parsed : {
1126
+ success: false,
1127
+ error: formatInvalidValueError(parsed.error)
1128
+ };
1129
+ Object.defineProperty(result, "validateValue", {
1130
+ value(v) {
1131
+ let stringified;
1132
+ try {
1133
+ stringified = vp.format(v);
1134
+ } catch {
1135
+ return wrapForMode(vpMode, {
1136
+ success: true,
1137
+ value: v
1138
+ });
1139
+ }
1140
+ if (typeof stringified !== "string") return wrapForMode(vpMode, {
1141
+ success: true,
1142
+ value: v
1143
+ });
1144
+ return dispatchByMode(vpMode, () => wrapParseResult(syncValueParser.parse(stringified)), async () => wrapParseResult(await vp.parse(stringified)));
1145
+ },
1146
+ configurable: true,
1147
+ enumerable: false
1148
+ });
1149
+ }
1078
1150
  Object.defineProperty(result, "placeholder", {
1079
1151
  get() {
1080
1152
  try {
@@ -1316,6 +1388,11 @@ function command(name, parser, options = {}) {
1316
1388
  configurable: true,
1317
1389
  enumerable: false
1318
1390
  });
1391
+ if (typeof parser.validateValue === "function") Object.defineProperty(result, "validateValue", {
1392
+ value: parser.validateValue.bind(parser),
1393
+ configurable: true,
1394
+ enumerable: false
1395
+ });
1319
1396
  return result;
1320
1397
  }
1321
1398
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1758+0d05b449",
3
+ "version": "1.0.0-dev.1772+a1288062",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",