@adecore/intelligent-ui 0.21.1 → 0.22.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/dist/compiler.js CHANGED
@@ -20,6 +20,50 @@ export function uiMayReferenceHost(text) {
20
20
  .map(([name]) => name);
21
21
  return new RegExp(`<(?:${links.join('|')})\\b`).test(text);
22
22
  }
23
+ /* A spent budget ends the whole block; every other failure only marks the node or declaration it came from. */
24
+ function rethrowBudget(error) {
25
+ if (error instanceof UiFailure && error.code === 'budget_exceeded') {
26
+ throw error;
27
+ }
28
+ }
29
+ function isQuery(expression) {
30
+ return expression.kind === 'call' && expression.name === 'Query';
31
+ }
32
+ /* Throws when a query argument reads anything but declared local state. */
33
+ function checkQueryReferences(part, defaults) {
34
+ if (!part || typeof part !== 'object') {
35
+ return;
36
+ }
37
+ if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(defaults, String(part.name))) {
38
+ throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
39
+ }
40
+ for (const value of Object.values(part)) {
41
+ checkQueryReferences(value, defaults);
42
+ }
43
+ }
44
+ function compileQuery(expression, block, budget, querySchemas) {
45
+ if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
46
+ throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
47
+ }
48
+ const querySource = evaluateUiExpression(expression.args[0], {}, budget);
49
+ const argument = expression.args[1];
50
+ checkQueryReferences(argument, block.defaults);
51
+ const args = evaluateUiExpression(argument, block.defaults, budget);
52
+ if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
53
+ throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
54
+ }
55
+ if (!querySchemas || !Object.hasOwn(querySchemas, querySource)) {
56
+ throw new UiFailure('unknown_query', 'The host did not register this query source.');
57
+ }
58
+ const checked = querySchemas[querySource].safeParse(args);
59
+ if (!checked.success) {
60
+ throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
61
+ }
62
+ if (Object.keys(block.queries).length >= UI_HOST_LIMITS.queries) {
63
+ throw new UiFailure('budget_exceeded', `A block may declare at most ${UI_HOST_LIMITS.queries} queries.`);
64
+ }
65
+ return { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
66
+ }
23
67
  function isConstant(expression) {
24
68
  switch (expression.kind) {
25
69
  case 'literal':
@@ -84,9 +128,7 @@ export function compileUiBlock(source, options) {
84
128
  node.fallback = uiPlainText(evaluateUiExpression(syntax.expression, block.defaults, budget));
85
129
  }
86
130
  catch (error) {
87
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
88
- throw error;
89
- }
131
+ rethrowBudget(error);
90
132
  }
91
133
  }
92
134
  else {
@@ -150,9 +192,7 @@ export function compileUiBlock(source, options) {
150
192
  evaluated[key] = evaluateUiExpression(expression, block.defaults, budget);
151
193
  }
152
194
  catch (error) {
153
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
154
- throw error;
155
- }
195
+ rethrowBudget(error);
156
196
  unresolved = true;
157
197
  }
158
198
  }
@@ -165,9 +205,7 @@ export function compileUiBlock(source, options) {
165
205
  }
166
206
  }
167
207
  catch (error) {
168
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
169
- throw error;
170
- }
208
+ rethrowBudget(error);
171
209
  node.error = error instanceof UiFailure ? error.code : 'invalid_props';
172
210
  diagnose(error, syntax.start, syntax.end, syntax.id);
173
211
  }
