@player-tools/dsl 0.5.0--canary.62.1176 → 0.5.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.
Files changed (52) hide show
  1. package/dist/cjs/index.cjs +973 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +910 -0
  4. package/dist/index.mjs +910 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +27 -33
  7. package/src/__tests__/asset-api.test.tsx +354 -0
  8. package/src/__tests__/edge-cases.test.tsx +81 -0
  9. package/src/__tests__/helpers/asset-library.tsx +371 -0
  10. package/src/__tests__/helpers/mock-data-refs.ts +21 -0
  11. package/src/__tests__/json.test.ts +12 -0
  12. package/src/__tests__/jsx.test.tsx +138 -0
  13. package/src/__tests__/schema.test.tsx +378 -0
  14. package/src/__tests__/switch.test.tsx +251 -0
  15. package/src/__tests__/template.test.tsx +294 -0
  16. package/src/__tests__/view-api.test.tsx +46 -0
  17. package/src/auto-id.tsx +10 -10
  18. package/src/compiler/__tests__/compiler.test.tsx +264 -0
  19. package/src/compiler/__tests__/schema.test.ts +127 -0
  20. package/src/compiler/compiler.ts +50 -46
  21. package/src/compiler/index.ts +3 -3
  22. package/src/compiler/schema.ts +29 -25
  23. package/src/compiler/types.ts +15 -12
  24. package/src/compiler/utils.ts +7 -7
  25. package/src/components.tsx +17 -12
  26. package/src/index.ts +11 -11
  27. package/src/string-templates/__tests__/binding.test.ts +87 -0
  28. package/src/string-templates/__tests__/edge-cases.test.ts +6 -0
  29. package/src/string-templates/__tests__/expression.test.ts +9 -0
  30. package/src/string-templates/__tests__/react.test.tsx +75 -0
  31. package/src/string-templates/index.ts +40 -22
  32. package/src/switch.tsx +12 -12
  33. package/src/template.tsx +38 -34
  34. package/src/types.ts +5 -5
  35. package/src/utils.tsx +8 -8
  36. package/types/auto-id.d.ts +33 -0
  37. package/types/compiler/compiler.d.ts +27 -0
  38. package/types/compiler/index.d.ts +4 -0
  39. package/types/compiler/schema.d.ts +61 -0
  40. package/types/compiler/types.d.ts +55 -0
  41. package/types/compiler/utils.d.ts +4 -0
  42. package/types/components.d.ts +90 -0
  43. package/types/index.d.ts +12 -0
  44. package/types/string-templates/index.d.ts +50 -0
  45. package/types/switch.d.ts +19 -0
  46. package/types/template.d.ts +20 -0
  47. package/types/types.d.ts +42 -0
  48. package/types/utils.d.ts +33 -0
  49. package/README.md +0 -9
  50. package/dist/index.cjs.js +0 -990
  51. package/dist/index.d.ts +0 -404
  52. package/dist/index.esm.js +0 -923
