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