@@ -182,41 +220,12 @@ export function compileUiBlock(source, options) {
182
220
  try {
183
221
  const syntax = parseUiSyntax(source, options.id, options.final, budget);
184
222
  block.diagnostics = syntax.diagnostics;
185
- for (const [name, expression] of Object.entries(syntax.declarations).sort(([, a], [, b]) => Number(a.kind === 'call' && a.name === 'Query') - Number(b.kind === 'call' && b.name === 'Query'))) {
223
+ // Defaults first, so a query argument can read every one of them.
224
+ const declarations = Object.entries(syntax.declarations).sort(([, left], [, right]) => Number(isQuery(left)) - Number(isQuery(right)));
225
+ for (const [name, expression] of declarations) {
186
226
  try {
187
227
  if (expression.kind === 'call' && expression.name === 'Query') {
188
- if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
189
- throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
190
- }
191
- const querySource = evaluateUiExpression(expression.args[0], {}, budget);
192
- const argument = expression.args[1];
193
- const references = (part) => {
194
- if (!part || typeof part !== 'object') {
195
- return;
196
- }
197
- if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(block.defaults, String(part.name))) {
198
- throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
199
- }
200
- for (const value of Object.values(part)) {
201
- references(value);
202
- }
203
- };
204
- references(argument);
205
- const args = evaluateUiExpression(argument, block.defaults, budget);
206
- if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
207
- throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
208
- }
209
- if (!options.querySchemas || !Object.hasOwn(options.querySchemas, querySource)) {
210
- throw new UiFailure('unknown_query', 'The host did not register this query source.');
211
- }
212
- const checked = options.querySchemas[querySource].safeParse(args);
213
- if (!checked.success) {
214
- throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
215
- }
216
- if (Object.keys(block.queries).length >= UI_HOST_LIMITS.queries) {
217
- throw new UiFailure('budget_exceeded', `A block may declare at most ${UI_HOST_LIMITS.queries} queries.`);
218
- }
219
- block.queries[name] = { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
228
+ block.queries[name] = compileQuery(expression, block, budget, options.querySchemas);
220
229
  }
221
230
  else {
222
231
  if (!isConstant(expression)) {
@@ -226,9 +235,7 @@ export function compileUiBlock(source, options) {
226
235
  }
227
236
  }
228
237
  catch (error) {
229
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
230
- throw error;
231
- }
238
+ rethrowBudget(error);
232
239
  diagnose(error, 0, source.length);
233
240
  }
234
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/intelligent-ui",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "A streaming UI catalog, compiler and bounded expression interpreter without a framework or DOM.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
package/src/compiler.ts CHANGED
@@ -70,6 +70,59 @@ export interface UiCompileOptions {
70
70
  now?: () => number;
71
71
  }
72
72
 
73
+ /* A spent budget ends the whole block; every other failure only marks the node or declaration it came from. */
74
+ function rethrowBudget(error: unknown): void {
75
+ if (error instanceof UiFailure && error.code === 'budget_exceeded') {
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ function isQuery(expression: UiExpression): boolean {
81
+ return expression.kind === 'call' && expression.name === 'Query';
82
+ }
83
+
84
+ /* Throws when a query argument reads anything but declared local state. */
85
+ function checkQueryReferences(part: unknown, defaults: Readonly<Record<string, UiValue>>): void {
86
+ if (!part || typeof part !== 'object') {
87
+ return;
88
+ }
89
+ if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(defaults, String(part.name))) {
90
+ throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
91
+ }
92
+ for (const value of Object.values(part)) {
93
+ checkQueryReferences(value, defaults);
94
+ }
95
+ }
96
+
97
+ function compileQuery(
98
+ expression: Extract<UiExpression, { kind: 'call' }>,
99
+ block: UiBlock,
100
+ budget: UiBudget,
101
+ querySchemas: UiCompileOptions['querySchemas']
102
+ ): UiQuery {
103
+ if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
104
+ throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
105
+ }
106
+ const querySource = evaluateUiExpression(expression.args[0], {}, budget);
107
+ const argument = expression.args[1];
108
+ checkQueryReferences(argument, block.defaults);
109
+ const args = evaluateUiExpression(argument, block.defaults, budget);
110
+ if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
111
+ throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
112
+ }
113
+ if (!querySchemas || !Object.hasOwn(querySchemas, querySource)) {
114
+ throw new UiFailure('unknown_query', 'The host did not register this query source.');
115
+ }
116
+ const checked = querySchemas[querySource].safeParse(args);
117
+ if (!checked.success) {
118
+ throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
119
+ }
120
+ if (Object.keys(block.queries).length >= UI_HOST_LIMITS.queries) {
121
+ throw new UiFailure('budget_exceeded', `A block may declare at most ${UI_HOST_LIMITS.queries} queries.`);
122
+ }
123
+ return { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
124
+ }
125
+
73
126
  function isConstant(expression: UiExpression): boolean {
74
127
  switch (expression.kind) {
75
128
  case 'literal':
@@ -134,9 +187,7 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
134
187
  try {
135
188
  node.fallback = uiPlainText(evaluateUiExpression(syntax.expression, block.defaults, budget));
136
189
  } catch (error) {
137
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
138
- throw error;
139
- }
190
+ rethrowBudget(error);
140
191
  }
141
192
  } else {
142
193
  node.props.text = syntax.text ?? '';
@@ -199,9 +250,7 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
199
250
  try {
200
251
  evaluated[key] = evaluateUiExpression(expression, block.defaults, budget);
201
252
  } catch (error) {
202
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
203
- throw error;
204
- }
253
+ rethrowBudget(error);
205
254
  unresolved = true;
206
255
  }
207
256
  }
@@ -213,9 +262,7 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
213
262
  }
214
263
  }
