@player-ui/player 0.4.0-next.4 → 0.4.0-next.6

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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@player-ui/player",
3
- "version": "0.4.0-next.4",
3
+ "version": "0.4.0-next.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org"
7
7
  },
8
8
  "peerDependencies": {},
9
9
  "dependencies": {
10
- "@player-ui/partial-match-registry": "0.4.0-next.4",
11
- "@player-ui/types": "0.4.0-next.4",
10
+ "@player-ui/partial-match-registry": "0.4.0-next.6",
11
+ "@player-ui/types": "0.4.0-next.6",
12
12
  "dequal": "^2.0.2",
13
13
  "p-defer": "^3.0.0",
14
14
  "queue-microtask": "^1.2.3",
@@ -157,8 +157,6 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
157
157
 
158
158
  this.hooks.onSet.call(normalizedTransaction);
159
159
 
160
- this.hooks.onSet.call(normalizedTransaction);
161
-
162
160
  if (setUpdates.length > 0) {
163
161
  this.hooks.onUpdate.call(setUpdates, options);
164
162
  }
@@ -1,5 +1,5 @@
1
1
  import { SyncWaterfallHook, SyncBailHook } from 'tapable-ts';
2
- import parse from './parser';
2
+ import { parseExpression } from './parser';
3
3
  import * as DEFAULT_EXPRESSION_HANDLERS from './evaluator-functions';
4
4
  import { isExpressionNode } from './types';
5
5
  import type {
@@ -212,7 +212,7 @@ export class ExpressionEvaluator {
212
212
  return this._execAST(storedAST, options);
213
213
  }
214
214
 
215
- const expAST = parse(matchedExp);
215
+ const expAST = parseExpression(matchedExp);
216
216
  this.expressionsCache.set(matchedExp, expAST);
217
217
 
218
218
  return this._execAST(expAST, options);
@@ -1,3 +1,4 @@
1
1
  export * from './evaluator';
2
2
  export * from './types';
3
3
  export * from './utils';
4
+ export * from './parser';
@@ -195,7 +195,15 @@ function isModelRefStart(ch0: number, ch1: number) {
195
195
  }
196
196
 
197
197
  /** Parse out an expression from the string */
198
- export default function parseExpression(expr: string): ExpressionNode {
198
+ export function parseExpression(
199
+ expr: string,
200
+ options?: {
201
+ /** If true (the default), will throw on invalid expressions */
202
+ strict?: boolean;
203
+ }
204
+ ): ExpressionNode {
205
+ const strictMode = options?.strict ?? true;
206
+
199
207
  // `index` stores the character number we are currently at while `length` is a constant
200
208
  // All of the gobbles below will modify `index` as we move along
201
209
  const charAtFunc = expr.charAt;
@@ -789,6 +797,10 @@ export default function parseExpression(expr: string): ExpressionNode {
789
797
  args.push(node);
790
798
  }
791
799
 
800
+ if (charIndex !== termination) {
801
+ throwError(`Expected ${String.fromCharCode(termination)}`, index);
802
+ }
803
+
792
804
  return args;
793
805
  }
794
806
 
@@ -899,37 +911,51 @@ export default function parseExpression(expr: string): ExpressionNode {
899
911
 
900
912
  const nodes = [];
901
913
 
902
- while (index < length) {
903
- const chIndex = exprICode(index);
914
+ try {
915
+ while (index < length) {
916
+ const chIndex = exprICode(index);
917
+
918
+ // Expressions can be separated by semicolons, commas, or just inferred without any
919
+ // separators
920
+ if (chIndex === SEMCOL_CODE || chIndex === COMMA_CODE) {
921
+ index++; // ignore separators
922
+ continue;
923
+ }
924
+
925
+ const node = gobbleExpression();
904
926
 
905
- // Expressions can be separated by semicolons, commas, or just inferred without any
906
- // separators
907
- if (chIndex === SEMCOL_CODE || chIndex === COMMA_CODE) {
908
- index++; // ignore separators
909
- continue;
927
+ // Try to gobble each expression individually
928
+ if (node) {
929
+ nodes.push(node);
930
+ // If we weren't able to find a binary expression and are out of room, then
931
+ // the expression passed in probably has too much
932
+ } else if (index < length) {
933
+ throwError(`Unexpected "${exprI(index)}"`, index);
934
+ }
910
935
  }
911
936
 
912
- const node = gobbleExpression();
937
+ // If there's only one expression just try returning the expression
938
+ if (nodes.length === 1) {
939
+ return nodes[0];
940
+ }
913
941
 
914
- // Try to gobble each expression individually
915
- if (node) {
916
- nodes.push(node);
917
- // If we weren't able to find a binary expression and are out of room, then
918
- // the expression passed in probably has too much
919
- } else if (index < length) {
920
- throwError(`Unexpected "${exprI(index)}"`, index);
942
+ return {
943
+ __id: ExpNodeOpaqueIdentifier,
944
+ type: 'Compound',
945
+ body: nodes,
946
+ location: getLocation(0),
947
+ };
948
+ } catch (e) {
949
+ if (strictMode || !(e instanceof Error)) {
950
+ throw e;
921
951
  }
922
- }
923
952
 
924
- // If there's only one expression just try returning the expression
925
- if (nodes.length === 1) {
926
- return nodes[0];
953
+ return {
954
+ __id: ExpNodeOpaqueIdentifier,
955
+ type: 'Compound',
956
+ body: nodes,
957
+ location: getLocation(0),
958
+ error: e,
959
+ };
927
960
  }
928
-
929
- return {
930
- __id: ExpNodeOpaqueIdentifier,
931
- type: 'Compound',
932
- body: nodes,
933
- location: getLocation(0),
934
- };
935
961
  }
@@ -81,6 +81,12 @@ export interface BaseNode<T> {
81
81
 
82
82
  /** The location of the node in the source expression string */
83
83
  location?: NodeLocation;
84
+
85
+ /**
86
+ * The error that occurred while parsing this node
87
+ * This is only set if the parsing mode is set to non-strict
88
+ */
89
+ error?: Error;
84
90
  }
85
91
 
86
92
  /** A helper interface for nodes that container left and right children */
package/src/player.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  FlowController,
22
22
  } from './controllers';
23
23
  import { FlowExpPlugin } from './plugins/flow-exp-plugin';
24
+ import { DefaultExpPlugin } from './plugins/default-exp-plugin';
24
25
  import type {
25
26
  PlayerFlowState,
26
27
  InProgressState,
@@ -30,8 +31,8 @@ import type {
30
31
  import { NOT_STARTED_STATE } from './types';
31
32
 
32
33
  // Variables injected at build time
33
- const PLAYER_VERSION = '0.4.0-next.4';
34
- const COMMIT = 'e03645f7ab5fbdfab274edcc92542752712afe01';
34
+ const PLAYER_VERSION = '0.4.0-next.6';
35
+ const COMMIT = '380eb6be2e6d61675cf721c4c3e64821099e5fbe';
35
36
 
36
37
  export interface PlayerPlugin {
37
38
  /**
@@ -49,6 +50,18 @@ export interface PlayerPlugin {
49
50
  apply: (player: Player) => void;
50
51
  }
51
52
 
53
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
54
+ export interface ExtendedPlayerPlugin<
55
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
56
+ Assets = void,
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ Views = void,
59
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
60
+ Expressions = void,
61
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
62
+ DataTypes = void
63
+ > {}
64
+
52
65
  export interface PlayerConfigOptions {
53
66
  /** A set of plugins to load */
54
67
  plugins?: PlayerPlugin[];
@@ -117,17 +130,16 @@ export class Player {
117
130
  };
118
131
 
119
132
  constructor(config?: PlayerConfigOptions) {
120
- const initialPlugins: PlayerPlugin[] = [];
121
- const flowExpPlugin = new FlowExpPlugin();
122
-
123
- initialPlugins.push(flowExpPlugin);
124
-
125
133
  if (config?.logger) {
126
134
  this.logger.addHandler(config.logger);
127
135
  }
128
136
 
129
137
  this.config = config || {};
130
- this.config.plugins = [...(this.config.plugins || []), ...initialPlugins];
138
+ this.config.plugins = [
139
+ new DefaultExpPlugin(),
140
+ ...(this.config.plugins || []),
141
+ new FlowExpPlugin(),
142
+ ];
131
143
  this.config.plugins?.forEach((plugin) => {
132
144
  plugin.apply(this);
133
145
  });
@@ -414,19 +426,6 @@ export class Player {
414
426
  });
415
427
  this.hooks.viewController.call(viewController);
416
428
 
417
- /** Gets formatter for given formatName and formats value if found, returns value otherwise */
418
- const formatFunction: ExpressionHandler<[unknown, string], any> = (
419
- ctx,
420
- value,
421
- formatName
422
- ) => {
423
- return (
424
- schema.getFormatterForType({ type: formatName })?.format(value) ?? value
425
- );
426
- };
427
-
428
- expressionEvaluator.addExpressionFunction('format', formatFunction);
429
-
430
429
  return {
431
430
  start: () => {
432
431
  flowController
@@ -0,0 +1,57 @@
1
+ import type { ExpressionHandler, ExpressionType } from '../expressions';
2
+ import type { SchemaController } from '../schema';
3
+ import type { Player, PlayerPlugin } from '../player';
4
+
5
+ /** Gets formatter for given formatName and formats value if found, returns value otherwise */
6
+ const createFormatFunction = (schema: SchemaController) => {
7
+ /**
8
+ * The generated handler for the given schema
9
+ */
10
+ const handler: ExpressionHandler<[unknown, string], any> = (
11
+ ctx,
12
+ value,
13
+ formatName
14
+ ) => {
15
+ return (
16
+ schema.getFormatterForType({ type: formatName })?.format(value) ?? value
17
+ );
18
+ };
19
+
20
+ return handler;
21
+ };
22
+
23
+ /**
24
+ * A plugin that provides the out-of-the-box expressions for player
25
+ */
26
+ export class DefaultExpPlugin implements PlayerPlugin {
27
+ name = 'flow-exp-plugin';
28
+
29
+ apply(player: Player) {
30
+ let formatFunction: ExpressionHandler<[unknown, string]> | undefined;
31
+
32
+ player.hooks.schema.tap(this.name, (schemaController) => {
33
+ formatFunction = createFormatFunction(schemaController);
34
+ });
35
+
36
+ player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
37
+ if (formatFunction) {
38
+ expEvaluator.addExpressionFunction('format', formatFunction);
39
+ }
40
+
41
+ expEvaluator.addExpressionFunction('log', (ctx, ...args) => {
42
+ player.logger.info(...args);
43
+ });
44
+
45
+ expEvaluator.addExpressionFunction('debug', (ctx, ...args) => {
46
+ player.logger.debug(...args);
47
+ });
48
+
49
+ expEvaluator.addExpressionFunction(
50
+ 'eval',
51
+ (ctx, ...args: [ExpressionType]) => {
52
+ return ctx.evaluate(...args);
53
+ }
54
+ );
55
+ });
56
+ }
57
+ }
@@ -11,8 +11,8 @@ const identify = (val: any) => val;
11
11
  /** Expand the authored schema into a set of paths -> DataTypes */
12
12
  export function parse(
13
13
  schema: SchemaType.Schema
14
- ): Map<string, SchemaType.DataType> {
15
- const expandedPaths = new Map<string, SchemaType.DataType>();
14
+ ): Map<string, SchemaType.DataTypes> {
15
+ const expandedPaths = new Map<string, SchemaType.DataTypes>();
16
16
 
17
17
  if (!schema.ROOT) {
18
18
  return expandedPaths;
@@ -62,6 +62,10 @@ export function parse(
62
62
  nestedPath.push('[]');
63
63
  }
64
64
 
65
+ if (type.isRecord) {
66
+ nestedPath.push('{}');
67
+ }
68
+
65
69
  if (type.type && schema[type.type]) {
66
70
  parseQueue.push({
67
71
  path: nestedPath,
@@ -85,14 +89,14 @@ export class SchemaController implements ValidationProvider {
85
89
  new Map();
86
90
 
87
91
  private types: Map<string, SchemaType.DataType<any>> = new Map();
88
- public readonly schema: Map<string, SchemaType.DataType> = new Map();
92
+ public readonly schema: Map<string, SchemaType.DataTypes> = new Map();
89
93
 
90
94
  private bindingSchemaNormalizedCache: Map<BindingInstance, string> =
91
95
  new Map();
92
96
 
93
97
  public readonly hooks = {
94
98
  resolveTypeForBinding: new SyncWaterfallHook<
95
- [SchemaType.DataType | undefined, BindingInstance]
99
+ [SchemaType.DataTypes | undefined, BindingInstance]
96
100
  >(),
97
101
  };
98
102
 
@@ -135,17 +139,32 @@ export class SchemaController implements ValidationProvider {
135
139
  return cached;
136
140
  }
137
141
 
138
- const normalized = binding
139
- .asArray()
142
+ let bindingArray = binding.asArray();
143
+ let normalized = bindingArray
140
144
  .map((p) => (typeof p === 'number' ? '[]' : p))
141
145
  .join('.');
142
146
 
143
- this.bindingSchemaNormalizedCache.set(binding, normalized);
147
+ if (normalized) {
148
+ this.bindingSchemaNormalizedCache.set(binding, normalized);
149
+ bindingArray = normalized.split('.');
150
+ }
151
+
152
+ bindingArray.forEach((item) => {
153
+ const recordBinding = bindingArray
154
+ .map((p) => (p === item ? '{}' : p))
155
+ .join('.');
156
+
157
+ if (this.schema.get(recordBinding)) {
158
+ this.bindingSchemaNormalizedCache.set(binding, recordBinding);
159
+ bindingArray = recordBinding.split('.');
160
+ normalized = recordBinding;
161
+ }
162
+ });
144
163
 
145
164
  return normalized;
146
165
  }
147
166
 
148
- public getType(binding: BindingInstance): SchemaType.DataType | undefined {
167
+ public getType(binding: BindingInstance): SchemaType.DataTypes | undefined {
149
168
  return this.hooks.resolveTypeForBinding.call(
150
169
  this.schema.get(this.normalizeBinding(binding)),
151
170
  binding
@@ -154,7 +173,7 @@ export class SchemaController implements ValidationProvider {
154
173
 
155
174
  public getApparentType(
156
175
  binding: BindingInstance
157
- ): SchemaType.DataType | undefined {
176
+ ): SchemaType.DataTypes | undefined {
158
177
  const schemaType = this.getType(binding);
159
178
 
160
179
  if (schemaType === undefined) {
@@ -6,11 +6,17 @@ const DOUBLE_OPEN_CURLY = '{{';
6
6
  const DOUBLE_CLOSE_CURLY = '}}';
7
7
 
8
8
  export interface Options {
9
- /** The model to use when resolving refs */
10
- model: DataModelWithParser;
11
-
12
- /** A function to evaluate an expression */
13
- evaluate: (exp: Expression) => any;
9
+ /**
10
+ * The model to use when resolving refs
11
+ * Passing `false` will skip trying to resolve any direct model refs ({{foo}})
12
+ */
13
+ model: false | DataModelWithParser;
14
+
15
+ /**
16
+ * A function to evaluate an expression
17
+ * Passing `false` will skip trying to evaluate any expressions (@[ foo() ]@)
18
+ */
19
+ evaluate: false | ((exp: Expression) => any);
14
20
  }
15
21
 
16
22
  /** Search the given string for the coordinates of the next expression to resolve */
@@ -70,6 +76,10 @@ export function resolveExpressionsInString(
70
76
  val: string,
71
77
  { evaluate }: Options
72
78
  ): string {
79
+ if (!evaluate) {
80
+ return val;
81
+ }
82
+
73
83
  const expMatch = /@\[.*?\]@/;
74
84
  let newVal = val;
75
85
  let match = newVal.match(expMatch);
@@ -110,6 +120,7 @@ export function resolveDataRefsInString(val: string, options: Options): string {
110
120
  let workingString = resolveExpressionsInString(val, options);
111
121
 
112
122
  if (
123
+ !model ||
113
124
  typeof workingString !== 'string' ||
114
125
  workingString.indexOf(DOUBLE_OPEN_CURLY) === -1
115
126
  ) {
@@ -166,13 +177,13 @@ function traverseObject<T>(val: T, options: Options): T {
166
177
  let newVal = val;
167
178
 
168
179
  if (keys.length > 0) {
169
- for (const key of keys) {
180
+ keys.forEach((key) => {
170
181
  newVal = setIn(
171
182
  newVal as any,
172
183
  [key],
173
184
  traverseObject((val as any)[key], options)
174
185
  ) as any;
175
- }
186
+ });
176
187
  }
177
188
 
178
189
  return newVal;
@@ -72,7 +72,10 @@ export class ValidationMiddleware implements DataModelMiddleware {
72
72
  } else if (validations instanceof Set) {
73
73
  validations.forEach((validation) => {
74
74
  invalidBindings.push(validation.binding);
75
- if (!validation.isStrong) {
75
+ if (
76
+ !validation.isStrong &&
77
+ validation.binding.asString() === binding.asString()
78
+ ) {
76
79
  nextTransaction.push([validation.binding, value]);
77
80
  }
78
81
  });
@@ -120,7 +120,13 @@ export class Parser {
120
120
 
121
121
  const objEntries = Array.isArray(localObj)
122
122
  ? localObj.map((v, i) => [i, v])
123
- : Object.entries(localObj);
123
+ : [
124
+ ...Object.entries(localObj),
125
+ ...Object.getOwnPropertySymbols(localObj).map((s) => [
126
+ s,
127
+ (localObj as any)[s],
128
+ ]),
129
+ ];
124
130
 
125
131
  const defaultValue: NestedObj = {
126
132
  children: [],