@player-ui/player 0.13.0 → 0.14.0-next.1
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/Player.native.js +213 -239
- package/dist/Player.native.js.map +1 -1
- package/dist/cjs/index.cjs +75 -116
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +75 -116
- package/dist/index.mjs +75 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/binding/__tests__/resolver.test.ts +5 -0
- package/src/binding/resolver.ts +5 -1
- package/src/binding-grammar/__tests__/parser.test.ts +0 -32
- package/src/binding-grammar/__tests__/test-utils/ast-cases.ts +31 -0
- package/src/binding-grammar/ast.ts +2 -2
- package/src/binding-grammar/custom/index.ts +17 -9
- package/src/view/__tests__/view.test.ts +61 -1
- package/src/view/builder/index.ts +6 -1
- package/src/view/parser/index.ts +45 -33
- package/src/view/parser/types.ts +5 -0
- package/src/view/plugins/__tests__/multi-node.test.ts +36 -0
- package/src/view/plugins/multi-node.ts +14 -14
- package/src/view/resolver/__tests__/index.test.ts +153 -0
- package/src/view/resolver/index.ts +109 -157
- package/src/view/view.ts +2 -2
- package/types/binding-grammar/ast.d.ts +2 -2
- package/types/view/builder/index.d.ts +1 -1
- package/types/view/parser/index.d.ts +38 -22
- package/types/view/parser/types.d.ts +4 -0
- package/types/view/resolver/index.d.ts +37 -25
- package/types/view/view.d.ts +1 -1
package/package.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"types"
|
|
7
7
|
],
|
|
8
8
|
"name": "@player-ui/player",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.14.0-next.1",
|
|
10
10
|
"main": "dist/cjs/index.cjs",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@player-ui/partial-match-registry": "0.
|
|
13
|
-
"@player-ui/make-flow": "0.
|
|
14
|
-
"@player-ui/types": "0.
|
|
12
|
+
"@player-ui/partial-match-registry": "0.14.0-next.1",
|
|
13
|
+
"@player-ui/make-flow": "0.14.0-next.1",
|
|
14
|
+
"@player-ui/types": "0.14.0-next.1",
|
|
15
15
|
"@types/dlv": "^1.1.4",
|
|
16
16
|
"dequal": "^2.0.2",
|
|
17
17
|
"dlv": "^1.1.3",
|
|
@@ -11,14 +11,17 @@ export const testModel = {
|
|
|
11
11
|
{
|
|
12
12
|
name: "ginger",
|
|
13
13
|
type: "dog",
|
|
14
|
+
isDog: true,
|
|
14
15
|
},
|
|
15
16
|
{
|
|
16
17
|
name: "daisy",
|
|
17
18
|
type: "dog",
|
|
19
|
+
isDog: true,
|
|
18
20
|
},
|
|
19
21
|
{
|
|
20
22
|
name: "frodo",
|
|
21
23
|
type: "cat",
|
|
24
|
+
isDog: false,
|
|
22
25
|
},
|
|
23
26
|
"other",
|
|
24
27
|
],
|
|
@@ -33,6 +36,8 @@ export const testCases: Array<[string, string]> = [
|
|
|
33
36
|
["foo.pets[01].name", "foo.pets.1.name"],
|
|
34
37
|
['foo.pets[name = "frodo"].type', "foo.pets.2.type"],
|
|
35
38
|
['foo.pets["name" = "sprinkles"].type', "foo.pets.4.type"],
|
|
39
|
+
['foo.pets["isDog" = false].type', "foo.pets.2.type"],
|
|
40
|
+
['foo.pets["isDog" = true].type', "foo.pets.0.type"],
|
|
36
41
|
];
|
|
37
42
|
|
|
38
43
|
test.each(testCases)("Resolving binding: %s", (binding, expectedResolved) => {
|
package/src/binding/resolver.ts
CHANGED
|
@@ -107,7 +107,11 @@ export function resolveBindingAST(
|
|
|
107
107
|
break;
|
|
108
108
|
|
|
109
109
|
case "Value":
|
|
110
|
-
appendPathSegments(
|
|
110
|
+
appendPathSegments(
|
|
111
|
+
typeof resolvedNode.value === "boolean"
|
|
112
|
+
? String(resolvedNode.value)
|
|
113
|
+
: resolvedNode.value,
|
|
114
|
+
);
|
|
111
115
|
break;
|
|
112
116
|
|
|
113
117
|
case "Query": {
|
|
@@ -6,40 +6,8 @@ import {
|
|
|
6
6
|
VALID_AST_PARSER_CUSTOM_TESTS,
|
|
7
7
|
} from "./test-utils/ast-cases";
|
|
8
8
|
import type { ParserSuccessResult, ParserFailureResult } from "../ast";
|
|
9
|
-
import { parse as parseParsimmon } from "./parsimmon";
|
|
10
|
-
import { parse as parseEBNF } from "./ebnf";
|
|
11
9
|
import { parse as parseCustom } from "../custom";
|
|
12
10
|
|
|
13
|
-
describe("parsimmon", () => {
|
|
14
|
-
test.each(VALID_AST_PARSER_TESTS)("Parsimmon Valid: %s", (binding, AST) => {
|
|
15
|
-
const result = parseParsimmon(binding);
|
|
16
|
-
|
|
17
|
-
expect(result.status).toBe(true);
|
|
18
|
-
expect((result as ParserSuccessResult).path).toStrictEqual(AST);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test.each(INVALID_AST_PARSER_TESTS)("Parsimmon Invalid: %s", (binding) => {
|
|
22
|
-
const result = parseParsimmon(binding);
|
|
23
|
-
expect(result.status).toBe(false);
|
|
24
|
-
expect((result as ParserFailureResult).error.length > 0).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("ebnf", () => {
|
|
29
|
-
test.each(VALID_AST_PARSER_TESTS)("EBNF Valid: %s", (binding, AST) => {
|
|
30
|
-
const result = parseEBNF(binding);
|
|
31
|
-
|
|
32
|
-
expect(result.status).toBe(true);
|
|
33
|
-
expect((result as ParserSuccessResult).path).toStrictEqual(AST);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test.each(INVALID_AST_PARSER_TESTS)("EBNF Invalid: %s", (binding) => {
|
|
37
|
-
const result = parseEBNF(binding);
|
|
38
|
-
expect(result.status).toBe(false);
|
|
39
|
-
expect((result as ParserFailureResult).error.length > 0).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
11
|
describe("custom", () => {
|
|
44
12
|
test.each(VALID_AST_PARSER_TESTS)("Custom Valid: %s", (binding, AST) => {
|
|
45
13
|
const result = parseCustom(binding);
|
|
@@ -54,6 +54,31 @@ export const VALID_AST_PARSER_TESTS: Array<[string, PathNode]> = [
|
|
|
54
54
|
toQuery(toValue("Month"), toValue("New[.]]Mo,nth.")),
|
|
55
55
|
]),
|
|
56
56
|
],
|
|
57
|
+
// Boolean values only treated as bools for query compare values
|
|
58
|
+
[
|
|
59
|
+
`foo.true['true'=true]`,
|
|
60
|
+
toPath([
|
|
61
|
+
toValue("foo"),
|
|
62
|
+
toValue("true"),
|
|
63
|
+
toQuery(toValue("true"), toValue(true)),
|
|
64
|
+
]),
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
`foo.false['false'=false]`,
|
|
68
|
+
toPath([
|
|
69
|
+
toValue("foo"),
|
|
70
|
+
toValue("false"),
|
|
71
|
+
toQuery(toValue("false"), toValue(false)),
|
|
72
|
+
]),
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
`foo.bar[baz == true]`,
|
|
76
|
+
toPath([
|
|
77
|
+
toValue("foo"),
|
|
78
|
+
toValue("bar"),
|
|
79
|
+
toQuery(toValue("baz"), toValue(true)),
|
|
80
|
+
]),
|
|
81
|
+
],
|
|
57
82
|
|
|
58
83
|
// Nested Paths
|
|
59
84
|
["{{foo}}", toPath([toPath([toValue("foo")])])],
|
|
@@ -176,6 +201,12 @@ export const VALID_AST_PARSER_TESTS: Array<[string, PathNode]> = [
|
|
|
176
201
|
toValue("baz"),
|
|
177
202
|
]),
|
|
178
203
|
],
|
|
204
|
+
|
|
205
|
+
// With numbers
|
|
206
|
+
[
|
|
207
|
+
"foo.0[1=2]",
|
|
208
|
+
toPath([toValue("foo"), toValue(0), toQuery(toValue(1), toValue(2))]),
|
|
209
|
+
],
|
|
179
210
|
];
|
|
180
211
|
|
|
181
212
|
export const INVALID_AST_PARSER_TESTS: Array<string> = [
|
|
@@ -27,7 +27,7 @@ export interface QueryNode extends Node<"Query"> {
|
|
|
27
27
|
/** A simple segment */
|
|
28
28
|
export interface ValueNode extends Node<"Value"> {
|
|
29
29
|
/** The segment value */
|
|
30
|
-
value: string | number;
|
|
30
|
+
value: string | number | boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** A nested expression */
|
|
@@ -37,7 +37,7 @@ export interface ExpressionNode extends Node<"Expression"> {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Helper to create a value node */
|
|
40
|
-
export const toValue = (value: string | number): ValueNode => ({
|
|
40
|
+
export const toValue = (value: string | number | boolean): ValueNode => ({
|
|
41
41
|
name: "Value",
|
|
42
42
|
value,
|
|
43
43
|
});
|
|
@@ -76,7 +76,7 @@ export const parse: Parser = (path) => {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
/** get an identifier if you can */
|
|
79
|
-
const identifier = (): ValueNode | undefined => {
|
|
79
|
+
const identifier = (allowBoolValue = false): ValueNode | undefined => {
|
|
80
80
|
if (!isIdentifierChar(ch)) {
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
@@ -91,6 +91,15 @@ export const parse: Parser = (path) => {
|
|
|
91
91
|
value += ch;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
if (allowBoolValue) {
|
|
95
|
+
if (value === "true") {
|
|
96
|
+
return toValue(true);
|
|
97
|
+
}
|
|
98
|
+
if (value === "false") {
|
|
99
|
+
return toValue(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
if (value) {
|
|
95
104
|
const maybeNumber = Number(value);
|
|
96
105
|
value = isNaN(maybeNumber) ? value : maybeNumber;
|
|
@@ -156,7 +165,8 @@ export const parse: Parser = (path) => {
|
|
|
156
165
|
};
|
|
157
166
|
|
|
158
167
|
/** get a simple segment node */
|
|
159
|
-
const simpleSegment = (
|
|
168
|
+
const simpleSegment = (allowBoolValue = false) =>
|
|
169
|
+
nestedPath() ?? expression() ?? identifier(allowBoolValue);
|
|
160
170
|
|
|
161
171
|
/** Parse a segment */
|
|
162
172
|
const segment = ():
|
|
@@ -182,11 +192,9 @@ export const parse: Parser = (path) => {
|
|
|
182
192
|
};
|
|
183
193
|
|
|
184
194
|
/** get an optionally quoted block */
|
|
185
|
-
const optionallyQuotedSegment = (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
| ExpressionNode
|
|
189
|
-
| undefined => {
|
|
195
|
+
const optionallyQuotedSegment = (
|
|
196
|
+
allowBoolValue = false,
|
|
197
|
+
): ValueNode | PathNode | ExpressionNode | undefined => {
|
|
190
198
|
whitespace();
|
|
191
199
|
|
|
192
200
|
// see if we have a quote
|
|
@@ -199,7 +207,7 @@ export const parse: Parser = (path) => {
|
|
|
199
207
|
return id;
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
return simpleSegment();
|
|
210
|
+
return simpleSegment(allowBoolValue);
|
|
203
211
|
};
|
|
204
212
|
|
|
205
213
|
/** eat equals signs */
|
|
@@ -231,7 +239,7 @@ export const parse: Parser = (path) => {
|
|
|
231
239
|
whitespace();
|
|
232
240
|
if (equals()) {
|
|
233
241
|
whitespace();
|
|
234
|
-
const second = optionallyQuotedSegment();
|
|
242
|
+
const second = optionallyQuotedSegment(true);
|
|
235
243
|
value = toQuery(value, second);
|
|
236
244
|
whitespace();
|
|
237
245
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, test } from "vitest";
|
|
1
|
+
import { describe, it, expect, test, vi } from "vitest";
|
|
2
2
|
import { LocalModel, withParser } from "../../data";
|
|
3
3
|
import { ExpressionEvaluator } from "../../expressions";
|
|
4
4
|
import { BindingParser } from "../../binding";
|
|
@@ -1014,4 +1014,64 @@ describe("view", () => {
|
|
|
1014
1014
|
|
|
1015
1015
|
expect(view).toBeDefined();
|
|
1016
1016
|
});
|
|
1017
|
+
|
|
1018
|
+
test("should call onUpdate with undefined when triggering an async update with no resolver", () => {
|
|
1019
|
+
const model = withParser(new LocalModel({}), parseBinding);
|
|
1020
|
+
const evaluator = new ExpressionEvaluator({ model });
|
|
1021
|
+
const schema = new SchemaController();
|
|
1022
|
+
|
|
1023
|
+
const view = new ViewInstance(
|
|
1024
|
+
{
|
|
1025
|
+
id: "view",
|
|
1026
|
+
type: "asset",
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
model,
|
|
1030
|
+
parseBinding,
|
|
1031
|
+
evaluator,
|
|
1032
|
+
schema,
|
|
1033
|
+
},
|
|
1034
|
+
);
|
|
1035
|
+
new StringResolverPlugin().apply(view);
|
|
1036
|
+
|
|
1037
|
+
const onUpdateTap = vi.fn();
|
|
1038
|
+
view.hooks.onUpdate.tap("test", onUpdateTap);
|
|
1039
|
+
|
|
1040
|
+
view.updateAsync("test");
|
|
1041
|
+
|
|
1042
|
+
expect(onUpdateTap).toHaveBeenCalledWith(undefined);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test("should call onUpdate with the view when triggering an async update with a resolver", () => {
|
|
1046
|
+
const model = withParser(new LocalModel({}), parseBinding);
|
|
1047
|
+
const evaluator = new ExpressionEvaluator({ model });
|
|
1048
|
+
const schema = new SchemaController();
|
|
1049
|
+
|
|
1050
|
+
const view = new ViewInstance(
|
|
1051
|
+
{
|
|
1052
|
+
id: "view",
|
|
1053
|
+
type: "asset",
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
model,
|
|
1057
|
+
parseBinding,
|
|
1058
|
+
evaluator,
|
|
1059
|
+
schema,
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
new StringResolverPlugin().apply(view);
|
|
1064
|
+
|
|
1065
|
+
const onUpdateTap = vi.fn();
|
|
1066
|
+
|
|
1067
|
+
// Trigger first update to get resolver ready.
|
|
1068
|
+
view.update();
|
|
1069
|
+
|
|
1070
|
+
view.hooks.onUpdate.tap("test", onUpdateTap);
|
|
1071
|
+
view.updateAsync("test");
|
|
1072
|
+
expect(onUpdateTap).toHaveBeenCalledWith({
|
|
1073
|
+
id: "view",
|
|
1074
|
+
type: "asset",
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1017
1077
|
});
|
|
@@ -62,11 +62,16 @@ export class Builder {
|
|
|
62
62
|
*
|
|
63
63
|
* @param id - the id of async node. It should be identical for each async node
|
|
64
64
|
*/
|
|
65
|
-
static asyncNode(
|
|
65
|
+
static asyncNode(
|
|
66
|
+
id: string,
|
|
67
|
+
flatten = true,
|
|
68
|
+
onValueReceived?: (node: Node.Node) => Node.Node,
|
|
69
|
+
): Node.Async {
|
|
66
70
|
return {
|
|
67
71
|
id,
|
|
68
72
|
type: NodeType.Async,
|
|
69
73
|
flatten: flatten,
|
|
74
|
+
onValueReceived,
|
|
70
75
|
value: {
|
|
71
76
|
type: NodeType.Value,
|
|
72
77
|
value: {
|
package/src/view/parser/index.ts
CHANGED
|
@@ -21,6 +21,47 @@ export interface ParseObjectChildOptions {
|
|
|
21
21
|
parentObj: object;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export type ParserHooks = {
|
|
25
|
+
/**
|
|
26
|
+
* A hook to interact with an object _before_ parsing it into an AST
|
|
27
|
+
*
|
|
28
|
+
* @param value - The object we're are about to parse
|
|
29
|
+
* @returns - A new value to parse.
|
|
30
|
+
* If undefined, the original value is used.
|
|
31
|
+
* If null, we stop parsing this node.
|
|
32
|
+
*/
|
|
33
|
+
onParseObject: SyncWaterfallHook<[object, NodeType]>;
|
|
34
|
+
/**
|
|
35
|
+
* A callback to interact with an AST _after_ we parse it into the AST
|
|
36
|
+
*
|
|
37
|
+
* @param value - The object we parsed
|
|
38
|
+
* @param node - The AST node we generated
|
|
39
|
+
* @returns - A new AST node to use
|
|
40
|
+
* If undefined, the original value is used.
|
|
41
|
+
* If null, we ignore this node all together
|
|
42
|
+
*/
|
|
43
|
+
onCreateASTNode: SyncWaterfallHook<[Node.Node | undefined | null, object]>;
|
|
44
|
+
/** A hook to call when parsing an object into an AST node
|
|
45
|
+
*
|
|
46
|
+
* @param obj - The object we're are about to parse
|
|
47
|
+
* @param nodeType - The type of node we're parsing
|
|
48
|
+
* @param parseOptions - Additional options when parsing
|
|
49
|
+
* @param childOptions - Additional options that are populated when the node being parsed is a child of another node
|
|
50
|
+
* @returns - A new AST node to use
|
|
51
|
+
* If undefined, the original value is used.
|
|
52
|
+
* If null, we ignore this node all together
|
|
53
|
+
*/
|
|
54
|
+
parseNode: SyncBailHook<
|
|
55
|
+
[
|
|
56
|
+
obj: object,
|
|
57
|
+
nodeType: Node.ChildrenTypes,
|
|
58
|
+
parseOptions: ParseObjectOptions,
|
|
59
|
+
childOptions?: ParseObjectChildOptions,
|
|
60
|
+
],
|
|
61
|
+
Node.Node | Node.Child[]
|
|
62
|
+
>;
|
|
63
|
+
};
|
|
64
|
+
|
|
24
65
|
interface NestedObj {
|
|
25
66
|
/** The values of a nested local object */
|
|
26
67
|
children: Node.Child[];
|
|
@@ -32,39 +73,10 @@ interface NestedObj {
|
|
|
32
73
|
* It provides a few ways to interact with the parsing, including mutating an object before and after creation of an AST node
|
|
33
74
|
*/
|
|
34
75
|
export class Parser {
|
|
35
|
-
public readonly hooks = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* @param value - The object we're are about to parse
|
|
40
|
-
* @returns - A new value to parse.
|
|
41
|
-
* If undefined, the original value is used.
|
|
42
|
-
* If null, we stop parsing this node.
|
|
43
|
-
*/
|
|
44
|
-
onParseObject: new SyncWaterfallHook<[object, NodeType]>(),
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A callback to interact with an AST _after_ we parse it into the AST
|
|
48
|
-
*
|
|
49
|
-
* @param value - The object we parsed
|
|
50
|
-
* @param node - The AST node we generated
|
|
51
|
-
* @returns - A new AST node to use
|
|
52
|
-
* If undefined, the original value is used.
|
|
53
|
-
* If null, we ignore this node all together
|
|
54
|
-
*/
|
|
55
|
-
onCreateASTNode: new SyncWaterfallHook<
|
|
56
|
-
[Node.Node | undefined | null, object]
|
|
57
|
-
>(),
|
|
58
|
-
|
|
59
|
-
parseNode: new SyncBailHook<
|
|
60
|
-
[
|
|
61
|
-
obj: object,
|
|
62
|
-
nodeType: Node.ChildrenTypes,
|
|
63
|
-
parseOptions: ParseObjectOptions,
|
|
64
|
-
childOptions?: ParseObjectChildOptions,
|
|
65
|
-
],
|
|
66
|
-
Node.Node | Node.Child[]
|
|
67
|
-
>(),
|
|
76
|
+
public readonly hooks: ParserHooks = {
|
|
77
|
+
onParseObject: new SyncWaterfallHook(),
|
|
78
|
+
onCreateASTNode: new SyncWaterfallHook(),
|
|
79
|
+
parseNode: new SyncBailHook(),
|
|
68
80
|
};
|
|
69
81
|
|
|
70
82
|
public parseView(value: AnyAssetType): Node.View {
|
package/src/view/parser/types.ts
CHANGED
|
@@ -22,6 +22,9 @@ export declare namespace Node {
|
|
|
22
22
|
|
|
23
23
|
/** Every node (outside of the root) contains a reference to it's parent */
|
|
24
24
|
parent?: Node;
|
|
25
|
+
|
|
26
|
+
/** The ids of async nodes resolved within this node */
|
|
27
|
+
asyncNodesResolved?: string[];
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export type PathSegment = string | number;
|
|
@@ -123,6 +126,8 @@ export declare namespace Node {
|
|
|
123
126
|
* Should the content streamed in be flattened during resolving
|
|
124
127
|
*/
|
|
125
128
|
flatten?: boolean;
|
|
129
|
+
/** Function to run against parsed content from the node to manipulate the content before resolving it. */
|
|
130
|
+
onValueReceived?: (node: Node.Node) => Node.Node;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
export interface PluginOptions {
|
|
@@ -35,4 +35,40 @@ describe("multi-node", () => {
|
|
|
35
35
|
}),
|
|
36
36
|
).toMatchSnapshot();
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
it("should parse an array into a multi node", () => {
|
|
40
|
+
expect(
|
|
41
|
+
parser.parseObject([
|
|
42
|
+
{
|
|
43
|
+
asset: {
|
|
44
|
+
type: "type",
|
|
45
|
+
id: "asset-1",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
]),
|
|
49
|
+
).toStrictEqual({
|
|
50
|
+
override: false,
|
|
51
|
+
type: "multi-node",
|
|
52
|
+
values: [
|
|
53
|
+
{
|
|
54
|
+
type: "value",
|
|
55
|
+
value: undefined,
|
|
56
|
+
parent: expect.anything(),
|
|
57
|
+
children: [
|
|
58
|
+
{
|
|
59
|
+
path: ["asset"],
|
|
60
|
+
value: {
|
|
61
|
+
type: "asset",
|
|
62
|
+
parent: expect.anything(),
|
|
63
|
+
value: {
|
|
64
|
+
id: "asset-1",
|
|
65
|
+
type: "type",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
});
|
|
38
74
|
});
|
|
@@ -10,7 +10,7 @@ import { hasTemplateValues, hasTemplateKey } from "../parser/utils";
|
|
|
10
10
|
|
|
11
11
|
/** A view plugin to resolve multi nodes */
|
|
12
12
|
export default class MultiNodePlugin implements ViewPlugin {
|
|
13
|
-
applyParser(parser: Parser) {
|
|
13
|
+
applyParser(parser: Parser): void {
|
|
14
14
|
parser.hooks.parseNode.tap(
|
|
15
15
|
"multi-node",
|
|
16
16
|
(
|
|
@@ -20,8 +20,7 @@ export default class MultiNodePlugin implements ViewPlugin {
|
|
|
20
20
|
childOptions?: ParseObjectChildOptions,
|
|
21
21
|
) => {
|
|
22
22
|
if (
|
|
23
|
-
childOptions &&
|
|
24
|
-
!hasTemplateKey(childOptions.key) &&
|
|
23
|
+
(childOptions === undefined || !hasTemplateKey(childOptions.key)) &&
|
|
25
24
|
Array.isArray(obj)
|
|
26
25
|
) {
|
|
27
26
|
const values = obj
|
|
@@ -37,10 +36,9 @@ export default class MultiNodePlugin implements ViewPlugin {
|
|
|
37
36
|
const multiNode = parser.createASTNode(
|
|
38
37
|
{
|
|
39
38
|
type: NodeType.MultiNode,
|
|
40
|
-
override:
|
|
41
|
-
childOptions
|
|
42
|
-
childOptions.key,
|
|
43
|
-
),
|
|
39
|
+
override:
|
|
40
|
+
childOptions !== undefined &&
|
|
41
|
+
!hasTemplateValues(childOptions.parentObj, childOptions.key),
|
|
44
42
|
values,
|
|
45
43
|
},
|
|
46
44
|
obj,
|
|
@@ -56,18 +54,20 @@ export default class MultiNodePlugin implements ViewPlugin {
|
|
|
56
54
|
});
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
return childOptions === undefined
|
|
58
|
+
? multiNode
|
|
59
|
+
: [
|
|
60
|
+
{
|
|
61
|
+
path: [...childOptions.path, childOptions.key],
|
|
62
|
+
value: multiNode,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
apply(view: ViewInstance) {
|
|
70
|
+
apply(view: ViewInstance): void {
|
|
71
71
|
view.hooks.parser.tap("multi-node", this.applyParser.bind(this));
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, beforeEach, vi, expect } from "vitest";
|
|
2
|
+
import { BindingParser } from "../../../binding";
|
|
3
|
+
import { ExpressionEvaluator } from "../../../expressions";
|
|
4
|
+
import { LocalModel, withParser } from "../../../data";
|
|
5
|
+
import { SchemaController } from "../../../schema";
|
|
6
|
+
import { Resolve, Resolver } from "..";
|
|
7
|
+
import type { Node } from "../../parser";
|
|
8
|
+
import { NodeType, Parser } from "../../parser";
|
|
9
|
+
|
|
10
|
+
const simpleViewWithAsync: Node.View = {
|
|
11
|
+
type: NodeType.View,
|
|
12
|
+
children: [
|
|
13
|
+
{
|
|
14
|
+
path: ["value"],
|
|
15
|
+
value: {
|
|
16
|
+
type: NodeType.Async,
|
|
17
|
+
id: "async-node",
|
|
18
|
+
value: {
|
|
19
|
+
type: NodeType.Value,
|
|
20
|
+
value: {
|
|
21
|
+
id: "async-node",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: ["value"],
|
|
28
|
+
value: {
|
|
29
|
+
type: NodeType.Value,
|
|
30
|
+
value: {
|
|
31
|
+
id: "value-node",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
value: {
|
|
37
|
+
type: "view",
|
|
38
|
+
id: "view",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("Async Node Resolution", () => {
|
|
43
|
+
let resolverOptions: Resolve.ResolverOptions;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
const model = new LocalModel({});
|
|
47
|
+
const parser = new Parser();
|
|
48
|
+
const bindingParser = new BindingParser();
|
|
49
|
+
|
|
50
|
+
resolverOptions = {
|
|
51
|
+
model,
|
|
52
|
+
parseBinding: bindingParser.parse.bind(bindingParser),
|
|
53
|
+
parseNode: parser.parseObject.bind(parser),
|
|
54
|
+
evaluator: new ExpressionEvaluator({
|
|
55
|
+
model: withParser(model, bindingParser.parse),
|
|
56
|
+
}),
|
|
57
|
+
schema: new SchemaController(),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should clear the cache for the async node and its parent when it is updated", () => {
|
|
62
|
+
const beforeResolveFunction = vi.fn((node: Node.Node | null) => node);
|
|
63
|
+
|
|
64
|
+
const resolver = new Resolver(simpleViewWithAsync, resolverOptions);
|
|
65
|
+
resolver.hooks.beforeResolve.tap("test", beforeResolveFunction);
|
|
66
|
+
|
|
67
|
+
// Update once to setup cache
|
|
68
|
+
resolver.update();
|
|
69
|
+
// Should call beforeResolve once for each node.
|
|
70
|
+
expect(beforeResolveFunction).toHaveBeenCalledTimes(3);
|
|
71
|
+
|
|
72
|
+
// Clear call information before next update.
|
|
73
|
+
beforeResolveFunction.mockClear();
|
|
74
|
+
|
|
75
|
+
// Confirm cache by running another update with no changes.
|
|
76
|
+
resolver.update(new Set());
|
|
77
|
+
// Should not need to call before resolve on cached nodes.
|
|
78
|
+
expect(beforeResolveFunction).toHaveBeenCalledTimes(0);
|
|
79
|
+
|
|
80
|
+
// Updating with changes marked on "async-node". Should invalidate cache for itself and its parent.
|
|
81
|
+
resolver.update(new Set(), new Set(["async-node"]));
|
|
82
|
+
// Should be called for the async node and the view parent.
|
|
83
|
+
expect(beforeResolveFunction).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(beforeResolveFunction).toHaveBeenCalledWith(
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
type: NodeType.View,
|
|
87
|
+
value: {
|
|
88
|
+
type: "view",
|
|
89
|
+
id: "view",
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
expect.anything(),
|
|
93
|
+
);
|
|
94
|
+
expect(beforeResolveFunction).toHaveBeenCalledWith(
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
type: NodeType.Async,
|
|
97
|
+
id: "async-node",
|
|
98
|
+
value: {
|
|
99
|
+
type: NodeType.Value,
|
|
100
|
+
value: {
|
|
101
|
+
id: "async-node",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
expect.anything(),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should clear the cache for anything with a matching async node in its resolved list on update", () => {
|
|
110
|
+
const beforeResolveFunction = vi.fn((node: Node.Node | null) => {
|
|
111
|
+
// Add asyncNodesResolved to view to test tracking and invalidation of just the view.
|
|
112
|
+
if (node?.type === NodeType.View) {
|
|
113
|
+
return {
|
|
114
|
+
...node,
|
|
115
|
+
asyncNodesResolved: ["other-async-id"],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return node;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const resolver = new Resolver(simpleViewWithAsync, resolverOptions);
|
|
123
|
+
resolver.hooks.beforeResolve.tap("test", beforeResolveFunction);
|
|
124
|
+
|
|
125
|
+
// Update once to setup cache
|
|
126
|
+
resolver.update();
|
|
127
|
+
// Should call beforeResolve once for each node.
|
|
128
|
+
expect(beforeResolveFunction).toHaveBeenCalledTimes(3);
|
|
129
|
+
|
|
130
|
+
// Clear call information before next update.
|
|
131
|
+
beforeResolveFunction.mockClear();
|
|
132
|
+
|
|
133
|
+
// Confirm cache by running another update with no changes.
|
|
134
|
+
resolver.update(new Set());
|
|
135
|
+
// Should not need to call before resolve on cached nodes.
|
|
136
|
+
expect(beforeResolveFunction).toHaveBeenCalledTimes(0);
|
|
137
|
+
|
|
138
|
+
// Updating with changes marked on "async-node". Should invalidate cache for itself and its parent.
|
|
139
|
+
resolver.update(new Set(), new Set(["other-async-id"]));
|
|
140
|
+
// Should be called just for the view.
|
|
141
|
+
expect(beforeResolveFunction).toHaveBeenCalledOnce();
|
|
142
|
+
expect(beforeResolveFunction).toHaveBeenCalledWith(
|
|
143
|
+
expect.objectContaining({
|
|
144
|
+
type: NodeType.View,
|
|
145
|
+
value: {
|
|
146
|
+
type: "view",
|
|
147
|
+
id: "view",
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
expect.anything(),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|