@optique/derived-defaults 1.2.0-dev.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright 2025–2026 Hong Minhee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ @optique/derived-defaults
2
+ =========================
3
+
4
+ Derived default value support for [Optique].
5
+
6
+ This package lets CLI applications compute default values from the first-pass
7
+ parse result while keeping clear priority handling:
8
+
9
+ CLI arguments > derived defaults > static defaults.
10
+
11
+ [Optique]: https://optique.dev/
12
+
13
+
14
+ Installation
15
+ ------------
16
+
17
+ ~~~~ bash
18
+ deno add jsr:@optique/derived-defaults
19
+ npm add @optique/derived-defaults
20
+ pnpm add @optique/derived-defaults
21
+ yarn add @optique/derived-defaults
22
+ bun add @optique/derived-defaults
23
+ ~~~~
24
+
25
+
26
+ Quick start
27
+ -----------
28
+
29
+ ~~~~ typescript
30
+ import { object } from "@optique/core/constructs";
31
+ import { option } from "@optique/core/primitives";
32
+ import { string } from "@optique/core/valueparser";
33
+ import { runAsync } from "@optique/run";
34
+ import {
35
+ bindDerivedDefault,
36
+ createDerivedDefaults,
37
+ } from "@optique/derived-defaults";
38
+
39
+ const derived = createDerivedDefaults({
40
+ workspaceRoot: (parsed: { readonly serviceRoot?: string }) =>
41
+ parsed.serviceRoot == null
42
+ ? undefined
43
+ : `${parsed.serviceRoot}/workspace`,
44
+ });
45
+
46
+ const parser = object({
47
+ serviceRoot: option("--service-root", string()),
48
+ workspaceRoot: bindDerivedDefault(option("--workspace-root", string()), {
49
+ context: derived.context,
50
+ key: "workspaceRoot",
51
+ }),
52
+ });
53
+
54
+ const result = await runAsync(parser, {
55
+ args: ["--service-root", "/srv/app"],
56
+ contexts: [derived.context],
57
+ });
58
+
59
+ console.log(result);
60
+ ~~~~
61
+
62
+
63
+ Features
64
+ --------
65
+
66
+ - *Two-pass defaults* derived from already parsed CLI values
67
+ - *Async resolver support* with `runAsync()`/`runWith()`
68
+ - *Fallback validation* through the wrapped Optique parser
69
+ - *Custom help text* for values that are computed at runtime
70
+ - *Composable contexts* with `run()`/`runAsync()`/`runWith()`
71
+
72
+
73
+ Documentation
74
+ -------------
75
+
76
+ For full documentation, visit
77
+ <https://optique.dev/concepts/derived-defaults>.
78
+
79
+
80
+ License
81
+ -------
82
+
83
+ MIT License. See [LICENSE](../../LICENSE) for details.
package/dist/index.cjs ADDED
@@ -0,0 +1,348 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const __optique_core_message = __toESM(require("@optique/core/message"));
25
+ const __optique_core_extension = __toESM(require("@optique/core/extension"));
26
+ const __optique_core_fluent = __toESM(require("@optique/core/fluent"));
27
+ const __optique_core_annotations = __toESM(require("@optique/core/annotations"));
28
+
29
+ //#region src/index.ts
30
+ const phase1DerivedDefaultAnnotationMarker = Symbol("@optique/derived-defaults/phase1Annotation");
31
+ function getTypeName(value) {
32
+ if (value === null) return "null";
33
+ if (Array.isArray(value)) return "array";
34
+ return typeof value;
35
+ }
36
+ function isPromiseLike(value) {
37
+ return value != null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
38
+ }
39
+ function validateSourceContextRequest(request) {
40
+ if (request === void 0) return;
41
+ 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)}.`);
42
+ }
43
+ function replaceDefaultDescription(fragments, description) {
44
+ if (description == null) return fragments;
45
+ return fragments.map((fragment) => {
46
+ if (fragment.type === "entry") return {
47
+ ...fragment,
48
+ default: description
49
+ };
50
+ return {
51
+ ...fragment,
52
+ entries: fragment.entries.map((entry) => ({
53
+ ...entry,
54
+ default: description
55
+ }))
56
+ };
57
+ });
58
+ }
59
+ function validateFallbackValue(mode, innerParser, value) {
60
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return (0, __optique_core_extension.wrapForMode)(mode, {
61
+ success: true,
62
+ value
63
+ });
64
+ return (0, __optique_core_extension.wrapForMode)(mode, innerParser.validateValue(value));
65
+ }
66
+ /**
67
+ * Creates a derived-default context.
68
+ *
69
+ * @param resolvers Resolver map keyed by derived value name.
70
+ * @returns A derived-default context bundle.
71
+ * @throws {TypeError} If `resolvers` is not an object or contains a
72
+ * non-function resolver.
73
+ * @since 1.2.0
74
+ */
75
+ function createDerivedDefaults(resolvers) {
76
+ if (resolvers == null || typeof resolvers !== "object") throw new TypeError(`Expected resolvers to be an object, but got: ${getTypeName(resolvers)}.`);
77
+ for (const key of Reflect.ownKeys(resolvers)) {
78
+ const resolver = resolvers[key];
79
+ if (typeof resolver !== "function") throw new TypeError(`Expected resolver ${String(key)} to be a function, but got: ${getTypeName(resolver)}.`);
80
+ }
81
+ const contextId = Symbol(`@optique/derived-defaults:${Math.random()}`);
82
+ const context = {
83
+ id: contextId,
84
+ phase: "two-pass",
85
+ getInternalAnnotations(request, annotations) {
86
+ if (request.phase === "phase1") return { [contextId]: phase1DerivedDefaultAnnotationMarker };
87
+ return contextId in annotations ? void 0 : { [contextId]: void 0 };
88
+ },
89
+ getAnnotations(request) {
90
+ validateSourceContextRequest(request);
91
+ if (request?.phase !== "phase2") return {};
92
+ const values = {};
93
+ const pending = [];
94
+ for (const key of Reflect.ownKeys(resolvers)) {
95
+ const resolver = resolvers[key];
96
+ if (request.parsed == null && resolver.length > 0) continue;
97
+ const resolved = resolver(request.parsed ?? void 0);
98
+ if (isPromiseLike(resolved)) pending.push(Promise.resolve(resolved).then((value) => {
99
+ if (value !== void 0) values[key] = value;
100
+ }));
101
+ else if (resolved !== void 0) values[key] = resolved;
102
+ }
103
+ const buildAnnotations = () => Reflect.ownKeys(values).length === 0 ? {} : { [contextId]: { values } };
104
+ return pending.length === 0 ? buildAnnotations() : Promise.all(pending).then(buildAnnotations);
105
+ },
106
+ [Symbol.dispose]() {}
107
+ };
108
+ return { context };
109
+ }
110
+ /**
111
+ * Binds a parser to a derived default value.
112
+ *
113
+ * @param parser Parser that reads the CLI value.
114
+ * @param options Derived default binding options.
115
+ * @returns A parser with derived fallback behavior.
116
+ * @throws {TypeError} If `options.key` is not a property key.
117
+ * @since 1.2.0
118
+ */
119
+ function bindDerivedDefault(parser, options) {
120
+ const keyType = typeof options.key;
121
+ if (keyType !== "string" && keyType !== "number" && keyType !== "symbol") throw new TypeError(`Expected key to be a property key, but got: ${getTypeName(options.key)}.`);
122
+ const bindStateKey = Symbol("@optique/derived-defaults/bindState");
123
+ function isBindState(value) {
124
+ return value != null && typeof value === "object" && bindStateKey in value;
125
+ }
126
+ function shouldDeferCompletion(state, exec) {
127
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
128
+ if (annotations?.[options.context.id] === phase1DerivedDefaultAnnotationMarker) return true;
129
+ return parser.shouldDeferCompletion?.(getInnerState(state), exec) === true;
130
+ }
131
+ function getInnerState(state) {
132
+ if (!isBindState(state)) return state;
133
+ if (state.cliState !== void 0) return state.cliState;
134
+ const initialState = parser.initialState;
135
+ if (initialState != null && typeof initialState !== "object") return initialState;
136
+ const annotated = (0, __optique_core_extension.inheritAnnotations)(state, initialState);
137
+ if (annotated === initialState && initialState != null && typeof initialState === "object") {
138
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
139
+ return annotations == null ? initialState : (0, __optique_core_extension.withAnnotationView)(initialState, annotations);
140
+ }
141
+ return annotated;
142
+ }
143
+ function getDerivedValue(state) {
144
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
145
+ const annotationValue = annotations?.[options.context.id];
146
+ if (annotationValue == null || typeof annotationValue !== "object" || !("values" in annotationValue)) return void 0;
147
+ return annotationValue.values[options.key];
148
+ }
149
+ function hasDerivedFallback(state) {
150
+ if (getDerivedValue(state) !== void 0) return true;
151
+ return options.default !== void 0;
152
+ }
153
+ function getDerivedOrDefault(state, mode, exec, innerParser) {
154
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
155
+ const contextId = options.context.id;
156
+ const contextAbsent = annotations != null && !(contextId in annotations);
157
+ const derivedValue = getDerivedValue(state);
158
+ if (derivedValue !== void 0) return validateFallbackValue(mode, innerParser, derivedValue);
159
+ if (options.default !== void 0) return validateFallbackValue(mode, innerParser, options.default);
160
+ if (innerParser?.canSkip?.(getInnerState(state), exec) === true) return (0, __optique_core_extension.mapModeValue)(mode, (0, __optique_core_extension.wrapForMode)(mode, innerParser.complete(getInnerState(state), exec)), (result) => {
161
+ if (result.success) return result;
162
+ if (contextAbsent) return {
163
+ success: false,
164
+ error: __optique_core_message.message`Derived default value could not be read: the derived default context was not passed to run()'s contexts option.`
165
+ };
166
+ return result;
167
+ });
168
+ if (contextAbsent) return (0, __optique_core_extension.wrapForMode)(mode, {
169
+ success: false,
170
+ error: __optique_core_message.message`Derived default value could not be read: the derived default context was not passed to run()'s contexts option.`
171
+ });
172
+ return (0, __optique_core_extension.wrapForMode)(mode, {
173
+ success: false,
174
+ error: __optique_core_message.message`Missing required derived default value.`
175
+ });
176
+ }
177
+ function getDerivedSourceValue(state, innerState, mode, extractInnerSourceValue, innerParser) {
178
+ const derivedValue = getDerivedValue(state);
179
+ const validateFallback = (parsed) => {
180
+ if (!parsed.success) return parsed;
181
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return parsed;
182
+ return (0, __optique_core_extension.wrapForMode)(mode, innerParser.validateValue(parsed.value));
183
+ };
184
+ if (derivedValue !== void 0) return (0, __optique_core_extension.wrapForMode)(mode, validateFallback({
185
+ success: true,
186
+ value: derivedValue
187
+ }));
188
+ if (options.default !== void 0) return (0, __optique_core_extension.wrapForMode)(mode, validateFallback({
189
+ success: true,
190
+ value: options.default
191
+ }));
192
+ return (0, __optique_core_extension.wrapForMode)(mode, extractInnerSourceValue(innerState));
193
+ }
194
+ const boundParser = {
195
+ mode: parser.mode,
196
+ $valueType: parser.$valueType,
197
+ $stateType: parser.$stateType,
198
+ priority: parser.priority,
199
+ usage: options.default !== void 0 ? [{
200
+ type: "optional",
201
+ terms: parser.usage
202
+ }] : parser.usage,
203
+ leadingNames: parser.leadingNames,
204
+ acceptingAnyToken: parser.acceptingAnyToken,
205
+ initialState: parser.initialState,
206
+ canSkip(state, exec) {
207
+ if (isBindState(state)) {
208
+ if (state.hasCliValue) return parser.canSkip?.(state.cliState, exec) === true;
209
+ if (hasDerivedFallback(state)) return true;
210
+ return parser.canSkip?.(getInnerState(state), exec) === true;
211
+ }
212
+ if (hasDerivedFallback(state)) return true;
213
+ return parser.canSkip?.(state, exec) === true;
214
+ },
215
+ getSuggestRuntimeNodes(state, path) {
216
+ const innerState = getInnerState(state);
217
+ return (0, __optique_core_extension.delegateSuggestNodes)(parser, boundParser, state, path, innerState);
218
+ },
219
+ parse(context) {
220
+ const annotations = (0, __optique_core_annotations.getAnnotations)(context.state);
221
+ const innerState = isBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
222
+ const innerContext = innerState !== context.state ? {
223
+ ...context,
224
+ state: innerState
225
+ } : context;
226
+ const processResult = (result) => {
227
+ if (result.success) {
228
+ const cliConsumed = result.consumed.length > 0;
229
+ const nextState$1 = (0, __optique_core_extension.injectAnnotations)({
230
+ [bindStateKey]: true,
231
+ hasCliValue: cliConsumed,
232
+ cliState: result.next.state
233
+ }, annotations);
234
+ return {
235
+ success: true,
236
+ ...result.provisional ? { provisional: true } : {},
237
+ next: {
238
+ ...result.next,
239
+ state: nextState$1
240
+ },
241
+ consumed: result.consumed
242
+ };
243
+ }
244
+ if (result.consumed > 0) return result;
245
+ const nextState = (0, __optique_core_extension.injectAnnotations)({
246
+ [bindStateKey]: true,
247
+ hasCliValue: false
248
+ }, annotations);
249
+ return {
250
+ success: true,
251
+ next: {
252
+ ...innerContext,
253
+ state: nextState
254
+ },
255
+ consumed: []
256
+ };
257
+ };
258
+ return (0, __optique_core_extension.mapModeValue)(parser.mode, (0, __optique_core_extension.wrapForMode)(parser.mode, parser.parse(innerContext)), processResult);
259
+ },
260
+ complete(state, exec) {
261
+ if (isBindState(state) && state.hasCliValue) return (0, __optique_core_extension.wrapForMode)(parser.mode, parser.complete(state.cliState, exec));
262
+ return getDerivedOrDefault(state, parser.mode, exec, parser);
263
+ },
264
+ suggest(context, prefix) {
265
+ const innerState = getInnerState(context.state);
266
+ const innerContext = innerState !== context.state ? {
267
+ ...context,
268
+ state: innerState
269
+ } : context;
270
+ return parser.suggest(innerContext, prefix);
271
+ },
272
+ shouldDeferCompletion,
273
+ getDocFragments(state, upperDefaultValue) {
274
+ const defaultValue = upperDefaultValue ?? options.default;
275
+ const fragments = parser.getDocFragments(state, defaultValue);
276
+ if (upperDefaultValue !== void 0 || options.defaultDescription == null) return fragments;
277
+ return {
278
+ ...fragments,
279
+ fragments: replaceDefaultDescription(fragments.fragments, options.defaultDescription)
280
+ };
281
+ }
282
+ };
283
+ Object.defineProperty(boundParser, __optique_core_extension.extractPhase2SeedKey, {
284
+ value(state, exec) {
285
+ const annotations = (0, __optique_core_annotations.getAnnotations)(state);
286
+ if (annotations?.[options.context.id] !== phase1DerivedDefaultAnnotationMarker) {
287
+ const extractInnerPhase2Seed = parser[__optique_core_extension.extractPhase2SeedKey];
288
+ return extractInnerPhase2Seed == null ? (0, __optique_core_extension.wrapForMode)(parser.mode, null) : (0, __optique_core_extension.wrapForMode)(parser.mode, extractInnerPhase2Seed(getInnerState(state), exec));
289
+ }
290
+ return (0, __optique_core_extension.wrapForMode)(parser.mode, {
291
+ value: void 0,
292
+ deferred: true
293
+ });
294
+ },
295
+ configurable: true
296
+ });
297
+ (0, __optique_core_extension.defineTraits)(boundParser, {
298
+ inheritsAnnotations: true,
299
+ completesFromSource: true
300
+ });
301
+ if ("placeholder" in parser) Object.defineProperty(boundParser, "placeholder", {
302
+ get() {
303
+ return parser.placeholder;
304
+ },
305
+ configurable: true,
306
+ enumerable: false
307
+ });
308
+ if (typeof parser.normalizeValue === "function") Object.defineProperty(boundParser, "normalizeValue", {
309
+ value: parser.normalizeValue.bind(parser),
310
+ configurable: true,
311
+ enumerable: false
312
+ });
313
+ if (typeof parser.validateValue === "function") Object.defineProperty(boundParser, "validateValue", {
314
+ value: parser.validateValue.bind(parser),
315
+ configurable: true,
316
+ enumerable: false
317
+ });
318
+ const dependencyMetadata = (0, __optique_core_extension.mapSourceMetadata)(parser, (sourceMetadata) => ({
319
+ ...sourceMetadata,
320
+ getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => {
321
+ if (typeof parser.validateValue === "function") return (0, __optique_core_extension.wrapForMode)(parser.mode, parser.validateValue(options.default));
322
+ return (0, __optique_core_extension.wrapForMode)(parser.mode, {
323
+ success: true,
324
+ value: options.default
325
+ });
326
+ } : void 0,
327
+ extractSourceValue: (state) => {
328
+ if (!isBindState(state)) {
329
+ if (sourceMetadata.preservesSourceValue) return getDerivedSourceValue(state, state, parser.mode, sourceMetadata.extractSourceValue, parser);
330
+ return (0, __optique_core_extension.wrapForMode)(parser.mode, sourceMetadata.extractSourceValue(state));
331
+ }
332
+ if (state.hasCliValue) return (0, __optique_core_extension.wrapForMode)(parser.mode, sourceMetadata.extractSourceValue(state.cliState));
333
+ const fallbackState = state.cliState ?? state;
334
+ if (!sourceMetadata.preservesSourceValue) return (0, __optique_core_extension.wrapForMode)(parser.mode, sourceMetadata.extractSourceValue(fallbackState));
335
+ return getDerivedSourceValue(state, fallbackState, parser.mode, sourceMetadata.extractSourceValue, parser);
336
+ }
337
+ }));
338
+ if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
339
+ value: dependencyMetadata,
340
+ configurable: true,
341
+ enumerable: false
342
+ });
343
+ return (0, __optique_core_fluent.fluent)(boundParser);
344
+ }
345
+
346
+ //#endregion
347
+ exports.bindDerivedDefault = bindDerivedDefault;
348
+ exports.createDerivedDefaults = createDerivedDefaults;
@@ -0,0 +1,75 @@
1
+ import { Message } from "@optique/core/message";
2
+ import { FluentParser } from "@optique/core/fluent";
3
+ import { Mode, Parser } from "@optique/core/parser";
4
+ import { SourceContext } from "@optique/core/context";
5
+
6
+ //#region src/index.d.ts
7
+
8
+ /**
9
+ * Resolver function for a derived default value.
10
+ *
11
+ * @since 1.2.0
12
+ */
13
+ type DerivedDefaultResolver<TParsed, TValue> = (parsed: TParsed) => TValue | undefined | Promise<TValue | undefined>;
14
+ /**
15
+ * Values produced by a derived-default resolver map.
16
+ *
17
+ * @since 1.2.0
18
+ */
19
+ type DerivedDefaultValues<TResolvers> = { readonly [K in keyof TResolvers]: TResolvers[K] extends ((parsed: infer _TParsed) => infer TValue) ? Exclude<Awaited<TValue>, undefined> : never };
20
+ /**
21
+ * Keys whose derived default values are assignable to a parser value type.
22
+ *
23
+ * @since 1.2.0
24
+ */
25
+ type DerivedDefaultKey<TValues extends object, TValue> = { readonly [K in keyof TValues]: TValues[K] extends TValue ? K : never }[keyof TValues];
26
+ /**
27
+ * Source context for derived defaults.
28
+ *
29
+ * @since 1.2.0
30
+ */
31
+ interface DerivedDefaultsContext<TValues extends object> extends SourceContext {
32
+ readonly phase: "two-pass";
33
+ readonly $values?: TValues;
34
+ }
35
+ /**
36
+ * Derived-default context bundle.
37
+ *
38
+ * @since 1.2.0
39
+ */
40
+ interface DerivedDefaults<TValues extends object> {
41
+ readonly context: DerivedDefaultsContext<TValues>;
42
+ }
43
+ /**
44
+ * Options for binding a parser to a derived default value.
45
+ *
46
+ * @since 1.2.0
47
+ */
48
+ interface BindDerivedDefaultOptions<TValues extends object, TValue, TKey extends DerivedDefaultKey<TValues, TValue>> {
49
+ readonly context: DerivedDefaultsContext<TValues>;
50
+ readonly key: TKey;
51
+ readonly default?: TValue;
52
+ readonly defaultDescription?: Message;
53
+ }
54
+ /**
55
+ * Creates a derived-default context.
56
+ *
57
+ * @param resolvers Resolver map keyed by derived value name.
58
+ * @returns A derived-default context bundle.
59
+ * @throws {TypeError} If `resolvers` is not an object or contains a
60
+ * non-function resolver.
61
+ * @since 1.2.0
62
+ */
63
+ declare function createDerivedDefaults<const TResolvers extends object>(resolvers: TResolvers): DerivedDefaults<DerivedDefaultValues<TResolvers>>;
64
+ /**
65
+ * Binds a parser to a derived default value.
66
+ *
67
+ * @param parser Parser that reads the CLI value.
68
+ * @param options Derived default binding options.
69
+ * @returns A parser with derived fallback behavior.
70
+ * @throws {TypeError} If `options.key` is not a property key.
71
+ * @since 1.2.0
72
+ */
73
+ declare function bindDerivedDefault<M extends Mode, TValue, TState, TValues extends object, TKey extends DerivedDefaultKey<TValues, TValue>>(parser: Parser<M, TValue, TState>, options: BindDerivedDefaultOptions<TValues, TValue, TKey>): FluentParser<M, TValue, TState>;
74
+ //#endregion
75
+ export { BindDerivedDefaultOptions, DerivedDefaultKey, DerivedDefaultResolver, DerivedDefaultValues, DerivedDefaults, DerivedDefaultsContext, bindDerivedDefault, createDerivedDefaults };
@@ -0,0 +1,75 @@
1
+ import { Message } from "@optique/core/message";
2
+ import { FluentParser } from "@optique/core/fluent";
3
+ import { Mode, Parser } from "@optique/core/parser";
4
+ import { SourceContext } from "@optique/core/context";
5
+
6
+ //#region src/index.d.ts
7
+
8
+ /**
9
+ * Resolver function for a derived default value.
10
+ *
11
+ * @since 1.2.0
12
+ */
13
+ type DerivedDefaultResolver<TParsed, TValue> = (parsed: TParsed) => TValue | undefined | Promise<TValue | undefined>;
14
+ /**
15
+ * Values produced by a derived-default resolver map.
16
+ *
17
+ * @since 1.2.0
18
+ */
19
+ type DerivedDefaultValues<TResolvers> = { readonly [K in keyof TResolvers]: TResolvers[K] extends ((parsed: infer _TParsed) => infer TValue) ? Exclude<Awaited<TValue>, undefined> : never };
20
+ /**
21
+ * Keys whose derived default values are assignable to a parser value type.
22
+ *
23
+ * @since 1.2.0
24
+ */
25
+ type DerivedDefaultKey<TValues extends object, TValue> = { readonly [K in keyof TValues]: TValues[K] extends TValue ? K : never }[keyof TValues];
26
+ /**
27
+ * Source context for derived defaults.
28
+ *
29
+ * @since 1.2.0
30
+ */
31
+ interface DerivedDefaultsContext<TValues extends object> extends SourceContext {
32
+ readonly phase: "two-pass";
33
+ readonly $values?: TValues;
34
+ }
35
+ /**
36
+ * Derived-default context bundle.
37
+ *
38
+ * @since 1.2.0
39
+ */
40
+ interface DerivedDefaults<TValues extends object> {
41
+ readonly context: DerivedDefaultsContext<TValues>;
42
+ }
43
+ /**
44
+ * Options for binding a parser to a derived default value.
45
+ *
46
+ * @since 1.2.0
47
+ */
48
+ interface BindDerivedDefaultOptions<TValues extends object, TValue, TKey extends DerivedDefaultKey<TValues, TValue>> {
49
+ readonly context: DerivedDefaultsContext<TValues>;
50
+ readonly key: TKey;
51
+ readonly default?: TValue;
52
+ readonly defaultDescription?: Message;
53
+ }
54
+ /**
55
+ * Creates a derived-default context.
56
+ *
57
+ * @param resolvers Resolver map keyed by derived value name.
58
+ * @returns A derived-default context bundle.
59
+ * @throws {TypeError} If `resolvers` is not an object or contains a
60
+ * non-function resolver.
61
+ * @since 1.2.0
62
+ */
63
+ declare function createDerivedDefaults<const TResolvers extends object>(resolvers: TResolvers): DerivedDefaults<DerivedDefaultValues<TResolvers>>;
64
+ /**
65
+ * Binds a parser to a derived default value.
66
+ *
67
+ * @param parser Parser that reads the CLI value.
68
+ * @param options Derived default binding options.
69
+ * @returns A parser with derived fallback behavior.
70
+ * @throws {TypeError} If `options.key` is not a property key.
71
+ * @since 1.2.0
72
+ */
73
+ declare function bindDerivedDefault<M extends Mode, TValue, TState, TValues extends object, TKey extends DerivedDefaultKey<TValues, TValue>>(parser: Parser<M, TValue, TState>, options: BindDerivedDefaultOptions<TValues, TValue, TKey>): FluentParser<M, TValue, TState>;
74
+ //#endregion
75
+ export { BindDerivedDefaultOptions, DerivedDefaultKey, DerivedDefaultResolver, DerivedDefaultValues, DerivedDefaults, DerivedDefaultsContext, bindDerivedDefault, createDerivedDefaults };
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ import { message } from "@optique/core/message";
2
+ import { defineTraits, delegateSuggestNodes, extractPhase2SeedKey, inheritAnnotations, injectAnnotations, mapModeValue, mapSourceMetadata, withAnnotationView, wrapForMode } from "@optique/core/extension";
3
+ import { fluent } from "@optique/core/fluent";
4
+ import { getAnnotations } from "@optique/core/annotations";
5
+
6
+ //#region src/index.ts
7
+ const phase1DerivedDefaultAnnotationMarker = Symbol("@optique/derived-defaults/phase1Annotation");
8
+ function getTypeName(value) {
9
+ if (value === null) return "null";
10
+ if (Array.isArray(value)) return "array";
11
+ return typeof value;
12
+ }
13
+ function isPromiseLike(value) {
14
+ return value != null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
15
+ }
16
+ function validateSourceContextRequest(request) {
17
+ if (request === void 0) return;
18
+ 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)}.`);
19
+ }
20
+ function replaceDefaultDescription(fragments, description) {
21
+ if (description == null) return fragments;
22
+ return fragments.map((fragment) => {
23
+ if (fragment.type === "entry") return {
24
+ ...fragment,
25
+ default: description
26
+ };
27
+ return {
28
+ ...fragment,
29
+ entries: fragment.entries.map((entry) => ({
30
+ ...entry,
31
+ default: description
32
+ }))
33
+ };
34
+ });
35
+ }
36
+ function validateFallbackValue(mode, innerParser, value) {
37
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return wrapForMode(mode, {
38
+ success: true,
39
+ value
40
+ });
41
+ return wrapForMode(mode, innerParser.validateValue(value));
42
+ }
43
+ /**
44
+ * Creates a derived-default context.
45
+ *
46
+ * @param resolvers Resolver map keyed by derived value name.
47
+ * @returns A derived-default context bundle.
48
+ * @throws {TypeError} If `resolvers` is not an object or contains a
49
+ * non-function resolver.
50
+ * @since 1.2.0
51
+ */
52
+ function createDerivedDefaults(resolvers) {
53
+ if (resolvers == null || typeof resolvers !== "object") throw new TypeError(`Expected resolvers to be an object, but got: ${getTypeName(resolvers)}.`);
54
+ for (const key of Reflect.ownKeys(resolvers)) {
55
+ const resolver = resolvers[key];
56
+ if (typeof resolver !== "function") throw new TypeError(`Expected resolver ${String(key)} to be a function, but got: ${getTypeName(resolver)}.`);
57
+ }
58
+ const contextId = Symbol(`@optique/derived-defaults:${Math.random()}`);
59
+ const context = {
60
+ id: contextId,
61
+ phase: "two-pass",
62
+ getInternalAnnotations(request, annotations) {
63
+ if (request.phase === "phase1") return { [contextId]: phase1DerivedDefaultAnnotationMarker };
64
+ return contextId in annotations ? void 0 : { [contextId]: void 0 };
65
+ },
66
+ getAnnotations(request) {
67
+ validateSourceContextRequest(request);
68
+ if (request?.phase !== "phase2") return {};
69
+ const values = {};
70
+ const pending = [];
71
+ for (const key of Reflect.ownKeys(resolvers)) {
72
+ const resolver = resolvers[key];
73
+ if (request.parsed == null && resolver.length > 0) continue;
74
+ const resolved = resolver(request.parsed ?? void 0);
75
+ if (isPromiseLike(resolved)) pending.push(Promise.resolve(resolved).then((value) => {
76
+ if (value !== void 0) values[key] = value;
77
+ }));
78
+ else if (resolved !== void 0) values[key] = resolved;
79
+ }
80
+ const buildAnnotations = () => Reflect.ownKeys(values).length === 0 ? {} : { [contextId]: { values } };
81
+ return pending.length === 0 ? buildAnnotations() : Promise.all(pending).then(buildAnnotations);
82
+ },
83
+ [Symbol.dispose]() {}
84
+ };
85
+ return { context };
86
+ }
87
+ /**
88
+ * Binds a parser to a derived default value.
89
+ *
90
+ * @param parser Parser that reads the CLI value.
91
+ * @param options Derived default binding options.
92
+ * @returns A parser with derived fallback behavior.
93
+ * @throws {TypeError} If `options.key` is not a property key.
94
+ * @since 1.2.0
95
+ */
96
+ function bindDerivedDefault(parser, options) {
97
+ const keyType = typeof options.key;
98
+ if (keyType !== "string" && keyType !== "number" && keyType !== "symbol") throw new TypeError(`Expected key to be a property key, but got: ${getTypeName(options.key)}.`);
99
+ const bindStateKey = Symbol("@optique/derived-defaults/bindState");
100
+ function isBindState(value) {
101
+ return value != null && typeof value === "object" && bindStateKey in value;
102
+ }
103
+ function shouldDeferCompletion(state, exec) {
104
+ const annotations = getAnnotations(state);
105
+ if (annotations?.[options.context.id] === phase1DerivedDefaultAnnotationMarker) return true;
106
+ return parser.shouldDeferCompletion?.(getInnerState(state), exec) === true;
107
+ }
108
+ function getInnerState(state) {
109
+ if (!isBindState(state)) return state;
110
+ if (state.cliState !== void 0) return state.cliState;
111
+ const initialState = parser.initialState;
112
+ if (initialState != null && typeof initialState !== "object") return initialState;
113
+ const annotated = inheritAnnotations(state, initialState);
114
+ if (annotated === initialState && initialState != null && typeof initialState === "object") {
115
+ const annotations = getAnnotations(state);
116
+ return annotations == null ? initialState : withAnnotationView(initialState, annotations);
117
+ }
118
+ return annotated;
119
+ }
120
+ function getDerivedValue(state) {
121
+ const annotations = getAnnotations(state);
122
+ const annotationValue = annotations?.[options.context.id];
123
+ if (annotationValue == null || typeof annotationValue !== "object" || !("values" in annotationValue)) return void 0;
124
+ return annotationValue.values[options.key];
125
+ }
126
+ function hasDerivedFallback(state) {
127
+ if (getDerivedValue(state) !== void 0) return true;
128
+ return options.default !== void 0;
129
+ }
130
+ function getDerivedOrDefault(state, mode, exec, innerParser) {
131
+ const annotations = getAnnotations(state);
132
+ const contextId = options.context.id;
133
+ const contextAbsent = annotations != null && !(contextId in annotations);
134
+ const derivedValue = getDerivedValue(state);
135
+ if (derivedValue !== void 0) return validateFallbackValue(mode, innerParser, derivedValue);
136
+ if (options.default !== void 0) return validateFallbackValue(mode, innerParser, options.default);
137
+ if (innerParser?.canSkip?.(getInnerState(state), exec) === true) return mapModeValue(mode, wrapForMode(mode, innerParser.complete(getInnerState(state), exec)), (result) => {
138
+ if (result.success) return result;
139
+ if (contextAbsent) return {
140
+ success: false,
141
+ error: message`Derived default value could not be read: the derived default context was not passed to run()'s contexts option.`
142
+ };
143
+ return result;
144
+ });
145
+ if (contextAbsent) return wrapForMode(mode, {
146
+ success: false,
147
+ error: message`Derived default value could not be read: the derived default context was not passed to run()'s contexts option.`
148
+ });
149
+ return wrapForMode(mode, {
150
+ success: false,
151
+ error: message`Missing required derived default value.`
152
+ });
153
+ }
154
+ function getDerivedSourceValue(state, innerState, mode, extractInnerSourceValue, innerParser) {
155
+ const derivedValue = getDerivedValue(state);
156
+ const validateFallback = (parsed) => {
157
+ if (!parsed.success) return parsed;
158
+ if (innerParser == null || typeof innerParser.validateValue !== "function") return parsed;
159
+ return wrapForMode(mode, innerParser.validateValue(parsed.value));
160
+ };
161
+ if (derivedValue !== void 0) return wrapForMode(mode, validateFallback({
162
+ success: true,
163
+ value: derivedValue
164
+ }));
165
+ if (options.default !== void 0) return wrapForMode(mode, validateFallback({
166
+ success: true,
167
+ value: options.default
168
+ }));
169
+ return wrapForMode(mode, extractInnerSourceValue(innerState));
170
+ }
171
+ const boundParser = {
172
+ mode: parser.mode,
173
+ $valueType: parser.$valueType,
174
+ $stateType: parser.$stateType,
175
+ priority: parser.priority,
176
+ usage: options.default !== void 0 ? [{
177
+ type: "optional",
178
+ terms: parser.usage
179
+ }] : parser.usage,
180
+ leadingNames: parser.leadingNames,
181
+ acceptingAnyToken: parser.acceptingAnyToken,
182
+ initialState: parser.initialState,
183
+ canSkip(state, exec) {
184
+ if (isBindState(state)) {
185
+ if (state.hasCliValue) return parser.canSkip?.(state.cliState, exec) === true;
186
+ if (hasDerivedFallback(state)) return true;
187
+ return parser.canSkip?.(getInnerState(state), exec) === true;
188
+ }
189
+ if (hasDerivedFallback(state)) return true;
190
+ return parser.canSkip?.(state, exec) === true;
191
+ },
192
+ getSuggestRuntimeNodes(state, path) {
193
+ const innerState = getInnerState(state);
194
+ return delegateSuggestNodes(parser, boundParser, state, path, innerState);
195
+ },
196
+ parse(context) {
197
+ const annotations = getAnnotations(context.state);
198
+ const innerState = isBindState(context.state) ? context.state.hasCliValue ? context.state.cliState : parser.initialState : context.state;
199
+ const innerContext = innerState !== context.state ? {
200
+ ...context,
201
+ state: innerState
202
+ } : context;
203
+ const processResult = (result) => {
204
+ if (result.success) {
205
+ const cliConsumed = result.consumed.length > 0;
206
+ const nextState$1 = injectAnnotations({
207
+ [bindStateKey]: true,
208
+ hasCliValue: cliConsumed,
209
+ cliState: result.next.state
210
+ }, annotations);
211
+ return {
212
+ success: true,
213
+ ...result.provisional ? { provisional: true } : {},
214
+ next: {
215
+ ...result.next,
216
+ state: nextState$1
217
+ },
218
+ consumed: result.consumed
219
+ };
220
+ }
221
+ if (result.consumed > 0) return result;
222
+ const nextState = injectAnnotations({
223
+ [bindStateKey]: true,
224
+ hasCliValue: false
225
+ }, annotations);
226
+ return {
227
+ success: true,
228
+ next: {
229
+ ...innerContext,
230
+ state: nextState
231
+ },
232
+ consumed: []
233
+ };
234
+ };
235
+ return mapModeValue(parser.mode, wrapForMode(parser.mode, parser.parse(innerContext)), processResult);
236
+ },
237
+ complete(state, exec) {
238
+ if (isBindState(state) && state.hasCliValue) return wrapForMode(parser.mode, parser.complete(state.cliState, exec));
239
+ return getDerivedOrDefault(state, parser.mode, exec, parser);
240
+ },
241
+ suggest(context, prefix) {
242
+ const innerState = getInnerState(context.state);
243
+ const innerContext = innerState !== context.state ? {
244
+ ...context,
245
+ state: innerState
246
+ } : context;
247
+ return parser.suggest(innerContext, prefix);
248
+ },
249
+ shouldDeferCompletion,
250
+ getDocFragments(state, upperDefaultValue) {
251
+ const defaultValue = upperDefaultValue ?? options.default;
252
+ const fragments = parser.getDocFragments(state, defaultValue);
253
+ if (upperDefaultValue !== void 0 || options.defaultDescription == null) return fragments;
254
+ return {
255
+ ...fragments,
256
+ fragments: replaceDefaultDescription(fragments.fragments, options.defaultDescription)
257
+ };
258
+ }
259
+ };
260
+ Object.defineProperty(boundParser, extractPhase2SeedKey, {
261
+ value(state, exec) {
262
+ const annotations = getAnnotations(state);
263
+ if (annotations?.[options.context.id] !== phase1DerivedDefaultAnnotationMarker) {
264
+ const extractInnerPhase2Seed = parser[extractPhase2SeedKey];
265
+ return extractInnerPhase2Seed == null ? wrapForMode(parser.mode, null) : wrapForMode(parser.mode, extractInnerPhase2Seed(getInnerState(state), exec));
266
+ }
267
+ return wrapForMode(parser.mode, {
268
+ value: void 0,
269
+ deferred: true
270
+ });
271
+ },
272
+ configurable: true
273
+ });
274
+ defineTraits(boundParser, {
275
+ inheritsAnnotations: true,
276
+ completesFromSource: true
277
+ });
278
+ if ("placeholder" in parser) Object.defineProperty(boundParser, "placeholder", {
279
+ get() {
280
+ return parser.placeholder;
281
+ },
282
+ configurable: true,
283
+ enumerable: false
284
+ });
285
+ if (typeof parser.normalizeValue === "function") Object.defineProperty(boundParser, "normalizeValue", {
286
+ value: parser.normalizeValue.bind(parser),
287
+ configurable: true,
288
+ enumerable: false
289
+ });
290
+ if (typeof parser.validateValue === "function") Object.defineProperty(boundParser, "validateValue", {
291
+ value: parser.validateValue.bind(parser),
292
+ configurable: true,
293
+ enumerable: false
294
+ });
295
+ const dependencyMetadata = mapSourceMetadata(parser, (sourceMetadata) => ({
296
+ ...sourceMetadata,
297
+ getMissingSourceValue: sourceMetadata.preservesSourceValue !== false && options.default !== void 0 ? () => {
298
+ if (typeof parser.validateValue === "function") return wrapForMode(parser.mode, parser.validateValue(options.default));
299
+ return wrapForMode(parser.mode, {
300
+ success: true,
301
+ value: options.default
302
+ });
303
+ } : void 0,
304
+ extractSourceValue: (state) => {
305
+ if (!isBindState(state)) {
306
+ if (sourceMetadata.preservesSourceValue) return getDerivedSourceValue(state, state, parser.mode, sourceMetadata.extractSourceValue, parser);
307
+ return wrapForMode(parser.mode, sourceMetadata.extractSourceValue(state));
308
+ }
309
+ if (state.hasCliValue) return wrapForMode(parser.mode, sourceMetadata.extractSourceValue(state.cliState));
310
+ const fallbackState = state.cliState ?? state;
311
+ if (!sourceMetadata.preservesSourceValue) return wrapForMode(parser.mode, sourceMetadata.extractSourceValue(fallbackState));
312
+ return getDerivedSourceValue(state, fallbackState, parser.mode, sourceMetadata.extractSourceValue, parser);
313
+ }
314
+ }));
315
+ if (dependencyMetadata != null) Object.defineProperty(boundParser, "dependencyMetadata", {
316
+ value: dependencyMetadata,
317
+ configurable: true,
318
+ enumerable: false
319
+ });
320
+ return fluent(boundParser);
321
+ }
322
+
323
+ //#endregion
324
+ export { bindDerivedDefault, createDerivedDefaults };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@optique/derived-defaults",
3
+ "version": "1.2.0-dev.0",
4
+ "description": "Derived default value support for Optique",
5
+ "keywords": [
6
+ "CLI",
7
+ "command-line",
8
+ "commandline",
9
+ "parser",
10
+ "defaults",
11
+ "source-context"
12
+ ],
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Hong Minhee",
16
+ "email": "hong@minhee.org",
17
+ "url": "https://hongminhee.org/"
18
+ },
19
+ "homepage": "https://optique.dev/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dahlia/optique.git",
23
+ "directory": "packages/derived-defaults/"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/dahlia/optique/issues"
27
+ },
28
+ "funding": [
29
+ "https://github.com/sponsors/dahlia"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20.0.0",
33
+ "bun": ">=1.2.0",
34
+ "deno": ">=2.3.0"
35
+ },
36
+ "files": [
37
+ "dist/",
38
+ "package.json",
39
+ "README.md"
40
+ ],
41
+ "type": "module",
42
+ "module": "./dist/index.js",
43
+ "main": "./dist/index.cjs",
44
+ "types": "./dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "types": {
48
+ "import": "./dist/index.d.ts",
49
+ "require": "./dist/index.d.cts"
50
+ },
51
+ "import": "./dist/index.js",
52
+ "require": "./dist/index.cjs"
53
+ }
54
+ },
55
+ "imports": {
56
+ "#src/*.ts": {
57
+ "node": "./dist/*.js",
58
+ "default": "./src/*.ts"
59
+ }
60
+ },
61
+ "sideEffects": false,
62
+ "dependencies": {
63
+ "@optique/core": "1.2.0"
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^24.0.0",
67
+ "fast-check": "^4.7.0",
68
+ "tsdown": "^0.13.0",
69
+ "typescript": "^5.8.3",
70
+ "@optique/config": "1.2.0",
71
+ "@optique/env": "1.2.0",
72
+ "@optique/run": "1.2.0"
73
+ },
74
+ "scripts": {
75
+ "build": "tsdown",
76
+ "prepublish": "tsdown",
77
+ "test": "node --test",
78
+ "test:bun": "bun test",
79
+ "test:deno": "deno test",
80
+ "test-all": "tsdown && node --test && bun test && deno test"
81
+ }
82
+ }