@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,137 @@
1
+ import type { Asset, AssetWrapper } from "@player-ui/types";
2
+ import type { BaseBuildContext, FluentBuilder } from "./types";
3
+ import { FLUENT_BUILDER_SYMBOL } from "./types";
4
+
5
+ /**
6
+ * Type guard to check if a value is a FluentBuilder instance
7
+ * Checks for the builder symbol and build method
8
+ */
9
+ export function isFluentBuilder<
10
+ T = unknown,
11
+ C extends BaseBuildContext = BaseBuildContext,
12
+ >(value: unknown): value is FluentBuilder<T, C> {
13
+ if (value === null || typeof value !== "object") {
14
+ return false;
15
+ }
16
+
17
+ if (!(FLUENT_BUILDER_SYMBOL in value)) {
18
+ return false;
19
+ }
20
+
21
+ const obj = value as Record<symbol | string, unknown>;
22
+
23
+ return obj[FLUENT_BUILDER_SYMBOL] === true && typeof obj.build === "function";
24
+ }
25
+
26
+ /**
27
+ * Type guard to check if a value is an array of FluentBuilders
28
+ */
29
+ export function isBuilderArray<
30
+ T = unknown,
31
+ C extends BaseBuildContext = BaseBuildContext,
32
+ >(value: unknown): value is Array<FluentBuilder<T, C>> {
33
+ return Array.isArray(value) && value.every(isFluentBuilder);
34
+ }
35
+
36
+ /**
37
+ * Type guard to check if a value is a plain object (not a class instance)
38
+ * Returns true for objects created with {} or Object.create(null)
39
+ */
40
+ export function isPlainObject(
41
+ value: unknown,
42
+ ): value is Record<string, unknown> {
43
+ if (!value || typeof value !== "object") return false;
44
+ const proto = Object.getPrototypeOf(value);
45
+ return proto === Object.prototype || proto === null;
46
+ }
47
+
48
+ /**
49
+ * Type guard to check if a value is an Asset (has required 'type' property)
50
+ * Assets are the core building blocks in Player UI
51
+ */
52
+ export function isAsset(value: unknown): value is Asset {
53
+ if (!value || typeof value !== "object") return false;
54
+ const obj = value as Record<string, unknown>;
55
+ return "type" in obj && typeof obj.type === "string";
56
+ }
57
+
58
+ /**
59
+ * Type guard to check if a value is an AssetWrapper
60
+ * AssetWrapper has a single 'asset' property containing the wrapped value
61
+ */
62
+ export function isAssetWrapper<T extends Asset = Asset>(
63
+ value: unknown,
64
+ ): value is AssetWrapper<T> {
65
+ if (!value || typeof value !== "object") return false;
66
+ const obj = value as Record<string, unknown>;
67
+ return "asset" in obj && typeof obj.asset === "object" && obj.asset !== null;
68
+ }
69
+
70
+ /**
71
+ * Type guard to check if a value is an AssetWrapper containing an Asset
72
+ * More strict than isAssetWrapper - verifies the wrapped value is actually an Asset
73
+ */
74
+ export function isAssetWrapperWithAsset<T extends Asset = Asset>(
75
+ value: unknown,
76
+ ): value is AssetWrapper<T> {
77
+ return isAssetWrapper(value) && isAsset(value.asset);
78
+ }
79
+
80
+ /**
81
+ * Type guard to check if a value is an AssetWrapper value object
82
+ * This is the internal representation: { asset: unknown }
83
+ * Used to detect values that need special handling during build
84
+ */
85
+ export function isAssetWrapperValue(
86
+ value: unknown,
87
+ ): value is { asset: unknown } {
88
+ return (
89
+ typeof value === "object" &&
90
+ value !== null &&
91
+ "asset" in value &&
92
+ Object.keys(value).length === 1
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Determines if a property value needs to be wrapped in AssetWrapper format
98
+ * Used during value storage to decide wrapping strategy
99
+ */
100
+ export function needsAssetWrapper(value: unknown): boolean {
101
+ // Don't wrap if already an AssetWrapper
102
+ if (isAssetWrapper(value)) return false;
103
+
104
+ // Don't wrap if it's a FluentBuilder (will be resolved later)
105
+ if (isFluentBuilder(value)) return false;
106
+
107
+ // Wrap if it's an Asset
108
+ return isAsset(value);
109
+ }
110
+
111
+ /**
112
+ * Type guard to check if a value is a switch result object
113
+ * Switch results contain staticSwitch or dynamicSwitch arrays
114
+ */
115
+ export function isSwitchResult(value: unknown): value is Record<
116
+ string,
117
+ unknown
118
+ > & {
119
+ staticSwitch?: unknown[];
120
+ dynamicSwitch?: unknown[];
121
+ } {
122
+ if (typeof value !== "object" || value === null) {
123
+ return false;
124
+ }
125
+ const obj = value as Record<string, unknown>;
126
+ return "staticSwitch" in obj || "dynamicSwitch" in obj;
127
+ }
128
+
129
+ /**
130
+ * Type guard to check if a value is a string or undefined
131
+ * Useful for validating optional string properties
132
+ */
133
+ export function isStringOrUndefined(
134
+ value: unknown,
135
+ ): value is string | undefined {
136
+ return value === undefined || typeof value === "string";
137
+ }
@@ -0,0 +1,290 @@
1
+ import { globalIdRegistry } from "./registry";
2
+ import {
3
+ BranchTypes,
4
+ type AssetMetadata,
5
+ type BaseBuildContext,
6
+ } from "../types";
7
+
8
+ export { globalIdRegistry, createIdRegistry, IDRegistry } from "./registry";
9
+
10
+ /**
11
+ * Resets the global ID registry, clearing all registered IDs.
12
+ * This is useful for testing scenarios where you need a clean slate.
13
+ */
14
+ export const resetGlobalIdSet = (): void => {
15
+ globalIdRegistry.reset();
16
+ };
17
+
18
+ /**
19
+ * Internal function that generates a base ID from context without registry operations.
20
+ * This is the core ID generation logic shared between peekId and genId.
21
+ *
22
+ * @param context - The context containing parent ID and optional branch information
23
+ * @param functionName - The name of the calling function (for error messages)
24
+ * @returns The generated base ID
25
+ */
26
+ const _generateBaseId = (
27
+ context: BaseBuildContext,
28
+ functionName: string,
29
+ ): string => {
30
+ // Validate context
31
+ if (!context) {
32
+ throw new Error(
33
+ `${functionName}: Context is undefined. Please provide a valid BaseBuildContext object.`,
34
+ );
35
+ }
36
+
37
+ const { parentId, branch } = context;
38
+
39
+ let baseId: string;
40
+
41
+ if (!branch) {
42
+ baseId = parentId || "";
43
+ } else {
44
+ switch (branch.type) {
45
+ case "custom":
46
+ baseId = parentId || "";
47
+ break;
48
+
49
+ case "slot":
50
+ if (!branch.name) {
51
+ throw new Error(
52
+ `${functionName}: Slot branch requires a 'name' property. ` +
53
+ `Context: ${JSON.stringify(context)}`,
54
+ );
55
+ }
56
+ baseId = `${parentId ? `${parentId}-` : ""}${branch.name}`;
57
+ break;
58
+
59
+ case "array-item":
60
+ if (typeof branch.index !== "number") {
61
+ throw new Error(
62
+ `${functionName}: Array-item branch requires a numeric 'index' property. ` +
63
+ `Got: ${typeof branch.index}. Context: ${JSON.stringify(context)}`,
64
+ );
65
+ }
66
+ if (branch.index < 0) {
67
+ throw new Error(
68
+ `${functionName}: Array-item index must be non-negative. ` +
69
+ `Got: ${branch.index}. Context: ${JSON.stringify(context)}`,
70
+ );
71
+ }
72
+ baseId = `${parentId}-${branch.index}`;
73
+ break;
74
+
75
+ case "template":
76
+ if (branch.depth !== undefined && branch.depth < 0) {
77
+ throw new Error(
78
+ `${functionName}: Template depth must be non-negative. ` +
79
+ `Got: ${branch.depth}. Context: ${JSON.stringify(context)}`,
80
+ );
81
+ }
82
+ baseId = `${parentId}-_index${branch.depth || ""}_`;
83
+ break;
84
+
85
+ case "switch":
86
+ if (typeof branch.index !== "number") {
87
+ throw new Error(
88
+ `${functionName}: Switch branch requires a numeric 'index' property. ` +
89
+ `Got: ${typeof branch.index}. Context: ${JSON.stringify(context)}`,
90
+ );
91
+ }
92
+ if (branch.index < 0) {
93
+ throw new Error(
94
+ `${functionName}: Switch index must be non-negative. ` +
95
+ `Got: ${branch.index}. Context: ${JSON.stringify(context)}`,
96
+ );
97
+ }
98
+ if (!branch.kind || !["static", "dynamic"].includes(branch.kind)) {
99
+ throw new Error(
100
+ `${functionName}: Switch branch requires 'kind' to be 'static' or 'dynamic'. ` +
101
+ `Got: ${branch.kind}. Context: ${JSON.stringify(context)}`,
102
+ );
103
+ }
104
+ baseId = `${parentId}-${branch.kind}Switch-${branch.index}`;
105
+ break;
106
+
107
+ default: {
108
+ const exhaustiveCheck: never = branch;
109
+ throw new Error(
110
+ `${functionName}: Unhandled branch type: ${JSON.stringify(exhaustiveCheck)}`,
111
+ );
112
+ }
113
+ }
114
+ }
115
+
116
+ return baseId;
117
+ };
118
+
119
+ /**
120
+ * Generates an ID without registering it in the global registry.
121
+ * This is useful for intermediate ID lookups where you don't want to consume the ID.
122
+ *
123
+ * @param context - The context containing parent ID and optional branch information
124
+ * @returns The generated ID (without collision detection or registration)
125
+ *
126
+ * @example
127
+ * // Use this when you need to generate a parent ID for nested context creation
128
+ * const parentId = peekId({ parentId: 'collection' }); // Doesn't register 'collection'
129
+ * const nestedCtx = { parentId, branch: { type: 'slot', name: 'label' } };
130
+ */
131
+ export const peekId = (context: BaseBuildContext): string => {
132
+ return _generateBaseId(context, "peekId");
133
+ };
134
+
135
+ /**
136
+ * Generates a unique identifier based on the parent ID and branch information.
137
+ *
138
+ * This function creates hierarchical IDs for various element types in a DSL structure,
139
+ * maintaining relationships between parent and child elements through consistent naming patterns.
140
+ * IDs are automatically checked for uniqueness and modified if collisions are detected.
141
+ *
142
+ * @param {BaseBuildContext} context - The context containing parent ID and optional branch information
143
+ * @param {string} context.parentId - The ID of the parent element
144
+ * @param {IdBranch} [context.branch] - Optional branch information to specify the type of child element
145
+ *
146
+ * @returns {string} A generated ID string representing the element's unique identifier
147
+ *
148
+ * @throws {Error} If context is invalid or incomplete
149
+ *
150
+ * @example
151
+ * // Slot branch
152
+ * genId({ parentId: 'parent', branch: { type: 'slot', name: 'header' } })
153
+ * // Returns: 'parent-header'
154
+ *
155
+ * @example
156
+ * // Array item branch
157
+ * genId({ parentId: 'list', branch: { type: 'array-item', index: 2 } })
158
+ * // Returns: 'list-2'
159
+ *
160
+ * @example
161
+ * // Template branch
162
+ * genId({ parentId: 'template', branch: { type: 'template', depth: 1 } })
163
+ * // Returns: 'template-_index1_'
164
+ *
165
+ * @example
166
+ * // Switch branch
167
+ * genId({ parentId: 'condition', branch: { type: 'switch', index: 0, kind: 'static' } })
168
+ * // Returns: 'condition-staticSwitch-0'
169
+ *
170
+ * @example
171
+ * // Custom ID case (no branch)
172
+ * genId({ parentId: 'custom-id' })
173
+ * // Returns: 'custom-id'
174
+ */
175
+ export const genId = (context: BaseBuildContext): string => {
176
+ const baseId = _generateBaseId(context, "genId");
177
+
178
+ const { parentId, branch } = context;
179
+
180
+ if (process.env.NODE_ENV !== "production" && !parentId && !branch) {
181
+ console.warn(
182
+ "genId: Context appears incomplete (no parentId or branch). " +
183
+ "This may result in an empty or invalid ID. " +
184
+ "Consider using context helper functions from 'fluent/utils/context'.",
185
+ );
186
+ }
187
+
188
+ if (process.env.NODE_ENV !== "production" && parentId === "") {
189
+ console.warn(
190
+ "genId: parentId is an empty string. " +
191
+ "This may indicate a missing or improperly initialized context.",
192
+ );
193
+ }
194
+
195
+ // Ensure the generated ID is unique
196
+ const uniqueId = globalIdRegistry.ensureUnique(baseId);
197
+
198
+ // Warn if collision was detected
199
+ if (process.env.NODE_ENV !== "production" && uniqueId !== baseId) {
200
+ console.warn(
201
+ `genId: ID collision detected. Original: "${baseId}", Modified to: "${uniqueId}". ` +
202
+ `Consider providing more specific IDs to avoid collisions.`,
203
+ );
204
+ }
205
+
206
+ return uniqueId;
207
+ };
208
+
209
+ export function determineSlotName(
210
+ parameterName: string,
211
+ assetMetadata?: AssetMetadata,
212
+ ): string {
213
+ if (!assetMetadata) {
214
+ return parameterName;
215
+ }
216
+
217
+ const { type, binding, value } = assetMetadata;
218
+
219
+ // Rule 1: If the asset type is `action`, append last segment of `value` if any
220
+ // Note: value can be a binding (TaggedTemplateValue) or plain string
221
+ if (type === "action" && value) {
222
+ const cleanValue = value.replace(/^\{\{|\}\}$/g, "");
223
+ const segments = cleanValue.split(".");
224
+ const lastSegment = segments[segments.length - 1];
225
+ return lastSegment ? `${type}-${lastSegment}` : type;
226
+ }
227
+
228
+ // Rule 2: If it's not `action` but has `binding`, append last fragment of binding
229
+ if (type !== "action" && binding) {
230
+ const cleanBinding = binding.replace(/^\{\{|\}\}$/g, "");
231
+ const segments = cleanBinding.split(".");
232
+ const lastSegment = segments[segments.length - 1];
233
+ if (lastSegment) {
234
+ return `${type || parameterName}-${lastSegment}`;
235
+ }
236
+ }
237
+
238
+ // Rule 3: Otherwise, just use the type
239
+ return type || parameterName;
240
+ }
241
+
242
+ export function generateAssetId<C extends BaseBuildContext>(params: {
243
+ readonly context?: C;
244
+ readonly parameterName?: string;
245
+ readonly assetMetadata?: AssetMetadata;
246
+ readonly explicitId?: string;
247
+ }): string {
248
+ const {
249
+ context,
250
+ parameterName = "asset",
251
+ assetMetadata,
252
+ explicitId,
253
+ } = params;
254
+
255
+ if (explicitId) {
256
+ return explicitId;
257
+ }
258
+
259
+ if (context && "parentId" in context) {
260
+ // Determine the slot name based on asset metadata (action value, binding, or type)
261
+ const slotName = determineSlotName(parameterName, assetMetadata);
262
+
263
+ if (context.branch) {
264
+ // When there's a branch, generate base ID then append asset type
265
+ // This creates IDs like "parent-0-questionAnswer", "parent-slot-text", etc.
266
+ const baseId = genId(context);
267
+
268
+ // Append the asset type as a suffix
269
+ // Use genId again to ensure uniqueness and proper registration
270
+ return genId({
271
+ ...context,
272
+ parentId: baseId,
273
+ branch: { type: BranchTypes.SLOT, name: slotName },
274
+ } as C);
275
+ }
276
+
277
+ if (context.parentId) {
278
+ // When there's a parentId but no branch, use slotName (determined from metadata)
279
+ // This creates IDs like "parent-text", "parent-action-next", "parent-input-firstName", etc.
280
+ // Generate and register the ID to enable collision detection
281
+ return genId({
282
+ ...context,
283
+ branch: { type: BranchTypes.SLOT, name: slotName },
284
+ } as C);
285
+ }
286
+ }
287
+
288
+ const slotName = determineSlotName(parameterName, assetMetadata);
289
+ return slotName;
290
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * ID Registry for tracking and ensuring unique IDs across asset generation.
3
+ * This registry maintains a set of used IDs and provides collision resolution
4
+ * by appending numeric suffixes when duplicates are detected.
5
+ *
6
+ * Special handling for template placeholders:
7
+ * - IDs ending with template placeholders like `_index_`, `_row_` are allowed to be duplicated
8
+ * - IDs with placeholders followed by additional segments enforce uniqueness normally
9
+ */
10
+ export class IDRegistry {
11
+ private usedIds: Set<string>;
12
+ private isEnabled: boolean;
13
+
14
+ constructor(enabled = true) {
15
+ this.usedIds = new Set<string>();
16
+ this.isEnabled = enabled;
17
+ }
18
+
19
+ /**
20
+ * Ensures the given ID is unique, modifying it if necessary.
21
+ * If the ID already exists, appends a numeric suffix (-1, -2, etc.)
22
+ * until a unique ID is found.
23
+ *
24
+ * Special handling for template placeholders:
25
+ * - IDs ending with `_index_`, `_row_`, etc. are allowed as duplicates
26
+ * - IDs with placeholders followed by segments (e.g., `_index_.field`) enforce uniqueness
27
+ *
28
+ * @param baseId - The desired ID
29
+ * @returns A unique ID (either the original or modified with suffix)
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const registry = new IDRegistry();
34
+ * registry.ensureUnique("my-id"); // "my-id"
35
+ * registry.ensureUnique("my-id"); // "my-id-1"
36
+ * registry.ensureUnique("list-_index_"); // "list-_index_" (allowed duplicate)
37
+ * registry.ensureUnique("list-_index_"); // "list-_index_" (allowed duplicate)
38
+ * ```
39
+ */
40
+ ensureUnique(baseId: string): string {
41
+ // If registry is disabled, return the ID as-is
42
+ if (!this.isEnabled) {
43
+ return baseId;
44
+ }
45
+
46
+ // Check if this ID contains template placeholders
47
+ if (this.isTemplatePlaceholderID(baseId)) {
48
+ // For template placeholder IDs, don't enforce uniqueness
49
+ // These will be replaced at runtime, so duplicates are acceptable
50
+ return baseId;
51
+ }
52
+
53
+ // If the ID hasn't been used, register and return it
54
+ if (!this.usedIds.has(baseId)) {
55
+ this.usedIds.add(baseId);
56
+ return baseId;
57
+ }
58
+
59
+ // ID collision detected - append counter until unique
60
+ let counter = 1;
61
+ let uniqueId = `${baseId}-${counter}`;
62
+
63
+ while (this.usedIds.has(uniqueId)) {
64
+ counter++;
65
+ uniqueId = `${baseId}-${counter}`;
66
+ }
67
+
68
+ this.usedIds.add(uniqueId);
69
+ return uniqueId;
70
+ }
71
+
72
+ /**
73
+ * Checks if an ID has already been used.
74
+ *
75
+ * @param id - The ID to check
76
+ * @returns true if the ID has been used, false otherwise
77
+ */
78
+ has(id: string): boolean {
79
+ return this.usedIds.has(id);
80
+ }
81
+
82
+ /**
83
+ * Clears all registered IDs from the registry.
84
+ * Useful for resetting state between test runs or separate flows.
85
+ */
86
+ reset(): void {
87
+ this.usedIds.clear();
88
+ }
89
+
90
+ /**
91
+ * Enables or disables the uniqueness checking.
92
+ * When disabled, all IDs pass through unchanged.
93
+ *
94
+ * @param enabled - Whether to enable uniqueness checking
95
+ */
96
+ setEnabled(enabled: boolean): void {
97
+ this.isEnabled = enabled;
98
+ }
99
+
100
+ /**
101
+ * Returns the number of unique IDs currently registered.
102
+ *
103
+ * @returns The count of registered IDs
104
+ */
105
+ size(): number {
106
+ return this.usedIds.size;
107
+ }
108
+
109
+ /**
110
+ * Returns a snapshot of all registered IDs.
111
+ * Useful for debugging and testing.
112
+ *
113
+ * @returns An array of all registered IDs
114
+ */
115
+ getRegisteredIds(): string[] {
116
+ return Array.from(this.usedIds);
117
+ }
118
+
119
+ /**
120
+ * Checks if an ID contains template placeholders that should be exempt from uniqueness checks.
121
+ * Template placeholders are patterns like `_index_`, `_index1_`, `_row_` that are replaced at runtime.
122
+ *
123
+ * IDs ending with just a placeholder (e.g., "parent-_index_") are allowed as duplicates.
124
+ * IDs with placeholders followed by additional segments (e.g., "parent-_index_-field") are not.
125
+ *
126
+ * @param id - The ID to check
127
+ * @returns true if the ID should be exempt from uniqueness checks
128
+ */
129
+ private isTemplatePlaceholderID(id: string): boolean {
130
+ // Pattern to match template placeholder at the end of an ID
131
+ // Matches: _index_, _index1_, _row_, _item_, etc. at the end of the string
132
+ const templatePlaceholderPattern = /_(?:index|row|item)\d*_$/;
133
+ return templatePlaceholderPattern.test(id);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Global singleton instance of the ID registry.
139
+ * This ensures consistent ID tracking across the entire application.
140
+ */
141
+ export const globalIdRegistry: IDRegistry = new IDRegistry();
142
+
143
+ /**
144
+ * Creates a new isolated ID registry instance.
145
+ * Useful for testing or when you need separate ID tracking contexts.
146
+ *
147
+ * @param enabled - Whether the registry should be enabled by default
148
+ * @returns A new IDRegistry instance
149
+ */
150
+ export function createIdRegistry(enabled = true): IDRegistry {
151
+ return new IDRegistry(enabled);
152
+ }
@@ -0,0 +1,72 @@
1
+ export {
2
+ FLUENT_BUILDER_SYMBOL,
3
+ BranchTypes,
4
+ StorageKeys,
5
+ PropertyKeys,
6
+ type NestedContextParams,
7
+ type NestedContextGenerator,
8
+ type AssetMetadata,
9
+ type BaseBuildContext,
10
+ type FluentBuilder,
11
+ type AnyAssetBuilder,
12
+ type MixedArrayMetadata,
13
+ type TemplateMetadata,
14
+ type IdBranch,
15
+ type SlotBranch,
16
+ type ArrayItemBranch,
17
+ type TemplateBranch,
18
+ type SwitchBranch,
19
+ type CustomBranch,
20
+ type ValuePath,
21
+ type SwitchMetadata,
22
+ type ConditionalValue,
23
+ type FluentPartial,
24
+ type FluentPartialValue,
25
+ } from "./types";
26
+
27
+ export {
28
+ isFluentBuilder,
29
+ isBuilderArray,
30
+ isPlainObject,
31
+ isAsset,
32
+ isAssetWrapper,
33
+ isAssetWrapperWithAsset,
34
+ needsAssetWrapper,
35
+ isAssetWrapperValue,
36
+ isSwitchResult,
37
+ isStringOrUndefined,
38
+ } from "./guards";
39
+
40
+ export {
41
+ determineSlotName,
42
+ generateAssetId,
43
+ genId,
44
+ peekId,
45
+ resetGlobalIdSet,
46
+ globalIdRegistry,
47
+ createIdRegistry,
48
+ IDRegistry,
49
+ } from "./id/generator";
50
+
51
+ export {
52
+ createNestedContext,
53
+ createTemplateContext,
54
+ createSwitchContext,
55
+ } from "./context";
56
+
57
+ export {
58
+ extractValue,
59
+ resolveValue,
60
+ resolveAndWrapAsset,
61
+ } from "./resolution/value-resolver";
62
+
63
+ export { FluentBuilderBase } from "./fluent-builder-base";
64
+
65
+ export { createInspectMethod } from "./utils";
66
+
67
+ export {
68
+ ErrorCodes,
69
+ FluentError,
70
+ createFluentError,
71
+ type ErrorCode,
72
+ } from "./errors";