@player-tools/fluent 0.13.0--canary.221.5662
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +2257 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2143 -0
- package/dist/index.mjs +2143 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2257 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +411 -0
- package/src/core/base-builder/__tests__/switch.test.ts +501 -0
- package/src/core/base-builder/__tests__/template.test.ts +449 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +151 -0
- package/src/core/base-builder/fluent-builder-base.ts +261 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +286 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +60 -0
- package/src/core/base-builder/resolution/path-resolver.ts +108 -0
- package/src/core/base-builder/resolution/pipeline.ts +96 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +85 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +117 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +63 -0
- package/src/core/base-builder/resolution/steps/templates.ts +30 -0
- package/src/core/base-builder/resolution/value-resolver.ts +308 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +280 -0
- package/src/core/base-builder/types.ts +184 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +141 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +109 -0
- package/src/core/mocks/generated/choice.builder.ts +161 -0
- package/src/core/mocks/generated/choiceItem.builder.ts +133 -0
- package/src/core/mocks/generated/collection.builder.ts +117 -0
- package/src/core/mocks/generated/index.ts +7 -0
- package/src/core/mocks/generated/info.builder.ts +80 -0
- package/src/core/mocks/generated/input.builder.ts +75 -0
- package/src/core/mocks/generated/text.builder.ts +63 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +76 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +191 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +2 -0
- package/src/gen/plugin.mjs +315 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +132 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +8 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +25 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +37 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +141 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +3 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +163 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { globalIdRegistry } from "./registry";
|
|
2
|
+
import type { AssetMetadata, BaseBuildContext } from "../types";
|
|
3
|
+
|
|
4
|
+
export { globalIdRegistry, createIdRegistry, IDRegistry } from "./registry";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resets the global ID registry, clearing all registered IDs.
|
|
8
|
+
* This is useful for testing scenarios where you need a clean slate.
|
|
9
|
+
*/
|
|
10
|
+
export const resetGlobalIdSet = (): void => {
|
|
11
|
+
globalIdRegistry.reset();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal function that generates a base ID from context without registry operations.
|
|
16
|
+
* This is the core ID generation logic shared between peekId and genId.
|
|
17
|
+
*
|
|
18
|
+
* @param context - The context containing parent ID and optional branch information
|
|
19
|
+
* @param functionName - The name of the calling function (for error messages)
|
|
20
|
+
* @returns The generated base ID
|
|
21
|
+
*/
|
|
22
|
+
const _generateBaseId = (
|
|
23
|
+
context: BaseBuildContext,
|
|
24
|
+
functionName: string,
|
|
25
|
+
): string => {
|
|
26
|
+
// Validate context
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`${functionName}: Context is undefined. Please provide a valid BaseBuildContext object.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { parentId, branch } = context;
|
|
34
|
+
|
|
35
|
+
let baseId: string;
|
|
36
|
+
|
|
37
|
+
if (!branch) {
|
|
38
|
+
baseId = parentId || "";
|
|
39
|
+
} else {
|
|
40
|
+
switch (branch.type) {
|
|
41
|
+
case "custom":
|
|
42
|
+
baseId = parentId || "";
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case "slot":
|
|
46
|
+
if (!branch.name) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${functionName}: Slot branch requires a 'name' property. ` +
|
|
49
|
+
`Context: ${JSON.stringify(context)}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
baseId = `${parentId ? `${parentId}-` : ""}${branch.name}`;
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case "array-item":
|
|
56
|
+
if (typeof branch.index !== "number") {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${functionName}: Array-item branch requires a numeric 'index' property. ` +
|
|
59
|
+
`Got: ${typeof branch.index}. Context: ${JSON.stringify(context)}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (branch.index < 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${functionName}: Array-item index must be non-negative. ` +
|
|
65
|
+
`Got: ${branch.index}. Context: ${JSON.stringify(context)}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
baseId = `${parentId}-${branch.index}`;
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case "template":
|
|
72
|
+
if (branch.depth !== undefined && branch.depth < 0) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`${functionName}: Template depth must be non-negative. ` +
|
|
75
|
+
`Got: ${branch.depth}. Context: ${JSON.stringify(context)}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
baseId = `${parentId}-_index${branch.depth || ""}_`;
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "switch":
|
|
82
|
+
if (typeof branch.index !== "number") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`${functionName}: Switch branch requires a numeric 'index' property. ` +
|
|
85
|
+
`Got: ${typeof branch.index}. Context: ${JSON.stringify(context)}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (branch.index < 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${functionName}: Switch index must be non-negative. ` +
|
|
91
|
+
`Got: ${branch.index}. Context: ${JSON.stringify(context)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!branch.kind || !["static", "dynamic"].includes(branch.kind)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${functionName}: Switch branch requires 'kind' to be 'static' or 'dynamic'. ` +
|
|
97
|
+
`Got: ${branch.kind}. Context: ${JSON.stringify(context)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
baseId = `${parentId}-${branch.kind}Switch-${branch.index}`;
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
default: {
|
|
104
|
+
const exhaustiveCheck: never = branch;
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${functionName}: Unhandled branch type: ${JSON.stringify(exhaustiveCheck)}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return baseId;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates an ID without registering it in the global registry.
|
|
117
|
+
* This is useful for intermediate ID lookups where you don't want to consume the ID.
|
|
118
|
+
*
|
|
119
|
+
* @param context - The context containing parent ID and optional branch information
|
|
120
|
+
* @returns The generated ID (without collision detection or registration)
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Use this when you need to generate a parent ID for nested context creation
|
|
124
|
+
* const parentId = peekId({ parentId: 'collection' }); // Doesn't register 'collection'
|
|
125
|
+
* const nestedCtx = { parentId, branch: { type: 'slot', name: 'label' } };
|
|
126
|
+
*/
|
|
127
|
+
export const peekId = (context: BaseBuildContext): string => {
|
|
128
|
+
return _generateBaseId(context, "peekId");
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generates a unique identifier based on the parent ID and branch information.
|
|
133
|
+
*
|
|
134
|
+
* This function creates hierarchical IDs for various element types in a DSL structure,
|
|
135
|
+
* maintaining relationships between parent and child elements through consistent naming patterns.
|
|
136
|
+
* IDs are automatically checked for uniqueness and modified if collisions are detected.
|
|
137
|
+
*
|
|
138
|
+
* @param {BaseBuildContext} context - The context containing parent ID and optional branch information
|
|
139
|
+
* @param {string} context.parentId - The ID of the parent element
|
|
140
|
+
* @param {IdBranch} [context.branch] - Optional branch information to specify the type of child element
|
|
141
|
+
*
|
|
142
|
+
* @returns {string} A generated ID string representing the element's unique identifier
|
|
143
|
+
*
|
|
144
|
+
* @throws {Error} If context is invalid or incomplete
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Slot branch
|
|
148
|
+
* genId({ parentId: 'parent', branch: { type: 'slot', name: 'header' } })
|
|
149
|
+
* // Returns: 'parent-header'
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // Array item branch
|
|
153
|
+
* genId({ parentId: 'list', branch: { type: 'array-item', index: 2 } })
|
|
154
|
+
* // Returns: 'list-2'
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* // Template branch
|
|
158
|
+
* genId({ parentId: 'template', branch: { type: 'template', depth: 1 } })
|
|
159
|
+
* // Returns: 'template-_index1_'
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* // Switch branch
|
|
163
|
+
* genId({ parentId: 'condition', branch: { type: 'switch', index: 0, kind: 'static' } })
|
|
164
|
+
* // Returns: 'condition-staticSwitch-0'
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // Custom ID case (no branch)
|
|
168
|
+
* genId({ parentId: 'custom-id' })
|
|
169
|
+
* // Returns: 'custom-id'
|
|
170
|
+
*/
|
|
171
|
+
export const genId = (context: BaseBuildContext): string => {
|
|
172
|
+
const baseId = _generateBaseId(context, "genId");
|
|
173
|
+
|
|
174
|
+
const { parentId, branch } = context;
|
|
175
|
+
|
|
176
|
+
if (!parentId && !branch) {
|
|
177
|
+
console.warn(
|
|
178
|
+
"genId: Context appears incomplete (no parentId or branch). " +
|
|
179
|
+
"This may result in an empty or invalid ID. " +
|
|
180
|
+
"Consider using context helper functions from 'fluent/utils/context'.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (parentId === "") {
|
|
185
|
+
console.warn(
|
|
186
|
+
"genId: parentId is an empty string. " +
|
|
187
|
+
"This may indicate a missing or improperly initialized context.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Ensure the generated ID is unique
|
|
192
|
+
const uniqueId = globalIdRegistry.ensureUnique(baseId);
|
|
193
|
+
|
|
194
|
+
// Warn if collision was detected
|
|
195
|
+
if (process.env.NODE_ENV !== "production" && uniqueId !== baseId) {
|
|
196
|
+
console.warn(
|
|
197
|
+
`genId: ID collision detected. Original: "${baseId}", Modified to: "${uniqueId}". ` +
|
|
198
|
+
`Consider providing more specific IDs to avoid collisions.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return uniqueId;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export function determineSlotName(
|
|
206
|
+
parameterName: string,
|
|
207
|
+
assetMetadata?: AssetMetadata,
|
|
208
|
+
): string {
|
|
209
|
+
if (!assetMetadata) {
|
|
210
|
+
return parameterName;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { type, binding, value } = assetMetadata;
|
|
214
|
+
|
|
215
|
+
// Rule 1: If the asset type is `action`, append last segment of `value` if any
|
|
216
|
+
// Note: value can be a binding (TaggedTemplateValue) or plain string
|
|
217
|
+
if (type === "action" && value) {
|
|
218
|
+
const cleanValue = value.replace(/^\{\{|\}\}$/g, "");
|
|
219
|
+
const segments = cleanValue.split(".");
|
|
220
|
+
const lastSegment = segments[segments.length - 1];
|
|
221
|
+
return lastSegment ? `${type}-${lastSegment}` : type;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Rule 2: If it's not `action` but has `binding`, append last fragment of binding
|
|
225
|
+
if (type !== "action" && binding) {
|
|
226
|
+
const cleanBinding = binding.replace(/^\{\{|\}\}$/g, "");
|
|
227
|
+
const segments = cleanBinding.split(".");
|
|
228
|
+
const lastSegment = segments[segments.length - 1];
|
|
229
|
+
if (lastSegment) {
|
|
230
|
+
return `${type || parameterName}-${lastSegment}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Rule 3: Otherwise, just use the type
|
|
235
|
+
return type || parameterName;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function generateAssetId<C extends BaseBuildContext>(params: {
|
|
239
|
+
readonly context?: C;
|
|
240
|
+
readonly parameterName?: string;
|
|
241
|
+
readonly assetMetadata?: AssetMetadata;
|
|
242
|
+
readonly explicitId?: string;
|
|
243
|
+
}): string {
|
|
244
|
+
const {
|
|
245
|
+
context,
|
|
246
|
+
parameterName = "asset",
|
|
247
|
+
assetMetadata,
|
|
248
|
+
explicitId,
|
|
249
|
+
} = params;
|
|
250
|
+
|
|
251
|
+
if (explicitId) {
|
|
252
|
+
return explicitId;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (context && "parentId" in context) {
|
|
256
|
+
// Determine the slot name based on asset metadata (action value, binding, or type)
|
|
257
|
+
const slotName = determineSlotName(parameterName, assetMetadata);
|
|
258
|
+
|
|
259
|
+
if (context.branch) {
|
|
260
|
+
// When there's a branch, generate base ID then append asset type
|
|
261
|
+
// This creates IDs like "parent-0-questionAnswer", "parent-slot-text", etc.
|
|
262
|
+
const baseId = genId(context);
|
|
263
|
+
|
|
264
|
+
// Append the asset type as a suffix
|
|
265
|
+
// Use genId again to ensure uniqueness and proper registration
|
|
266
|
+
return genId({
|
|
267
|
+
...context,
|
|
268
|
+
parentId: baseId,
|
|
269
|
+
branch: { type: "slot", name: slotName },
|
|
270
|
+
} as C);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (context.parentId) {
|
|
274
|
+
// When there's a parentId but no branch, use slotName (determined from metadata)
|
|
275
|
+
// This creates IDs like "parent-text", "parent-action-next", "parent-input-firstName", etc.
|
|
276
|
+
// Generate and register the ID to enable collision detection
|
|
277
|
+
return genId({
|
|
278
|
+
...context,
|
|
279
|
+
branch: { type: "slot", name: slotName },
|
|
280
|
+
} as C);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const slotName = determineSlotName(parameterName, assetMetadata);
|
|
285
|
+
return slotName;
|
|
286
|
+
}
|
|
@@ -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,60 @@
|
|
|
1
|
+
export {
|
|
2
|
+
FLUENT_BUILDER_SYMBOL,
|
|
3
|
+
type NestedContextParams,
|
|
4
|
+
type NestedContextGenerator,
|
|
5
|
+
type AssetMetadata,
|
|
6
|
+
type BaseBuildContext,
|
|
7
|
+
type FluentBuilder,
|
|
8
|
+
type AnyAssetBuilder,
|
|
9
|
+
type MixedArrayMetadata,
|
|
10
|
+
type TemplateMetadata,
|
|
11
|
+
type IdBranch,
|
|
12
|
+
type SlotBranch,
|
|
13
|
+
type ArrayItemBranch,
|
|
14
|
+
type TemplateBranch,
|
|
15
|
+
type SwitchBranch,
|
|
16
|
+
type CustomBranch,
|
|
17
|
+
type ValuePath,
|
|
18
|
+
type SwitchMetadata,
|
|
19
|
+
type ConditionalValue,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
isFluentBuilder,
|
|
24
|
+
isBuilderArray,
|
|
25
|
+
isPlainObject,
|
|
26
|
+
isAsset,
|
|
27
|
+
isAssetWrapper,
|
|
28
|
+
isAssetWrapperWithAsset,
|
|
29
|
+
needsAssetWrapper,
|
|
30
|
+
isAssetWrapperValue,
|
|
31
|
+
isSwitchResult,
|
|
32
|
+
isStringOrUndefined,
|
|
33
|
+
} from "./guards";
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
determineSlotName,
|
|
37
|
+
generateAssetId,
|
|
38
|
+
genId,
|
|
39
|
+
peekId,
|
|
40
|
+
resetGlobalIdSet,
|
|
41
|
+
globalIdRegistry,
|
|
42
|
+
createIdRegistry,
|
|
43
|
+
IDRegistry,
|
|
44
|
+
} from "./id/generator";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
createNestedContext,
|
|
48
|
+
createTemplateContext,
|
|
49
|
+
createSwitchContext,
|
|
50
|
+
} from "./context";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
extractValue,
|
|
54
|
+
resolveValue,
|
|
55
|
+
resolveAndWrapAsset,
|
|
56
|
+
} from "./resolution/value-resolver";
|
|
57
|
+
|
|
58
|
+
export { FluentBuilderBase } from "./fluent-builder-base";
|
|
59
|
+
|
|
60
|
+
export { createInspectMethod } from "./utils";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ValuePath } from "../types";
|
|
2
|
+
import { isAssetWrapperValue } from "../guards";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sets a value at a nested path in an object
|
|
6
|
+
* Handles nested objects, arrays, and AssetWrapper structures
|
|
7
|
+
*
|
|
8
|
+
* @param obj - The target object to modify
|
|
9
|
+
* @param path - Array of keys/indices representing the path
|
|
10
|
+
* @param value - The value to set at the path
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* setValueAtPath(obj, ["actions", 0, "label"], "Click me")
|
|
14
|
+
* // Sets obj.actions[0].label = "Click me"
|
|
15
|
+
*/
|
|
16
|
+
export function setValueAtPath(
|
|
17
|
+
obj: Record<string, unknown>,
|
|
18
|
+
path: ValuePath,
|
|
19
|
+
value: unknown,
|
|
20
|
+
): void {
|
|
21
|
+
if (path.length === 0) return;
|
|
22
|
+
|
|
23
|
+
if (path.length === 1) {
|
|
24
|
+
obj[path[0]] = value;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [currentKey, ...restPath] = path;
|
|
29
|
+
const nextKey = restPath[0];
|
|
30
|
+
const currentValue = obj[currentKey];
|
|
31
|
+
|
|
32
|
+
// Check if current value is an AssetWrapper containing an array
|
|
33
|
+
const isAssetWrapperWithArray =
|
|
34
|
+
isAssetWrapperValue(currentValue) && Array.isArray(currentValue.asset);
|
|
35
|
+
|
|
36
|
+
if (isAssetWrapperWithArray && typeof nextKey === "number") {
|
|
37
|
+
setValueInAssetWrapperArray(
|
|
38
|
+
obj,
|
|
39
|
+
currentKey,
|
|
40
|
+
currentValue as { asset: unknown[] },
|
|
41
|
+
nextKey,
|
|
42
|
+
restPath,
|
|
43
|
+
value,
|
|
44
|
+
);
|
|
45
|
+
} else if (Array.isArray(currentValue) && typeof nextKey === "number") {
|
|
46
|
+
setValueInArray(obj, currentKey, currentValue, nextKey, restPath, value);
|
|
47
|
+
} else {
|
|
48
|
+
setValueInObject(obj, currentKey, restPath, value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets value in an array within an AssetWrapper
|
|
54
|
+
*/
|
|
55
|
+
function setValueInAssetWrapperArray(
|
|
56
|
+
obj: Record<string, unknown>,
|
|
57
|
+
currentKey: string | number,
|
|
58
|
+
wrappedArray: { asset: unknown[] },
|
|
59
|
+
nextKey: number,
|
|
60
|
+
restPath: ValuePath,
|
|
61
|
+
value: unknown,
|
|
62
|
+
): void {
|
|
63
|
+
const arrayResult = [...wrappedArray.asset];
|
|
64
|
+
if (restPath.length === 1) {
|
|
65
|
+
arrayResult[nextKey] = value;
|
|
66
|
+
} else {
|
|
67
|
+
const nestedObj = (arrayResult[nextKey] as Record<string, unknown>) ?? {};
|
|
68
|
+
setValueAtPath(nestedObj, restPath.slice(1), value);
|
|
69
|
+
arrayResult[nextKey] = nestedObj;
|
|
70
|
+
}
|
|
71
|
+
obj[currentKey] = { asset: arrayResult };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets value in a regular array
|
|
76
|
+
*/
|
|
77
|
+
function setValueInArray(
|
|
78
|
+
obj: Record<string, unknown>,
|
|
79
|
+
currentKey: string | number,
|
|
80
|
+
array: unknown[],
|
|
81
|
+
nextKey: number,
|
|
82
|
+
restPath: ValuePath,
|
|
83
|
+
value: unknown,
|
|
84
|
+
): void {
|
|
85
|
+
const arrayResult = [...array];
|
|
86
|
+
if (restPath.length === 1) {
|
|
87
|
+
arrayResult[nextKey] = value;
|
|
88
|
+
} else {
|
|
89
|
+
const nestedObj = (arrayResult[nextKey] as Record<string, unknown>) ?? {};
|
|
90
|
+
setValueAtPath(nestedObj, restPath.slice(1), value);
|
|
91
|
+
arrayResult[nextKey] = nestedObj;
|
|
92
|
+
}
|
|
93
|
+
obj[currentKey] = arrayResult;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sets value in a nested object
|
|
98
|
+
*/
|
|
99
|
+
function setValueInObject(
|
|
100
|
+
obj: Record<string, unknown>,
|
|
101
|
+
currentKey: string | number,
|
|
102
|
+
restPath: ValuePath,
|
|
103
|
+
value: unknown,
|
|
104
|
+
): void {
|
|
105
|
+
const nestedObj = (obj[currentKey] as Record<string, unknown>) ?? {};
|
|
106
|
+
setValueAtPath(nestedObj, restPath, value);
|
|
107
|
+
obj[currentKey] = nestedObj;
|
|
108
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { BaseBuildContext } from "../types";
|
|
2
|
+
import type { ValueStorage } from "../storage/value-storage";
|
|
3
|
+
import type { AuxiliaryStorage } from "../storage/auxiliary-storage";
|
|
4
|
+
import { resolveStaticValues } from "./steps/static-values";
|
|
5
|
+
import { generateAssetIdForBuilder } from "./steps/asset-id";
|
|
6
|
+
import { resolveAssetWrappers } from "./steps/asset-wrappers";
|
|
7
|
+
import { resolveMixedArrays } from "./steps/mixed-arrays";
|
|
8
|
+
import { resolveBuilders } from "./steps/builders";
|
|
9
|
+
import { resolveSwitches } from "./steps/switches";
|
|
10
|
+
import { resolveTemplates } from "./steps/templates";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a nested context for child assets
|
|
14
|
+
* This is Step 3 of the build process
|
|
15
|
+
*/
|
|
16
|
+
function createNestedParentContext<C extends BaseBuildContext>(
|
|
17
|
+
result: Record<string, unknown>,
|
|
18
|
+
context: C | undefined,
|
|
19
|
+
): C | undefined {
|
|
20
|
+
if (!context) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Extract parent ID with type checking
|
|
25
|
+
const parentId =
|
|
26
|
+
"id" in result && typeof result.id === "string" ? result.id : undefined;
|
|
27
|
+
|
|
28
|
+
// Create context for nested assets
|
|
29
|
+
// We clear the branch to avoid double-nesting
|
|
30
|
+
return {
|
|
31
|
+
...context,
|
|
32
|
+
parentId,
|
|
33
|
+
branch: undefined,
|
|
34
|
+
} as C;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Executes the complete build pipeline
|
|
39
|
+
*
|
|
40
|
+
* The pipeline consists of 8 steps that transform builder state into a final object:
|
|
41
|
+
* 1. Resolve static values (extract TaggedTemplateValue, resolve simple builders)
|
|
42
|
+
* 2. Generate asset ID if needed
|
|
43
|
+
* 3. Create nested context for child assets
|
|
44
|
+
* 4. Resolve AssetWrapper values
|
|
45
|
+
* 5. Resolve mixed arrays
|
|
46
|
+
* 6. Resolve builders
|
|
47
|
+
* 7. Resolve switches
|
|
48
|
+
* 8. Resolve templates
|
|
49
|
+
*
|
|
50
|
+
* @param valueStorage - Storage containing property values
|
|
51
|
+
* @param auxiliaryStorage - Storage containing metadata (templates, switches)
|
|
52
|
+
* @param defaults - Optional default values to merge into result
|
|
53
|
+
* @param context - Optional build context
|
|
54
|
+
* @param arrayProperties - Set of property names that are array types
|
|
55
|
+
* @returns The fully built object
|
|
56
|
+
*/
|
|
57
|
+
export function executeBuildPipeline<T, C extends BaseBuildContext>(
|
|
58
|
+
valueStorage: ValueStorage<T>,
|
|
59
|
+
auxiliaryStorage: AuxiliaryStorage,
|
|
60
|
+
defaults: Partial<T> | undefined,
|
|
61
|
+
context: C | undefined,
|
|
62
|
+
arrayProperties: ReadonlySet<string>,
|
|
63
|
+
): T {
|
|
64
|
+
const result: Record<string, unknown> = defaults ? { ...defaults } : {};
|
|
65
|
+
|
|
66
|
+
// Step 1: Resolve static values
|
|
67
|
+
resolveStaticValues(valueStorage, result, context);
|
|
68
|
+
|
|
69
|
+
// Step 2: Generate asset ID if needed
|
|
70
|
+
generateAssetIdForBuilder(valueStorage, result, context);
|
|
71
|
+
|
|
72
|
+
// Step 3: Create nested context for child assets
|
|
73
|
+
const nestedParentContext = createNestedParentContext(result, context);
|
|
74
|
+
|
|
75
|
+
// Step 4: Resolve AssetWrapper values
|
|
76
|
+
resolveAssetWrappers(valueStorage, result, nestedParentContext);
|
|
77
|
+
|
|
78
|
+
// Step 5: Resolve mixed arrays
|
|
79
|
+
resolveMixedArrays(valueStorage, result, nestedParentContext);
|
|
80
|
+
|
|
81
|
+
// Step 6: Resolve builders
|
|
82
|
+
resolveBuilders(valueStorage, result, nestedParentContext);
|
|
83
|
+
|
|
84
|
+
// Step 7: Resolve switches
|
|
85
|
+
resolveSwitches(
|
|
86
|
+
auxiliaryStorage,
|
|
87
|
+
result,
|
|
88
|
+
nestedParentContext,
|
|
89
|
+
arrayProperties,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Step 8: Resolve templates
|
|
93
|
+
resolveTemplates(auxiliaryStorage, result, context);
|
|
94
|
+
|
|
95
|
+
return result as T;
|
|
96
|
+
}
|