@player-ui/player 0.4.0 → 0.4.1-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.
Files changed (47) hide show
  1. package/dist/index.cjs.js +795 -390
  2. package/dist/index.d.ts +238 -81
  3. package/dist/index.esm.js +787 -388
  4. package/dist/player.dev.js +4768 -5282
  5. package/dist/player.prod.js +1 -1
  6. package/package.json +12 -3
  7. package/src/binding/binding.ts +8 -0
  8. package/src/binding/index.ts +14 -4
  9. package/src/binding/resolver.ts +1 -1
  10. package/src/binding-grammar/custom/index.ts +17 -9
  11. package/src/controllers/constants/index.ts +9 -5
  12. package/src/controllers/{data.ts → data/controller.ts} +60 -61
  13. package/src/controllers/data/index.ts +1 -0
  14. package/src/controllers/data/utils.ts +42 -0
  15. package/src/controllers/flow/controller.ts +16 -12
  16. package/src/controllers/flow/flow.ts +6 -1
  17. package/src/controllers/index.ts +1 -1
  18. package/src/controllers/validation/binding-tracker.ts +42 -19
  19. package/src/controllers/validation/controller.ts +359 -145
  20. package/src/controllers/view/asset-transform.ts +4 -1
  21. package/src/controllers/view/controller.ts +20 -3
  22. package/src/data/dependency-tracker.ts +14 -0
  23. package/src/data/local-model.ts +25 -1
  24. package/src/data/model.ts +55 -8
  25. package/src/data/noop-model.ts +2 -0
  26. package/src/expressions/evaluator-functions.ts +24 -2
  27. package/src/expressions/evaluator.ts +37 -33
  28. package/src/expressions/index.ts +1 -0
  29. package/src/expressions/parser.ts +53 -27
  30. package/src/expressions/types.ts +23 -5
  31. package/src/expressions/utils.ts +19 -0
  32. package/src/player.ts +47 -48
  33. package/src/plugins/default-exp-plugin.ts +57 -0
  34. package/src/plugins/flow-exp-plugin.ts +2 -2
  35. package/src/schema/schema.ts +28 -9
  36. package/src/string-resolver/index.ts +25 -9
  37. package/src/types.ts +6 -3
  38. package/src/validator/binding-map-splice.ts +59 -0
  39. package/src/validator/index.ts +1 -0
  40. package/src/validator/types.ts +11 -3
  41. package/src/validator/validation-middleware.ts +38 -4
  42. package/src/view/parser/index.ts +51 -3
  43. package/src/view/plugins/applicability.ts +1 -1
  44. package/src/view/plugins/string-resolver.ts +8 -4
  45. package/src/view/plugins/template-plugin.ts +1 -6
  46. package/src/view/resolver/index.ts +119 -54
  47. package/src/view/resolver/types.ts +48 -7
@@ -92,7 +92,10 @@ export class AssetTransformCorePlugin {
92
92
  const transform = this.registry.get(node.value);
93
93
 
94
94
  if (transform?.beforeResolve) {
95
- const store = getStore(node, this.beforeResolveSymbol);
95
+ const store = getStore(
96
+ options.node ?? node,
97
+ this.beforeResolveSymbol
98
+ );
96
99
 
97
100
  return transform.beforeResolve(node, options, store);
98
101
  }
@@ -8,7 +8,7 @@ import type { Resolve } from '../../view';
8
8
  import { ViewInstance } from '../../view';
9
9
  import type { Logger } from '../../logger';
10
10
  import type { FlowInstance, FlowController } from '../flow';
11
- import type { DataController } from '../data';
11
+ import type { DataController } from '../data/controller';
12
12
  import { AssetTransformCorePlugin } from './asset-transform';
13
13
  import type { TransformRegistry } from './types';
14
14
  import type { BindingInstance } from '../../binding';
@@ -75,14 +75,31 @@ export class ViewController {
75
75
  }
76
76
  );
