@optique/core 1.0.0-dev.552 → 1.0.0-dev.556

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.
@@ -59,6 +59,24 @@ function getAnnotations(state) {
59
59
  return void 0;
60
60
  }
61
61
  /**
62
+ * Reattaches annotations to a freshly created array state.
63
+ *
64
+ * Array spread copies elements but drops symbol properties, so parsers that
65
+ * rebuild array states need to copy annotations back explicitly.
66
+ *
67
+ * @param source The original state that may carry annotations.
68
+ * @param target The freshly created array state.
69
+ * @returns The target array, with annotations copied when available.
70
+ * @internal
71
+ */
72
+ function annotateFreshArray(source, target) {
73
+ const annotations = getAnnotations(source);
74
+ if (annotations === void 0) return target;
75
+ const annotated = target;
76
+ annotated[annotationKey] = annotations;
77
+ return annotated;
78
+ }
79
+ /**
62
80
  * Propagates annotations from one parser state to another.
63
81
  *
64
82
  * This is mainly used by parsers that rebuild array states with spread syntax.
@@ -212,6 +230,7 @@ function isInjectedAnnotationWrapper(value) {
212
230
  }
213
231
 
214
232
  //#endregion
233
+ exports.annotateFreshArray = annotateFreshArray;
215
234
  exports.annotationKey = annotationKey;
216
235
  exports.annotationStateValueKey = annotationStateValueKey;
217
236
  exports.annotationWrapperKey = annotationWrapperKey;
@@ -68,6 +68,18 @@ interface ParseOptions {
68
68
  * ```
69
69
  */
70
70
  declare function getAnnotations(state: unknown): Annotations | undefined;
71
+ /**
72
+ * Reattaches annotations to a freshly created array state.
73
+ *
74
+ * Array spread copies elements but drops symbol properties, so parsers that
75
+ * rebuild array states need to copy annotations back explicitly.
76
+ *
77
+ * @param source The original state that may carry annotations.
78
+ * @param target The freshly created array state.
79
+ * @returns The target array, with annotations copied when available.
80
+ * @internal
81
+ */
82
+ declare function annotateFreshArray<T>(source: unknown, target: readonly T[]): readonly T[];
71
83
  /**
72
84
  * Propagates annotations from one parser state to another.
73
85
  *
@@ -115,4 +127,4 @@ declare function unwrapInjectedAnnotationWrapper<T>(value: T): T;
115
127
  */
116
128
  declare function isInjectedAnnotationWrapper(value: unknown): boolean;
117
129
  //#endregion
