@player-tools/dsl 0.4.0-next.2 → 0.4.0-next.4

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.
@@ -3,11 +3,24 @@ import type { JsonType } from 'react-json-reconciler';
3
3
  import { SourceMapGenerator, SourceMapConsumer } from 'source-map-js';
4
4
  import { render } from 'react-json-reconciler';
5
5
  import type { Flow, View, Navigation as PlayerNav } from '@player-ui/types';
6
- import { SyncHook } from 'tapable-ts';
7
- import type { SerializeType } from './types';
6
+ import {
7
+ AsyncSeriesHook,
8
+ AsyncSeriesWaterfallHook,
9
+ SyncHook,
10
+ } from 'tapable-ts';
11
+ import type { LoggingInterface, SerializeType } from './types';
8
12
  import type { Navigation } from '../types';
9
13
  import { SchemaGenerator } from './schema';
10
14
 
15
+ /**
16
+ * Argument passed to the DSLCompiler onEnd hook
17
+ * Defined as an object so additional fields can be added later without breaking API
18
+ * */
19
+ export interface OnEndArg {
20
+ /** target output directory **/
21
+ output: string;
22
+ }
23
+
11
24
  /** Recursively find BindingTemplateInstance and call toValue on them */
12
25
  const parseNavigationExpressions = (nav: Navigation): PlayerNav => {
13
26
  /** Same as above but signature changed */
@@ -93,10 +106,22 @@ const mergeSourceMaps = (
93
106
 
94
107
  /** A compiler for transforming DSL content into JSON */
95
108
  export class DSLCompiler {
109
+ public readonly logger: LoggingInterface;
96
110
  public hooks = {
111
+ // Hook to access the schema generator instance when initialized
97
112
  schemaGenerator: new SyncHook<[SchemaGenerator]>(),
113
+ // Hook to access pre-compilation object
114
+ preProcessFlow: new AsyncSeriesWaterfallHook<[object]>(),
115
+ // Hook to access post-compilation Flow before output is written
116
+ postProcessFlow: new AsyncSeriesWaterfallHook<[Flow]>(),
117
+ // Hook called after all files are compiled. Revives the output directory
118
+ onEnd: new AsyncSeriesHook<[OnEndArg]>(),
98
119
  };
99
120
 
121
+ constructor(logger?: LoggingInterface) {
122
+ this.logger = logger ?? console;
123
+ }
124
+
100
125
  /** Convert an object (flow, view, schema, etc) into it's JSON representation */
101
126
  async serialize(value: unknown): Promise<{
102
127
  /** the JSON value of the source */
@@ -112,6 +137,9 @@ export class DSLCompiler {
112
137
  throw new Error('Unable to serialize non-object');
113
138
  }
114
139
 
140
+ const schemaGenerator = new SchemaGenerator(this.logger);
141
+ this.hooks.schemaGenerator.call(schemaGenerator);
142
+
115
143
  if (React.isValidElement(value)) {
116
144
  const { jsonValue, sourceMap } = await render(value, {
117
145
  collectSourceMap: true,
@@ -124,14 +152,16 @@ export class DSLCompiler {
124
152
  };
125
153
  }
126
154
 
127
- if ('navigation' in value) {
155
+ const preProcessedValue = await this.hooks.preProcessFlow.call(value);
156
+
157
+ if ('navigation' in preProcessedValue) {
128
158
  // Source maps from all the nested views
129
159
  // Merge these together before returning
130
160
  const allSourceMaps: SourceMapList = [];
131
161
 
132
162
  // Assume this is a flow
133
163
  const copiedValue: Flow = {
134
- ...(value as any),
164
+ ...(preProcessedValue as any),
135
165
  };
136
166
 
137
167
  copiedValue.views = (await Promise.all(
@@ -165,31 +195,37 @@ export class DSLCompiler {
165
195
  )) as View[];
166
196
 
167
197
  // 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;
198
+ if ('navigation' in preProcessedValue) {
199
+ Object.entries((preProcessedValue as Flow).navigation).forEach(
200
+ ([navKey, node]) => {
201
+ if (typeof node === 'object') {
202
+ Object.entries(node).forEach(([nodeKey, flowNode]) => {
203
+ if (
204
+ flowNode &&
205
+ typeof flowNode === 'object' &&
206
+ 'state_type' in flowNode &&
207
+ flowNode.state_type === 'VIEW' &&
208
+ React.isValidElement(flowNode.ref)
209
+ ) {
210
+ const actualViewIndex = (
211
+ preProcessedValue as Flow
212
+ ).views?.indexOf?.(flowNode.ref as any);
213
+
214
+ if (actualViewIndex !== undefined && actualViewIndex > -1) {
215
+ const actualId = copiedValue.views?.[actualViewIndex]?.id;
216
+
217
+ (copiedValue as any).navigation[navKey][nodeKey].ref =
218
+ actualId;
219
+ }
188
220
  }
189
- }
190
- });
221
+ });
222
+ }
191
223
  }
192
- });
224
+ );
225
+
226
+ if ('schema' in preProcessedValue) {
227
+ copiedValue.schema = schemaGenerator.toSchema(copiedValue.schema);
228
+ }
193
229
 
194
230
  copiedValue.navigation = parseNavigationExpressions(
195
231
  copiedValue.navigation
@@ -197,8 +233,12 @@ export class DSLCompiler {
197
233
  }
198
234
 
199
235
  if (value) {
236
+ const postProcessFlow = await this.hooks.postProcessFlow.call(
237
+ copiedValue
238
+ );
239
+
200
240
  return {
201
- value: copiedValue as JsonType,
241
+ value: postProcessFlow as JsonType,
202
242
  contentType: 'flow',
203
243
  sourceMap: mergeSourceMaps(
204
244
  allSourceMaps,
@@ -208,11 +248,8 @@ export class DSLCompiler {
208
248
  }
209
249
  }
210
250
 
211
- const schemaGenerator = new SchemaGenerator();
212
- this.hooks.schemaGenerator.call(schemaGenerator);
213
-
214
251
  return {
215
- value: schemaGenerator.toSchema(value) as JsonType,
252
+ value: schemaGenerator.toSchema(preProcessedValue) as JsonType,
216
253
  contentType: 'schema',
217
254
  };
218
255
  }
@@ -1,7 +1,7 @@
1
1
  import type { Schema, Language } from '@player-ui/types';
2
- import signale from 'signale';
3
2
  import { dequal } from 'dequal';
4
3
  import { SyncWaterfallHook } from 'tapable-ts';
4
+ import type { LoggingInterface } from '..';
5
5
  import { binding as b } from '..';
6
6
  import type { BindingTemplateInstance } from '../string-templates';
7
7
 
@@ -9,6 +9,13 @@ const bindingSymbol = Symbol('binding');
9
9
 
10
10
  export const SchemaTypeName = Symbol('Schema Rename');
11
11
 
12
+ interface GeneratedDataType {
13
+ /** The SchemaNode that was generated */
14
+ node: SchemaNode;
15
+ /** How many times it has been generated */
16
+ count: number;
17
+ }
18
+
12
19
  interface SchemaChildren {
13
20
  /** Object property that will be used to create the intermediate type */
14
21
  name: string;
@@ -36,7 +43,8 @@ const isTypeDef = (property: SchemaNode): property is Schema.DataType => {
36
43
  */
37
44
  export class SchemaGenerator {
38
45
  private children: Array<SchemaChildren>;
39
- private generatedDataTypeNames: Map<string, SchemaNode>;
46
+ private generatedDataTypes: Map<string, GeneratedDataType>;
47
+ private logger: LoggingInterface;
40
48
 
41
49
  public hooks = {
42
50
  createSchemaNode: new SyncWaterfallHook<
@@ -47,9 +55,10 @@ export class SchemaGenerator {
47
55
  >(),
48
56
  };
49
57
 
50
- constructor() {
58
+ constructor(logger?: LoggingInterface) {
51
59
  this.children = [];
52
- this.generatedDataTypeNames = new Map();
60
+ this.generatedDataTypes = new Map();
61
+ this.logger = logger ?? console;
53
62
  }
54
63
 
55
64
  /**
@@ -62,12 +71,12 @@ export class SchemaGenerator {
62
71
  };
63
72
 
64
73
  this.children = [];
65
- this.generatedDataTypeNames.clear();
74
+ this.generatedDataTypes.clear();
66
75
 
67
76
  Object.keys(schema).forEach((property) => {
68
77
  const subType = schema[property] as SchemaNode;
69
78
  newSchema.ROOT[property] = this.hooks.createSchemaNode.call(
70
- this.processChildren(property, subType),
79
+ this.processChild(property, subType),
71
80
  subType as any
72
81
  );
73
82
  });
@@ -84,7 +93,7 @@ export class SchemaGenerator {
84
93
  Object.keys(child).forEach((property) => {
85
94
  const subType = (child as any)[property] as SchemaNode;
86
95
  typeDef[property] = this.hooks.createSchemaNode.call(
87
- this.processChildren(property, subType),
96
+ this.processChild(property, subType),
88
97
  subType as any
89
98
  );
90
99
  });
@@ -98,10 +107,7 @@ export class SchemaGenerator {
98
107
  * Processes the children of an object Node
99
108
  * Newly discovered children get added to the provided array
100
109
  */
101
- private processChildren(
102
- property: string,
103
- subType: SchemaNode
104
- ): Schema.DataType {
110
+ private processChild(property: string, subType: SchemaNode): Schema.DataType {
105
111
  if (isTypeDef(subType)) {
106
112
  return subType;
107
113
  }
@@ -110,7 +116,7 @@ export class SchemaGenerator {
110
116
 
111
117
  if (Array.isArray(subType)) {
112
118
  if (subType.length > 1) {
113
- signale.warn(
119
+ this.logger.warn(
114
120
  `Type ${property} has multiple types in array, should only contain one top level object type. Only taking first defined type`
115
121
  );
116
122
  }
@@ -124,23 +130,34 @@ export class SchemaGenerator {
124
130
  this.children.push({ name: intermediateType.type, child: subType });
125
131
  }
126
132
 
127
- if (this.generatedDataTypeNames.has(intermediateType.type)) {
133
+ if (this.generatedDataTypes.has(intermediateType.type)) {
134
+ const generatedType = this.generatedDataTypes.get(
135
+ intermediateType.type
136
+ ) as GeneratedDataType;
128
137
  if (
129
138
  !dequal(
130
139
  subType,
131
- this.generatedDataTypeNames.get(intermediateType.type) as object
140
+ this.generatedDataTypes.get(intermediateType.type)?.node as object
132
141
  )
133
142
  ) {
134
- throw new Error(
135
- `Error: Generated two intermediate types with the name: ${intermediateType.type} that are of different shapes`
143
+ generatedType.count += 1;
144
+ const newIntermediateType = {
145
+ ...intermediateType,
146
+ type: `${intermediateType.type}${generatedType.count}`,
147
+ };
148
+ this.logger.warn(
149
+ `WARNING: Generated two intermediate types with the name: ${intermediateType.type} that are of different shapes, using artificial type ${newIntermediateType.type}`
136
150
  );
151
+ intermediateType = newIntermediateType;
152
+ this.children.pop();
153
+ this.children.push({ name: intermediateType.type, child: subType });
137
154
  }
138
-
139
- // remove last added type since we don't need to reprocess it
140
- this.children.pop();
141
155
  }
142
156
 
143
- this.generatedDataTypeNames.set(intermediateType.type, subType);
157
+ this.generatedDataTypes.set(intermediateType.type, {
158
+ node: subType,
159
+ count: 1,
160
+ });
144
161
  return intermediateType;
145
162
  }
146
163
 
@@ -1,14 +1,59 @@
1
- import type { Schema, Navigation, Flow } from '@player-ui/types';
1
+ import type {
2
+ Schema,
3
+ Navigation,
4
+ Flow,
5
+ Expression,
6
+ ExpressionObject,
7
+ NavigationFlow,
8
+ NavigationFlowState,
9
+ NavigationFlowViewState,
10
+ } from '@player-ui/types';
2
11
  import type { RemoveUnknownIndex, AddUnknownIndex } from '../types';
3
12
 
13
+ export type NavigationFlowReactViewState = Omit<
14
+ NavigationFlowViewState,
15
+ 'ref'
16
+ > & {
17
+ /** The view element */
18
+ ref: React.ReactElement | string;
19
+ };
20
+
21
+ export type NavFlowState =
22
+ | Exclude<NavigationFlowState, NavigationFlowViewState>
23
+ | NavigationFlowReactViewState;
24
+
25
+ export type NavigationFlowWithReactView = Pick<
26
+ NavigationFlow,
27
+ 'startState' | 'onStart' | 'onEnd'
28
+ > & {
29
+ [key: string]:
30
+ | undefined
31
+ | string
32
+ | Expression
33
+ | ExpressionObject
34
+ | NavFlowState;
35
+ };
36
+
37
+ export type NavigationWithReactViews = Pick<Navigation, 'BEGIN'> &
38
+ Record<string, string | NavigationFlowWithReactView>;
39
+
4
40
  export type FlowWithoutUnknown = RemoveUnknownIndex<Flow>;
5
41
  export type FlowWithReactViews = AddUnknownIndex<
6
- Omit<FlowWithoutUnknown, 'views'> & {
42
+ Omit<FlowWithoutUnknown, 'views' | 'navigation'> & {
7
43
  /** An array of JSX view elements */
8
44
  views?: Array<React.ReactElement>;
45
+
46
+ /** The navigation element */
47
+ navigation?: NavigationWithReactViews;
9
48
  }
10
49
  >;
11
50
 
51
+ export type DSLFlow = FlowWithReactViews;
52
+
53
+ export interface DSLSchema {
54
+ [key: string]: Schema.DataType | DSLSchema;
55
+ }
56
+
12
57
  export type SerializeType = 'view' | 'flow' | 'schema' | 'navigation';
13
58
 
14
59
  export type SerializablePlayerExportTypes =
@@ -16,3 +61,5 @@ export type SerializablePlayerExportTypes =
16
61
  | FlowWithReactViews
17
62
  | Schema.Schema
18
63
  | Navigation;
64
+
65
+ export type LoggingInterface = Pick<Console, 'warn' | 'error' | 'log'>;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import flattenChildren from 'react-flatten-children';
3
3
  import type { ObjectNode, PropertyNode } from 'react-json-reconciler';
4
4
  import mergeRefs from 'react-merge-refs';
5
+ import type { View as ViewType } from '@player-ui/types';
5
6
  import type { PlayerApplicability, WithChildren } from './types';
6
7
  import {
7
8
  IDProvider,
@@ -13,9 +14,24 @@ import {
13
14
  import {
14
15
  normalizeText,
15
16
  normalizeToCollection,
17
+ toJsonElement,
16
18
  toJsonProperties,
17
19
  } from './utils';
18
20
 
21
+ export type AssetProps = PlayerApplicability & {
22
+ /** id of the asset */
23
+ id?: string;
24
+
25
+ /** the asset type */
26
+ type: string;
27
+
28
+ /** Any other properties on the asset */
29
+ children?: React.ReactNode;
30
+
31
+ /** other things that we don't know about */
32
+ [key: string]: unknown;
33
+ };
34
+
19
35
  export const SlotContext = React.createContext<
20
36
  | {
21
37
  /** The property name for the slot */
@@ -64,22 +80,7 @@ export const GeneratedIDProperty = (props: {
64
80
  };
65
81
 
66
82
  /** An asset */
67
- export const Asset = React.forwardRef<
68
- ObjectNode,
69
- {
70
- /** id of the asset */
71
- id?: string;
72
-
73
- /** the asset type */
74
- type: string;
75
-
76
- /** Any other properties on the asset */
77
- children?: React.ReactNode;
78
-
79
- /** other things that we don't know about */
80
- [key: string]: unknown;
81
- } & PlayerApplicability
82
- >((props, ref) => {
83
+ export const Asset = React.forwardRef<ObjectNode, AssetProps>((props, ref) => {
83
84
  const { id, type, applicability, children, ...rest } = props;
84
85
  const slotContext = React.useContext(SlotContext);
85
86
  const localRef = React.useRef<ObjectNode>(null);
@@ -110,7 +111,7 @@ export const Asset = React.forwardRef<
110
111
  value={
111
112
  typeof applicability === 'boolean'
112
113
  ? applicability
113
- : applicability.toRefString()
114
+ : applicability.toValue()
114
115
  }
115
116
  />
116
117
  </property>
@@ -130,6 +131,30 @@ Asset.defaultProps = {
130
131
  children: undefined,
131
132
  };
132
133
 
134
+ export const View = React.forwardRef<ObjectNode, AssetProps & ViewType>(
135
+ (props, ref) => {
136
+ const { validation, children, ...rest } = props;
137
+
138
+ return (
139
+ <Asset ref={ref} {...rest}>
140
+ {validation && (
141
+ <property key="validation" name="validation">
142
+ {toJsonElement(validation, 'validation', {
143
+ propertiesToSkip: ['ref'],
144
+ })}
145
+ </property>
146
+ )}
147
+ {children}
148
+ </Asset>
149
+ );
150
+ }
151
+ );
152
+
153
+ View.defaultProps = {
154
+ id: undefined,
155
+ children: undefined,
156
+ };
157
+
133
158
  /** A component to generate a named property slot */
134
159
  export const Slot = (props: {
135
160
  /** The name of the slot */
@@ -177,5 +177,9 @@ export const expression = (
177
177
  export const isTemplateStringInstance = (
178
178
  val: unknown
179
179
  ): val is ExpressionTemplateInstance | BindingTemplateInstance => {
180
- return typeof val === 'object' && (val as any)[OpaqueIdentifier] === true;
180
+ return (
181
+ val !== null &&
182
+ typeof val === 'object' &&
183
+ (val as any)[OpaqueIdentifier] === true
184
+ );
181
185
  };
package/src/template.tsx CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { OptionalIDSuffixProvider } from './auto-id';
12
12
  import type { BindingTemplateInstance } from './string-templates';
13
13
  import type { WithChildren } from './types';
14
+ import { toJsonElement } from './utils';
14
15
 
15
16
  export interface TemplateContextType {
16
17
  /** The number of nested templates */
@@ -30,6 +31,9 @@ export interface TemplateProps {
30
31
 
31
32
  /** The template value */
32
33
  children: React.ReactNode;
34
+
35
+ /** boolean that specifies whether template should recompute when data changes */
36
+ dynamic?: boolean;
33
37
  }
34
38
 
35
39
  /** Add a template instance to the object */
@@ -127,6 +131,7 @@ const getParentProperty = (node: JsonNode): PropertyNode | undefined => {
127
131
  /** A template allows users to dynamically map over an array of data */
128
132
  export const Template = (props: TemplateProps) => {
129
133
  const baseContext = React.useContext(TemplateContext);
134
+ const dynamicProp = props.dynamic ?? false;
130
135
  const [outputProp, setOutputProp] = React.useState<string | undefined>(
131
136
  props.output
132
137
  );
@@ -175,6 +180,9 @@ export const Template = (props: TemplateProps) => {
175
180
  <property name="data">{props.data.toValue()}</property>
176
181
  <property name="output">{outputProp}</property>
177
182
  <property name="value">{props.children}</property>
183
+ {dynamicProp && (
184
+ <property name="dynamic">{toJsonElement(dynamicProp)}</property>
185
+ )}
178
186
  </object>
179
187
  </TemplateProvider>
180
188
  </OptionalIDSuffixProvider>,
package/src/types.ts CHANGED
@@ -79,3 +79,11 @@ export type Navigation = DeepReplace<
79
79
  Expression,
80
80
  ExpressionTemplateInstance | ExpressionTemplateInstance[] | Expression
81
81
  >;
82
+
83
+ export interface toJsonOptions {
84
+ /**
85
+ * List of string keys that should not be parsed in a special way
86
+ * default is 'applicability'
87
+ */
88
+ propertiesToSkip?: string[];
89
+ }
package/src/utils.tsx CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  isTemplateStringInstance,
4
4
  TemplateStringComponent,
5
5
  } from './string-templates';
6
+ import type { toJsonOptions } from './types';
6
7
 
7
8
  /** Get an array version of the value */
8
9
  export function toArray<T>(val: T | Array<T>): Array<T> {
@@ -10,44 +11,60 @@ export function toArray<T>(val: T | Array<T>): Array<T> {
10
11
  }
11
12
 
12
13
  /** Create a component version */
13
- export function toJsonElement(value: any, index?: number): React.ReactElement {
14
- const keyProp = index === undefined ? null : { key: index };
14
+ export function toJsonElement(
15
+ value: any,
16
+ indexOrKey?: number | string,
17
+ options?: toJsonOptions
18
+ ): React.ReactElement {
19
+ const indexProp = typeof indexOrKey === 'number' ? { key: indexOrKey } : null;
15
20
 
16
21
  if (Array.isArray(value)) {
17
22
  return (
18
- <array {...keyProp}>
19
- {value.map((item, idx) => toJsonElement(item, idx))}
23
+ <array {...indexProp}>
24
+ {value.map((item, idx) => toJsonElement(item, idx, options))}
20
25
  </array>
21
26
  );
22
27
  }
23
28
 
24
29
  /** Allow users to pass in BindingTemplateInstance and ExpressionTemplateInstance directly without turning them into strings first */
25
30
  if (isTemplateStringInstance(value)) {
26
- return <value {...keyProp}>{value.toRefString()}</value>;
31
+ if (
32
+ typeof indexOrKey === 'string' &&
33
+ options?.propertiesToSkip?.includes(indexOrKey)
34
+ ) {
35
+ return <value {...indexProp}>{value.toValue()}</value>;
36
+ }
37
+
38
+ return <value {...indexProp}>{value.toRefString()}</value>;
27
39
  }
28
40
 
29
41
  if (typeof value === 'object' && value !== null) {
30
42
  return (
31
- <obj {...keyProp}>
43
+ <obj {...indexProp}>
32
44
  {Object.keys(value).map((key) => (
33
45
  <property key={key} name={key}>
34
- {toJsonElement(value[key])}
46
+ {toJsonElement(value[key], key, options)}
35
47
  </property>
36
48
  ))}
37
49
  </obj>
38
50
  );
39
51
  }
40
52
 
41
- return <value {...keyProp} value={value} />;
53
+ return <value {...indexProp} value={value} />;
42
54
  }
43
55
 
44
56
  /** Create a fragment for the properties */
45
- export function toJsonProperties(value: Record<string, any>) {
46
- return Object.keys(value).map((key) => (
47
- <property key={key} name={key}>
48
- {toJsonElement(value[key])}
49
- </property>
50
- ));
57
+ export function toJsonProperties(
58
+ value: Record<string, any>,
59
+ options: toJsonOptions = { propertiesToSkip: ['applicability'] }
60
+ ) {
61
+ return Object.keys(value).map((key) => {
62
+ return (
63
+ <property key={key} name={key}>
64
+ {toJsonElement(value[key], key, options)}
65
+ </property>
66
+ );
67
+ });
51
68
  }
52
69
 
53
70
  /** Create a text asset if needed */