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