@@ -0,0 +1,81 @@
1
+ import { test, expect } from "vitest";
2
+ import React from "react";
3
+ import { render, expression as e } from "..";
4
+ import { Collection, Input, Text } from "./helpers/asset-library";
5
+
6
+ test("works with a Component that returns a Fragment of items", async () => {
7
+ const NestedItems = () => {
8
+ return (
9
+ <>
10
+ <Text>Before Input</Text>
11
+ <Input />
12
+ <Text>After Input</Text>
13
+ </>
14
+ );
15
+ };
16
+
17
+ const expected = {
18
+ id: "root",
19
+ type: "collection",
20
+ values: [
21
+ {
22
+ asset: {
23
+ id: "values-0",
24
+ type: "text",
25
+ value: "Before Input",
26
+ },
27
+ },
28
+ {
29
+ asset: {
30
+ id: "values-1",
31
+ type: "input",
32
+ },
33
+ },
34
+ {
35
+ asset: {
36
+ id: "values-2",
37
+ type: "text",
38
+ value: "After Input",
39
+ },
40
+ },
41
+ ],
42
+ };
43
+
44
+ const contentWithFragment = await render(
45
+ <Collection>
46
+ <Collection.Values>
47
+ <NestedItems />
48
+ </Collection.Values>
49
+ </Collection>
50
+ );
51
+
52
+ expect(contentWithFragment.jsonValue).toStrictEqual(expected);
53
+
54
+ const contentWithoutFragment = await render(
55
+ <Collection>
56
+ <Collection.Values>
57
+ <Text>Before Input</Text>
58
+ <Input />
59
+ <Text>After Input</Text>
60
+ </Collection.Values>
61
+ </Collection>
62
+ );
63
+
64
+ expect(contentWithoutFragment.jsonValue).toStrictEqual(expected);
65
+ });
66
+
67
+ test("handles invalid expressions", async () => {
68
+ const App = () => {
69
+ return (
70
+ <Collection>
71
+ <Collection.Values>
72
+ <Text applicability={e`foo() + '`}>Hello</Text>
73
+ </Collection.Values>
74
+ </Collection>
75
+ );
76
+ };
77
+
78
+ await expect(render(<App />)).rejects.toThrow(
79
+ 'Unclosed quote after "" at character 9'
80
+ );
81
+ });
@@ -0,0 +1,371 @@
1
+ import React from "react";
2
+ import type {
3
+ Asset as AssetType,
4
+ AssetWrapper,
5
+ Binding,
6
+ Expression,
7
+ Validation,
8
+ } from "@player-ui/types";
9
+ import type { JsonNode, ValueType } from "react-json-reconciler";
10
+ import {
11
+ ArrayNode,
12
+ ObjectNode,
13
+ PropertyNode,
14
+ ValueNode,
15
+ } from "react-json-reconciler";
16
+ import { Asset, View, createSlot } from "../../components";
17
+ import type { AssetPropsWithChildren, WithChildren } from "../../types";
18
+ import type { BindingTemplateInstance } from "../../string-templates";
19
+
20
+ // #region - Asset Types
21
+
22
+ const ActionRoles = [
23
+ "primary",
24
+ "secondary",
25
+ "tertiary",
26
+ "upsell",
27
+ "back",
28
+ "link",
29
+ ] as const;
30
+
31
+ export type ActionRole = (typeof ActionRoles)[number];
32
+
33
+ export interface TextAsset extends AssetType<"text"> {
34
+ /** value of the text asset */
35
+ value?: string;
36
+ }
37
+
38
+ export interface CollectionAsset extends AssetType<"collection"> {
39
+ /** The collection items to show */
40
+ values?: Array<AssetWrapper>;
41
+
42
+ /** The additional information to show */
43
+ additionalInfo?: AssetWrapper;
44
+
45
+ /** The result text to show */
46
+ resultText?: AssetWrapper;
47
+
48
+ /** The label defining the collection */
49
+ label?: AssetWrapper;
50
+
51
+ /** Actions attached to the collection */
52
+ actions?: Array<AssetWrapper<ActionAsset>>;
53
+ }
54
+
55
+ export interface ActionAsset extends AssetType<"action"> {
56
+ /** The transition value of the action in the state machine */
57
+ value?: string;
58
+
59
+ /** A text-like asset for the action's label */
60
+ label?: AssetWrapper;
61
+
62
+ /** An optional expression to execute before transitioning */
63
+ exp?: Expression;
64
+
65
+ /** more data */
66
+ metaData?: {
67
+ /** Additional data to beacon */
68
+ beacon?: string;
69
+
70
+ /** A semantic hint to render the action in different user contexts */
71
+ role?: ActionRole;
72
+
73
+ /** Force transition to the next view without checking for validation TODO need to update this to support an expression */
74
+ skipValidation?: boolean;
75
+
76
+ /** Size of the button */
77
+ size?: "small" | "medium" | "large";
78
+
79
+ /** true to indicate the button should be disabled */
80
+ disabled?: boolean;
81
+ };
82
+ }
83
+
84
+ export interface InputAsset extends AssetType<"input"> {
85
+ /** The location in the data-model to store the data */
86
+ binding: Binding;
87
+
88
+ /** Placeholder text when there is no value */
89
+ placeholder?: string;
90
+
91
+ /** Asset container for a field label. */
92
+ label?: AssetWrapper;
93
+
94
+ /** More stuff to show under the field */
95
+ note?: AssetWrapper;
96
+ }
97
+
98
+ export interface ArrayProp {
99
+ /** A dummy id */
100
+ id: string;
101
+ }
102
+ export interface AssetWithArrayProp extends AssetType<"assetWithArray"> {
103
+ /** An array of stuff to mimic validations */
104
+ stuff: ArrayProp[];
105
+ /** Want to make sure this will accept a binding */
106
+ optionalNumber?: number;
107
+
108
+ /** and nest into other */
109
+ metaData?: {
110
+ /** more complicated types */
111
+ optionalUnion?:
112
+ | "foo"
113
+ | "bar"
114
+ | {
115
+ /** including unions */
116
+ other?: "bar";
117
+ };
118
+ };
119
+ }
120
+
121
+ // #endregion
122
+
123
+ // #region - Components
124
+
125
+ /** Text asset */
126
+ export const Text = (props: AssetPropsWithChildren<TextAsset>) => {
127
+ return (
128
+ <Asset type="text" {...props}>
129
+ <property name="value">{props.children}</property>
130
+ </Asset>
131
+ );
132
+ };
133
+
134
+ /** get the parent object of a node */
135
+ const getParentObject = (node: JsonNode): ObjectNode | undefined => {
136
+ if (node.type === "object") {
137
+ return node;
138
+ }
139
+
140
+ if (!node.parent) {
141
+ return;
142
+ }
143
+
144
+ return getParentObject(node.parent);
145
+ };
146
+
147
+ /** text modifier */
148
+ const TextModifier = (props: {
149
+ /** value of modifier */
150
+ value: string;
151
+
152
+ /** type of modifier */
153
+ type: string;
154
+
155
+ /** the modifier content */
156
+ children: React.ReactNode;
157
+ }) => {
158
+ const ref = React.useRef<ValueNode<ValueType>>(null);
159
+
160
+ const [modifierName, setModifierName] = React.useState<string>("M0");
161
+
162
+ React.useEffect(() => {
163
+ if (!ref.current) {
164
+ return;
165
+ }
166
+
167
+ const objParent = getParentObject(ref.current);
168
+
169
+ if (objParent?.type === "object") {
170
+ const existingModifierArray = objParent.properties.find(
171
+ (p) => p.keyNode.value === "modifiers" && p.valueNode?.type === "array"
172
+ );
173
+
174
+ const newModifierLength = existingModifierArray
175
+ ? (existingModifierArray.valueNode as ArrayNode)?.items.length
176
+ : 0;
177
+
178
+ const newModifierName = `M${newModifierLength}`;
179
+
180
+ const modifierObject = new ObjectNode();
181
+ modifierObject.properties.push(
182
+ new PropertyNode(new ValueNode("value"), new ValueNode(props.value)),
183
+ new PropertyNode(new ValueNode("type"), new ValueNode(props.type)),
184
+ new PropertyNode(new ValueNode("name"), new ValueNode(newModifierName))
185
+ );
186
+
187
+ if (existingModifierArray) {
188
+ (existingModifierArray.valueNode as ArrayNode)?.items.push(
189
+ modifierObject
190
+ );
191
+ } else {
192
+ const modifiers = new ArrayNode();
193
+ modifiers.items.push(modifierObject);
194
+ objParent.properties.push(
195
+ new PropertyNode(new ValueNode("modifiers"), modifiers)
196
+ );
197
+ }
198
+
199
+ setModifierName(newModifierName);
200
+ }
201
+ }, []);
202
+
203
+ return (
204
+ <value ref={ref}>
205
+ {`[[${modifierName}]]`}
206
+ {props.children}
207
+ {`[[/${modifierName}]]`}
208
+ </value>
209
+ );
210
+ };
211
+
212
+ Text.Modifier = TextModifier;
213
+
214
+ /** collection asset */
215
+ export const Collection = (props: AssetPropsWithChildren<CollectionAsset>) => {
216
+ return <Asset type="collection" {...props} />;
217
+ };
218
+
219
+ /** an asset to test setting keys on arrays */
220
+ export const ArrayProp = (
221
+ props: AssetPropsWithChildren<AssetWithArrayProp>
222
+ ) => {
223
+ return <Asset type="assetWithArray" {...props} />;
224
+ };
225
+
226
+ Collection.Values = createSlot({
227
+ name: "values",
228
+ isArray: true,
229
+ wrapInAsset: true,
230
+ TextComp: Text,
231
+ });
232
+
233
+ Collection.Actions = createSlot({
234
+ name: "actions",
235
+ isArray: true,
236
+ });
237
+
238
+ /** the factory for making a collection */
239
+ const CollectionComp = (props: WithChildren) => {
240
+ return (
241
+ <Collection>
242
+ <Collection.Values>{props.children}</Collection.Values>
243
+ </Collection>
244
+ );
245
+ };
246
+
247
+ Collection.Label = createSlot({
248
+ name: "label",
249
+ TextComp: Text,
250
+ wrapInAsset: true,
251
+ CollectionComp,
252
+ });
253
+
254
+ /** input asset */
255
+ export const Input = (
256
+ props: Omit<AssetPropsWithChildren<InputAsset>, "binding"> & {
257
+ /** A binding type */
258
+ binding?: BindingTemplateInstance;
259
+ }
260
+ ) => {
261
+ const { binding, children, ...rest } = props;
262
+
263
+ return (
264
+ <Asset type="input" {...rest}>
265
+ <property name="binding">{binding?.toValue()}</property>
266
+ {children}
267
+ </Asset>
268
+ );
269
+ };
270
+
271
+ Input.Label = createSlot<{
272
+ /** Some thing not in the asset */
273
+ customLabelProp?: string;
274
+ }>({
275
+ name: "label",
276
+ TextComp: Text,
277
+ wrapInAsset: true,
278
+ CollectionComp,
279
+ });
280
+
281
+ export interface InfoAsset extends AssetType<"info"> {
282
+ /** Top level title for the view. */
283
+ title?: AssetWrapper;
284
+
285
+ /** Displayed below the top level title, typically in smaller text with a different color. */
286
+ subtitle?: AssetWrapper;
287
+
288
+ /** Actions for navigating in between views. */
289
+ actions?: Array<AssetWrapper>;
290
+
291
+ /** Where the main content goes, this should be the body of the page. */
292
+ primaryInfo?: AssetWrapper;
293
+
294
+ /** Below the main content but above the actions, typically used for important links at the bottom of a view. */
295
+ additionalInfo?: AssetWrapper;
296
+
297
+ /** The crossfield validations for this view */
298
+ validation?: Array<
299
+ Pick<
300
+ Validation.CrossfieldReference,
301
+ "type" | "message" | "severity" | "ref"
302
+ > & {
303
+ /** When validations should start to be tracked */
304
+ trigger?: Validation.Trigger;
305
+
306
+ /** Additional props to send down to a Validator */
307
+ [key: string]: unknown;
308
+ }
309
+ >;
310
+ }
311
+
312
+ /** input asset */
313
+ export const Info = (props: AssetPropsWithChildren<InfoAsset>) => {
314
+ const { children, ...rest } = props;
315
+
316
+ return (
317
+ <View type="info" {...rest}>
318
+ {children}
319
+ </View>
320
+ );
321
+ };
322
+
323
+ Info.Title = createSlot<{
324
+ /** Some thing not in the asset */
325
+ customLabelProp?: string;
326
+ }>({
327
+ name: "title",
328
+ TextComp: Text,
329
+ wrapInAsset: true,
330
+ CollectionComp,
331
+ });
332
+
333
+ Info.Subtitle = createSlot<{
334
+ /** Some thing not in the asset */
335
+ customLabelProp?: string;
336
+ }>({
337
+ name: "subtitle",
338
+ TextComp: Text,
339
+ wrapInAsset: true,
340
+ CollectionComp,
341
+ });
342
+
343
+ Info.Actions = createSlot<{
344
+ /** Some thing not in the asset */
345
+ customLabelProp?: string;
346
+ }>({
347
+ name: "title",
348
+ isArray: true,
349
+ wrapInAsset: true,
350
+ CollectionComp,
351
+ });
352
+
353
+ Info.PrimaryInfo = createSlot<{
354
+ /** Some thing not in the asset */
355
+ customLabelProp?: string;
356
+ }>({
357
+ name: "primaryInfo",
358
+ TextComp: Text,
359
+ wrapInAsset: true,
360
+ CollectionComp,
361
+ });
362
+
363
+ Info.AdditionalInfo = createSlot<{
364
+ /** Some thing not in the asset */
365
+ customLabelProp?: string;
366
+ }>({
367
+ name: "additionalInfo",
368
+ TextComp: Text,
369
+ wrapInAsset: true,
370
+ CollectionComp,
371
+ });
@@ -0,0 +1,21 @@
1
+ import type { Language, Schema } from "@player-ui/types";
2
+
3
+ export const FooTypeRef: Language.DataTypeRef = {
4
+ type: "FooType",
5
+ };
6
+
7
+ export const BarTypeRef: Language.DataTypeRef = {
8
+ type: "BarType",
9
+ };
10
+
11
+ export const LocalBazType: Schema.DataType = {
12
+ type: "BazType",
13
+ default: false,
14
+ validation: [
15
+ {
16
+ type: "someValidation",
17
+ message: "some message",
18
+ options: ["1", "2"],
19
+ },
20
+ ],
21
+ };
@@ -0,0 +1,12 @@
1
+ import { test, expect } from "vitest";
2
+ import { fromJSON, toJSON } from "..";
3
+
4
+ test("converts back and forth", () => {
5
+ const testObj = {
6
+ foo: [true, "bar", { key: "val" }],
7
+ bar: null,
8
+ other: 2,
9
+ };
10
+
11
+ expect(toJSON(fromJSON(testObj))).toStrictEqual(testObj);
12
+ });
@@ -0,0 +1,138 @@
1
+ import { test, expect } from "vitest";
2
+ import React from "react";
3
+ import { render } from "react-json-reconciler";
4
+ import { toJsonProperties } from "../utils";
5
+ import { ArrayProp, Collection, Text } from "./helpers/asset-library";
6
+ import { binding as b, expression as e } from "..";
7
+
8
+ const expectedBasicCollection = {
9
+ id: "root",
10
+ type: "collection",
11
+ label: { asset: { id: "label", type: "text", value: "Label" } },
12
+ values: [
13
+ {
14
+ asset: {
15
+ id: "values-0",
16
+ type: "text",
17
+ value: "value-1",
18
+ },
19
+ },
20
+ {
21
+ asset: {
22
+ id: "values-1",
23
+ type: "text",
24
+ value: "value-2",
25
+ },
26
+ },
27
+ ],
28
+ };
29
+
30
+ const expectedTemplateInstanceObjects = {
31
+ page_experience: "@[foo.bar.GetDataResult]@",
32
+ request_uuid: "{{foo.bar.UUID}}",
33
+ };
34
+
35
+ test("works with JSX", async () => {
36
+ const element = (
37
+ <Collection>
38
+ <Collection.Label>
39
+ <Text>Label</Text>
40
+ </Collection.Label>
41
+ <Collection.Values>
42
+ <Text>value-1</Text>
43
+ <Text value="value-2" />
44
+ </Collection.Values>
45
+ </Collection>
46
+ );
47
+
48
+ expect((await render(element)).jsonValue).toStrictEqual(
49
+ expectedBasicCollection
50
+ );
51
+ });
52
+
53
+ test("works for any json props", async () => {
54
+ const testObj = {
55
+ foo: false,
56
+ bar: true,
57
+ other: "",
58
+ };
59
+ expect(
60
+ (await render(<object>{toJsonProperties(testObj)}</object>)).jsonValue
61
+ ).toStrictEqual(testObj);
62
+ });
63
+
64
+ test("works for BindingTemplateInstances and ExpressionTemplateInstances", async () => {
65
+ const testObj = {
66
+ request_uuid: b`foo.bar.UUID`,
67
+ page_experience: e`foo.bar.GetDataResult`,
68
+ };
69
+ expect(
70
+ (await render(<object>{toJsonProperties(testObj)}</object>)).jsonValue
71
+ ).toStrictEqual(expectedTemplateInstanceObjects);
72
+ });
73
+
74
+ test("handles array props", async () => {
75
+ const expected = {
76
+ id: "root",
77
+ type: "assetWithArray",
78
+ stuff: [{ id: "1" }, { id: "2" }],
79
+ };
80
+
81
+ const things = [{ id: "1" }, { id: "2" }];
82
+
83
+ const element = <ArrayProp stuff={things} />;
84
+
85
+ expect((await render(element)).jsonValue).toStrictEqual(expected);
86
+ });
87
+
88
+ test("flattens fragments", async () => {
89
+ const element = (
90
+ <Collection>
91
+ <>
92
+ <Collection.Label>
93
+ <Text>Label</Text>
94
+ </Collection.Label>
95
+ <Collection.Values>
96
+ <>
97
+ <Text>value-1</Text>
98
+ <Text value="value-2" />
99
+ </>
100
+ </Collection.Values>
101
+ </>
102
+ </Collection>
103
+ );
104
+
105
+ expect((await render(element)).jsonValue).toStrictEqual(
106
+ expectedBasicCollection
107
+ );
108
+ });
109
+
110
+ test("can ignore json props", async () => {
111
+ const testObj = {
112
+ foo: b`test.foo`,
113
+ bar: true,
114
+ other: "",
115
+ };
116
+ const processedTestObj = {
117
+ foo: "{{test.foo}}",
118
+ bar: true,
119
+ other: "",
120
+ };
121
+ const unprocessedTestObj = {
122
+ foo: "test.foo",
123
+ bar: true,
124
+ other: "",
125
+ };
126
+ expect(
127
+ (await render(<object>{toJsonProperties(testObj)}</object>)).jsonValue
128
+ ).toStrictEqual(processedTestObj);
129
+ expect(
130
+ (
131
+ await render(
132
+ <object>
133
+ {toJsonProperties(testObj, { propertiesToSkip: ["foo"] })}
134
+ </object>
135
+ )
136
+ ).jsonValue
137
+ ).toStrictEqual(unprocessedTestObj);
138
+ });