77
77
 
78
- options.model.hooks.onUpdate.tap('viewController', (updates) => {
78
+ /** Trigger a view update */
79
+ const update = (updates: Set<BindingInstance>) => {
79
80
  if (this.currentView) {
80
81
  if (this.optimizeUpdates) {
81
- this.queueUpdate(new Set(updates.map((t) => t.binding)));
82
+ this.queueUpdate(updates);
82
83
  } else {
83
84
  this.currentView.update();
84
85
  }
85
86
  }
87
+ };
88
+
89
+ options.model.hooks.onUpdate.tap('viewController', (updates) => {
90
+ update(new Set(updates.map((t) => t.binding)));
91
+ });
92
+
93
+ options.model.hooks.onDelete.tap('viewController', (binding) => {
94
+ const parentBinding = binding.parent();
95
+ const property = binding.key();
96
+
97
+ // Deleting an array item will trigger an update for the entire array
98
+ if (typeof property === 'number' && parentBinding) {
99
+ update(new Set([parentBinding]));
100
+ } else {
101
+ update(new Set([binding]));
102
+ }
86
103
  });
87
104
  }
88
105
 
@@ -157,6 +157,15 @@ export class DependencyMiddleware
157
157
 
158
158
  return next?.get(binding, options);
159
159
  }
160
+
161
+ public delete(
162
+ binding: BindingInstance,
163
+ options?: DataModelOptions,
164
+ next?: DataModelImpl | undefined
165
+ ) {
166
+ this.addWriteDep(binding);
167
+ return next?.delete(binding, options);
168
+ }
160
169
  }
161
170
 
162
171
  /** A data-model that tracks dependencies of read/written data */
@@ -184,4 +193,9 @@ export class DependencyModel<Options = DataModelOptions>
184
193
 
185
194
  return this.rootModel.get(binding, options);
186
195
  }
196
+
197
+ public delete(binding: BindingInstance, options?: Options) {
198
+ this.addWriteDep(binding);
199
+ return this.rootModel.delete(binding, options);
200
+ }
187
201
  }
@@ -1,5 +1,5 @@
1
1
  import get from 'dlv';
2
- import { setIn } from 'timm';
2
+ import { setIn, omit, removeAt } from 'timm';
3
3
  import type { BindingInstance } from '../binding';
4
4
  import type { BatchSetTransaction, DataModelImpl, Updates } from './model';
5
5
 
@@ -38,4 +38,28 @@ export class LocalModel implements DataModelImpl {
38
38
  });
39
39
  return effectiveOperations;
40
40
  }
41
+
42
+ public delete(binding: BindingInstance) {
43
+ const parentBinding = binding.parent();
44
+
45
+ if (parentBinding) {
46
+ const parentValue = this.get(parentBinding);
47
+
48
+ if (parentValue !== undefined) {
49
+ if (Array.isArray(parentValue)) {
50
+ this.model = setIn(
51
+ this.model,
52
+ parentBinding.asArray(),
53
+ removeAt(parentValue, binding.key() as number)
54
+ ) as any;
55
+ } else {
56
+ this.model = setIn(
57
+ this.model,
58
+ parentBinding.asArray(),
59
+ omit(parentValue, binding.key() as string)
60
+ ) as any;
61
+ }
62
+ }
63
+ }
64
+ }
41
65
  }
