@optique/config 1.0.0-dev.921 → 1.0.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.cjs CHANGED
@@ -24,305 +24,46 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  const node_fs = __toESM(require("node:fs"));
25
25
  const node_path = __toESM(require("node:path"));
26
26
  const __optique_core_annotations = __toESM(require("@optique/core/annotations"));
27
+ const __optique_core_extension = __toESM(require("@optique/core/extension"));
27
28
  const __optique_core_message = __toESM(require("@optique/core/message"));
28
- const __optique_core_mode_dispatch = __toESM(require("@optique/core/mode-dispatch"));
29
29
 
30
30
  //#region src/index.ts
31
- const deferPromptUntilConfigResolvesKey = Symbol.for("@optique/config/deferPromptUntilResolved");
32
- const phase1ConfigAnnotationsKey = Symbol.for("@optique/config/phase1PromptAnnotations");
33
- const phase2UndefinedParsedValueKey = Symbol.for("@optique/config/phase2UndefinedParsedValue");
34
- const deferredPromptValueKey = Symbol.for("@optique/inquirer/deferredPromptValue");
35
- /**
36
- * Internal registry for active config data during config context execution.
37
- * This is a workaround for the limitation that object() doesn't propagate
38
- * annotations to child field parsers.
39
- * @internal
40
- */
41
- const activeConfigRegistry = /* @__PURE__ */ new Map();
42
- /**
43
- * Internal registry for active config metadata during config context execution.
44
- * @internal
45
- */
46
- const activeConfigMetaRegistry = /* @__PURE__ */ new Map();
47
31
  const phase1ConfigAnnotationMarker = Symbol("@optique/config/phase1Annotation");
