@player-ui/async-node-plugin 0.13.0-next.7 → 0.14.0-next.0
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/AsyncNodePlugin.native.js +327 -128
- package/dist/AsyncNodePlugin.native.js.map +1 -1
- package/dist/cjs/index.cjs +199 -20
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +199 -18
- package/dist/index.mjs +199 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/transform.test.ts.snap +1 -0
- package/src/__tests__/createAsyncTransform.test.ts +215 -0
- package/src/__tests__/index.test.ts +94 -13
- package/src/__tests__/transform.bench.ts +177 -0
- package/src/createAsyncTransform.ts +91 -0
- package/src/index.ts +93 -13
- package/src/transform.ts +5 -2
- package/src/types.ts +1 -0
- package/src/utils/__tests__/extractNodeFromPath.test.ts +181 -0
- package/src/utils/__tests__/traverseAndReplace.test.ts +182 -0
- package/src/utils/__tests__/unwrapAsset.test.ts +65 -0
- package/src/utils/extractNodeFromPath.ts +56 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/traverseAndReplace.ts +34 -0
- package/src/utils/unwrapAsset.ts +16 -0
- package/types/createAsyncTransform.d.ts +24 -0
- package/types/index.d.ts +16 -1
- package/types/transform.d.ts +2 -1
- package/types/types.d.ts +1 -1
- package/types/utils/extractNodeFromPath.d.ts +4 -0
- package/types/utils/index.d.ts +4 -0
- package/types/utils/traverseAndReplace.d.ts +4 -0
- package/types/utils/unwrapAsset.d.ts +3 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BeforeTransformFunction,
|
|
3
|
+
Builder,
|
|
4
|
+
Node,
|
|
5
|
+
NodeType,
|
|
6
|
+
} from "@player-ui/player";
|
|
7
|
+
import { extractNodeFromPath, traverseAndReplace, unwrapAsset } from "./utils";
|
|
8
|
+
|
|
9
|
+
export type AsyncTransformOptions = {
|
|
10
|
+
/** Whether or not to flatten the results into its container. Defaults to true */
|
|
11
|
+
flatten?: boolean;
|
|
12
|
+
/** The path to the array within the `wrapperAssetType` that will contain the async content. Defaults to ["values"] */
|
|
13
|
+
path?: string[];
|
|
14
|
+
/** The asset type that the transform is matching against. */
|
|
15
|
+
transformAssetType: string;
|
|
16
|
+
/** The asset type that will contain the async content. */
|
|
17
|
+
wrapperAssetType: string;
|
|
18
|
+
/** Function to get any nested asset that will need to be extracted and kept when creating the wrapper asset. */
|
|
19
|
+
getNestedAsset?: (node: Node.ViewOrAsset) => Node.Node | undefined;
|
|
20
|
+
/** Function to get the id for the async node being generated. Defaults to creating an id with the format of async-<ASSET.ID> */
|
|
21
|
+
getAsyncNodeId?: (node: Node.ViewOrAsset) => string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const defaultGetNodeId = (node: Node.ViewOrAsset): string => {
|
|
25
|
+
return `async-${node.value.id}`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Creates a BeforeTransformFunction that turns the given asset into a wrapper asset with an async node in it.
|
|
29
|
+
* By setting {@link AsyncTransformOptions.flatten} to true, you can chain multiple of the same asset type to create a flow of async content that
|
|
30
|
+
* exists within a single collection.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Options for managing the transform
|
|
33
|
+
* @returns The {@link BeforeTransformFunction} that can be used for your asset.
|
|
34
|
+
*/
|
|
35
|
+
export const createAsyncTransform = (
|
|
36
|
+
options: AsyncTransformOptions,
|
|
37
|
+
): BeforeTransformFunction => {
|
|
38
|
+
const {
|
|
39
|
+
transformAssetType,
|
|
40
|
+
wrapperAssetType,
|
|
41
|
+
getNestedAsset,
|
|
42
|
+
getAsyncNodeId = defaultGetNodeId,
|
|
43
|
+
path = ["values"],
|
|
44
|
+
flatten = true,
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
const replaceNode = (node: Node.Node): Node.Node => {
|
|
48
|
+
const unwrapped = unwrapAsset(node);
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
unwrapped.type !== NodeType.Asset ||
|
|
52
|
+
unwrapped.value.type !== transformAssetType
|
|
53
|
+
) {
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const transformed = asyncTransform(unwrapped);
|
|
58
|
+
return extractNodeFromPath(transformed, path) ?? node;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const replacer = (node: Node.Node) => traverseAndReplace(node, replaceNode);
|
|
62
|
+
|
|
63
|
+
const asyncTransform = (node: Node.ViewOrAsset) => {
|
|
64
|
+
const id = getAsyncNodeId(node);
|
|
65
|
+
const asset = getNestedAsset?.(node);
|
|
66
|
+
|
|
67
|
+
// If flattening is disabled, don't need to extract the multi-node when async node is resolved.
|
|
68
|
+
const replaceFunction = flatten ? replacer : undefined;
|
|
69
|
+
const asyncNode = Builder.asyncNode(id, flatten, replaceFunction);
|
|
70
|
+
|
|
71
|
+
let multiNode: Node.MultiNode | undefined;
|
|
72
|
+
|
|
73
|
+
if (asset) {
|
|
74
|
+
const assetNode = Builder.assetWrapper(asset);
|
|
75
|
+
multiNode = Builder.multiNode(assetNode, asyncNode);
|
|
76
|
+
} else {
|
|
77
|
+
multiNode = Builder.multiNode(asyncNode);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const wrapperAsset: Node.ViewOrAsset = Builder.asset({
|
|
81
|
+
id: wrapperAssetType + "-" + id,
|
|
82
|
+
type: wrapperAssetType,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Builder.addChild(wrapperAsset, path, multiNode);
|
|
86
|
+
|
|
87
|
+
return wrapperAsset;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return asyncTransform;
|
|
91
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -13,10 +13,10 @@ import type {
|
|
|
13
13
|
} from "@player-ui/player";
|
|
14
14
|
import { AsyncParallelBailHook, SyncBailHook } from "tapable-ts";
|
|
15
15
|
import queueMicrotask from "queue-microtask";
|
|
16
|
-
import { omit } from "timm";
|
|
17
16
|
|
|
18
17
|
export * from "./types";
|
|
19
18
|
export * from "./transform";
|
|
19
|
+
export * from "./createAsyncTransform";
|
|
20
20
|
|
|
21
21
|
/** Object type for storing data related to a single `apply` of the `AsyncNodePluginPlugin`
|
|
22
22
|
* This object should be setup once per ViewInstance to keep any cached info just for that view to avoid conflicts of shared async node ids across different view states.
|
|
@@ -46,8 +46,14 @@ export type AsyncHandler = (
|
|
|
46
46
|
callback?: (result: any) => void,
|
|
47
47
|
) => Promise<any>;
|
|
48
48
|
|
|
49
|
+
export type AsyncContent = {
|
|
50
|
+
async: true;
|
|
51
|
+
flatten?: boolean;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
49
55
|
/** Hook declaration for the AsyncNodePlugin */
|
|
50
|
-
type AsyncNodeHooks = {
|
|
56
|
+
export type AsyncNodeHooks = {
|
|
51
57
|
/** Async hook to get content for an async node */
|
|
52
58
|
onAsyncNode: AsyncParallelBailHook<[Node.Async, (result: any) => void], any>;
|
|
53
59
|
/** Sync hook to manage errors coming from the onAsyncNode hook. Return a fallback node or null to render a fallback. The first argument of passed in the call is the error thrown. */
|
|
@@ -131,9 +137,13 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
131
137
|
result: any,
|
|
132
138
|
options: Resolve.NodeResolveOptions,
|
|
133
139
|
) {
|
|
134
|
-
|
|
140
|
+
let parsedNode =
|
|
135
141
|
options.parseNode && result ? options.parseNode(result) : undefined;
|
|
136
142
|
|
|
143
|
+
if (parsedNode && node.onValueReceived) {
|
|
144
|
+
parsedNode = node.onValueReceived(parsedNode);
|
|
145
|
+
}
|
|
146
|
+
|
|
137
147
|
this.handleAsyncUpdate(node, context, parsedNode);
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -154,10 +164,20 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
154
164
|
const { nodeResolveCache, view } = context;
|
|
155
165
|
if (nodeResolveCache.get(node.id) !== newNode) {
|
|
156
166
|
nodeResolveCache.set(node.id, newNode ? newNode : node);
|
|
157
|
-
view.updateAsync();
|
|
167
|
+
view.updateAsync(node.id);
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
|
|
171
|
+
private hasValidMapping(
|
|
172
|
+
node: Node.Async,
|
|
173
|
+
context: AsyncPluginContext,
|
|
174
|
+
): boolean {
|
|
175
|
+
const { nodeResolveCache } = context;
|
|
176
|
+
return (
|
|
177
|
+
nodeResolveCache.has(node.id) && nodeResolveCache.get(node.id) !== node
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
/**
|
|
162
182
|
* Handles the asynchronous API integration for resolving nodes.
|
|
163
183
|
* This method sets up a hook on the resolver's `beforeResolve` event to process async nodes.
|
|
@@ -167,12 +187,12 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
167
187
|
applyResolver(resolver: Resolver, context: AsyncPluginContext): void {
|
|
168
188
|
resolver.hooks.beforeResolve.tap(this.name, (node, options) => {
|
|
169
189
|
if (!this.isAsync(node)) {
|
|
170
|
-
return node;
|
|
190
|
+
return node === null ? node : this.resolveAsyncChildren(node, context);
|
|
171
191
|
}
|
|
172
192
|
|
|
173
193
|
const resolvedNode = context.nodeResolveCache.get(node.id);
|
|
174
194
|
if (resolvedNode !== undefined) {
|
|
175
|
-
return resolvedNode;
|
|
195
|
+
return this.resolveAsyncChildren(resolvedNode, context);
|
|
176
196
|
}
|
|
177
197
|
|
|
178
198
|
if (context.inProgressNodes.has(node.id)) {
|
|
@@ -189,6 +209,63 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
189
209
|
});
|
|
190
210
|
}
|
|
191
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Replaces child async nodes with their resolved content and flattens when necessary. Resolving the children directly helps manage the `parent` reference without needing as much work within the resolver itself.
|
|
214
|
+
* Handles async node chains as well to make sure all applicable nodes can get flattened.
|
|
215
|
+
* @param node - The node whose children need to be resolved.
|
|
216
|
+
* @param context - the async plugin context needed to reach into the cache
|
|
217
|
+
* @returns The same node but with async node children mapped to their resolved AST.
|
|
218
|
+
*/
|
|
219
|
+
private resolveAsyncChildren(
|
|
220
|
+
node: Node.Node,
|
|
221
|
+
context: AsyncPluginContext,
|
|
222
|
+
): Node.Node {
|
|
223
|
+
const asyncNodesResolved: string[] = node.asyncNodesResolved ?? [];
|
|
224
|
+
node.asyncNodesResolved = asyncNodesResolved;
|
|
225
|
+
if (node.type === NodeType.MultiNode) {
|
|
226
|
+
// Using a while loop lets us catch when async nodes produce more async nodes that need to be flattened further
|
|
227
|
+
let index = 0;
|
|
228
|
+
while (index < node.values.length) {
|
|
229
|
+
const childNode = node.values[index];
|
|
230
|
+
if (
|
|
231
|
+
childNode?.type !== NodeType.Async ||
|
|
232
|
+
!this.hasValidMapping(childNode, context)
|
|
233
|
+
) {
|
|
234
|
+
index++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const mappedNode = context.nodeResolveCache.get(childNode.id);
|
|
239
|
+
asyncNodesResolved.push(childNode.id);
|
|
240
|
+
if (mappedNode.type === NodeType.MultiNode && childNode.flatten) {
|
|
241
|
+
mappedNode.values.forEach((v: Node.Node) => (v.parent = node));
|
|
242
|
+
node.values = [
|
|
243
|
+
...node.values.slice(0, index),
|
|
244
|
+
...mappedNode.values,
|
|
245
|
+
...node.values.slice(index + 1),
|
|
246
|
+
];
|
|
247
|
+
} else {
|
|
248
|
+
node.values[index] = mappedNode;
|
|
249
|
+
mappedNode.parent = node;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else if ("children" in node) {
|
|
253
|
+
node.children?.forEach((c) => {
|
|
254
|
+
// Similar to above, using a while loop lets us handle when async nodes produce more async nodes.
|
|
255
|
+
while (
|
|
256
|
+
c.value.type === NodeType.Async &&
|
|
257
|
+
this.hasValidMapping(c.value, context)
|
|
258
|
+
) {
|
|
259
|
+
asyncNodesResolved.push(c.value.id);
|
|
260
|
+
c.value = context.nodeResolveCache.get(c.value.id);
|
|
261
|
+
c.value.parent = node;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return node;
|
|
267
|
+
}
|
|
268
|
+
|
|
192
269
|
private async runAsyncNode(
|
|
193
270
|
node: Node.Async,
|
|
194
271
|
context: AsyncPluginContext,
|
|
@@ -234,8 +311,12 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
234
311
|
return node?.type === NodeType.Async;
|
|
235
312
|
}
|
|
236
313
|
|
|
237
|
-
private isDeterminedAsync(obj:
|
|
238
|
-
return
|
|
314
|
+
private isDeterminedAsync(obj: unknown): obj is AsyncContent {
|
|
315
|
+
return (
|
|
316
|
+
typeof obj === "object" &&
|
|
317
|
+
obj !== null &&
|
|
318
|
+
Object.prototype.hasOwnProperty.call(obj, "async")
|
|
319
|
+
);
|
|
239
320
|
}
|
|
240
321
|
|
|
241
322
|
applyParser(parser: Parser): void {
|
|
@@ -248,11 +329,9 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
248
329
|
childOptions?: ParseObjectChildOptions,
|
|
249
330
|
) => {
|
|
250
331
|
if (this.isDeterminedAsync(obj)) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
options,
|
|
255
|
-
);
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
333
|
+
const { async, flatten, ...rest } = obj;
|
|
334
|
+
const parsedAsync = parser.parseObject(rest, nodeType, options);
|
|
256
335
|
const parsedNodeId = getNodeID(parsedAsync);
|
|
257
336
|
|
|
258
337
|
if (parsedAsync === null || !parsedNodeId) {
|
|
@@ -264,6 +343,7 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
264
343
|
id: parsedNodeId,
|
|
265
344
|
type: NodeType.Async,
|
|
266
345
|
value: parsedAsync,
|
|
346
|
+
flatten,
|
|
267
347
|
},
|
|
268
348
|
obj,
|
|
269
349
|
);
|
package/src/transform.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { Builder } from "@player-ui/player";
|
|
|
2
2
|
import type { AsyncTransformFunc } from "./types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @deprecated Use {@link createAsyncTransform} to create your before transform function.
|
|
5
6
|
* Util function to generate transform function for async asset
|
|
6
7
|
* @param asset - async asset to apply beforeResolve transform
|
|
7
|
-
* @param transformedAssetType: transformed asset type for rendering
|
|
8
8
|
* @param wrapperAssetType: container asset type
|
|
9
9
|
* @param flatten: flatten the streamed in content
|
|
10
|
+
* @param path: property path to add the multinode containing the next async node to
|
|
10
11
|
* @returns - wrapper asset with children of transformed asset and async node
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -15,10 +16,12 @@ export const asyncTransform: AsyncTransformFunc = (
|
|
|
15
16
|
wrapperAssetType,
|
|
16
17
|
asset,
|
|
17
18
|
flatten,
|
|
19
|
+
path = ["values"],
|
|
18
20
|
) => {
|
|
19
21
|
const id = "async-" + assetId;
|
|
20
22
|
|
|
21
23
|
const asyncNode = Builder.asyncNode(id, flatten);
|
|
24
|
+
|
|
22
25
|
let multiNode;
|
|
23
26
|
let assetNode;
|
|
24
27
|
|
|
@@ -34,7 +37,7 @@ export const asyncTransform: AsyncTransformFunc = (
|
|
|
34
37
|
type: wrapperAssetType,
|
|
35
38
|
});
|
|
36
39
|
|
|
37
|
-
Builder.addChild(wrapperAsset,
|
|
40
|
+
Builder.addChild(wrapperAsset, path, multiNode);
|
|
38
41
|
|
|
39
42
|
return wrapperAsset;
|
|
40
43
|
};
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { NodeType, Node } from "@player-ui/player";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { extractNodeFromPath } from "../extractNodeFromPath";
|
|
4
|
+
|
|
5
|
+
describe("extractNodeFromPath", () => {
|
|
6
|
+
it("should return any child with an exact match", () => {
|
|
7
|
+
const node: Node.Value = {
|
|
8
|
+
type: NodeType.Value,
|
|
9
|
+
value: {},
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
path: ["value", "asset"],
|
|
13
|
+
value: {
|
|
14
|
+
type: NodeType.Asset,
|
|
15
|
+
value: {
|
|
16
|
+
id: "id-1",
|
|
17
|
+
type: "type-1",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: ["value"],
|
|
23
|
+
value: {
|
|
24
|
+
type: NodeType.Value,
|
|
25
|
+
value: {},
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
path: ["asset"],
|
|
29
|
+
value: {
|
|
30
|
+
type: NodeType.Asset,
|
|
31
|
+
value: {
|
|
32
|
+
id: "id-2",
|
|
33
|
+
type: "type-2",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const result = extractNodeFromPath(node, ["value", "asset"]);
|
|
44
|
+
|
|
45
|
+
expect(result).toStrictEqual({
|
|
46
|
+
type: NodeType.Asset,
|
|
47
|
+
value: {
|
|
48
|
+
id: "id-1",
|
|
49
|
+
type: "type-1",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should follow partial matches to find the path nested", () => {
|
|
55
|
+
const node: Node.Value = {
|
|
56
|
+
type: NodeType.Value,
|
|
57
|
+
value: {},
|
|
58
|
+
children: [
|
|
59
|
+
{
|
|
60
|
+
path: ["value"],
|
|
61
|
+
value: {
|
|
62
|
+
type: NodeType.Value,
|
|
63
|
+
value: {},
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
path: ["asset"],
|
|
67
|
+
value: {
|
|
68
|
+
type: NodeType.Asset,
|
|
69
|
+
value: {
|
|
70
|
+
id: "id-2",
|
|
71
|
+
type: "type-2",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = extractNodeFromPath(node, ["value", "asset"]);
|
|
82
|
+
|
|
83
|
+
expect(result).toStrictEqual({
|
|
84
|
+
type: NodeType.Asset,
|
|
85
|
+
value: {
|
|
86
|
+
id: "id-2",
|
|
87
|
+
type: "type-2",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const emptyPaths = [undefined, []];
|
|
93
|
+
it.each(emptyPaths)(
|
|
94
|
+
"should return the original node if the path is empty or undefined",
|
|
95
|
+
(path) => {
|
|
96
|
+
const node: Node.Value = {
|
|
97
|
+
type: NodeType.Value,
|
|
98
|
+
value: {},
|
|
99
|
+
children: [
|
|
100
|
+
{
|
|
101
|
+
path: ["value"],
|
|
102
|
+
value: {
|
|
103
|
+
type: NodeType.Value,
|
|
104
|
+
value: {},
|
|
105
|
+
children: [
|
|
106
|
+
{
|
|
107
|
+
path: ["asset"],
|
|
108
|
+
value: {
|
|
109
|
+
type: NodeType.Asset,
|
|
110
|
+
value: {
|
|
111
|
+
id: "id-2",
|
|
112
|
+
type: "type-2",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const result = extractNodeFromPath(node, path);
|
|
123
|
+
|
|
124
|
+
expect(result).toStrictEqual(node);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const noChildrenNodes: Node.Node[] = [
|
|
129
|
+
// No children property
|
|
130
|
+
{
|
|
131
|
+
id: "test",
|
|
132
|
+
type: NodeType.Async,
|
|
133
|
+
value: {
|
|
134
|
+
type: NodeType.Value,
|
|
135
|
+
value: {
|
|
136
|
+
id: "test",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
// Children explicitly set to undefined
|
|
141
|
+
{
|
|
142
|
+
type: NodeType.Value,
|
|
143
|
+
value: {},
|
|
144
|
+
children: undefined,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
it.each(noChildrenNodes)(
|
|
149
|
+
"should return undefined if there are no children in the node",
|
|
150
|
+
(node) => {
|
|
151
|
+
const result = extractNodeFromPath(node, ["value", "asset"]);
|
|
152
|
+
expect(result).toBeUndefined();
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
it("should return undefined if there is no match", () => {
|
|
157
|
+
const node: Node.Value = {
|
|
158
|
+
type: NodeType.Value,
|
|
159
|
+
children: [
|
|
160
|
+
{
|
|
161
|
+
path: ["very", "long", "path"],
|
|
162
|
+
value: {
|
|
163
|
+
type: NodeType.Value,
|
|
164
|
+
value: {},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
path: ["value", "not-asset"],
|
|
169
|
+
value: {
|
|
170
|
+
type: NodeType.Value,
|
|
171
|
+
value: {},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
value: {},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = extractNodeFromPath(node, ["value", "asset"]);
|
|
179
|
+
expect(result).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Node, NodeType } from "@player-ui/player";
|
|
3
|
+
import { traverseAndReplace } from "../traverseAndReplace";
|
|
4
|
+
|
|
5
|
+
describe("traverseAndReplace", () => {
|
|
6
|
+
it("should call the replace function against the given node if it is not a multi-node", () => {
|
|
7
|
+
const node: Node.Value = {
|
|
8
|
+
type: NodeType.Value,
|
|
9
|
+
value: {
|
|
10
|
+
prop: "value",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const replaceFunction = vi.fn();
|
|
15
|
+
replaceFunction.mockReturnValue({
|
|
16
|
+
type: NodeType.Value,
|
|
17
|
+
value: {
|
|
18
|
+
prop: "new-value",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const result = traverseAndReplace(node, replaceFunction);
|
|
23
|
+
|
|
24
|
+
expect(result).toStrictEqual({
|
|
25
|
+
type: "value",
|
|
26
|
+
value: {
|
|
27
|
+
prop: "new-value",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
expect(replaceFunction).toHaveBeenCalledOnce();
|
|
31
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
32
|
+
type: "value",
|
|
33
|
+
value: {
|
|
34
|
+
prop: "value",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should call the replace function once for each value in the multi-node", () => {
|
|
40
|
+
const node: Node.MultiNode = {
|
|
41
|
+
type: NodeType.MultiNode,
|
|
42
|
+
values: [
|
|
43
|
+
{
|
|
44
|
+
type: NodeType.Value,
|
|
45
|
+
value: {
|
|
46
|
+
prop: "value-1",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: NodeType.Value,
|
|
51
|
+
value: {
|
|
52
|
+
prop: "value-2",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const replaceFunction = vi.fn();
|
|
59
|
+
replaceFunction.mockReturnValue({
|
|
60
|
+
type: NodeType.Value,
|
|
61
|
+
value: {
|
|
62
|
+
prop: "new-value",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const result = traverseAndReplace(node, replaceFunction);
|
|
67
|
+
|
|
68
|
+
expect(result).toStrictEqual({
|
|
69
|
+
type: "multi-node",
|
|
70
|
+
values: [
|
|
71
|
+
{
|
|
72
|
+
type: NodeType.Value,
|
|
73
|
+
value: {
|
|
74
|
+
prop: "new-value",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: NodeType.Value,
|
|
79
|
+
value: {
|
|
80
|
+
prop: "new-value",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
expect(replaceFunction).toHaveBeenCalledTimes(2);
|
|
86
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
87
|
+
type: "value",
|
|
88
|
+
value: {
|
|
89
|
+
prop: "value-1",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
93
|
+
type: "value",
|
|
94
|
+
value: {
|
|
95
|
+
prop: "value-2",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should flatten multi-node values generated by the replace function if the top-level node is a multi-node", () => {
|
|
101
|
+
const node: Node.MultiNode = {
|
|
102
|
+
type: NodeType.MultiNode,
|
|
103
|
+
values: [
|
|
104
|
+
{
|
|
105
|
+
type: NodeType.Value,
|
|
106
|
+
value: {
|
|
107
|
+
prop: "first",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const replaceFunction = vi.fn();
|
|
114
|
+
replaceFunction.mockImplementation((node: Node.Node) => {
|
|
115
|
+
if (node.type === NodeType.Value && node.value.prop === "first") {
|
|
116
|
+
return {
|
|
117
|
+
type: NodeType.MultiNode,
|
|
118
|
+
values: [
|
|
119
|
+
{
|
|
120
|
+
type: NodeType.Value,
|
|
121
|
+
value: {
|
|
122
|
+
prop: "second",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: NodeType.Value,
|
|
127
|
+
value: {
|
|
128
|
+
prop: "third",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
type: NodeType.Value,
|
|
137
|
+
value: {
|
|
138
|
+
prop: "new-value",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = traverseAndReplace(node, replaceFunction);
|
|
144
|
+
|
|
145
|
+
expect(result).toStrictEqual({
|
|
146
|
+
type: "multi-node",
|
|
147
|
+
values: [
|
|
148
|
+
{
|
|
149
|
+
type: NodeType.Value,
|
|
150
|
+
value: {
|
|
151
|
+
prop: "new-value",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: NodeType.Value,
|
|
156
|
+
value: {
|
|
157
|
+
prop: "new-value",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
expect(replaceFunction).toHaveBeenCalledTimes(3);
|
|
163
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
164
|
+
type: "value",
|
|
165
|
+
value: {
|
|
166
|
+
prop: "first",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
170
|
+
type: "value",
|
|
171
|
+
value: {
|
|
172
|
+
prop: "second",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
expect(replaceFunction).toHaveBeenCalledWith({
|
|
176
|
+
type: "value",
|
|
177
|
+
value: {
|
|
178
|
+
prop: "third",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|