package/src/data/model.ts CHANGED
@@ -56,11 +56,13 @@ export interface DataModelOptions {
56
56
  export interface DataModelWithParser<Options = DataModelOptions> {
57
57
  get(binding: BindingLike, options?: Options): any;
58
58
  set(transaction: [BindingLike, any][], options?: Options): Updates;
59
+ delete(binding: BindingLike, options?: Options): void;
59
60
  }
60
61
 
61
62
  export interface DataModelImpl<Options = DataModelOptions> {
62
63
  get(binding: BindingInstance, options?: Options): any;
63
64
  set(transaction: BatchSetTransaction, options?: Options): Updates;
65
+ delete(binding: BindingInstance, options?: Options): void;
64
66
  }
65
67
 
66
68
  export interface DataModelMiddleware {
@@ -72,11 +74,19 @@ export interface DataModelMiddleware {
72
74
  options?: DataModelOptions,
73
75
  next?: DataModelImpl
74
76
  ): Updates;
77
+
75
78
  get(
76
79
  binding: BindingInstance,
77
80
  options?: DataModelOptions,
78
81
  next?: DataModelImpl
79
82
  ): any;
83
+
84
+ delete?(
85
+ binding: BindingInstance,
86
+ options?: DataModelOptions,
87
+ next?: DataModelImpl
88
+ ): void;
89
+
80
90
  reset?(): void;
81
91
  }
82
92
 
@@ -86,12 +96,16 @@ export function withParser<Options = unknown>(
86
96
  parseBinding: BindingFactory
87
97
  ): DataModelWithParser<Options> {
88
98
  /** Parse something into a binding if it requires it */
89
- function maybeParse(binding: BindingLike): BindingInstance {
99
+ function maybeParse(
100
+ binding: BindingLike,
101
+ readOnly: boolean
102
+ ): BindingInstance {
90
103
  const parsed = isBinding(binding)
91
104
  ? binding
92
105
  : parseBinding(binding, {
93
106
  get: model.get,
94
107
  set: model.set,
108
+ readOnly,
95
109
  });
96
110
 
97
111
  if (!parsed) {
@@ -103,14 +117,17 @@ export function withParser<Options = unknown>(
103
117
 
104
118
  return {
105
119
  get(binding, options?: Options) {
106
- return model.get(maybeParse(binding), options);
120
+ return model.get(maybeParse(binding, true), options);
107
121
  },
108
122
  set(transaction, options?: Options) {
109
123
  return model.set(
110
- transaction.map(([key, val]) => [maybeParse(key), val]),
124
+ transaction.map(([key, val]) => [maybeParse(key, false), val]),
111
125
  options
112
126
  );
113
127
  },
128
+ delete(binding, options?: Options) {
129
+ return model.delete(maybeParse(binding, false), options);
130
+ },
114
131
  };
115
132
  }
116
133
 
@@ -125,10 +142,33 @@ export function toModel(
125
142
  }
126
143
 
127
144
  return {
128
- get: (binding: BindingInstance, options?: DataModelOptions) =>
129
- middleware.get(binding, options ?? defaultOptions, next),
130
- set: (transaction: BatchSetTransaction, options?: DataModelOptions) =>
131
- middleware.set(transaction, options ?? defaultOptions, next),
145
+ get: (binding: BindingInstance, options?: DataModelOptions) => {
146
+ const resolvedOptions = options ?? defaultOptions;
147
+
148
+ if (middleware.get) {
149
+ return middleware.get(binding, resolvedOptions, next);
150
+ }
151
+
152
+ return next?.get(binding, resolvedOptions);
153
+ },
154
+ set: (transaction: BatchSetTransaction, options?: DataModelOptions) => {
155
+ const resolvedOptions = options ?? defaultOptions;
156
+
157
+ if (middleware.set) {
158
+ return middleware.set(transaction, resolvedOptions, next);
159
+ }
160
+
161
+ return next?.set(transaction, resolvedOptions);
162
+ },
163
+ delete: (binding: BindingInstance, options?: DataModelOptions) => {
164
+ const resolvedOptions = options ?? defaultOptions;
165
+
166
+ if (middleware.delete) {
167
+ return middleware.delete(binding, resolvedOptions, next);
168
+ }
169
+
170
+ return next?.delete(binding, resolvedOptions);
171
+ },
132
172
  };
133
173
  }
134
174
 
@@ -145,7 +185,7 @@ export function constructModelForPipeline(
145
185
  }
146
186
 
147
187
  if (pipeline.length === 1) {
148
- return pipeline[0];
188
+ return toModel(pipeline[0]);
149
189
  }
150
190
 
151
191
  /** Default and propagate the options into the nested calls */
@@ -166,6 +206,9 @@ export function constructModelForPipeline(
166
206
  set: (transaction, options) => {
167
207
  return createModelWithOptions(options)?.set(transaction, options);
168
208
  },
209
+ delete: (binding, options) => {
210
+ return createModelWithOptions(options)?.delete(binding, options);
211
+ },
169
212
  };
170
213
  }
171
214
 
@@ -218,4 +261,8 @@ export class PipelinedDataModel implements DataModelImpl {
218
261
  public get(binding: BindingInstance, options?: DataModelOptions): any {
219
262
  return this.effectiveDataModel.get(binding, options);
220
263
  }
264
+
265
+ public delete(binding: BindingInstance, options?: DataModelOptions): void {
266
+ return this.effectiveDataModel.delete(binding, options);
267
+ }
221
268
  }
@@ -12,6 +12,8 @@ export class NOOPDataModel implements DataModelImpl {
12
12
  set() {
13
13
  return [];
14
14
  }
15
+
16
+ delete() {}
15
17
  }
16
18
 
17
19
  /** You only really need 1 instance of the NOOP model */
@@ -1,7 +1,11 @@
1
1
  import type { Binding } from '@player-ui/types';
2
2
 
3
3
  import type { BindingLike } from '../binding';
4
- import type { ExpressionHandler, ExpressionContext } from './types';
4
+ import type {
5
+ ExpressionHandler,
6
+ ExpressionContext,
7
+ ExpressionNode,
8
+ } from './types';
5
9
 
6
10
  /** Sets a value to the data-model */
7
11
  export const setDataVal: ExpressionHandler<[Binding, any], any> = (
@@ -25,5 +29,23 @@ export const deleteDataVal: ExpressionHandler<[Binding], void> = (
25
29
  _context: ExpressionContext,
26
30
  binding
27
31
  ) => {
28
- return _context.model.set([[binding as BindingLike, undefined]]);
32
+ return _context.model.delete(binding);
29
33
  };
34
+
35
+ /** Conditional expression handler */
36
+ export const conditional: ExpressionHandler<
37
+ [ExpressionNode, ExpressionNode, ExpressionNode?]
38
+ > = (ctx, condition, ifTrue, ifFalse) => {
39
+ const resolution = ctx.evaluate(condition);
40
+ if (resolution) {
41
+ return ctx.evaluate(ifTrue);
42
+ }
43
+
44
+ if (ifFalse) {
45
+ return ctx.evaluate(ifFalse);
46
+ }
47
+
48
+ return null;
49
+ };
50
+
51
+ conditional.resolveParams = false;
@@ -1,7 +1,8 @@
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
+ import { isObjectExpression } from './utils';
5
6
  import type {
6
7
  ExpressionNode,
7
8
  BinaryOperator,
@@ -71,6 +72,11 @@ const DEFAULT_UNARY_OPERATORS: Record<string, UnaryOperator> = {
71
72
  export interface HookOptions extends ExpressionContext {
72
73
  /** Given an expression node */
73
74
  resolveNode: (node: ExpressionNode) => any;
75
+
76
+ /** Enabling this flag skips calling the onError hook, and just throws errors back to the caller.
77
+ * The caller is responsible for handling the error.
78
+ */
79
+ throwErrors?: boolean;
74
80
  }
75
81
 
76
82
  export type ExpressionEvaluatorOptions = Omit<
@@ -92,6 +98,12 @@ export class ExpressionEvaluator {
92
98
  /** Resolve an AST node for an expression to a value */
93
99
  resolve: new SyncWaterfallHook<[any, ExpressionNode, HookOptions]>(),
94
100
 
101
+ /** Gets the options that will be passed in calls to the resolve hook */
102
+ resolveOptions: new SyncWaterfallHook<[HookOptions]>(),
103
+
104
+ /** Allows users to change the expression to be evaluated before processing */
105
+ beforeEvaluate: new SyncWaterfallHook<[ExpressionType, HookOptions]>(),
106
+
95
107
  /**
96
108
  * An optional means of handling an error in the expression execution
97
109
  * Return true if handled, to stop propagation of the error
@@ -128,14 +140,22 @@ export class ExpressionEvaluator {
128
140
  }
129
141
 
130
142
  public evaluate(
131
- expression: ExpressionType,
143
+ expr: ExpressionType,
132
144
  options?: ExpressionEvaluatorOptions
133
145
  ): any {
134
- const opts = {
146
+ const resolvedOpts = this.hooks.resolveOptions.call({
135
147
  ...this.defaultHookOptions,
136
148
  ...options,
137
- resolveNode: (node: ExpressionNode) => this._execAST(node, opts),
138
- };
149
+ resolveNode: (node: ExpressionNode) => this._execAST(node, resolvedOpts),
150
+ });
151
+
152
+ let expression = this.hooks.beforeEvaluate.call(expr, resolvedOpts) ?? expr;
153
+
154
+ // Unwrap any returned expression type
155
+ // Since this could also be an object type, we need to recurse through it until we find the end
156
+ while (isObjectExpression(expression)) {
157
+ expression = expression.value;
158
+ }
139
159
 
140
160
  // Check for literals
141
161
  if (
@@ -149,21 +169,17 @@ export class ExpressionEvaluator {
149
169
 
150
170
  // Skip doing anything with objects that are _actually_ just parsed expression nodes
151
171
  if (isExpressionNode(expression)) {
152
- return this._execAST(expression, opts);
172
+ return this._execAST(expression, resolvedOpts);
153
173
  }
154
174
 
155
- if (typeof expression === 'object') {
156
- const values = Array.isArray(expression)
157
- ? expression
158
- : Object.values(expression);
159
-
160
- return values.reduce(
175
+ if (Array.isArray(expression)) {
176
+ return expression.reduce(
161
177
  (_nothing, exp) => this.evaluate(exp, options),
162
178
  null
163
179
  );
164
180
  }
165
181
 
166
- return this._execString(String(expression), opts);
182
+ return this._execString(String(expression), resolvedOpts);
167
183
  }
168
184
 
169
185
  public addExpressionFunction<T extends readonly unknown[], R>(
@@ -212,13 +228,13 @@ export class ExpressionEvaluator {
212
228
  return this._execAST(storedAST, options);
213
229
  }
214
230
 
215
- const expAST = parse(matchedExp);
231
+ const expAST = parseExpression(matchedExp);
216
232
  this.expressionsCache.set(matchedExp, expAST);
217
233
 
218
234
  return this._execAST(expAST, options);
219
235
  } catch (e: any) {
220
- if (!this.hooks.onError.call(e)) {
221
- // Only throw the error if it's not handled by the hook
236
+ if (options.throwErrors || !this.hooks.onError.call(e)) {
237
+ // Only throw the error if it's not handled by the hook, or throwErrors is true
222
238
  throw e;
223
239
  }
224
240
  }
@@ -305,35 +321,23 @@ export class ExpressionEvaluator {
305
321
  if (node.type === 'CallExpression') {
306
322
  const expressionName = node.callTarget.name;
307
323
 
308
- // Treat the conditional operator as special.
309
- // Don't exec the arguments that don't apply
310
- if (expressionName === 'conditional') {
311
- const condition = resolveNode(node.args[0]);
312
-
313
- if (condition) {
314
- return resolveNode(node.args[1]);
315
- }
316
-
317
- if (node.args[2]) {
318
- return resolveNode(node.args[2]);
319
- }
320
-
321
- return null;
322
- }
323
-
324
324
  const operator = this.operators.expressions.get(expressionName);
325
325
 
326
326
  if (!operator) {
327
327
  throw new Error(`Unknown expression function: ${expressionName}`);
328
328
  }
329
329
 
330
+ if ('resolveParams' in operator && operator.resolveParams === false) {
331
+ return operator(expressionContext, ...node.args);
332
+ }
333
+
330
334
  const args = node.args.map((n) => resolveNode(n));
331
335
 
332
336
  return operator(expressionContext, ...args);
333
337
  }
334
338
 
335
339
  if (node.type === 'ModelRef') {
336
- return model.get(node.ref);
340
+ return model.get(node.ref, { context: { model: options.model } });
337
341
  }
338
342
 
339
343
  if (node.type === 'MemberExpression') {
@@ -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
  }
@@ -1,17 +1,24 @@
1
1
  import type { DataModelWithParser } from '../data';
2
2
  import type { Logger } from '../logger';
3
3
 
4
+ export type ExpressionObjectType = {
5
+ /** The expression to eval */
6
+ value: BasicExpressionTypes;
7
+ };
8
+
4
9
  export type ExpressionLiteralType =
5
10
  | string
6
11
  | number
7
12
  | boolean
8
13
  | undefined
9
14
  | null;
10
- export type ExpressionType =
11
- | object
15
+
16
+ export type BasicExpressionTypes =
12
17
  | ExpressionLiteralType
13
- | Array<ExpressionLiteralType>
14
- | ExpressionNode;
18
+ | ExpressionObjectType
19
+ | Array<ExpressionLiteralType | ExpressionObjectType>;
20
+
21
+ export type ExpressionType = BasicExpressionTypes | ExpressionNode;
15
22
 
16
23
  export interface OperatorProcessingOptions {
17
24
  /**
@@ -53,7 +60,12 @@ export const ExpNodeOpaqueIdentifier = Symbol('Expression Node ID');
53
60
 
54
61
  /** Checks if the input is an already processed Expression node */
55
62
  export function isExpressionNode(x: any): x is ExpressionNode {
56
- return typeof x === 'object' && x.__id === ExpNodeOpaqueIdentifier;
63
+ return (
64
+ typeof x === 'object' &&
65
+ x !== null &&
66
+ !Array.isArray(x) &&
67
+ x.__id === ExpNodeOpaqueIdentifier
68
+ );
57
69
  }
58
70
 
59
71
  export interface NodePosition {
@@ -81,6 +93,12 @@ export interface BaseNode<T> {
81
93
 
82
94
  /** The location of the node in the source expression string */
83
95
  location?: NodeLocation;
96
+
97
+ /**
98
+ * The error that occurred while parsing this node
99
+ * This is only set if the parsing mode is set to non-strict
100
+ */
101
+ error?: Error;
84
102
  }
85
103
 
86
104
  /** A helper interface for nodes that container left and right children */
@@ -1,6 +1,9 @@
1
+ import { isExpressionNode } from './types';
1
2
  import type {
2
3
  ExpressionHandler,
3
4
  ExpressionNode,
5
+ ExpressionObjectType,
6
+ ExpressionType,
4
7
  NodeLocation,
5
8
  NodePosition,
6
9
  } from './types';
@@ -129,3 +132,19 @@ export function findClosestNodeAtPosition(
129
132
  return node;
130
133
  }
131
134
  }
135
+
136
+ /** Checks if the expression is a simple type */
137
+ export function isObjectExpression(
138
+ expr: ExpressionType
139
+ ): expr is ExpressionObjectType {
140
+ if (isExpressionNode(expr)) {
141
+ return false;
142
+ }
143
+
144
+ return (
145
+ typeof expr === 'object' &&
146
+ expr !== null &&
147
+ !Array.isArray(expr) &&
148
+ 'value' in expr
149
+ );
150
+ }