118
- export { Annotations, ParseOptions, annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
130
+ export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -68,6 +68,18 @@ interface ParseOptions {
68
68
  * ```
69
69
  */
70
70
  declare function getAnnotations(state: unknown): Annotations | undefined;
71
+ /**
72
+ * Reattaches annotations to a freshly created array state.
73
+ *
74
+ * Array spread copies elements but drops symbol properties, so parsers that
75
+ * rebuild array states need to copy annotations back explicitly.
76
+ *
77
+ * @param source The original state that may carry annotations.
78
+ * @param target The freshly created array state.
79
+ * @returns The target array, with annotations copied when available.
80
+ * @internal
81
+ */
82
+ declare function annotateFreshArray<T>(source: unknown, target: readonly T[]): readonly T[];
71
83
  /**
72
84
  * Propagates annotations from one parser state to another.
73
85
  *
@@ -115,4 +127,4 @@ declare function unwrapInjectedAnnotationWrapper<T>(value: T): T;
115
127
  */
116
128
  declare function isInjectedAnnotationWrapper(value: unknown): boolean;
117
129
  //#endregion
118
- export { Annotations, ParseOptions, annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
130
+ export { Annotations, ParseOptions, annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -58,6 +58,24 @@ function getAnnotations(state) {
58
58
  return void 0;
59
59
  }
60
60
  /**
61
+ * Reattaches annotations to a freshly created array state.
62
+ *
63
+ * Array spread copies elements but drops symbol properties, so parsers that
64
+ * rebuild array states need to copy annotations back explicitly.
65
+ *
66
+ * @param source The original state that may carry annotations.
67
+ * @param target The freshly created array state.
68
+ * @returns The target array, with annotations copied when available.
69
+ * @internal
70
+ */
71
+ function annotateFreshArray(source, target) {
72
+ const annotations = getAnnotations(source);
73
+ if (annotations === void 0) return target;
74
+ const annotated = target;
75
+ annotated[annotationKey] = annotations;
76
+ return annotated;
77
+ }
78
+ /**
61
79
  * Propagates annotations from one parser state to another.
62
80
  *
63
81
  * This is mainly used by parsers that rebuild array states with spread syntax.
@@ -211,4 +229,4 @@ function isInjectedAnnotationWrapper(value) {
211
229
  }
212
230
 
213
231
  //#endregion
214
- export { annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
232
+ export { annotateFreshArray, annotationKey, annotationStateValueKey, annotationWrapperKey, getAnnotations, inheritAnnotations, injectAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper };
@@ -1436,9 +1436,10 @@ function tuple(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
1436
1436
  function merge(...args) {
1437
1437
  const label = typeof args[0] === "string" ? args[0] : void 0;
1438
1438
  const lastArg = args[args.length - 1];
1439
- const options = lastArg && typeof lastArg === "object" && !("parse" in lastArg) && !("complete" in lastArg) ? lastArg : {};
1439
+ const hasOptions = lastArg != null && typeof lastArg === "object" && !("$valueType" in lastArg);
1440
+ const options = hasOptions ? lastArg : {};
1440
1441
  const startIndex = typeof args[0] === "string" ? 1 : 0;
1441
- const endIndex = lastArg && typeof lastArg === "object" && !("parse" in lastArg) && !("complete" in lastArg) ? args.length - 1 : args.length;
1442
+ const endIndex = hasOptions ? args.length - 1 : args.length;
1442
1443
  const rawParsers = args.slice(startIndex, endIndex);
1443
1444
  const combinedMode = rawParsers.some((p) => p.$mode === "async") ? "async" : "sync";
1444
1445
  const isAsync = combinedMode === "async";
@@ -127,8 +127,9 @@ type OrArityGuard<TParsers extends readonly unknown[]> = IsTuple<TParsers> exten
127
127
  * @template TStateB The type of the state used by the second parser.
128
128
  * @param a The first {@link Parser} to try.
129
129
  * @param b The second {@link Parser} to try.
130
- * @returns A {@link Parser} that tries to parse using the provided parsers
131
- * in order, returning the result of the first successful parser.
130
+ * @return A {@link Parser} that tries to parse using the provided parsers
131
+ * in order, returning the result of the first successful parser.
132
+ * @since 0.3.0
132
133
  */
133
134
  declare function or<MA extends Mode, MB extends Mode, TA, TB, TStateA, TStateB>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>): Parser<CombineModes<readonly [MA, MB]>, TA | TB, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>]>;
134
135
  /**
@@ -149,6 +150,7 @@ declare function or<MA extends Mode, MB extends Mode, TA, TB, TStateA, TStateB>(
149
150
  * @param c The third {@link Parser} to try.
150
151
  * @return A {@link Parser} that tries to parse using the provided parsers
151
152
  * in order, returning the result of the first successful parser.
153
+ * @since 0.3.0
152
154
  */
153
155
  declare function or<MA extends Mode, MB extends Mode, MC extends Mode, TA, TB, TC, TStateA, TStateB, TStateC>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>, c: Parser<MC, TC, TStateC>): Parser<CombineModes<readonly [MA, MB, MC]>, TA | TB | TC, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>] | [2, ParserResult<TStateC>]>;
154
156
  /**
@@ -173,6 +175,7 @@ declare function or<MA extends Mode, MB extends Mode, MC extends Mode, TA, TB, T
173
175
  * @param d The fourth {@link Parser} to try.
174
176
  * @return A {@link Parser} that tries to parse using the provided parsers
175
177
  * in order, returning the result of the first successful parser.
178
+ * @since 0.3.0
176
179
  */
177
180
  declare function or<MA extends Mode, MB extends Mode, MC extends Mode, MD extends Mode, TA, TB, TC, TD, TStateA, TStateB, TStateC, TStateD>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>, c: Parser<MC, TC, TStateC>, d: Parser<MD, TD, TStateD>): Parser<CombineModes<readonly [MA, MB, MC, MD]>, TA | TB | TC | TD, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>] | [2, ParserResult<TStateC>] | [3, ParserResult<TStateD>]>;
178
181
  /**
@@ -779,7 +782,7 @@ interface LongestMatchOptions {
779
782
  errors?: LongestMatchErrorOptions;
780
783
  }
781
784
  /**
782
- * Options for customizing error messages in the {@link longesMatch} parser.
785
+ * Options for customizing error messages in the {@link longestMatch} parser.
783
786
  * @since 0.5.0
784
787
  */
785
788
  interface LongestMatchErrorOptions {
@@ -883,6 +886,28 @@ declare function longestMatch<const TParsers extends readonly Parser<Mode, unkno
883
886
  * @since 0.3.0
884
887
  */
885
888
  declare function longestMatch<const TParsers extends readonly Parser<Mode, unknown, unknown>[]>(...parsers: TParsers & LongestMatchArityGuard<TParsers>): Parser<CombineModes<{ readonly [K in keyof TParsers]: ExtractMode<TParsers[K]> }>, InferValue<TParsers[number]>, LongestMatchState<TParsers>>;
889
+ /**
890
+ * Creates a parser that selects the successful branch that consumed
891
+ * the most input tokens from a homogeneous parser list.
892
+ *
893
+ * This overload is intended for spread arrays whose element types are
894
+ * homogeneous enough that callers only need the shared value type.
895
+ *
896
+ * @param parsers Parsers to evaluate and compare by consumed input.
897
+ * @returns A parser that preserves the shared parser mode and value type.
898
+ * @since 1.0.0
899
+ */
900
+ declare function longestMatch<M extends Mode, TValue, TState, const TParsers extends readonly Parser<M, TValue, TState>[]>(...parsers: TParsers & LongestMatchArityGuard<TParsers>): Parser<M, TValue, unknown>;
901
+ /**
902
+ * Creates a parser that selects the successful branch that consumed
903
+ * the most input tokens from a homogeneous parser list, with custom
904
+ * error options.
905
+ *
906
+ * @param rest Parsers to compare, followed by error customization options.
907
+ * @returns A parser that preserves the shared parser mode and value type.
908
+ * @since 1.0.0
909
+ */
910
+ declare function longestMatch<M extends Mode, TValue, TState, const TParsers extends readonly Parser<M, TValue, TState>[]>(...rest: [...parsers: TParsers, options: LongestMatchTailOptions] & LongestMatchArityGuard<TParsers>): Parser<M, TValue, unknown>;
886
911
  /**
887
912
  * Options for the {@link object} parser.
888
913
  * @since 0.5.0
@@ -127,8 +127,9 @@ type OrArityGuard<TParsers extends readonly unknown[]> = IsTuple<TParsers> exten
127
127
  * @template TStateB The type of the state used by the second parser.
128
128
  * @param a The first {@link Parser} to try.
129
129
  * @param b The second {@link Parser} to try.
130
- * @returns A {@link Parser} that tries to parse using the provided parsers
131
- * in order, returning the result of the first successful parser.
130
+ * @return A {@link Parser} that tries to parse using the provided parsers
131
+ * in order, returning the result of the first successful parser.
132
+ * @since 0.3.0
132
133
  */
133
134
  declare function or<MA extends Mode, MB extends Mode, TA, TB, TStateA, TStateB>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>): Parser<CombineModes<readonly [MA, MB]>, TA | TB, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>]>;
134
135
  /**
@@ -149,6 +150,7 @@ declare function or<MA extends Mode, MB extends Mode, TA, TB, TStateA, TStateB>(
149
150
  * @param c The third {@link Parser} to try.
150
151
  * @return A {@link Parser} that tries to parse using the provided parsers
151
152
  * in order, returning the result of the first successful parser.
153
+ * @since 0.3.0
152
154
  */
153
155
  declare function or<MA extends Mode, MB extends Mode, MC extends Mode, TA, TB, TC, TStateA, TStateB, TStateC>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>, c: Parser<MC, TC, TStateC>): Parser<CombineModes<readonly [MA, MB, MC]>, TA | TB | TC, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>] | [2, ParserResult<TStateC>]>;
154
156
  /**
@@ -173,6 +175,7 @@ declare function or<MA extends Mode, MB extends Mode, MC extends Mode, TA, TB, T
173
175
  * @param d The fourth {@link Parser} to try.
174
176
  * @return A {@link Parser} that tries to parse using the provided parsers
175
177
  * in order, returning the result of the first successful parser.
178
+ * @since 0.3.0
176
179
  */
177
180
  declare function or<MA extends Mode, MB extends Mode, MC extends Mode, MD extends Mode, TA, TB, TC, TD, TStateA, TStateB, TStateC, TStateD>(a: Parser<MA, TA, TStateA>, b: Parser<MB, TB, TStateB>, c: Parser<MC, TC, TStateC>, d: Parser<MD, TD, TStateD>): Parser<CombineModes<readonly [MA, MB, MC, MD]>, TA | TB | TC | TD, undefined | [0, ParserResult<TStateA>] | [1, ParserResult<TStateB>] | [2, ParserResult<TStateC>] | [3, ParserResult<TStateD>]>;
178
181
  /**
@@ -779,7 +782,7 @@ interface LongestMatchOptions {
779
782
  errors?: LongestMatchErrorOptions;
780
783
  }
781
784
  /**
782
- * Options for customizing error messages in the {@link longesMatch} parser.
785
+ * Options for customizing error messages in the {@link longestMatch} parser.
783
786
  * @since 0.5.0
784
787
  */
785
788
  interface LongestMatchErrorOptions {
@@ -883,6 +886,28 @@ declare function longestMatch<const TParsers extends readonly Parser<Mode, unkno
883
886
  * @since 0.3.0
884
887
  */
885
888
  declare function longestMatch<const TParsers extends readonly Parser<Mode, unknown, unknown>[]>(...parsers: TParsers & LongestMatchArityGuard<TParsers>): Parser<CombineModes<{ readonly [K in keyof TParsers]: ExtractMode<TParsers[K]> }>, InferValue<TParsers[number]>, LongestMatchState<TParsers>>;
889
+ /**
890
+ * Creates a parser that selects the successful branch that consumed
891
+ * the most input tokens from a homogeneous parser list.
892
+ *
893
+ * This overload is intended for spread arrays whose element types are
894
+ * homogeneous enough that callers only need the shared value type.
895
+ *
896
+ * @param parsers Parsers to evaluate and compare by consumed input.
897
+ * @returns A parser that preserves the shared parser mode and value type.
898
+ * @since 1.0.0
899
+ */
900
+ declare function longestMatch<M extends Mode, TValue, TState, const TParsers extends readonly Parser<M, TValue, TState>[]>(...parsers: TParsers & LongestMatchArityGuard<TParsers>): Parser<M, TValue, unknown>;
901
+ /**
902
+ * Creates a parser that selects the successful branch that consumed
903
+ * the most input tokens from a homogeneous parser list, with custom
904
+ * error options.
905
+ *
906
+ * @param rest Parsers to compare, followed by error customization options.
907
+ * @returns A parser that preserves the shared parser mode and value type.
908
+ * @since 1.0.0
909
+ */
910
+ declare function longestMatch<M extends Mode, TValue, TState, const TParsers extends readonly Parser<M, TValue, TState>[]>(...rest: [...parsers: TParsers, options: LongestMatchTailOptions] & LongestMatchArityGuard<TParsers>): Parser<M, TValue, unknown>;
886
911
  /**
887
912
  * Options for the {@link object} parser.
888
913
  * @since 0.5.0
@@ -1436,9 +1436,10 @@ function tuple(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
1436
1436
  function merge(...args) {
1437
1437
  const label = typeof args[0] === "string" ? args[0] : void 0;
1438
1438
  const lastArg = args[args.length - 1];
1439
- const options = lastArg && typeof lastArg === "object" && !("parse" in lastArg) && !("complete" in lastArg) ? lastArg : {};
1439
+ const hasOptions = lastArg != null && typeof lastArg === "object" && !("$valueType" in lastArg);
1440
+ const options = hasOptions ? lastArg : {};
1440
1441
  const startIndex = typeof args[0] === "string" ? 1 : 0;
1441
- const endIndex = lastArg && typeof lastArg === "object" && !("parse" in lastArg) && !("complete" in lastArg) ? args.length - 1 : args.length;
1442
+ const endIndex = hasOptions ? args.length - 1 : args.length;
1442
1443
  const rawParsers = args.slice(startIndex, endIndex);
1443
1444
  const combinedMode = rawParsers.some((p) => p.$mode === "async") ? "async" : "sync";
1444
1445
  const isAsync = combinedMode === "async";
package/dist/context.cjs CHANGED
@@ -4,8 +4,9 @@
4
4
  * Checks whether a context is static (returns annotations without needing
5
5
  * parsed results).
6
6
  *
7
- * A context is considered static if `getAnnotations()` called without
8
- * arguments returns a non-empty annotations object synchronously.
7
+ * A context is considered static if it declares `mode: "static"` or if
8
+ * `getAnnotations()` called without arguments returns a non-empty
9
+ * annotations object synchronously.
9
10
  *
10
11
  * @param context The source context to check.
11
12
  * @returns `true` if the context is static, `false` otherwise.
@@ -154,8 +154,9 @@ interface SourceContext<TRequiredOptions = void> {
154
154
  * Checks whether a context is static (returns annotations without needing
155
155
  * parsed results).
156
156
  *
157
- * A context is considered static if `getAnnotations()` called without
158
- * arguments returns a non-empty annotations object synchronously.
157
+ * A context is considered static if it declares `mode: "static"` or if
158
+ * `getAnnotations()` called without arguments returns a non-empty
159
+ * annotations object synchronously.
159
160
  *
160
161
  * @param context The source context to check.
161
162
  * @returns `true` if the context is static, `false` otherwise.
package/dist/context.d.ts CHANGED
@@ -154,8 +154,9 @@ interface SourceContext<TRequiredOptions = void> {
154
154
  * Checks whether a context is static (returns annotations without needing
155
155
  * parsed results).
156
156
  *
157
- * A context is considered static if `getAnnotations()` called without
158
- * arguments returns a non-empty annotations object synchronously.
157
+ * A context is considered static if it declares `mode: "static"` or if
158
+ * `getAnnotations()` called without arguments returns a non-empty
159
+ * annotations object synchronously.
159
160
  *
160
161
  * @param context The source context to check.
161
162
  * @returns `true` if the context is static, `false` otherwise.
package/dist/context.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * Checks whether a context is static (returns annotations without needing
4
4
  * parsed results).
5
5
  *
6
- * A context is considered static if `getAnnotations()` called without
7
- * arguments returns a non-empty annotations object synchronously.
6
+ * A context is considered static if it declares `mode: "static"` or if
7
+ * `getAnnotations()` called without arguments returns a non-empty
8
+ * annotations object synchronously.
8
9
  *
9
10
  * @param context The source context to check.
10
11
  * @returns `true` if the context is static, `false` otherwise.
package/dist/facade.cjs CHANGED
@@ -339,7 +339,6 @@ function classifyResult(result, args, helpOptionNames, helpCommandNames, version
339
339
  const hasHelpOption = helpOptionNames.some((n) => args.includes(n));
340
340
  const hasHelpCommand = args.length > 0 && helpCommandNames.includes(args[0]);
341
341
  const hasCompletionCommand = args.length > 0 && completionCommandNames.includes(args[0]);
342
- if (hasVersionOption && hasHelpOption && !hasVersionCommand && !hasHelpCommand) {}
343
342
  if (hasVersionCommand && hasHelpOption && parsedValue.helpFlag) return {
344
343
  type: "help",
345
344
  commands: [args[0]]
@@ -870,13 +869,8 @@ async function collectPhase1Annotations(contexts, options) {
870
869
  let hasDynamic = false;
871
870
  for (const context of contexts) {
872
871
  const result = context.getAnnotations(void 0, options);
873
- if (result instanceof Promise) {
874
- hasDynamic = true;
875
- annotationsList.push(await result);
876
- } else {
877
- if (Object.getOwnPropertySymbols(result).length === 0) hasDynamic = true;
878
- annotationsList.push(result);
879
- }
872
+ hasDynamic ||= needsTwoPhaseContext(context, result);
873
+ annotationsList.push(result instanceof Promise ? await result : result);
880
874
  }
881
875
  return {
882
876
  annotations: mergeAnnotations(annotationsList),
@@ -914,7 +908,7 @@ function collectPhase1AnnotationsSync(contexts, options) {
914
908
  for (const context of contexts) {
915
909
  const result = context.getAnnotations(void 0, options);
916
910
  if (result instanceof Promise) throw new Error(`Context ${String(context.id)} returned a Promise in sync mode. Use runWith() or runWithAsync() for async contexts.`);
917
- if (Object.getOwnPropertySymbols(result).length === 0) hasDynamic = true;
911
+ hasDynamic ||= needsTwoPhaseContext(context, result);
918
912
  annotationsList.push(result);
919
913
  }
920
914
  return {
@@ -923,6 +917,18 @@ function collectPhase1AnnotationsSync(contexts, options) {
923
917
  };
924
918
  }
925
919
  /**
920
+ * Determines whether a context requires a second parse pass.
921
+ *
922
+ * Explicit `mode` declarations take precedence over legacy heuristics so
923
+ * static contexts are not forced into two-phase parsing when they return
924
+ * empty annotations or a Promise.
925
+ */
926
+ function needsTwoPhaseContext(context, result) {
927
+ if (context.mode !== void 0) return context.mode === "dynamic";
928
+ if (result instanceof Promise) return true;
929
+ return Object.getOwnPropertySymbols(result).length === 0;
930
+ }
931
+ /**
926
932
  * Collects annotations from all contexts synchronously.
927
933
  *
928
934
  * @param contexts Source contexts to collect annotations from.
package/dist/facade.js CHANGED
@@ -339,7 +339,6 @@ function classifyResult(result, args, helpOptionNames, helpCommandNames, version
339
339
  const hasHelpOption = helpOptionNames.some((n) => args.includes(n));
340
340
  const hasHelpCommand = args.length > 0 && helpCommandNames.includes(args[0]);
341
341
  const hasCompletionCommand = args.length > 0 && completionCommandNames.includes(args[0]);
342
- if (hasVersionOption && hasHelpOption && !hasVersionCommand && !hasHelpCommand) {}
343
342
  if (hasVersionCommand && hasHelpOption && parsedValue.helpFlag) return {
344
343
  type: "help",
345
344
  commands: [args[0]]
@@ -870,13 +869,8 @@ async function collectPhase1Annotations(contexts, options) {
870
869
  let hasDynamic = false;
871
870
  for (const context of contexts) {
872
871
  const result = context.getAnnotations(void 0, options);
873
- if (result instanceof Promise) {
874
- hasDynamic = true;
875
- annotationsList.push(await result);
876
- } else {
877
- if (Object.getOwnPropertySymbols(result).length === 0) hasDynamic = true;
878
- annotationsList.push(result);
879
- }
872
+ hasDynamic ||= needsTwoPhaseContext(context, result);
873
+ annotationsList.push(result instanceof Promise ? await result : result);
880
874
  }
881
875
  return {
882
876
  annotations: mergeAnnotations(annotationsList),
@@ -914,7 +908,7 @@ function collectPhase1AnnotationsSync(contexts, options) {
914
908
  for (const context of contexts) {
915
909
  const result = context.getAnnotations(void 0, options);
916
910
  if (result instanceof Promise) throw new Error(`Context ${String(context.id)} returned a Promise in sync mode. Use runWith() or runWithAsync() for async contexts.`);
917
- if (Object.getOwnPropertySymbols(result).length === 0) hasDynamic = true;
911
+ hasDynamic ||= needsTwoPhaseContext(context, result);
918
912
  annotationsList.push(result);
919
913
  }
920
914
  return {
@@ -923,6 +917,18 @@ function collectPhase1AnnotationsSync(contexts, options) {
923
917
  };
924
918
  }
925
919
  /**
920
+ * Determines whether a context requires a second parse pass.
921
+ *
922
+ * Explicit `mode` declarations take precedence over legacy heuristics so
923
+ * static contexts are not forced into two-phase parsing when they return
924
+ * empty annotations or a Promise.
925
+ */
926
+ function needsTwoPhaseContext(context, result) {
927
+ if (context.mode !== void 0) return context.mode === "dynamic";
928
+ if (result instanceof Promise) return true;
929
+ return Object.getOwnPropertySymbols(result).length === 0;
930
+ }
931
+ /**
926
932
  * Collects annotations from all contexts synchronously.
927
933
  *
928
934
  * @param contexts Source contexts to collect annotations from.
@@ -18,6 +18,11 @@ function dispatchByMode(mode, syncFn, asyncFn) {
18
18
  if (mode === "async") return asyncFn();
19
19
  return syncFn();
20
20
  }
21
+ function wrapForMode(mode, value) {
22
+ if (mode === "async") return Promise.resolve(value);
23
+ if (value instanceof Promise) throw new TypeError("Synchronous mode cannot wrap Promise value.");
24
+ return value;
25
+ }
21
26
  /**
22
27
  * Maps a mode-wrapped value while preserving its execution mode.
23
28
  *
@@ -30,6 +35,7 @@ function dispatchByMode(mode, syncFn, asyncFn) {
30
35
  */
31
36
  function mapModeValue(mode, value, mapFn) {
32
37
  if (mode === "async") return Promise.resolve(value).then(mapFn);
38
+ if (value instanceof Promise) throw new TypeError("Synchronous mode cannot map Promise value.");
33
39
  return mapFn(value);
34
40
  }
35
41
  /**
@@ -50,4 +56,5 @@ function dispatchIterableByMode(mode, syncFn, asyncFn) {
50
56
  //#endregion
51
57
  exports.dispatchByMode = dispatchByMode;
52
58
  exports.dispatchIterableByMode = dispatchIterableByMode;
53
- exports.mapModeValue = mapModeValue;
59
+ exports.mapModeValue = mapModeValue;
60
+ exports.wrapForMode = wrapForMode;
@@ -0,0 +1,55 @@
1
+ import { Mode, ModeIterable, ModeValue } from "./parser.cjs";
2
+
3
+ //#region src/mode-dispatch.d.ts
4
+
5
+ /**
6
+ * Dispatches to sync or async implementation based on mode.
7
+ *
8
+ * This function encapsulates the necessary type assertions when branching
9
+ * on runtime mode values. TypeScript cannot narrow `ModeValue<M, T>` based
10
+ * on `mode === "async"` checks, so we must use type assertions.
11
+ *
12
+ * @param mode The execution mode.
13
+ * @param syncFn Function to call for sync execution.
14
+ * @param asyncFn Function to call for async execution.
15
+ * @returns The result with correct mode wrapping.
16
+ * @internal
17
+ * @since 0.10.0
18
+ */
19
+ declare function dispatchByMode<M extends Mode, T>(mode: M, syncFn: () => T, asyncFn: () => Promise<T>): ModeValue<M, T>;
20
+ /**
21
+ * Wraps a value so it matches the parser execution mode.
22
+ *
23
+ * @param mode The execution mode.
24
+ * @param value The value to wrap.
25
+ * @returns The wrapped value with correct mode semantics.
26
+ * @internal
27
+ * @since 1.0.0
28
+ */
29
+ declare function wrapForMode<T>(mode: "sync", value: T | Promise<T>): T;
30
+ declare function wrapForMode<T>(mode: "async", value: T | Promise<T>): Promise<T>;
31
+ declare function wrapForMode<M extends Mode, T>(mode: M, value: T | Promise<T>): ModeValue<M, T>;
32
+ /**
33
+ * Maps a mode-wrapped value while preserving its execution mode.
34
+ *
35
+ * @param mode The execution mode.
36
+ * @param value The mode-wrapped value to transform.
37
+ * @param mapFn Mapping function applied to the unwrapped value.
38
+ * @returns The mapped value with correct mode wrapping.
39
+ * @internal
40
+ * @since 1.0.0
41
+ */
42
+ declare function mapModeValue<M extends Mode, T, U>(mode: M, value: ModeValue<M, T>, mapFn: (value: T) => U): ModeValue<M, U>;
43
+ /**
44
+ * Dispatches iterable to sync or async implementation based on mode.
45
+ *
46
+ * @param mode The execution mode.
47
+ * @param syncFn Function returning sync iterable.
48
+ * @param asyncFn Function returning async iterable.
49
+ * @returns The iterable with correct mode wrapping.
50
+ * @internal
51
+ * @since 0.10.0
52
+ */
53
+ declare function dispatchIterableByMode<M extends Mode, T>(mode: M, syncFn: () => Iterable<T>, asyncFn: () => AsyncIterable<T>): ModeIterable<M, T>;
54
+ //#endregion
55
+ export { dispatchByMode, dispatchIterableByMode, mapModeValue, wrapForMode };
@@ -0,0 +1,55 @@
1
+ import { Mode, ModeIterable, ModeValue } from "./parser.js";
2
+
3
+ //#region src/mode-dispatch.d.ts
4
+
5
+ /**
6
+ * Dispatches to sync or async implementation based on mode.
7
+ *
8
+ * This function encapsulates the necessary type assertions when branching
9
+ * on runtime mode values. TypeScript cannot narrow `ModeValue<M, T>` based
10
+ * on `mode === "async"` checks, so we must use type assertions.
11
+ *
12
+ * @param mode The execution mode.
13
+ * @param syncFn Function to call for sync execution.
14
+ * @param asyncFn Function to call for async execution.
15
+ * @returns The result with correct mode wrapping.
16
+ * @internal
17
+ * @since 0.10.0
18
+ */
19
+ declare function dispatchByMode<M extends Mode, T>(mode: M, syncFn: () => T, asyncFn: () => Promise<T>): ModeValue<M, T>;
20
+ /**
21
+ * Wraps a value so it matches the parser execution mode.
22
+ *
23
+ * @param mode The execution mode.
24
+ * @param value The value to wrap.
25
+ * @returns The wrapped value with correct mode semantics.
26
+ * @internal
27
+ * @since 1.0.0
28
+ */
29
+ declare function wrapForMode<T>(mode: "sync", value: T | Promise<T>): T;
30
+ declare function wrapForMode<T>(mode: "async", value: T | Promise<T>): Promise<T>;
31
+ declare function wrapForMode<M extends Mode, T>(mode: M, value: T | Promise<T>): ModeValue<M, T>;
32
+ /**
33
+ * Maps a mode-wrapped value while preserving its execution mode.
34
+ *
35
+ * @param mode The execution mode.
36
+ * @param value The mode-wrapped value to transform.
37
+ * @param mapFn Mapping function applied to the unwrapped value.
38
+ * @returns The mapped value with correct mode wrapping.
39
+ * @internal
40
+ * @since 1.0.0
41
+ */
42
+ declare function mapModeValue<M extends Mode, T, U>(mode: M, value: ModeValue<M, T>, mapFn: (value: T) => U): ModeValue<M, U>;
43
+ /**
44
+ * Dispatches iterable to sync or async implementation based on mode.
45
+ *
46
+ * @param mode The execution mode.
47
+ * @param syncFn Function returning sync iterable.
48
+ * @param asyncFn Function returning async iterable.
49
+ * @returns The iterable with correct mode wrapping.
50
+ * @internal
51
+ * @since 0.10.0
52
+ */
53
+ declare function dispatchIterableByMode<M extends Mode, T>(mode: M, syncFn: () => Iterable<T>, asyncFn: () => AsyncIterable<T>): ModeIterable<M, T>;
54
+ //#endregion
55
+ export { dispatchByMode, dispatchIterableByMode, mapModeValue, wrapForMode };
@@ -17,6 +17,11 @@ function dispatchByMode(mode, syncFn, asyncFn) {
17
17
  if (mode === "async") return asyncFn();
18
18
  return syncFn();
19
19
  }
20
+ function wrapForMode(mode, value) {
21
+ if (mode === "async") return Promise.resolve(value);
22
+ if (value instanceof Promise) throw new TypeError("Synchronous mode cannot wrap Promise value.");
23
+ return value;
24
+ }
20
25
  /**
21
26
  * Maps a mode-wrapped value while preserving its execution mode.
22
27
  *
@@ -29,6 +34,7 @@ function dispatchByMode(mode, syncFn, asyncFn) {
29
34
  */
30
35
  function mapModeValue(mode, value, mapFn) {
31
36
  if (mode === "async") return Promise.resolve(value).then(mapFn);
37
+ if (value instanceof Promise) throw new TypeError("Synchronous mode cannot map Promise value.");
32
38
  return mapFn(value);
33
39
  }
34
40
  /**
@@ -47,4 +53,4 @@ function dispatchIterableByMode(mode, syncFn, asyncFn) {
47
53
  }
48
54
 
49
55
  //#endregion
50
- export { dispatchByMode, dispatchIterableByMode, mapModeValue };
56
+ export { dispatchByMode, dispatchIterableByMode, mapModeValue, wrapForMode };
@@ -411,13 +411,6 @@ function multiple(parser, options = {}) {
411
411
  const syncParser = parser;
412
412
  const { min = 0, max = Infinity } = options;
413
413
  const unwrapInjectedWrapper = require_annotations.unwrapInjectedAnnotationWrapper;
414
- const annotateFreshArray = (source, target) => {
415
- const annotations = require_annotations.getAnnotations(source);
416
- if (annotations === void 0) return target;
417
- const annotated = target;
418
- annotated[require_annotations.annotationKey] = annotations;
419
- return annotated;
420
- };
421
414
  const completeSyncWithUnwrappedFallback = (state) => {
422
415
  try {
423
416
  return syncParser.complete(state);
@@ -488,7 +481,7 @@ function multiple(parser, options = {}) {
488
481
  success: true,
489
482
  next: {
490
483
  ...result.next,
491
- state: annotateFreshArray(context.state, [...added ? context.state : context.state.slice(0, -1), nextItemState])
484
+ state: require_annotations.annotateFreshArray(context.state, [...added ? context.state : context.state.slice(0, -1), nextItemState])
492
485
  },
493
486
  consumed: result.consumed
494
487
  };
@@ -515,7 +508,7 @@ function multiple(parser, options = {}) {
515
508
  success: true,
516
509
  next: {
517
510
  ...result.next,
518
- state: annotateFreshArray(context.state, [...added ? context.state : context.state.slice(0, -1), nextItemState])
511
+ state: require_annotations.annotateFreshArray(context.state, [...added ? context.state : context.state.slice(0, -1), nextItemState])
519
512
  },
520
513
  consumed: result.consumed
521
514
  };
package/dist/modifiers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { annotationKey, getAnnotations, inheritAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./annotations.js";
1
+ import { annotateFreshArray, inheritAnnotations, isInjectedAnnotationWrapper, unwrapInjectedAnnotationWrapper } from "./annotations.js";
2
2
  import { formatMessage, message, text } from "./message.js";
3
3
  import { createDependencySourceState, dependencyId, isDependencySourceState, isPendingDependencySourceState, isWrappedDependencySource, transformsDependencyValue, transformsDependencyValueMarker, wrappedDependencySourceMarker } from "./dependency.js";
4
4
  import { dispatchByMode, dispatchIterableByMode, mapModeValue } from "./mode-dispatch.js";
@@ -411,13 +411,6 @@ function multiple(parser, options = {}) {
411
411
  const syncParser = parser;
412
412
  const { min = 0, max = Infinity } = options;
413
413
  const unwrapInjectedWrapper = unwrapInjectedAnnotationWrapper;
414
- const annotateFreshArray = (source, target) => {
415
- const annotations = getAnnotations(source);
416
- if (annotations === void 0) return target;
417
- const annotated = target;
418
- annotated[annotationKey] = annotations;
419
- return annotated;
420
- };
421
414
  const completeSyncWithUnwrappedFallback = (state) => {
422
415
  try {
423
416
  return syncParser.complete(state);
@@ -1116,13 +1116,6 @@ function passThrough(options = {}) {
1116
1116
  const format = options.format ?? "equalsOnly";
1117
1117
  const optionPattern = /^-[a-z0-9-]|^--[a-z0-9-]+/i;
1118
1118
  const equalsOptionPattern = /^--[a-z0-9-]+=/i;
1119
- const annotateFreshArray = (source, target) => {
1120
- const annotations = require_annotations.getAnnotations(source);
1121
- if (annotations === void 0) return target;
1122
- const annotated = target;
1123
- annotated[require_annotations.annotationKey] = annotations;
1124
- return annotated;
1125
- };
1126
1119
  return {
1127
1120
  $valueType: [],
1128
1121
  $stateType: [],
@@ -1147,7 +1140,7 @@ function passThrough(options = {}) {
1147
1140
  next: {
1148
1141
  ...context,
1149
1142
  buffer: [],
1150
- state: annotateFreshArray(context.state, [...context.state, ...captured])
1143
+ state: require_annotations.annotateFreshArray(context.state, [...context.state, ...captured])
1151
1144
  },
1152
1145
  consumed: captured
1153
1146
  };
@@ -1168,7 +1161,7 @@ function passThrough(options = {}) {
1168
1161
  next: {
1169
1162
  ...context,
1170
1163
  buffer: context.buffer.slice(1),
1171
- state: annotateFreshArray(context.state, [...context.state, token])
1164
+ state: require_annotations.annotateFreshArray(context.state, [...context.state, token])
1172
1165
  },
1173
1166
  consumed: [token]
1174
1167
  };
@@ -1184,7 +1177,7 @@ function passThrough(options = {}) {
1184
1177
  next: {
1185
1178
  ...context,
1186
1179
  buffer: context.buffer.slice(1),
1187
- state: annotateFreshArray(context.state, [...context.state, token])
1180
+ state: require_annotations.annotateFreshArray(context.state, [...context.state, token])
1188
1181
  },
1189
1182
  consumed: [token]
1190
1183
  };
@@ -1194,7 +1187,7 @@ function passThrough(options = {}) {
1194
1187
  next: {
1195
1188
  ...context,
1196
1189
  buffer: context.buffer.slice(2),
1197
- state: annotateFreshArray(context.state, [
1190
+ state: require_annotations.annotateFreshArray(context.state, [
1198
1191
  ...context.state,
1199
1192
  token,
1200
1193
  nextToken
@@ -1207,7 +1200,7 @@ function passThrough(options = {}) {
1207
1200
  next: {
1208
1201
  ...context,
1209
1202
  buffer: context.buffer.slice(1),
1210
- state: annotateFreshArray(context.state, [...context.state, token])
1203
+ state: require_annotations.annotateFreshArray(context.state, [...context.state, token])
1211
1204
  },
1212
1205
  consumed: [token]
1213
1206
  };
@@ -1,4 +1,4 @@
1
- import { annotationKey, getAnnotations } from "./annotations.js";
1
+ import { annotateFreshArray, getAnnotations } from "./annotations.js";
2
2
  import { message, metavar, optionName, optionNames, text, valueSet } from "./message.js";
3
3
  import { createDeferredParseState, createDependencySourceState, createPendingDependencySourceState, dependencyId, getDefaultValuesFunction, getDependencyIds, isDeferredParseState, isDependencySource, isDependencySourceState, isDerivedValueParser, isPendingDependencySourceState, suggestWithDependency } from "./dependency.js";
4
4
  import { dispatchIterableByMode } from "./mode-dispatch.js";
@@ -1116,13 +1116,6 @@ function passThrough(options = {}) {
1116
1116
  const format = options.format ?? "equalsOnly";
1117
1117
  const optionPattern = /^-[a-z0-9-]|^--[a-z0-9-]+/i;
1118
1118
  const equalsOptionPattern = /^--[a-z0-9-]+=/i;
1119
- const annotateFreshArray = (source, target) => {
1120
- const annotations = getAnnotations(source);
1121
- if (annotations === void 0) return target;
1122
- const annotated = target;
1123
- annotated[annotationKey] = annotations;
1124
- return annotated;
1125
- };
1126
1119
  return {
1127
1120
  $valueType: [],
1128
1121
  $stateType: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.552+30c7ea22",
3
+ "version": "1.0.0-dev.556+f691dcba",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -115,6 +115,14 @@
115
115
  "import": "./dist/message.js",
116
116
  "require": "./dist/message.cjs"
117
117
  },
118
+ "./mode-dispatch": {
119
+ "types": {
120
+ "import": "./dist/mode-dispatch.d.ts",
121
+ "require": "./dist/mode-dispatch.d.cts"
122
+ },
123
+ "import": "./dist/mode-dispatch.js",
124
+ "require": "./dist/mode-dispatch.cjs"
125
+ },
118
126
  "./modifiers": {
119
127
  "types": {
120
128
  "import": "./dist/modifiers.d.ts",