@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.
- package/dist/cjs/index.cjs +2396 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2276 -0
- package/dist/index.mjs +2276 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
- package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +534 -0
- package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
- package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
- package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
- package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
- package/src/core/base-builder/__tests__/switch.test.ts +815 -0
- package/src/core/base-builder/__tests__/template.test.ts +596 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +152 -0
- package/src/core/base-builder/errors.ts +69 -0
- package/src/core/base-builder/fluent-builder-base.ts +308 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +290 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +72 -0
- package/src/core/base-builder/resolution/path-resolver.ts +116 -0
- package/src/core/base-builder/resolution/pipeline.ts +103 -0
- package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -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 +84 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
- package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +71 -0
- package/src/core/base-builder/resolution/steps/templates.ts +40 -0
- package/src/core/base-builder/resolution/value-resolver.ts +333 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +282 -0
- package/src/core/base-builder/types.ts +266 -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 +118 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +92 -0
- package/src/core/mocks/generated/choice-item.builder.ts +120 -0
- package/src/core/mocks/generated/choice.builder.ts +134 -0
- package/src/core/mocks/generated/collection.builder.ts +93 -0
- package/src/core/mocks/generated/field-collection.builder.ts +86 -0
- package/src/core/mocks/generated/index.ts +10 -0
- package/src/core/mocks/generated/info.builder.ts +64 -0
- package/src/core/mocks/generated/input.builder.ts +63 -0
- package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
- package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
- package/src/core/mocks/generated/text.builder.ts +47 -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 +81 -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 +196 -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 +15 -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/errors.d.ts +45 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +147 -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 +9 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +27 -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/nested-asset-wrappers.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 +62 -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 +183 -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 +6 -0
- package/types/index.d.ts +3 -0
- 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";
|