@player-tools/dsl 0.0.2-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/README.md +9 -0
- package/dist/index.cjs.js +865 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.esm.js +818 -0
- package/package.json +51 -0
- package/src/auto-id.tsx +136 -0
- package/src/compiler/compiler.ts +219 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/schema.ts +250 -0
- package/src/compiler/types.ts +18 -0
- package/src/components.tsx +234 -0
- package/src/index.ts +10 -0
- package/src/string-templates/index.ts +172 -0
- package/src/switch.tsx +136 -0
- package/src/template.tsx +186 -0
- package/src/types.ts +81 -0
- package/src/utils.tsx +109 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@player-tools/dsl",
|
|
3
|
+
"version": "0.0.2-next.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"react": "^17.0.2"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@player-ui/types": "^0.2.0",
|
|
13
|
+
"@types/mkdirp": "^1.0.2",
|
|
14
|
+
"@types/signale": "^1.4.2",
|
|
15
|
+
"chalk": "^4.0.1",
|
|
16
|
+
"command-line-application": "^0.10.1",
|
|
17
|
+
"fs-extra": "^10.0.0",
|
|
18
|
+
"globby": "^11.0.1",
|
|
19
|
+
"jsonc-parser": "^2.3.1",
|
|
20
|
+
"mkdirp": "^1.0.4",
|
|
21
|
+
"react-flatten-children": "^1.1.2",
|
|
22
|
+
"react-json-reconciler": "^2.0.0",
|
|
23
|
+
"react-merge-refs": "^1.1.0",
|
|
24
|
+
"source-map-js": "^1.0.2",
|
|
25
|
+
"signale": "^1.4.0",
|
|
26
|
+
"ts-node": "^10.4.0",
|
|
27
|
+
"typescript": "4.4.4",
|
|
28
|
+
"tapable-ts": "^0.1.0",
|
|
29
|
+
"dequal": "^2.0.2",
|
|
30
|
+
"@babel/runtime": "7.15.4"
|
|
31
|
+
},
|
|
32
|
+
"main": "dist/index.cjs.js",
|
|
33
|
+
"module": "dist/index.esm.js",
|
|
34
|
+
"typings": "dist/index.d.ts",
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/player-ui/tools"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/player-ui/tools/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://player-ui.github.io",
|
|
45
|
+
"contributors": [
|
|
46
|
+
{
|
|
47
|
+
"name": "Ketan Reddy",
|
|
48
|
+
"url": "https://github.com/KetanReddy"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
package/src/auto-id.tsx
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsonNode } from 'react-json-reconciler';
|
|
3
|
+
import { flattenNodes } from 'react-json-reconciler';
|
|
4
|
+
import { SlotContext } from './components';
|
|
5
|
+
import type { WithChildren } from './types';
|
|
6
|
+
|
|
7
|
+
const IDSuffixContext = React.createContext<string>('root');
|
|
8
|
+
|
|
9
|
+
export const IndexSuffixStopContext = React.createContext<boolean>(false);
|
|
10
|
+
|
|
11
|
+
/** Get the generated id */
|
|
12
|
+
export const useGetIdPrefix = () => {
|
|
13
|
+
return React.useContext(IDSuffixContext);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Add a suffix to a generated id */
|
|
17
|
+
export const IDSuffixProvider = (
|
|
18
|
+
props: WithChildren<{
|
|
19
|
+
/** The suffix to append */
|
|
20
|
+
suffix: string;
|
|
21
|
+
}>
|
|
22
|
+
) => {
|
|
23
|
+
const currentPrefix = useGetIdPrefix();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<IDSuffixContext.Provider
|
|
27
|
+
value={[
|
|
28
|
+
currentPrefix === 'root' ? undefined : currentPrefix,
|
|
29
|
+
props.suffix,
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join('-')}
|
|
33
|
+
>
|
|
34
|
+
{props.children}
|
|
35
|
+
</IDSuffixContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Override the generated id with the supplied one */
|
|
40
|
+
export const IDProvider = (
|
|
41
|
+
props: WithChildren<{
|
|
42
|
+
/** The new id to use */
|
|
43
|
+
id?: string;
|
|
44
|
+
}>
|
|
45
|
+
) => {
|
|
46
|
+
if (props.id) {
|
|
47
|
+
return (
|
|
48
|
+
<IDSuffixContext.Provider value={props.id}>
|
|
49
|
+
{props.children}
|
|
50
|
+
</IDSuffixContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
55
|
+
return <>{props.children}</>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Get the index of an item in a slot */
|
|
59
|
+
export const useIndexInSlot = (ref: React.RefObject<JsonNode>) => {
|
|
60
|
+
const [index, setIndex] = React.useState(-1);
|
|
61
|
+
const slotContext = React.useContext(SlotContext);
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!slotContext?.isArray) {
|
|
65
|
+
throw new Error('Cannot get index in non-array slot');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (ref.current && slotContext?.ref.current?.valueNode?.type === 'array') {
|
|
69
|
+
const allChildren = flattenNodes(
|
|
70
|
+
slotContext.ref.current.valueNode.children
|
|
71
|
+
);
|
|
72
|
+
const foundIndex = allChildren.indexOf(ref.current);
|
|
73
|
+
|
|
74
|
+
if (foundIndex !== index) {
|
|
75
|
+
setIndex(foundIndex);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, [index, ref, slotContext?.isArray, slotContext?.ref]);
|
|
79
|
+
|
|
80
|
+
return index;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Add the index to the id path when in an array slot */
|
|
84
|
+
export const IDSuffixIndexProvider = (
|
|
85
|
+
props: WithChildren<{
|
|
86
|
+
/** The ref to use */
|
|
87
|
+
wrapperRef: React.RefObject<JsonNode>;
|
|
88
|
+
|
|
89
|
+
/** if the suffix is in a template, the id to use */
|
|
90
|
+
templateIndex?: string;
|
|
91
|
+
}>
|
|
92
|
+
) => {
|
|
93
|
+
const slotIndex = useIndexInSlot(props.wrapperRef);
|
|
94
|
+
|
|
95
|
+
const stopIndex = React.useContext(IndexSuffixStopContext);
|
|
96
|
+
|
|
97
|
+
if (stopIndex) {
|
|
98
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
99
|
+
return <>{props.children}</>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<IDSuffixProvider suffix={props.templateIndex ?? String(slotIndex)}>
|
|
104
|
+
<IndexSuffixStopContext.Provider value>
|
|
105
|
+
{props.children}
|
|
106
|
+
</IndexSuffixStopContext.Provider>
|
|
107
|
+
</IDSuffixProvider>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Wrap a slot with the index if in an array slot */
|
|
112
|
+
export const OptionalIDSuffixProvider = (
|
|
113
|
+
props: WithChildren<{
|
|
114
|
+
/** The ref to walk upwards and use as an index */
|
|
115
|
+
wrapperRef: React.RefObject<JsonNode>;
|
|
116
|
+
|
|
117
|
+
/** if the suffix is in a template, the id to use */
|
|
118
|
+
templateIndex?: string;
|
|
119
|
+
}>
|
|
120
|
+
) => {
|
|
121
|
+
const slotContext = React.useContext(SlotContext);
|
|
122
|
+
|
|
123
|
+
if (slotContext?.isArray) {
|
|
124
|
+
return (
|
|
125
|
+
<IDSuffixIndexProvider
|
|
126
|
+
wrapperRef={props.wrapperRef}
|
|
127
|
+
templateIndex={props.templateIndex}
|
|
128
|
+
>
|
|
129
|
+
{props.children}
|
|
130
|
+
</IDSuffixIndexProvider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
135
|
+
return <>{props.children}</>;
|
|
136
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JsonType } from 'react-json-reconciler';
|
|
3
|
+
import { SourceMapGenerator, SourceMapConsumer } from 'source-map-js';
|
|
4
|
+
import { render } from 'react-json-reconciler';
|
|
5
|
+
import type { Flow, View, Navigation as PlayerNav } from '@player-ui/types';
|
|
6
|
+
import { SyncHook } from 'tapable-ts';
|
|
7
|
+
import type { SerializeType } from './types';
|
|
8
|
+
import type { Navigation } from '../types';
|
|
9
|
+
import { SchemaGenerator } from './schema';
|
|
10
|
+
|
|
11
|
+
/** Recursively find BindingTemplateInstance and call toValue on them */
|
|
12
|
+
const parseNavigationExpressions = (nav: Navigation): PlayerNav => {
|
|
13
|
+
/** Same as above but signature changed */
|
|
14
|
+
function replaceExpWithStr(obj: any): any {
|
|
15
|
+
/** call toValue if BindingTemplateInstance otherwise continue */
|
|
16
|
+
function convExp(value: any): any {
|
|
17
|
+
return value && typeof value === 'object' && value.__type === 'expression'
|
|
18
|
+
? value.toValue() // exp, onStart, and onEnd don't need to be wrapped in @[]@
|
|
19
|
+
: replaceExpWithStr(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(obj)) {
|
|
23
|
+
return obj.map(convExp);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof obj === 'object') {
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.entries(obj).map(([key, value]) => [key, convExp(value)])
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return obj;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return replaceExpWithStr(nav);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type SourceMapList = Array<{
|
|
39
|
+
/** The mappings of the original */
|
|
40
|
+
sourceMap: string;
|
|
41
|
+
/**
|
|
42
|
+
* The id of the view we're indexing off of
|
|
43
|
+
* This should be a unique global identifier within the generated code
|
|
44
|
+
* e.g. `"id": "view_0",`
|
|
45
|
+
*/
|
|
46
|
+
offsetIndexSearch: string;
|
|
47
|
+
/** The generated source that produced the map */
|
|
48
|
+
source: string;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
/** Given a list of source maps for all generated views, merge them into 1 */
|
|
52
|
+
const mergeSourceMaps = (
|
|
53
|
+
sourceMaps: SourceMapList,
|
|
54
|
+
generated: string
|
|
55
|
+
): string => {
|
|
56
|
+
const generator = new SourceMapGenerator();
|
|
57
|
+
sourceMaps.forEach(({ sourceMap, offsetIndexSearch, source }) => {
|
|
58
|
+
const generatedLineOffset = generated
|
|
59
|
+
.split('\n')
|
|
60
|
+
.findIndex((line) => line.includes(offsetIndexSearch));
|
|
61
|
+
|
|
62
|
+
const sourceLineOffset = source
|
|
63
|
+
.split('\n')
|
|
64
|
+
.findIndex((line) => line.includes(offsetIndexSearch));
|
|
65
|
+
|
|
66
|
+
const lineOffset = generatedLineOffset - sourceLineOffset;
|
|
67
|
+
|
|
68
|
+
const generatedLine = generated.split('\n')[generatedLineOffset];
|
|
69
|
+
const sourceLine = source.split('\n')[sourceLineOffset];
|
|
70
|
+
|
|
71
|
+
const generatedColumn = generatedLine.indexOf(offsetIndexSearch);
|
|
72
|
+
const sourceColumn = sourceLine.indexOf(offsetIndexSearch);
|
|
73
|
+
const columnOffset = generatedColumn - sourceColumn;
|
|
74
|
+
|
|
75
|
+
const consumer = new SourceMapConsumer(JSON.parse(sourceMap));
|
|
76
|
+
consumer.eachMapping((mapping) => {
|
|
77
|
+
generator.addMapping({
|
|
78
|
+
generated: {
|
|
79
|
+
line: mapping.generatedLine + lineOffset,
|
|
80
|
+
column: mapping.generatedColumn + columnOffset,
|
|
81
|
+
},
|
|
82
|
+
original: {
|
|
83
|
+
line: mapping.originalLine,
|
|
84
|
+
column: mapping.originalColumn,
|
|
85
|
+
},
|
|
86
|
+
source: mapping.source,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return generator.toString();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** A compiler for transforming DSL content into JSON */
|
|
95
|
+
export class DSLCompiler {
|
|
96
|
+
public hooks = {
|
|
97
|
+
schemaGenerator: new SyncHook<[SchemaGenerator]>(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Convert an object (flow, view, schema, etc) into it's JSON representation */
|
|
101
|
+
async serialize(value: unknown): Promise<{
|
|
102
|
+
/** the JSON value of the source */
|
|
103
|
+
value: JsonType | undefined;
|
|
104
|
+
|
|
105
|
+
/** the fingerprinted content type of the source */
|
|
106
|
+
contentType: SerializeType;
|
|
107
|
+
|
|
108
|
+
/** The sourcemap of the content */
|
|
109
|
+
sourceMap?: string;
|
|
110
|
+
}> {
|
|
111
|
+
if (typeof value !== 'object' || value === null) {
|
|
112
|
+
throw new Error('Unable to serialize non-object');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (React.isValidElement(value)) {
|
|
116
|
+
const { jsonValue, sourceMap } = await render(value, {
|
|
117
|
+
collectSourceMap: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
value: jsonValue,
|
|
122
|
+
sourceMap,
|
|
123
|
+
contentType: 'view',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if ('navigation' in value) {
|
|
128
|
+
// Source maps from all the nested views
|
|
129
|
+
// Merge these together before returning
|
|
130
|
+
const allSourceMaps: SourceMapList = [];
|
|
131
|
+
|
|
132
|
+
// Assume this is a flow
|
|
133
|
+
const copiedValue: Flow = {
|
|
134
|
+
...(value as any),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
copiedValue.views = (await Promise.all(
|
|
138
|
+
copiedValue?.views?.map(async (node: any) => {
|
|
139
|
+
const { jsonValue, sourceMap, stringValue } = await render(node, {
|
|
140
|
+
collectSourceMap: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (sourceMap) {
|
|
144
|
+
// Find the line that is the id of the view
|
|
145
|
+
// Use that as the identifier for the sourcemap offset calc
|
|
146
|
+
const searchIdLine = stringValue
|
|
147
|
+
.split('\n')
|
|
148
|
+
.find((line) =>
|
|
149
|
+
line.includes(
|
|
150
|
+
`"id": "${(jsonValue as Record<string, string>).id}"`
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (searchIdLine) {
|
|
155
|
+
allSourceMaps.push({
|
|
156
|
+
sourceMap,
|
|
157
|
+
offsetIndexSearch: searchIdLine,
|
|
158
|
+
source: stringValue,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return jsonValue;
|
|
164
|
+
}) ?? []
|
|
165
|
+
)) as View[];
|
|
166
|
+
|
|
167
|
+
// Go through the flow and sub out any view refs that are react elements w/ the right id
|
|
168
|
+
if ('navigation' in value) {
|
|
169
|
+
Object.entries((value as Flow).navigation).forEach(([navKey, node]) => {
|
|
170
|
+
if (typeof node === 'object') {
|
|
171
|
+
Object.entries(node).forEach(([nodeKey, flowNode]) => {
|
|
172
|
+
if (
|
|
173
|
+
flowNode &&
|
|
174
|
+
typeof flowNode === 'object' &&
|
|
175
|
+
'state_type' in flowNode &&
|
|
176
|
+
flowNode.state_type === 'VIEW' &&
|
|
177
|
+
React.isValidElement(flowNode.ref)
|
|
178
|
+
) {
|
|
179
|
+
const actualViewIndex = (value as Flow).views?.indexOf?.(
|
|
180
|
+
flowNode.ref as any
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (actualViewIndex !== undefined && actualViewIndex > -1) {
|
|
184
|
+
const actualId = copiedValue.views?.[actualViewIndex]?.id;
|
|
185
|
+
|
|
186
|
+
(copiedValue as any).navigation[navKey][nodeKey].ref =
|
|
187
|
+
actualId;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
copiedValue.navigation = parseNavigationExpressions(
|
|
195
|
+
copiedValue.navigation
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (value) {
|
|
200
|
+
return {
|
|
201
|
+
value: copiedValue as JsonType,
|
|
202
|
+
contentType: 'flow',
|
|
203
|
+
sourceMap: mergeSourceMaps(
|
|
204
|
+
allSourceMaps,
|
|
205
|
+
JSON.stringify(copiedValue, null, 2)
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const schemaGenerator = new SchemaGenerator();
|
|
212
|
+
this.hooks.schemaGenerator.call(schemaGenerator);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
value: schemaGenerator.toSchema(value) as JsonType,
|
|
216
|
+
contentType: 'schema',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { Schema, Language } from '@player-ui/types';
|
|
2
|
+
import signale from 'signale';
|
|
3
|
+
import { dequal } from 'dequal';
|
|
4
|
+
import { SyncWaterfallHook } from 'tapable-ts';
|
|
5
|
+
import { binding as b } from '..';
|
|
6
|
+
import type { BindingTemplateInstance } from '../string-templates';
|
|
7
|
+
|
|
8
|
+
const bindingSymbol = Symbol('binding');
|
|
9
|
+
|
|
10
|
+
export const SchemaTypeName = Symbol('Schema Rename');
|
|
11
|
+
|
|
12
|
+
interface SchemaChildren {
|
|
13
|
+
/** Object property that will be used to create the intermediate type */
|
|
14
|
+
name: string;
|
|
15
|
+
|
|
16
|
+
/** Object properties children that will be parsed */
|
|
17
|
+
child: object;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SchemaNode = (Schema.DataType | Language.DataTypeRef) & {
|
|
21
|
+
/** Overwrite the name of the generated type */
|
|
22
|
+
[SchemaTypeName]?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type Guard for the `Schema.DataType` and `Language.DataTypeRef` type
|
|
27
|
+
* A bit hacky but since `Schema.Schema` must have a `Schema.DataType` as
|
|
28
|
+
* the final product we have to call it that even if it is a `Language.DataTypeRef`
|
|
29
|
+
*/
|
|
30
|
+
const isTypeDef = (property: SchemaNode): property is Schema.DataType => {
|
|
31
|
+
return (property as Schema.DataType).type !== undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generator for `Schema.Schema` Objects
|
|
36
|
+
*/
|
|
37
|
+
export class SchemaGenerator {
|
|
38
|
+
private children: Array<SchemaChildren>;
|
|
39
|
+
private generatedDataTypeNames: Map<string, SchemaNode>;
|
|
40
|
+
|
|
41
|
+
public hooks = {
|
|
42
|
+
createSchemaNode: new SyncWaterfallHook<
|
|
43
|
+
[
|
|
44
|
+
node: Schema.DataType,
|
|
45
|
+
originalProperty: Record<string | symbol, unknown>
|
|
46
|
+
]
|
|
47
|
+
>(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
this.children = [];
|
|
52
|
+
this.generatedDataTypeNames = new Map();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts an object to a `Schema.Schema` representation
|
|
57
|
+
* Note: uses iteration to prevent potentially very deep recursion on large objects
|
|
58
|
+
*/
|
|
59
|
+
public toSchema = (schema: any): Schema.Schema => {
|
|
60
|
+
const newSchema: Schema.Schema = {
|
|
61
|
+
ROOT: {},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.children = [];
|
|
65
|
+
this.generatedDataTypeNames.clear();
|
|
66
|
+
|
|
67
|
+
Object.keys(schema).forEach((property) => {
|
|
68
|
+
const subType = schema[property] as SchemaNode;
|
|
69
|
+
newSchema.ROOT[property] = this.hooks.createSchemaNode.call(
|
|
70
|
+
this.processChildren(property, subType),
|
|
71
|
+
subType as any
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
while (this.children.length > 0) {
|
|
76
|
+
const c = this.children.pop();
|
|
77
|
+
if (c === undefined) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { name, child } = c;
|
|
82
|
+
const typeDef = {} as any;
|
|
83
|
+
|
|
84
|
+
Object.keys(child).forEach((property) => {
|
|
85
|
+
const subType = (child as any)[property] as SchemaNode;
|
|
86
|
+
typeDef[property] = this.hooks.createSchemaNode.call(
|
|
87
|
+
this.processChildren(property, subType),
|
|
88
|
+
subType as any
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
newSchema[name] = typeDef;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return newSchema;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Processes the children of an object Node
|
|
99
|
+
* Newly discovered children get added to the provided array
|
|
100
|
+
*/
|
|
101
|
+
private processChildren(
|
|
102
|
+
property: string,
|
|
103
|
+
subType: SchemaNode
|
|
104
|
+
): Schema.DataType {
|
|
105
|
+
if (isTypeDef(subType)) {
|
|
106
|
+
return subType;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let intermediateType;
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(subType)) {
|
|
112
|
+
if (subType.length > 1) {
|
|
113
|
+
signale.warn(
|
|
114
|
+
`Type ${property} has multiple types in array, should only contain one top level object type. Only taking first defined type`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const subTypeName = subType[0][SchemaTypeName] ?? property;
|
|
119
|
+
intermediateType = this.makePlaceholderArrayType(subTypeName);
|
|
120
|
+
this.children.push({ name: intermediateType.type, child: subType[0] });
|
|
121
|
+
} else {
|
|
122
|
+
const subTypeName = subType[SchemaTypeName] ?? property;
|
|
123
|
+
intermediateType = this.makePlaceholderType(subTypeName);
|
|
124
|
+
this.children.push({ name: intermediateType.type, child: subType });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.generatedDataTypeNames.has(intermediateType.type)) {
|
|
128
|
+
if (
|
|
129
|
+
!dequal(
|
|
130
|
+
subType,
|
|
131
|
+
this.generatedDataTypeNames.get(intermediateType.type) as object
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Error: Generated two intermediate types with the name: ${intermediateType.type} that are of different shapes`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// remove last added type since we don't need to reprocess it
|
|
140
|
+
this.children.pop();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.generatedDataTypeNames.set(intermediateType.type, subType);
|
|
144
|
+
return intermediateType;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Make an intermediate `Schema.DataType` object given a name
|
|
149
|
+
*/
|
|
150
|
+
private makePlaceholderType = (typeName: string): Schema.DataType => {
|
|
151
|
+
return {
|
|
152
|
+
type: `${typeName}Type`,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Make an intermediate `Schema.DataType` object with array support given a name
|
|
158
|
+
*/
|
|
159
|
+
private makePlaceholderArrayType(typeName: string): Schema.DataType {
|
|
160
|
+
return {
|
|
161
|
+
type: `${typeName}Type`,
|
|
162
|
+
isArray: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type MakeArrayIntoIndexRef<T extends any[]> = {
|
|
168
|
+
[key: number]: MakeBindingRefable<T[0]>;
|
|
169
|
+
/** Used when referencing bindings from within a template */
|
|
170
|
+
_index_: MakeBindingRefable<T[0]>;
|
|
171
|
+
} & BindingTemplateInstance;
|
|
172
|
+
|
|
173
|
+
export type MakeBindingRefable<T> = {
|
|
174
|
+
[P in keyof T]: T[P] extends any[]
|
|
175
|
+
? MakeArrayIntoIndexRef<T[P]>
|
|
176
|
+
: MakeBindingRefable<T[P]>;
|
|
177
|
+
} & BindingTemplateInstance;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Adds bindings to an object so that the object can be directly used in JSX
|
|
181
|
+
*/
|
|
182
|
+
export function makeBindingsForObject<Type>(
|
|
183
|
+
obj: Type,
|
|
184
|
+
arrayAccessorKeys = ['_index_']
|
|
185
|
+
): MakeBindingRefable<Type> {
|
|
186
|
+
/** Proxy to track binding callbacks */
|
|
187
|
+
const accessor = (paths: string[]) => {
|
|
188
|
+
const bindingMap = new WeakMap<any, BindingTemplateInstance>();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
ownKeys(target: any) {
|
|
192
|
+
return Reflect.ownKeys(target);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
get(target: any, key: any): any {
|
|
196
|
+
const bindingKeys = Object.keys(target);
|
|
197
|
+
|
|
198
|
+
if (!bindingMap.has(target)) {
|
|
199
|
+
bindingMap.set(target, b`${paths.join('.')}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (key === bindingSymbol) {
|
|
203
|
+
return paths;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
Array.isArray(target) &&
|
|
208
|
+
(arrayAccessorKeys.includes(key) || typeof key === 'number')
|
|
209
|
+
) {
|
|
210
|
+
return new Proxy(target[0], accessor(paths.concat([key])));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (bindingKeys.includes(key) && typeof target[key] === 'object') {
|
|
214
|
+
return new Proxy(target[key], accessor(paths.concat([key])));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const createdInstance = bindingMap.get(target) as any;
|
|
218
|
+
return createdInstance?.[key];
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return new Proxy(obj, accessor([])) as MakeBindingRefable<Type>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generates binding for an object property
|
|
228
|
+
*/
|
|
229
|
+
export const getBindingFromObject = (obj: any) => {
|
|
230
|
+
const baseBindings = obj[bindingSymbol] as string[];
|
|
231
|
+
if (!Array.isArray(baseBindings) || baseBindings.length === 0) {
|
|
232
|
+
throw new Error(`Unable to get binding for ${obj}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return b`${baseBindings.join('.')}`;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Returns the binding string from an object path
|
|
240
|
+
*/
|
|
241
|
+
export const getBindingStringFromObject = (obj: any) => {
|
|
242
|
+
return getBindingFromObject(obj).toString();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Returns the ref string from an object path
|
|
247
|
+
*/
|
|
248
|
+
export const getRefStringFromObject = (obj: any) => {
|
|
249
|
+
return getBindingFromObject(obj).toRefString();
|
|
250
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Schema, Navigation, Flow } from '@player-ui/types';
|
|
2
|
+
import type { RemoveUnknownIndex, AddUnknownIndex } from '../types';
|
|
3
|
+
|
|
4
|
+
export type FlowWithoutUnknown = RemoveUnknownIndex<Flow>;
|
|
5
|
+
export type FlowWithReactViews = AddUnknownIndex<
|
|
6
|
+
Omit<FlowWithoutUnknown, 'views'> & {
|
|
7
|
+
/** An array of JSX view elements */
|
|
8
|
+
views?: Array<React.ReactElement>;
|
|
9
|
+
}
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export type SerializeType = 'view' | 'flow' | 'schema' | 'navigation';
|
|
13
|
+
|
|
14
|
+
export type SerializablePlayerExportTypes =
|
|
15
|
+
| React.ReactElement
|
|
16
|
+
| FlowWithReactViews
|
|
17
|
+
| Schema.Schema
|
|
18
|
+
| Navigation;
|