48
- function isDeferredPromptValue(value) {
49
- return value != null && typeof value === "object" && deferredPromptValueKey in value;
50
- }
51
- function isPhase2UndefinedParsedValue(value) {
52
- return value != null && typeof value === "object" && phase2UndefinedParsedValueKey in value;
53
- }
54
- function isPlainObject(value) {
55
- const proto = Object.getPrototypeOf(value);
56
- return proto === Object.prototype || proto === null;
32
+ function getTypeName(value) {
33
+ if (value === null) return "null";
34
+ if (Array.isArray(value)) return "array";
35
+ return typeof value;
57
36
  }
58
- function shouldSkipCollectionOwnKey(value, key) {
59
- if (Array.isArray(value)) return key === "length" || typeof key === "string" && Number.isInteger(Number(key)) && String(Number(key)) === key;
60
- return false;
61
- }
62
- function containsDeferredPromptValuesInOwnProperties(value, seen) {
63
- for (const key of Reflect.ownKeys(value)) {
64
- if (shouldSkipCollectionOwnKey(value, key)) continue;
65
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
66
- if (descriptor != null && "value" in descriptor && containsDeferredPromptValues(descriptor.value, seen)) return true;
67
- }
68
- return false;
69
- }
70
- function copySanitizedOwnProperties(source, target, seen) {
71
- for (const key of Reflect.ownKeys(source)) {
72
- if (shouldSkipCollectionOwnKey(source, key)) continue;
73
- const descriptor = Object.getOwnPropertyDescriptor(source, key);
74
- if (descriptor == null) continue;
75
- if ("value" in descriptor) descriptor.value = stripDeferredPromptValues(descriptor.value, seen);
76
- Object.defineProperty(target, key, descriptor);
77
- }
78
- }
79
- function containsDeferredPromptValues(value, seen = /* @__PURE__ */ new WeakSet()) {
80
- if (isDeferredPromptValue(value)) return true;
81
- if (value == null || typeof value !== "object") return false;
82
- if (seen.has(value)) return false;
83
- seen.add(value);
84
- if (Array.isArray(value)) {
85
- if (value.some((item) => containsDeferredPromptValues(item, seen))) return true;
86
- return containsDeferredPromptValuesInOwnProperties(value, seen);
87
- }
88
- if (value instanceof Set) {
89
- for (const entryValue of value) if (containsDeferredPromptValues(entryValue, seen)) return true;
90
- return containsDeferredPromptValuesInOwnProperties(value, seen);
91
- }
92
- if (value instanceof Map) {
93
- for (const [key, entryValue] of value) if (containsDeferredPromptValues(key, seen) || containsDeferredPromptValues(entryValue, seen)) return true;
94
- return containsDeferredPromptValuesInOwnProperties(value, seen);
95
- }
96
- return containsDeferredPromptValuesInOwnProperties(value, seen);
97
- }
98
- const SANITIZE_FAILED = Symbol("sanitizeFailed");
99
- const activeSanitizations = /* @__PURE__ */ new WeakMap();
100
- function callWithSanitizedOwnProperties(target, fn, args, strip, seen) {
101
- let active = activeSanitizations.get(target);
102
- if (active != null) active.count++;
103
- else {
104
- const saved = /* @__PURE__ */ new Map();
105
- const sanitizedValues = /* @__PURE__ */ new Map();
106
- for (const key of Reflect.ownKeys(target)) {
107
- const desc = Object.getOwnPropertyDescriptor(target, key);
108
- if (desc != null && "value" in desc) {
109
- let stripped;
110
- try {
111
- stripped = strip(desc.value, seen);
112
- } catch {
113
- for (const [k, d] of saved) try {
114
- Object.defineProperty(target, k, d);
115
- } catch {}
116
- return SANITIZE_FAILED;
117
- }
118
- if (stripped !== desc.value) try {
119
- Object.defineProperty(target, key, {
120
- ...desc,
121
- value: stripped
122
- });
123
- saved.set(key, desc);
124
- sanitizedValues.set(key, stripped);
125
- } catch {
126
- for (const [k, d] of saved) try {
127
- Object.defineProperty(target, k, d);
128
- } catch {}
129
- return SANITIZE_FAILED;
130
- }
131
- }
132
- }
133
- active = {
134
- saved,
135
- sanitizedValues,
136
- count: 1
137
- };
138
- activeSanitizations.set(target, active);
139
- }
140
- function release() {
141
- active.count--;
142
- if (active.count === 0) {
143
- activeSanitizations.delete(target);
144
- for (const [key, desc] of active.saved) try {
145
- const current = Object.getOwnPropertyDescriptor(target, key);
146
- if (current == null) continue;
147
- if ("value" in current && current.value !== active.sanitizedValues.get(key)) continue;
148
- Object.defineProperty(target, key, desc);
149
- } catch {}
150
- }
151
- }
152
- let result;
153
- try {
154
- result = fn.apply(target, args);
155
- } catch (e) {
156
- release();
157
- throw e;
158
- }
159
- if (result instanceof Promise) return result.then((v) => {
160
- release();
161
- return strip(v, seen);
162
- }, (e) => {
163
- release();
164
- throw e;
165
- });
166
- release();
167
- return strip(result, seen);
168
- }
169
- function callMethodOnSanitizedTarget(fn, proxy, target, args, strip, seen) {
170
- const result = callWithSanitizedOwnProperties(target, fn, args, strip, seen);
171
- if (result !== SANITIZE_FAILED) return result;
172
- const fallback = fn.apply(proxy, args);
173
- if (fallback instanceof Promise) return fallback.then((v) => strip(v, seen));
174
- return strip(fallback, seen);
175
- }
176
- function createSanitizedNonPlainView(value, seen) {
177
- const methodCache = /* @__PURE__ */ new Map();
178
- const proxy = new Proxy(value, {
179
- get(target, key, receiver) {
180
- const descriptor = Object.getOwnPropertyDescriptor(target, key);
181
- if (descriptor != null && "value" in descriptor) {
182
- if (!descriptor.configurable && !descriptor.writable) return descriptor.value;
183
- const val = stripDeferredPromptValues(descriptor.value, seen);
184
- if (typeof val === "function") {
185
- if (!descriptor.configurable && !descriptor.writable || /^class[\s{]/.test(Function.prototype.toString.call(val))) return val;
186
- const cached = methodCache.get(key);
187
- if (cached != null && cached.fn === val) return cached.wrapper;
188
- const wrapper = function(...args) {
189
- if (this !== proxy) return stripDeferredPromptValues(val.apply(this, args), seen);
190
- return callMethodOnSanitizedTarget(val, proxy, target, args, stripDeferredPromptValues, seen);
191
- };
192
- methodCache.set(key, {
193
- fn: val,
194
- wrapper
195
- });
196
- return wrapper;
197
- }
198
- return val;
199
- }
200
- let isAccessor = false;
201
- for (let proto = target; proto != null; proto = Object.getPrototypeOf(proto)) {
202
- const d = Object.getOwnPropertyDescriptor(proto, key);
203
- if (d != null) {
204
- isAccessor = "get" in d;
205
- break;
206
- }
207
- }
208
- const result = Reflect.get(target, key, receiver);
209
- if (typeof result === "function") {
210
- if (/^class[\s{]/.test(Function.prototype.toString.call(result))) return result;
211
- if (!isAccessor) {
212
- const cached = methodCache.get(key);
213
- if (cached != null && cached.fn === result) return cached.wrapper;
214
- const wrapper = function(...args) {
215
- if (this !== proxy) return stripDeferredPromptValues(result.apply(this, args), seen);
216
- return callMethodOnSanitizedTarget(result, proxy, target, args, stripDeferredPromptValues, seen);
217
- };
218
- methodCache.set(key, {
219
- fn: result,
220
- wrapper
221
- });
222
- return wrapper;
223
- }
224
- return function(...args) {
225
- if (this !== proxy) return stripDeferredPromptValues(result.apply(this, args), seen);
226
- return callMethodOnSanitizedTarget(result, proxy, target, args, stripDeferredPromptValues, seen);
227
- };
228
- }
229
- return stripDeferredPromptValues(result, seen);
230
- },
231
- getOwnPropertyDescriptor(target, key) {
232
- const descriptor = Object.getOwnPropertyDescriptor(target, key);
233
- if (descriptor == null || !("value" in descriptor)) return descriptor;
234
- if (!descriptor.configurable && !descriptor.writable) return descriptor;
235
- return {
236
- ...descriptor,
237
- value: stripDeferredPromptValues(descriptor.value, seen)
238
- };
239
- }
240
- });
241
- seen.set(value, proxy);
242
- return proxy;
243
- }
244
- function stripDeferredPromptValues(value, seen = /* @__PURE__ */ new WeakMap()) {
245
- if (isDeferredPromptValue(value)) return void 0;
246
- if (value == null || typeof value !== "object") return value;
247
- const cached = seen.get(value);
248
- if (cached !== void 0) return cached;
249
- if (Array.isArray(value)) {
250
- if (!containsDeferredPromptValues(value)) return value;
251
- const clone$1 = new Array(value.length);
252
- seen.set(value, clone$1);
253
- for (let i = 0; i < value.length; i++) clone$1[i] = stripDeferredPromptValues(value[i], seen);
254
- copySanitizedOwnProperties(value, clone$1, seen);
255
- return clone$1;
256
- }
257
- if (value instanceof Set) {
258
- if (!containsDeferredPromptValues(value)) return value;
259
- const clone$1 = /* @__PURE__ */ new Set();
260
- seen.set(value, clone$1);
261
- for (const entryValue of value) clone$1.add(stripDeferredPromptValues(entryValue, seen));
262
- copySanitizedOwnProperties(value, clone$1, seen);
263
- return clone$1;
264
- }
265
- if (value instanceof Map) {
266
- if (!containsDeferredPromptValues(value)) return value;
267
- const clone$1 = /* @__PURE__ */ new Map();
268
- seen.set(value, clone$1);
269
- for (const [key, entryValue] of value) clone$1.set(stripDeferredPromptValues(key, seen), stripDeferredPromptValues(entryValue, seen));
270
- copySanitizedOwnProperties(value, clone$1, seen);
271
- return clone$1;
272
- }
273
- if (!isPlainObject(value)) return containsDeferredPromptValues(value) ? createSanitizedNonPlainView(value, seen) : value;
274
- if (!containsDeferredPromptValues(value)) return value;
275
- const clone = Object.create(Object.getPrototypeOf(value));
276
- seen.set(value, clone);
277
- for (const key of Reflect.ownKeys(value)) {
278
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
279
- if (descriptor == null) continue;
280
- if ("value" in descriptor) descriptor.value = stripDeferredPromptValues(descriptor.value, seen);
281
- Object.defineProperty(clone, key, descriptor);
282
- }
283
- return clone;
37
+ function isPromiseLike(value) {
38
+ return value != null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
284
39
  }
285
40
  /**
286
- * Sets active config data for a context.
287
- * @internal
41
+ * Detects native Promises and cross-realm Promises. Cross-realm
42
+ * Promises fail `instanceof Promise` but still carry the spec-required
43
+ * `Symbol.toStringTag === "Promise"`. Domain objects that merely
44
+ * define a `then()` method are not matched unless they also set
45
+ * `Symbol.toStringTag` to `"Promise"`.
288
46
  */
289
- function setActiveConfig(contextId, data) {
290
- activeConfigRegistry.set(contextId, data);
47
+ function isPromise(value) {
48
+ if (value instanceof Promise) return true;
49
+ if (value == null || typeof value !== "object" && typeof value !== "function") return false;
50
+ return isPromiseLike(value) && value[Symbol.toStringTag] === "Promise";
291
51
  }
292
- /**
293
- * Gets active config data for a context.
294
- * @internal
295
- */
296
- function getActiveConfig(contextId) {
297
- return activeConfigRegistry.get(contextId);
52
+ function validateLoadResult(loaded) {
53
+ if (loaded == null) return void 0;
54
+ if (typeof loaded !== "object" || Array.isArray(loaded)) throw new TypeError(`Expected load() to return an object, but got: ${getTypeName(loaded)}.`);
55
+ if (!("config" in loaded)) throw new TypeError("Expected load() result to have a config property.");
56
+ const result = loaded;
57
+ if (isPromise(result.config)) throw new TypeError("Expected config in load() result to not be a Promise. Resolve the Promise before returning.");
58
+ if (isPromise(result.meta)) throw new TypeError("Expected meta in load() result to not be a Promise. Resolve the Promise before returning.");
59
+ return loaded;
298
60
  }
299
- /**
300
- * Clears active config data for a context.
301
- * @internal
302
- */
303
- function clearActiveConfig(contextId) {
304
- activeConfigRegistry.delete(contextId);
305
- }
306
- /**
307
- * Sets active config metadata for a context.
308
- * @internal
309
- */
310
- function setActiveConfigMeta(contextId, meta) {
311
- activeConfigMetaRegistry.set(contextId, meta);
312
- }
313
- /**
314
- * Gets active config metadata for a context.
315
- * @internal
316
- */
317
- function getActiveConfigMeta(contextId) {
318
- return activeConfigMetaRegistry.get(contextId);
319
- }
320
- /**
321
- * Clears active config metadata for a context.
322
- * @internal
323
- */
324
- function clearActiveConfigMeta(contextId) {
325
- activeConfigMetaRegistry.delete(contextId);
61
+ function isStandardSchema(value) {
62
+ if (value == null || typeof value !== "object" && typeof value !== "function") return false;
63
+ if (!("~standard" in value)) return false;
64
+ const standard = value["~standard"];
65
+ if (standard == null || typeof standard !== "object" && typeof standard !== "function") return false;
66
+ return typeof standard.validate === "function";
326
67
  }
327
68
  function isErrnoException(error) {
328
69
  return typeof error === "object" && error !== null && "code" in error;
@@ -348,13 +89,24 @@ function validateWithSchema(schema, rawData) {
348
89
  *
349
90
  * The config context implements the `SourceContext` interface and can be used
350
91
  * with `runWith()` from *@optique/core* or `run()`/`runAsync()` from
351
- * *@optique/run* to provide configuration file support.
92
+ * *@optique/run* to provide configuration file support. Each runner call
93
+ * receives its own annotation snapshot, so the same `ConfigContext`
94
+ * instance can be reused safely across independent or concurrent runs.
95
+ * When calling `context.getAnnotations()` manually, omit the request for a
96
+ * phase-1 snapshot or pass `{ phase: "phase2", parsed }` for a phase-two
97
+ * snapshot, then thread the returned annotations into low-level APIs such
98
+ * as `parse()`, `parseAsync()`, `parser.complete()`, `suggest()`, or
99
+ * `getDocPage()`. Calling `getAnnotations()` by itself does not affect
100
+ * later parses unless those returned annotations are explicitly threaded
101
+ * into a low-level API call.
352
102
  *
353
103
  * @template T The output type of the config schema.
354
104
  * @template TConfigMeta The metadata type for config sources.
355
105
  * @param options Configuration options including schema and optional file
356
106
  * parser.
357
107
  * @returns A config context that can be used with `bindConfig()` and runners.
108
+ * @throws {TypeError} If `schema` is not a valid Standard Schema object.
109
+ * @throws {TypeError} If `fileParser` is provided but is not a function.
358
110
  * @since 0.10.0
359
111
  *
360
112
  * @example
@@ -371,46 +123,58 @@ function validateWithSchema(schema, rawData) {
371
123
  * ```
372
124
  */
373
125
  function createConfigContext(options) {
126
+ const rawSchema = options.schema;
127
+ if (!isStandardSchema(rawSchema)) throw new TypeError(`Expected schema to be a Standard Schema object, but got: ${getTypeName(rawSchema)}.`);
128
+ const rawFileParser = options.fileParser;
129
+ if (rawFileParser !== void 0 && typeof rawFileParser !== "function") throw new TypeError(`Expected fileParser to be a function, but got: ${getTypeName(rawFileParser)}.`);
374
130
  const contextId = Symbol(`@optique/config:${Math.random()}`);
375
131
  const context = {
376
132
  id: contextId,
377
- schema: options.schema,
378
- mode: "dynamic",
379
- [phase1ConfigAnnotationsKey](parsed, annotations) {
380
- if (parsed === void 0) return { [contextId]: phase1ConfigAnnotationMarker };
133
+ schema: rawSchema,
134
+ phase: "two-pass",
135
+ getInternalAnnotations(request, annotations) {
136
+ if (request.phase === "phase1") return { [contextId]: phase1ConfigAnnotationMarker };
381
137
  return Object.getOwnPropertySymbols(annotations).includes(contextId) ? void 0 : { [contextId]: void 0 };
382
138
  },
383
- getAnnotations(parsed, runtimeOptions) {
384
- if (parsed === void 0) return {};
139
+ getAnnotations(request, runtimeOptions) {
140
+ if (request === void 0) return {};
141
+ if (request === null || typeof request !== "object" || !("phase" in request) || request.phase !== "phase1" && request.phase !== "phase2" || request.phase === "phase2" && !("parsed" in request)) throw new TypeError(`Expected getAnnotations() to receive no request or a SourceContextRequest ({ phase: "phase1" } or { phase: "phase2", parsed }), but got: ${getTypeName(request)}.`);
142
+ if (request.phase === "phase1") return {};
385
143
  const opts = runtimeOptions;
386
144
  if (!opts || !opts.getConfigPath && !opts.load) throw new TypeError("Either getConfigPath or load must be provided in the runner options when using ConfigContext.");
387
- const parsedValue = isPhase2UndefinedParsedValue(parsed) ? void 0 : stripDeferredPromptValues(parsed);
388
- const parsedPlaceholder = parsedValue;
145
+ if (opts.load !== void 0 && typeof opts.load !== "function") throw new TypeError(`Expected load to be a function, but got: ${getTypeName(opts.load)}.`);
146
+ if (!opts.load && opts.getConfigPath !== void 0 && typeof opts.getConfigPath !== "function") throw new TypeError(`Expected getConfigPath to be a function, but got: ${getTypeName(opts.getConfigPath)}.`);
147
+ const parsedPlaceholder = request.parsed;
148
+ const emptyAnnotations = () => ({});
389
149
  const buildAnnotations = (configData, configMeta) => {
390
- if (configData === void 0 || configData === null) return {};
391
- setActiveConfig(contextId, configData);
392
- if (configMeta !== void 0) {
393
- setActiveConfigMeta(contextId, configMeta);
394
- return { [contextId]: {
395
- data: configData,
396
- meta: configMeta
397
- } };
398
- }
150
+ if (configData === void 0 || configData === null) return emptyAnnotations();
151
+ if (configMeta !== void 0) return { [contextId]: {
152
+ data: configData,
153
+ meta: configMeta
154
+ } };
399
155
  return { [contextId]: { data: configData } };
400
156
  };
401
157
  const validateAndBuildAnnotations = (rawData, configMeta) => {
402
- const validated = validateWithSchema(options.schema, rawData);
158
+ const validated = validateWithSchema(rawSchema, rawData);
403
159
  if (validated instanceof Promise) return validated.then((configData) => buildAnnotations(configData, configMeta));
404
160
  return buildAnnotations(validated, configMeta);
405
161
  };
406
162
  if (opts.load) {
407
163
  const loaded = opts.load(parsedPlaceholder);
408
- if (loaded instanceof Promise) return loaded.then(({ config, meta }) => validateAndBuildAnnotations(config, meta));
409
- return validateAndBuildAnnotations(loaded.config, loaded.meta);
164
+ if (isPromise(loaded)) return Promise.resolve(loaded).then((resolved) => {
165
+ const validated$1 = validateLoadResult(resolved);
166
+ if (validated$1 === void 0) return emptyAnnotations();
167
+ return validateAndBuildAnnotations(validated$1.config, validated$1.meta);
168
+ });
169
+ if (isPromiseLike(loaded)) throw new TypeError("Expected load() to return a plain object or Promise, but got a thenable. Use a real Promise instead.");
170
+ const validated = validateLoadResult(loaded);
171
+ if (validated === void 0) return emptyAnnotations();
172
+ return validateAndBuildAnnotations(validated.config, validated.meta);
410
173
  }
411
174
  if (opts.getConfigPath) {
412
175
  const configPath = opts.getConfigPath(parsedPlaceholder);
413
- if (!configPath) return {};
176
+ if (configPath !== void 0 && typeof configPath !== "string") throw new TypeError(`Expected getConfigPath() to return a string or undefined, but got: ${getTypeName(configPath)}.`);
177
+ if (!configPath) return emptyAnnotations();
414
178
  const absoluteConfigPath = (0, node_path.resolve)(configPath);
415
179
  const singleFileMeta = {
416
180
  configDir: (0, node_path.dirname)(absoluteConfigPath),
@@ -419,24 +183,21 @@ function createConfigContext(options) {
419
183
  try {
420
184
  const contents = (0, node_fs.readFileSync)(absoluteConfigPath);
421
185
  let rawData;
422
- if (options.fileParser) rawData = options.fileParser(contents);
186
+ if (rawFileParser) rawData = rawFileParser(contents);
423
187
  else {
424
188
  const text = new TextDecoder().decode(contents);
425
189
  rawData = JSON.parse(text);
426
190
  }
427
191
  return validateAndBuildAnnotations(rawData, singleFileMeta);
428
192
  } catch (error) {
429
- if (isErrnoException(error) && error.code === "ENOENT") return {};
193
+ if (isErrnoException(error) && error.code === "ENOENT") return emptyAnnotations();
430
194
  if (error instanceof SyntaxError) throw new Error(`Failed to parse config file ${absoluteConfigPath}: ${error.message}`);
431
195
  throw error;
432
196
  }
433
197
  }
434
- return {};
198
+ return emptyAnnotations();
435
199
  },
436
- [Symbol.dispose]() {
437
- clearActiveConfig(contextId);
438
- clearActiveConfigMeta(contextId);
439
- }
200
+ [Symbol.dispose]() {}
440
201
  };
441
202
  return context;
442
203
  }
@@ -456,6 +217,12 @@ function createConfigContext(options) {
456
217
  * @param parser The parser to bind to config values.
457
218
  * @param options Binding options including context, key, and default.
458
219
  * @returns A new parser with config fallback behavior.
220
+ * @throws {TypeError} If `key` is not a property key or function.
221
+ * @throws {Error} If the inner parser's {@link Parser.validateValue} hook
222
+ * throws while re-validating a fallback value (a
223
+ * config-sourced value or the configured `default`) —
224
+ * the hook can run even when no CLI tokens are parsed
225
+ * (see issue #414).
459
226
  * @since 0.10.0
460
227
  *
461
228
  * @example
@@ -472,16 +239,19 @@ function createConfigContext(options) {
472
239
  * ```
473
240
  */
474
241
  function bindConfig(parser, options) {
242
+ const keyType = typeof options.key;
243
+ if (keyType !== "string" && keyType !== "number" && keyType !== "symbol" && keyType !== "function") throw new TypeError(`Expected key to be a property key or function, but got: ${getTypeName(options.key)}.`);
475
244
  const configBindStateKey = Symbol("@optique/config/bindState");
476
245
  function isConfigBindState(value) {
477
246
  return value != null && typeof value === "object" && configBindStateKey in value;
478
247
  }
479
- function shouldDeferPromptUntilConfigResolves(state) {
248
+ function shouldDeferPromptUntilConfigResolves(state, _exec) {
480
249
  const annotations = (0, __optique_core_annotations.getAnnotations)(state);
481
250
  return annotations?.[options.context.id] === phase1ConfigAnnotationMarker;
482
251
  }
252
+ const getSuggestInnerState = (state) => isConfigBindState(state) ? state.cliState === void 0 ? (0, __optique_core_extension.inheritAnnotations)(state, parser.initialState) : state.cliState : state;
483
253
  const boundParser = {
484
- $mode: parser.$mode,
254
+ mode: parser.mode,
485
255
  $valueType: parser.$valueType,
486
256
  $stateType: parser.$stateType,
487
257
  priority: parser.priority,
@@ -489,7 +259,13 @@ function bindConfig(parser, options) {
489
259
  type: "optional",
490
260
  terms: parser.usage
491
261
  }] : parser.usage,
262
+ leadingNames: parser.leadingNames,
263
+ acceptingAnyToken: parser.acceptingAnyToken,
492
264
  initialState: parser.initialState,
265
+ getSuggestRuntimeNodes(state, path) {
266
+ const innerState = getSuggestInnerState(state);
267
+ return (0, __optique_core_extension.delegateSuggestNodes)(parser, boundParser, state, path, innerState);
268
+ },
493
269
  parse: (context) => {
494
270
  const annotations = (0, __optique_core_annotations.getAnnotations)(context.state);
495
271
  const innerState = isConfigBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
@@ -500,14 +276,14 @@ function bindConfig(parser, options) {
500
276
  const processResult = (result) => {
501
277
  if (result.success) {
502
278
  const cliConsumed = result.consumed.length > 0;
503
- const newState$1 = {
279
+ const newState$1 = (0, __optique_core_extension.injectAnnotations)({
504
280
  [configBindStateKey]: true,
505
281
  hasCliValue: cliConsumed,
506
- cliState: result.next.state,
507
- ...annotations && { [__optique_core_annotations.annotationKey]: annotations }
508
- };
282
+ cliState: result.next.state
283
+ }, annotations);
509
284
  return {
510
285
  success: true,
286
+ ...result.provisional ? { provisional: true } : {},
511
287
  next: {
512
288
  ...result.next,
513
289
  state: newState$1
@@ -516,11 +292,10 @@ function bindConfig(parser, options) {
516
292
  };
517
293
  }
518
294
  if (result.consumed > 0) return result;
519
- const newState = {
295
+ const newState = (0, __optique_core_extension.injectAnnotations)({
520
296
  [configBindStateKey]: true,
521
- hasCliValue: false,
522
- ...annotations && { [__optique_core_annotations.annotationKey]: annotations }
523
- };
297
+ hasCliValue: false
298
+ }, annotations);
524
299
  return {
525
300
  success: true,
526
301
  next: {
@@ -530,60 +305,182 @@ function bindConfig(parser, options) {
530
305
  consumed: []
531
306
  };
532
307
  };
533
- return (0, __optique_core_mode_dispatch.mapModeValue)(parser.$mode, parser.parse(innerContext), processResult);
308
+ return (0, __optique_core_extension.mapModeValue)(parser.mode, parser.parse(innerContext), processResult);
534
309
  },
535
- complete: (state) => {
536
- if (isConfigBindState(state) && state.hasCliValue) return parser.complete(state.cliState);
537
- return (0, __optique_core_mode_dispatch.wrapForMode)(parser.$mode, getConfigOrDefault(state, options));
310
+ complete: (state, exec) => {
311
+ if (isConfigBindState(state) && state.hasCliValue) return parser.complete(state.cliState, exec);
312
+ return getConfigOrDefault(state, options, parser.mode, parser);
538
313
  },
539
- suggest: parser.suggest,
540
- [deferPromptUntilConfigResolvesKey]: shouldDeferPromptUntilConfigResolves,
314
+ suggest: (context, prefix) => {
315
+ const innerState = getSuggestInnerState(context.state);
316
+ const innerContext = innerState !== context.state ? {
317
+ ...context,
318
+ state: innerState
319
+ } : context;
320
+ return parser.suggest(innerContext, prefix);
321
+ },
322
+ shouldDeferCompletion: shouldDeferPromptUntilConfigResolves,
541
323
  getDocFragments(state, upperDefaultValue) {
542
324
  const defaultValue = upperDefaultValue ?? options.default;
543
325
  return parser.getDocFragments(state, defaultValue);
544
326
  }
545
327
  };
328
+ (0, __optique_core_extension.defineTraits)(boundParser, {
329
+ inheritsAnnotations: true,
330
+ completesFromSource: true
331
+ });
332
+ if ("placeholder" in parser) Object.defineProperty(boundParser, "placeholder", {
333
+ get() {
334
+ return parser.placeholder;
335
+ },
336
+ configurable: true,
337
+ enumerable: false
338
+ });
339
+ if (typeof parser.normalizeValue === "function") Object.defineProperty(boundParser, "normalizeValue", {
340
+ value: parser.normalizeValue.bind(parser),
341
+ configurable: true,
342
+ enumerable: false
343
+ });
344
+ if (typeof parser.validateValue === "function") Object.defineProperty(boundParser, "validateValue", {
345
+ value: parser.validateValue.bind(parser),
346
+ configurable: true,
347
+ enumerable: false
348
+ });
349
+ const dependencyMetadata = (0, __optique_core_extension.mapSourceMetadata)(parser, (sourceMetadata) => ({
350
+ ...sourceMetadata,
351
+ getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => {
352
+ if (typeof parser.validateValue === "function") return parser.validateValue(options.default);
353
+ return {
354
+ success: true,
355
+ value: options.default
356
+ };
357
+ } : void 0,
358
+ extractSourceValue: (state) => {
359
+ if (!isConfigBindState(state)) {
360
+ if (sourceMetadata.preservesSourceValue) return getConfigSourceValue(state, options, state, sourceMetadata.extractSourceValue, parser);
361
+ return sourceMetadata.extractSourceValue(state);
362
+ }
363
+ if (state.hasCliValue) return sourceMetadata.extractSourceValue(state.cliState);
364
+ const fallbackState = state.cliState ?? state;
365
+ if (!sourceMetadata.preservesSourceValue) return sourceMetadata.extractSourceValue(fallbackState);
366
+ return getConfigSourceValue(state, options, fallbackState, sourceMetadata.extractSourceValue, parser);
367
+ }
368
+ }));
369
+ if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
370
+ value: dependencyMetadata,
371
+ configurable: true,
372
+ enumerable: false
373
+ });
546
374
  return boundParser;
547
375
  }
548
376
  /**
549
377
  * Helper function to get value from config or default.
550
- * Checks both annotations (for top-level parsers) and the active config
551
- * registry (for parsers nested inside object() when used with context-aware
552
- * runners).
378
+ * Reads only from explicit annotations carried by the current parse state.
379
+ *
380
+ * When `innerParser.validateValue` is available, the returned fallback
381
+ * value is routed through it so that the inner CLI parser's constraints
382
+ * (regex patterns, numeric bounds, choices, etc.) are enforced on
383
+ * config-sourced values and configured defaults (see issue #414). If
384
+ * `innerParser` is absent or does not implement `validateValue` (for
385
+ * example, the inner parser is wrapped in `map()`), the value is
386
+ * returned unchanged to preserve existing behavior.
387
+ *
388
+ * @throws {TypeError} If the key callback returns a Promise or thenable.
389
+ * @throws {Error} Propagates errors thrown by
390
+ * `innerParser.validateValue()` (via
391
+ * {@link validateFallbackValue}) while revalidating a
392
+ * config-sourced value or the configured `default`
393
+ * against the inner CLI parser's constraints (see
394
+ * issue #414).
553
395
  */
554
- function getConfigOrDefault(state, options) {
396
+ function getConfigOrDefault(state, options, mode, innerParser) {
555
397
  const annotations = (0, __optique_core_annotations.getAnnotations)(state);
556
398
  const contextId = options.context.id;
557
399
  const annotationValue = annotations?.[contextId];
558
- let configData = annotationValue?.data;
559
- let configMeta = annotationValue?.meta;
560
- if (configData === void 0 || configData === null) {
561
- configData = getActiveConfig(contextId);
562
- configMeta = getActiveConfigMeta(contextId);
563
- }
400
+ const configData = annotationValue?.data;
401
+ const configMeta = annotationValue?.meta;
564
402
  let configValue;
565
- if (configData !== void 0 && configData !== null) if (typeof options.key === "function") configValue = options.key(configData, configMeta);
566
- else configValue = configData[options.key];
567
- if (configValue !== void 0) return {
403
+ if (configData !== void 0 && configData !== null) if (typeof options.key === "function") {
404
+ configValue = options.key(configData, configMeta);
405
+ 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.");
406
+ } else configValue = configData[options.key];
407
+ if (configValue !== void 0) return validateFallbackValue(mode, innerParser, configValue);
408
+ if (options.default !== void 0) return validateFallbackValue(mode, innerParser, options.default);
409
+ return (0, __optique_core_extension.wrapForMode)(mode, {
410
+ success: false,
411
+ error: __optique_core_message.message`Missing required configuration value.`
412
+ });
413
+ }
414
+ /**
415
+ * Routes a (successful) fallback value through the inner parser's
416
+ * `validateValue()` hook, or returns the value unchanged when no
417
+ * validator is available. See {@link getConfigOrDefault} for context.
418
+ *
419
+ * @throws {Error} Propagates errors thrown by
420
+ * `innerParser.validateValue()` while revalidating the
421
+ * fallback value against the inner CLI parser's
422
+ * constraints (see issue #414). When the hook returns
423
+ * a failed {@link Result} the failure is propagated
424
+ * through the return value; only an actual exception
425
+ * thrown by the hook escapes through this path.
426
+ */
427
+ function validateFallbackValue(mode, innerParser, value) {
428
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return (0, __optique_core_extension.wrapForMode)(mode, {
568
429
  success: true,
569
- value: configValue
430
+ value
431
+ });
432
+ return innerParser.validateValue(value);
433
+ }
434
+ /**
435
+ * Resolves a config-backed dependency source with fallback priority.
436
+ *
437
+ * This first checks annotations via {@link getConfigOrDefault}. If no
438
+ * config-backed value is available, it falls back to `options.default` and
439
+ * finally delegates to the wrapped parser's source extractor.
440
+ *
441
+ * When `innerParser` exposes `validateValue`, the returned fallback is
442
+ * routed through it so that the inner CLI parser's constraints are
443
+ * enforced on config-sourced source values and configured defaults
444
+ * (see issue #414). This helper is only invoked from the
445
+ * `preservesSourceValue: true` branch in {@link bindConfig}, so the
446
+ * source value type is guaranteed to equal `TValue`.
447
+ *
448
+ * @param state The wrapper state, which may carry config annotations.
449
+ * @param options The binding options with lookup and default settings.
450
+ * @param innerState The unwrapped inner state for delegated extraction.
451
+ * @param extractInnerSourceValue The wrapped parser's source extractor.
452
+ * @param innerParser The wrapped parser, used to revalidate fallback values.
453
+ * @returns The resolved source value, an async source value, or `undefined`.
454
+ * @throws {TypeError} If {@link getConfigOrDefault} rejects a thenable-returning
455
+ * key callback.
456
+ * @throws {Error} Propagates errors thrown by
457
+ * `innerParser.validateValue()` (via
458
+ * {@link getConfigOrDefault} / {@link validateFallbackValue})
459
+ * while revalidating a config-sourced value or the
460
+ * configured `default` against the inner CLI parser's
461
+ * constraints (see issue #414).
462
+ */
463
+ function getConfigSourceValue(state, options, innerState, extractInnerSourceValue, innerParser) {
464
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
465
+ const contextId = options.context.id;
466
+ const annotationValue = annotations?.[contextId];
467
+ const configData = annotationValue?.data;
468
+ const validateFallback = (parsed) => {
469
+ if (!parsed.success) return parsed;
470
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return parsed;
471
+ return innerParser.validateValue(parsed.value);
570
472
  };
571
- if (options.default !== void 0) return {
473
+ if (configData !== void 0 && configData !== null) {
474
+ const resolved = getConfigOrDefault(state, options, "sync", void 0);
475
+ if (resolved.success) return validateFallback(resolved);
476
+ }
477
+ if (options.default !== void 0) return validateFallback({
572
478
  success: true,
573
479
  value: options.default
574
- };
575
- return {
576
- success: false,
577
- error: __optique_core_message.message`Missing required configuration value.`
578
- };
480
+ });
481
+ return extractInnerSourceValue(innerState);
579
482
  }
580
483
 
581
484
  //#endregion
582
485
  exports.bindConfig = bindConfig;
583
- exports.clearActiveConfig = clearActiveConfig;
584
- exports.clearActiveConfigMeta = clearActiveConfigMeta;
585
- exports.createConfigContext = createConfigContext;
586
- exports.getActiveConfig = getActiveConfig;
587
- exports.getActiveConfigMeta = getActiveConfigMeta;
588
- exports.setActiveConfig = setActiveConfig;
589
- exports.setActiveConfigMeta = setActiveConfigMeta;
486
+ exports.createConfigContext = createConfigContext;