@player-tools/fluent 0.12.1--canary.241.6077

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.
Files changed (134) hide show
  1. package/dist/cjs/index.cjs +2396 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2276 -0
  4. package/dist/index.mjs +2276 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
  8. package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
  9. package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
  10. package/src/core/base-builder/__tests__/registry.test.ts +534 -0
  11. package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
  12. package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
  13. package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
  14. package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
  15. package/src/core/base-builder/__tests__/switch.test.ts +815 -0
  16. package/src/core/base-builder/__tests__/template.test.ts +596 -0
  17. package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
  18. package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
  19. package/src/core/base-builder/conditional/index.ts +64 -0
  20. package/src/core/base-builder/context.ts +152 -0
  21. package/src/core/base-builder/errors.ts +69 -0
  22. package/src/core/base-builder/fluent-builder-base.ts +308 -0
  23. package/src/core/base-builder/guards.ts +137 -0
  24. package/src/core/base-builder/id/generator.ts +290 -0
  25. package/src/core/base-builder/id/registry.ts +152 -0
  26. package/src/core/base-builder/index.ts +72 -0
  27. package/src/core/base-builder/resolution/path-resolver.ts +116 -0
  28. package/src/core/base-builder/resolution/pipeline.ts +103 -0
  29. package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
  30. package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
  31. package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
  32. package/src/core/base-builder/resolution/steps/builders.ts +84 -0
  33. package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
  34. package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
  35. package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
  36. package/src/core/base-builder/resolution/steps/switches.ts +71 -0
  37. package/src/core/base-builder/resolution/steps/templates.ts +40 -0
  38. package/src/core/base-builder/resolution/value-resolver.ts +333 -0
  39. package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
  40. package/src/core/base-builder/storage/value-storage.ts +282 -0
  41. package/src/core/base-builder/types.ts +266 -0
  42. package/src/core/base-builder/utils.ts +10 -0
  43. package/src/core/flow/__tests__/index.test.ts +292 -0
  44. package/src/core/flow/index.ts +118 -0
  45. package/src/core/index.ts +8 -0
  46. package/src/core/mocks/generated/action.builder.ts +92 -0
  47. package/src/core/mocks/generated/choice-item.builder.ts +120 -0
  48. package/src/core/mocks/generated/choice.builder.ts +134 -0
  49. package/src/core/mocks/generated/collection.builder.ts +93 -0
  50. package/src/core/mocks/generated/field-collection.builder.ts +86 -0
  51. package/src/core/mocks/generated/index.ts +10 -0
  52. package/src/core/mocks/generated/info.builder.ts +64 -0
  53. package/src/core/mocks/generated/input.builder.ts +63 -0
  54. package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
  55. package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
  56. package/src/core/mocks/generated/text.builder.ts +47 -0
  57. package/src/core/mocks/index.ts +1 -0
  58. package/src/core/mocks/types/action.ts +92 -0
  59. package/src/core/mocks/types/choice.ts +129 -0
  60. package/src/core/mocks/types/collection.ts +140 -0
  61. package/src/core/mocks/types/info.ts +7 -0
  62. package/src/core/mocks/types/input.ts +7 -0
  63. package/src/core/mocks/types/text.ts +5 -0
  64. package/src/core/schema/__tests__/index.test.ts +127 -0
  65. package/src/core/schema/index.ts +195 -0
  66. package/src/core/schema/types.ts +7 -0
  67. package/src/core/switch/__tests__/index.test.ts +156 -0
  68. package/src/core/switch/index.ts +81 -0
  69. package/src/core/tagged-template/README.md +448 -0
  70. package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
  71. package/src/core/tagged-template/__tests__/index.test.ts +190 -0
  72. package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
  73. package/src/core/tagged-template/binding.ts +95 -0
  74. package/src/core/tagged-template/expression.ts +92 -0
  75. package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
  76. package/src/core/tagged-template/index.ts +5 -0
  77. package/src/core/tagged-template/std.ts +472 -0
  78. package/src/core/tagged-template/types.ts +123 -0
  79. package/src/core/template/__tests__/index.test.ts +380 -0
  80. package/src/core/template/index.ts +196 -0
  81. package/src/core/utils/index.ts +160 -0
  82. package/src/fp/README.md +411 -0
  83. package/src/fp/__tests__/index.test.ts +1178 -0
  84. package/src/fp/index.ts +386 -0
  85. package/src/gen/common.ts +15 -0
  86. package/src/index.ts +5 -0
  87. package/src/types.ts +203 -0
  88. package/types/core/base-builder/conditional/index.d.ts +21 -0
  89. package/types/core/base-builder/context.d.ts +39 -0
  90. package/types/core/base-builder/errors.d.ts +45 -0
  91. package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
  92. package/types/core/base-builder/guards.d.ts +58 -0
  93. package/types/core/base-builder/id/generator.d.ts +69 -0
  94. package/types/core/base-builder/id/registry.d.ts +93 -0
  95. package/types/core/base-builder/index.d.ts +9 -0
  96. package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
  97. package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
  98. package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
  99. package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
  100. package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
  101. package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
  102. package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
  103. package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
  104. package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
  105. package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
  106. package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
  107. package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
  108. package/types/core/base-builder/storage/value-storage.d.ts +82 -0
  109. package/types/core/base-builder/types.d.ts +183 -0
  110. package/types/core/base-builder/utils.d.ts +2 -0
  111. package/types/core/flow/index.d.ts +23 -0
  112. package/types/core/index.d.ts +8 -0
  113. package/types/core/mocks/index.d.ts +2 -0
  114. package/types/core/mocks/types/action.d.ts +58 -0
  115. package/types/core/mocks/types/choice.d.ts +95 -0
  116. package/types/core/mocks/types/collection.d.ts +102 -0
  117. package/types/core/mocks/types/info.d.ts +7 -0
  118. package/types/core/mocks/types/input.d.ts +7 -0
  119. package/types/core/mocks/types/text.d.ts +5 -0
  120. package/types/core/schema/index.d.ts +34 -0
  121. package/types/core/schema/types.d.ts +5 -0
  122. package/types/core/switch/index.d.ts +21 -0
  123. package/types/core/tagged-template/binding.d.ts +19 -0
  124. package/types/core/tagged-template/expression.d.ts +11 -0
  125. package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
  126. package/types/core/tagged-template/index.d.ts +6 -0
  127. package/types/core/tagged-template/std.d.ts +174 -0
  128. package/types/core/tagged-template/types.d.ts +69 -0
  129. package/types/core/template/index.d.ts +97 -0
  130. package/types/core/utils/index.d.ts +47 -0
  131. package/types/fp/index.d.ts +149 -0
  132. package/types/gen/common.d.ts +6 -0
  133. package/types/index.d.ts +3 -0
  134. package/types/types.d.ts +163 -0