215
264
  } catch (error) {
216
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
217
- throw error;
218
- }
265
+ rethrowBudget(error);
219
266
  node.error = error instanceof UiFailure ? error.code : 'invalid_props';
220
267
  diagnose(error, syntax.start, syntax.end, syntax.id);
221
268
  }
@@ -230,43 +277,12 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
230
277
  try {
231
278
  const syntax = parseUiSyntax(source, options.id, options.final, budget);
232
279
  block.diagnostics = syntax.diagnostics;
233
- for (const [name, expression] of Object.entries(syntax.declarations).sort(
234
- ([, a], [, b]) => Number(a.kind === 'call' && a.name === 'Query') - Number(b.kind === 'call' && b.name === 'Query')
235
- )) {
280
+ // Defaults first, so a query argument can read every one of them.
281
+ const declarations = Object.entries(syntax.declarations).sort(([, left], [, right]) => Number(isQuery(left)) - Number(isQuery(right)));
282
+ for (const [name, expression] of declarations) {
236
283
  try {
237
284
  if (expression.kind === 'call' && expression.name === 'Query') {
238
- if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
239
- throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
240
- }
241
- const querySource = evaluateUiExpression(expression.args[0], {}, budget);
242
- const argument = expression.args[1];
243
- const references = (part: unknown): void => {
244
- if (!part || typeof part !== 'object') {
245
- return;
246
- }
247
- if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(block.defaults, String(part.name))) {
248
- throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
249
- }
250
- for (const value of Object.values(part)) {
251
- references(value);
252
- }
253
- };
254
- references(argument);
255
- const args = evaluateUiExpression(argument, block.defaults, budget);
256
- if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
257
- throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
258
- }
259
- if (!options.querySchemas || !Object.hasOwn(options.querySchemas, querySource)) {
260
- throw new UiFailure('unknown_query', 'The host did not register this query source.');
261
- }
262
- const checked = options.querySchemas[querySource].safeParse(args);
263
- if (!checked.success) {
264
- throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
265
- }
266
- if (Object.keys(block.queries).length >= UI_HOST_LIMITS.queries) {
267
- throw new UiFailure('budget_exceeded', `A block may declare at most ${UI_HOST_LIMITS.queries} queries.`);
268
- }
269
- block.queries[name] = { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
285
+ block.queries[name] = compileQuery(expression, block, budget, options.querySchemas);
270
286
  } else {
271
287
  if (!isConstant(expression)) {
272
288
  throw new UiFailure('invalid_default', 'State defaults must be literal JSON values.');
@@ -274,9 +290,7 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
274
290
  block.defaults[name] = evaluateUiExpression(expression, {}, budget);
275
291
  }
276
292
  } catch (error) {
277
- if (error instanceof UiFailure && error.code === 'budget_exceeded') {
278
- throw error;
279
- }
293
+ rethrowBudget(error);
280
294
  diagnose(error, 0, source.length);
281
295
  }
282
296
  }