@@ -0,0 +1,95 @@
1
+ import type { BaseBuildContext } from "../../types";
2
+ import type { ValueStorage } from "../../storage/value-storage";
3
+ import { isAssetWrapperValue } from "../../guards";
4
+ import { resolveAndWrapAsset } from "../value-resolver";
5
+
6
+ /**
7
+ * Step 5: Resolves mixed arrays
8
+ *
9
+ * Processes arrays containing both builders and static values.
10
+ * Builders are resolved with proper context, static values pass through.
11
+ *
12
+ * @param storage - The value storage
13
+ * @param result - The result object being built
14
+ * @param nestedParentContext - Context for nested builders
15
+ */
16
+ export function resolveMixedArrays<T, C extends BaseBuildContext>(
17
+ storage: ValueStorage<T>,
18
+ result: Record<string, unknown>,
19
+ nestedParentContext: C | undefined,
20
+ ): void {
21
+ const mixedArrays = storage.getMixedArrays();
22
+
23
+ mixedArrays.forEach((metadata, key) => {
24
+ const { array, builderIndices, objectIndices } = metadata;
25
+
26
+ // Check if this is an AssetWrapper array
27
+ const currentValue = result[key];
28
+ if (isAssetWrapperValue(currentValue)) {
29
+ resolveMixedArrayAsAssetWrapper(
30
+ result,
31
+ key,
32
+ currentValue,
33
+ nestedParentContext,
34
+ );
35
+ } else {
36
+ resolveMixedArrayNormal(
37
+ result,
38
+ key,
39
+ array,
40
+ builderIndices,
41
+ objectIndices,
42
+ nestedParentContext,
43
+ );
44
+ }
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Helper: Resolves mixed array that is wrapped in AssetWrapper
50
+ */
51
+ function resolveMixedArrayAsAssetWrapper<C extends BaseBuildContext>(
52
+ result: Record<string, unknown>,
53
+ key: string,
54
+ wrapperValue: { asset: unknown },
55
+ nestedParentContext: C | undefined,
56
+ ): void {
57
+ const unwrappedArray = wrapperValue.asset;
58
+
59
+ if (Array.isArray(unwrappedArray)) {
60
+ // Filter out null/undefined values before processing
61
+ result[key] = unwrappedArray
62
+ .filter((item) => item !== null && item !== undefined)
63
+ .map((item, index) => {
64
+ const slotName = `${key}-${index}`;
65
+ return resolveAndWrapAsset(item, {
66
+ context: nestedParentContext,
67
+ slotName,
68
+ });
69
+ });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Helper: Resolves mixed array normally (not wrapped)
75
+ * Uses resolveAndWrapAsset to properly wrap Asset builders in { asset: ... } format
76
+ */
77
+ function resolveMixedArrayNormal<C extends BaseBuildContext>(
78
+ result: Record<string, unknown>,
79
+ key: string,
80
+ array: readonly unknown[],
81
+ builderIndices: ReadonlySet<number>,
82
+ objectIndices: ReadonlySet<number>,
83
+ nestedParentContext: C | undefined,
84
+ ): void {
85
+ // Filter out null/undefined values and resolve each item with proper wrapping
86
+ result[key] = array
87
+ .filter((item) => item !== null && item !== undefined)
88
+ .map((item, index) => {
89
+ const slotName = `${key}-${index}`;
90
+ return resolveAndWrapAsset(item, {
91
+ context: nestedParentContext,
92
+ slotName,
93
+ });
94
+ });
95
+ }
@@ -0,0 +1,124 @@
1
+ import type { BaseBuildContext } from "../../types";
2
+ import { resolveAndWrapAsset, resolveValue } from "../value-resolver";
3
+ import {
4
+ isAsset,
5
+ isAssetWrapper,
6
+ isFluentBuilder,
7
+ isPlainObject,
8
+ } from "../../guards";
9
+
10
+ /**
11
+ * Step 7: Resolves nested AssetWrapper paths
12
+ *
13
+ * This step handles cases where AssetWrapper properties are nested within
14
+ * interface types. It traverses paths like ["header", "left"] to find and
15
+ * wrap assets that should be in AssetWrapper format.
16
+ *
17
+ * @param result - The result object being built
18
+ * @param context - Context for nested assets
19
+ * @param assetWrapperPaths - Array of paths to AssetWrapper properties
20
+ */
21
+ export function resolveNestedAssetWrappers<C extends BaseBuildContext>(
22
+ result: Record<string, unknown>,
23
+ context: C | undefined,
24
+ assetWrapperPaths: ReadonlyArray<ReadonlyArray<string>>,
25
+ ): void {
26
+ if (assetWrapperPaths.length === 0) {
27
+ return;
28
+ }
29
+
30
+ for (const path of assetWrapperPaths) {
31
+ // Skip empty paths (defensive guard against malformed metadata)
32
+ if (path.length === 0) {
33
+ continue;
34
+ }
35
+
36
+ // Paths with length 1 are handled by direct AssetWrapper resolution (Step 4)
37
+ if (path.length < 2) {
38
+ continue;
39
+ }
40
+
41
+ resolvePathInResult(result, path, context);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Resolves a specific path in the result object.
47
+ * Handles intermediate FluentBuilders by resolving them before continuing traversal.
48
+ */
49
+ function resolvePathInResult<C extends BaseBuildContext>(
50
+ result: Record<string, unknown>,
51
+ path: ReadonlyArray<string>,
52
+ context: C | undefined,
53
+ ): void {
54
+ // Navigate to the parent object containing the AssetWrapper property
55
+ let current: unknown = result;
56
+
57
+ for (let i = 0; i < path.length - 1; i++) {
58
+ const key = path[i];
59
+
60
+ if (!isPlainObject(current)) {
61
+ return;
62
+ }
63
+
64
+ let next = (current as Record<string, unknown>)[key];
65
+
66
+ if (next === undefined || next === null) {
67
+ return;
68
+ }
69
+
70
+ // If intermediate value is a builder, resolve it first
71
+ if (isFluentBuilder(next)) {
72
+ next = resolveValue(next, { context, propertyName: key });
73
+ (current as Record<string, unknown>)[key] = next;
74
+ }
75
+
76
+ current = next;
77
+ }
78
+
79
+ // Now `current` is the parent object, and we need to wrap the final property
80
+ const finalKey = path[path.length - 1];
81
+
82
+ if (!isPlainObject(current)) {
83
+ return;
84
+ }
85
+
86
+ const parent = current as Record<string, unknown>;
87
+ const value = parent[finalKey];
88
+
89
+ if (value === undefined || value === null) {
90
+ return;
91
+ }
92
+
93
+ // If it's already an AssetWrapper, skip
94
+ if (isAssetWrapper(value)) {
95
+ return;
96
+ }
97
+
98
+ // Generate slot name from the full path
99
+ const slotName = path.join("-");
100
+
101
+ // Handle arrays of AssetWrappers
102
+ if (Array.isArray(value)) {
103
+ parent[finalKey] = value
104
+ .filter((item) => item !== null && item !== undefined)
105
+ .map((item, index) => {
106
+ if (isAssetWrapper(item)) {
107
+ return item;
108
+ }
109
+ return resolveAndWrapAsset(item, {
110
+ context,
111
+ slotName: `${slotName}-${index}`,
112
+ });
113
+ });
114
+ return;
115
+ }
116
+
117
+ // Handle single value that needs wrapping
118
+ if (isFluentBuilder(value) || isAsset(value)) {
119
+ parent[finalKey] = resolveAndWrapAsset(value, {
120
+ context,
121
+ slotName,
122
+ });
123
+ }
124
+ }
@@ -0,0 +1,35 @@
1
+ import type { BaseBuildContext } from "../../types";
2
+ import type { ValueStorage } from "../../storage/value-storage";
3
+ import { isAssetWrapperValue } from "../../guards";
4
+ import { extractValue, resolveValue } from "../value-resolver";
5
+
6
+ /**
7
+ * Step 1: Resolves static values
8
+ *
9
+ * Converts TaggedTemplateValue to strings and resolves nested FluentBuilders
10
+ * in static storage. AssetWrapper values are preserved for later processing.
11
+ *
12
+ * @param storage - The value storage containing static values
13
+ * @param result - The result object being built
14
+ * @param context - Optional build context
15
+ */
16
+ export function resolveStaticValues<T, C extends BaseBuildContext>(
17
+ storage: ValueStorage<T>,
18
+ result: Record<string, unknown>,
19
+ context: C | undefined,
20
+ ): void {
21
+ const values = storage.getValues();
22
+
23
+ for (const [key, value] of Object.entries(values)) {
24
+ if (!isAssetWrapperValue(value)) {
25
+ // First extract any TaggedTemplateValue instances (recursively)
26
+ // Pass the property key to handle special cases like 'binding'
27
+ const extracted = extractValue(value, { propertyKey: key });
28
+ // Then resolve FluentBuilders and nested contexts
29
+ result[key] = resolveValue(extracted, { context, propertyName: key });
30
+ } else {
31
+ // Keep AssetWrapper values for later processing
32
+ result[key] = value;
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ type BaseBuildContext,
3
+ type SwitchMetadata,
4
+ StorageKeys,
5
+ } from "../../types";
6
+ import type { AuxiliaryStorage } from "../../storage/auxiliary-storage";
7
+ import { isSwitchResult } from "../../guards";
8
+ import { setValueAtPath } from "../path-resolver";
9
+
10
+ /**
11
+ * Step 7: Resolves switches
12
+ *
13
+ * Processes switch expressions and injects them at the specified paths.
14
+ * Handles both static and dynamic switches with proper ID generation.
15
+ *
16
+ * @param auxiliaryStorage - Storage containing switch metadata
17
+ * @param result - The result object being built
18
+ * @param nestedParentContext - Context for switch resolution
19
+ * @param arrayProperties - Set of property names that are array types
20
+ */
21
+ export function resolveSwitches<C extends BaseBuildContext>(
22
+ auxiliaryStorage: AuxiliaryStorage,
23
+ result: Record<string, unknown>,
24
+ nestedParentContext: C | undefined,
25
+ arrayProperties: ReadonlySet<string>,
26
+ ): void {
27
+ const switches = auxiliaryStorage.getArray<SwitchMetadata<C>>(
28
+ StorageKeys.SWITCHES,
29
+ );
30
+ if (switches.length === 0 || !nestedParentContext) {
31
+ return;
32
+ }
33
+
34
+ let globalCaseIndex = 0;
35
+ switches.forEach(({ path, switchFn }) => {
36
+ // Create a context that includes the property/slot name for proper ID generation
37
+ // For path ["label"], this creates: parent-label-staticSwitch-0-text
38
+ // We construct the parent ID directly without registering it (no genId call)
39
+ const propertyName = String(path[0]);
40
+ const switchParentId = nestedParentContext.parentId
41
+ ? `${nestedParentContext.parentId}-${propertyName}`
42
+ : propertyName;
43
+
44
+ const switchContext = {
45
+ ...nestedParentContext,
46
+ parentId: switchParentId,
47
+ branch: undefined,
48
+ } as C;
49
+
50
+ let switchResult = switchFn(switchContext, globalCaseIndex);
51
+
52
+ // Count cases for next switch's offset
53
+ if (isSwitchResult(switchResult)) {
54
+ const switchCases =
55
+ switchResult.staticSwitch ?? switchResult.dynamicSwitch;
56
+ if (Array.isArray(switchCases)) {
57
+ globalCaseIndex += switchCases.length;
58
+ }
59
+ }
60
+
61
+ // If this property is an array type (e.g., actions: Array<AssetWrapper<T>>),
62
+ // wrap the switch result in an array to match the expected schema type.
63
+ // Only wrap if we're replacing the entire property (path.length === 1),
64
+ // not a specific element in the array (path.length > 1)
65
+ if (arrayProperties.has(propertyName) && path.length === 1) {
66
+ switchResult = [switchResult];
67
+ }
68
+
69
+ setValueAtPath(result, path, switchResult);
70
+ });
71
+ }
@@ -0,0 +1,40 @@
1
+ import { type BaseBuildContext, StorageKeys } from "../../types";
2
+ import type { AuxiliaryStorage } from "../../storage/auxiliary-storage";
3
+ import type { template as easyDslTemplate } from "../../../index";
4
+
5
+ /**
6
+ * Step 8: Resolves templates
7
+ *
8
+ * Processes template functions and adds them to the result.
9
+ * Templates are special constructs that generate dynamic content.
10
+ *
11
+ * @param auxiliaryStorage - Storage containing template functions
12
+ * @param result - The result object being built
13
+ * @param context - Build context for template resolution
14
+ */
15
+ export function resolveTemplates<C extends BaseBuildContext>(
16
+ auxiliaryStorage: AuxiliaryStorage,
17
+ result: Record<string, unknown>,
18
+ context: C | undefined,
19
+ ): void {
20
+ const templateFns = auxiliaryStorage.getArray<
21
+ ReturnType<typeof easyDslTemplate>
22
+ >(StorageKeys.TEMPLATES);
23
+
24
+ if (templateFns.length === 0) {
25
+ return;
26
+ }
27
+
28
+ if (!context) {
29
+ if (process.env.NODE_ENV !== "production") {
30
+ console.warn(
31
+ `resolveTemplates: ${templateFns.length} template(s) exist but no context provided. ` +
32
+ "Templates will be ignored. Pass a build context to .build() to enable template resolution.",
33
+ );
34
+ }
35
+ return;
36
+ }
37
+
38
+ const templates = templateFns.map((fn) => fn(context));
39
+ result.template = templates;
40
+ }
@@ -0,0 +1,333 @@
1
+ import type { Asset, AssetWrapper } from "@player-ui/types";
2
+ import { BranchTypes, type BaseBuildContext } from "../types";
3
+ import {
4
+ isFluentBuilder,
5
+ isPlainObject,
6
+ isAsset,
7
+ isAssetWrapper,
8
+ } from "../guards";
9
+ import { createNestedContext } from "../context";
10
+ import { genId, peekId } from "../id/generator";
11
+ import { isTaggedTemplateValue } from "../../tagged-template";
12
+
13
+ /**
14
+ * Type-safe value extraction utilities for handling TaggedTemplateValue
15
+ * These functions properly unwrap TaggedTemplateValue while preserving structure
16
+ */
17
+
18
+ interface ExtractValueOptions {
19
+ readonly propertyKey?: string;
20
+ readonly visited?: WeakSet<object>;
21
+ }
22
+
23
+ /**
24
+ * Recursively extracts values from an object, handling nested TaggedTemplateValue instances
25
+ */
26
+ function extractObject<T extends Record<string, unknown>>(
27
+ value: T,
28
+ options: ExtractValueOptions,
29
+ ): T {
30
+ const visited = options.visited ?? new WeakSet();
31
+
32
+ if (visited.has(value)) {
33
+ return value;
34
+ }
35
+ visited.add(value);
36
+
37
+ const result: Record<string, unknown> = {};
38
+
39
+ for (const [key, val] of Object.entries(value)) {
40
+ result[key] = extractValue(val, { propertyKey: key, visited });
41
+ }
42
+
43
+ return result as T;
44
+ }
45
+
46
+ /**
47
+ * Recursively extracts values from a structure, converting TaggedTemplateValue instances
48
+ * to their string representations while preserving arrays and objects.
49
+ *
50
+ * **Return Type Note**: This function returns `unknown` because the input type is unknown
51
+ * and the transformation can change types (TaggedTemplateValue -> string). Callers should
52
+ * validate the returned value using type guards before using it.
53
+ *
54
+ * **Transformation Behavior**:
55
+ * - `TaggedTemplateValue` → `string` (via toString() or toValue())
56
+ * - Arrays → Arrays (elements recursively transformed)
57
+ * - Plain objects → Objects (properties recursively transformed)
58
+ * - FluentBuilder/AssetWrapper → passed through unchanged (resolved later)
59
+ * - Primitives → passed through unchanged
60
+ *
61
+ * @param value - The value to extract (may contain TaggedTemplateValue instances)
62
+ * @param options - Extraction options (propertyKey for special handling, visited for cycle detection)
63
+ * @returns The extracted value with TaggedTemplateValue instances converted to strings
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const result = extractValue({ name: taggedTemplate`Hello` });
68
+ * // result is { name: "Hello" } but typed as unknown
69
+ *
70
+ * // Caller must validate:
71
+ * if (isPlainObject(result)) {
72
+ * const name = result.name; // Use safely
73
+ * }
74
+ * ```
75
+ */
76
+ export function extractValue(
77
+ value: unknown,
78
+ options: ExtractValueOptions = {},
79
+ ): unknown {
80
+ const { propertyKey, visited = new WeakSet() } = options;
81
+
82
+ // Handle TaggedTemplateValue
83
+ if (isTaggedTemplateValue(value)) {
84
+ // Special case: 'data' property (in templates) and 'binding' property should use toValue() to get raw string
85
+ if (propertyKey === "data" || propertyKey === "binding") {
86
+ return value.toValue();
87
+ }
88
+ // Default: use toString() to preserve expression/binding syntax (@[]@ and {{}})
89
+ return value.toString();
90
+ }
91
+
92
+ // Handle arrays - recursively extract each element
93
+ if (Array.isArray(value)) {
94
+ if (visited.has(value)) {
95
+ return value;
96
+ }
97
+ visited.add(value);
98
+ // Don't pass propertyKey down to array elements
99
+ return value.map((item) => extractValue(item, { visited }));
100
+ }
101
+
102
+ // Handle plain objects - recursively extract each property
103
+ // But skip FluentBuilders and AssetWrappers (they'll be resolved later)
104
+ if (
105
+ isPlainObject(value) &&
106
+ !isFluentBuilder(value) &&
107
+ !isAssetWrapper(value)
108
+ ) {
109
+ return extractObject(value as Record<string, unknown>, { visited });
110
+ }
111
+
112
+ // Return primitives and special objects as-is
113
+ return value;
114
+ }
115
+
116
+ /**
117
+ * Creates an AssetWrapper for a nested asset
118
+ */
119
+ function wrapAsset<T extends Asset, C extends BaseBuildContext>(
120
+ asset: T,
121
+ context: C | undefined,
122
+ slotName: string,
123
+ ): AssetWrapper<T> {
124
+ if (!context) {
125
+ return { asset };
126
+ }
127
+
128
+ // If asset already has an ID, preserve it
129
+ // User-provided IDs should be respected
130
+ if (asset.id) {
131
+ return { asset: { ...asset } };
132
+ }
133
+
134
+ // Generate ID using slot branch for assets without IDs
135
+ const parentId = peekId(context);
136
+ const slotCtx: BaseBuildContext = {
137
+ parentId,
138
+ branch: { type: BranchTypes.SLOT, name: slotName },
139
+ };
140
+
141
+ return {
142
+ asset: {
143
+ ...asset,
144
+ id: genId(slotCtx),
145
+ },
146
+ };
147
+ }
148
+
149
+ interface ResolveValueOptions<C extends BaseBuildContext = BaseBuildContext> {
150
+ readonly context?: C;
151
+ readonly propertyName?: string;
152
+ readonly visited?: WeakSet<object>;
153
+ }
154
+
155
+ /**
156
+ * Resolves a value, handling FluentBuilders, AssetWrappers, and nested structures
157
+ */
158
+ export function resolveValue<T, C extends BaseBuildContext>(
159
+ value: unknown,
160
+ options: ResolveValueOptions<C> = {},
161
+ ): unknown {
162
+ const { context, propertyName, visited = new WeakSet() } = options;
163
+
164
+ // Handle TaggedTemplateValue
165
+ if (isTaggedTemplateValue(value)) {
166
+ // Special case: 'data' property (in templates) and 'binding' property should use toValue() to get raw string
167
+ if (propertyName === "data" || propertyName === "binding") {
168
+ return value.toValue();
169
+ }
170
+ // Default: use toString() to preserve expression/binding syntax (@[]@ and {{}})
171
+ return value.toString();
172
+ }
173
+
174
+ // Skip null or undefined values
175
+ if (value === null || value === undefined) {
176
+ return value;
177
+ }
178
+
179
+ // Handle FluentBuilder instances
180
+ if (isFluentBuilder<T, C>(value)) {
181
+ return value.build(context);
182
+ }
183
+
184
+ // Handle AssetWrapper types - unwrap, resolve inner asset, and re-wrap
185
+ if (isAssetWrapper(value)) {
186
+ if (visited.has(value)) return value;
187
+ visited.add(value);
188
+
189
+ const innerAsset = value.asset;
190
+
191
+ // Resolve the inner asset if it's a builder or contains nested structures
192
+ const resolvedAsset = resolveValue(innerAsset, {
193
+ context,
194
+ propertyName,
195
+ visited,
196
+ });
197
+
198
+ // Return wrapped with the resolved asset
199
+ return { asset: resolvedAsset };
200
+ }
201
+
202
+ // Handle arrays - recursively resolve each element
203
+ if (Array.isArray(value)) {
204
+ if (visited.has(value)) return value;
205
+ visited.add(value);
206
+
207
+ // Filter out null/undefined values before processing
208
+ return value
209
+ .filter((item) => item !== null && item !== undefined)
210
+ .map((item, index) => {
211
+ const arrayContext = context
212
+ ? createNestedContext({
213
+ parentContext: context,
214
+ parameterName: propertyName || "array",
215
+ index,
216
+ })
217
+ : undefined;
218
+ return resolveValue(item, {
219
+ context: arrayContext,
220
+ propertyName: String(index),
221
+ visited,
222
+ });
223
+ });
224
+ }
225
+
226
+ // Handle plain objects (but not Assets - they should be terminal)
227
+ if (isPlainObject(value) && !isAsset(value)) {
228
+ if (visited.has(value)) return value;
229
+ visited.add(value);
230
+
231
+ const resolved: Record<string, unknown> = {};
232
+ for (const [key, val] of Object.entries(value)) {
233
+ const nestedContext = context
234
+ ? createNestedContext({ parentContext: context, parameterName: key })
235
+ : undefined;
236
+ resolved[key] = resolveValue(val, {
237
+ context: nestedContext,
238
+ propertyName: key,
239
+ visited,
240
+ });
241
+ }
242
+
243
+ return resolved;
244
+ }
245
+
246
+ return value;
247
+ }
248
+
249
+ interface ResolveAndWrapAssetOptions<
250
+ C extends BaseBuildContext = BaseBuildContext,
251
+ > {
252
+ readonly context?: C;
253
+ readonly slotName: string;
254
+ readonly visited?: WeakSet<object>;
255
+ }
256
+
257
+ /**
258
+ * Resolves and wraps a nested asset value
259
+ * This is used when we know a property should contain an AssetWrapper
260
+ */
261
+ export function resolveAndWrapAsset<C extends BaseBuildContext>(
262
+ value: unknown,
263
+ options: ResolveAndWrapAssetOptions<C>,
264
+ ): AssetWrapper<Asset> | unknown {
265
+ const { context, slotName, visited = new WeakSet() } = options;
266
+
267
+ // Skip null or undefined values
268
+ if (value === null || value === undefined) {
269
+ return value;
270
+ }
271
+
272
+ // If it's already an AssetWrapper, just resolve its contents
273
+ if (isAssetWrapper(value)) {
274
+ if (visited.has(value)) return value;
275
+ visited.add(value);
276
+
277
+ const resolvedAsset = resolveValue(value.asset, {
278
+ context,
279
+ propertyName: slotName,
280
+ visited,
281
+ });
282
+ return { asset: resolvedAsset };
283
+ }
284
+
285
+ // If it's a FluentBuilder, we need to call it with a context that has a slot branch
286
+ // This ensures the built asset gets the correct ID based on the slot name
287
+ if (isFluentBuilder(value)) {
288
+ if (!context) {
289
+ // Without context, just build and wrap
290
+ const built = value.build(undefined);
291
+ if (isAssetWrapper(built)) {
292
+ return built;
293
+ }
294
+ // Only wrap if it's an Asset (has type field)
295
+ if (isAsset(built)) {
296
+ return { asset: built };
297
+ }
298
+ return built;
299
+ }
300
+
301
+ // Create a context with a slot branch for the builder.
302
+ // We use peekId to get the parent's ID without registering it, then create
303
+ // a slot branch context so the builder generates an ID like "parent-slotName".
304
+ const parentId = peekId(context);
305
+ const slotContext: C = {
306
+ ...context,
307
+ parentId,
308
+ branch: { type: BranchTypes.SLOT, name: slotName },
309
+ } as C;
310
+
311
+ const built = value.build(slotContext);
312
+
313
+ // If the builder produces an AssetWrapper, return it
314
+ if (isAssetWrapper(built)) {
315
+ return built;
316
+ }
317
+
318
+ // Only wrap if it's an Asset (has type field)
319
+ if (isAsset(built)) {
320
+ return { asset: built };
321
+ }
322
+
323
+ // Otherwise return as-is (for non-Asset objects like ChoiceItem)
324
+ return built;
325
+ }
326
+
327
+ // If it's an Asset, wrap it
328
+ if (isAsset(value)) {
329
+ return wrapAsset(value, context, slotName);
330
+ }
331
+
332
+ return resolveValue(value, { context, propertyName: slotName, visited });
333
+ }