@webstudio-is/react-sdk 0.82.0 → 0.84.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.
Files changed (71) hide show
  1. package/LICENSE +661 -21
  2. package/lib/cjs/component-renderer.js +125 -0
  3. package/lib/cjs/components/component-meta.js +10 -0
  4. package/lib/cjs/components/components-utils.js +1 -0
  5. package/lib/cjs/context.js +2 -1
  6. package/lib/cjs/css/index.js +0 -1
  7. package/lib/cjs/css/style-rules.js +1 -1
  8. package/lib/cjs/embed-template.js +130 -55
  9. package/lib/cjs/expression.js +47 -4
  10. package/lib/cjs/hook.js +34 -0
  11. package/lib/cjs/index.js +7 -0
  12. package/lib/cjs/instance-utils.js +65 -0
  13. package/lib/cjs/props.js +18 -3
  14. package/lib/cjs/tree/create-elements-tree.js +5 -4
  15. package/lib/cjs/tree/root.js +7 -2
  16. package/lib/cjs/tree/webstudio-component.js +26 -10
  17. package/lib/component-renderer.js +111 -0
  18. package/lib/components/component-meta.js +10 -0
  19. package/lib/components/components-utils.js +1 -0
  20. package/lib/context.js +2 -1
  21. package/lib/css/index.js +0 -1
  22. package/lib/css/style-rules.js +1 -1
  23. package/lib/embed-template.js +138 -55
  24. package/lib/expression.js +47 -4
  25. package/lib/hook.js +14 -0
  26. package/lib/index.js +10 -1
  27. package/lib/instance-utils.js +45 -0
  28. package/lib/props.js +19 -4
  29. package/lib/tree/create-elements-tree.js +8 -5
  30. package/lib/tree/root.js +14 -4
  31. package/lib/tree/webstudio-component.js +27 -11
  32. package/lib/types/app/root.d.ts +1 -2
  33. package/lib/types/component-renderer.d.ts +8 -0
  34. package/lib/types/components/component-meta.d.ts +14 -8
  35. package/lib/types/context.d.ts +3 -1
  36. package/lib/types/css/css.d.ts +19 -19
  37. package/lib/types/css/global-rules.d.ts +19 -19
  38. package/lib/types/css/index.d.ts +0 -1
  39. package/lib/types/css/normalize.d.ts +47 -47
  40. package/lib/types/embed-template.d.ts +297 -174
  41. package/lib/types/expression.d.ts +3 -2
  42. package/lib/types/hook.d.ts +31 -0
  43. package/lib/types/index.d.ts +5 -2
  44. package/lib/types/instance-utils.d.ts +16 -0
  45. package/lib/types/instance-utils.test.d.ts +1 -0
  46. package/lib/types/props.d.ts +48 -46
  47. package/lib/types/tree/create-elements-tree.d.ts +9 -6
  48. package/lib/types/tree/root.d.ts +8 -5
  49. package/lib/types/tree/webstudio-component.d.ts +16 -7
  50. package/package.json +18 -19
  51. package/src/component-renderer.tsx +117 -0
  52. package/src/components/component-meta.ts +10 -0
  53. package/src/context.tsx +4 -0
  54. package/src/css/index.ts +0 -1
  55. package/src/css/style-rules.ts +1 -1
  56. package/src/embed-template.test.ts +113 -26
  57. package/src/embed-template.ts +149 -56
  58. package/src/expression.test.ts +74 -6
  59. package/src/expression.ts +55 -2
  60. package/src/hook.ts +42 -0
  61. package/src/index.ts +5 -0
  62. package/src/instance-utils.test.ts +89 -0
  63. package/src/instance-utils.ts +65 -0
  64. package/src/props.ts +19 -2
  65. package/src/tree/create-elements-tree.tsx +25 -8
  66. package/src/tree/root.ts +22 -3
  67. package/src/tree/webstudio-component.tsx +42 -14
  68. package/lib/cjs/css/get-browser-style.js +0 -83
  69. package/lib/css/get-browser-style.js +0 -65
  70. package/lib/types/css/get-browser-style.d.ts +0 -2
  71. package/src/css/get-browser-style.ts +0 -81
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from "@jest/globals";
2
- import { generateDataFromEmbedTemplate } from "./embed-template";
2
+ import { generateDataFromEmbedTemplate, namespaceMeta } from "./embed-template";
3
3
  import { showAttribute } from "./tree";
4
4
 
5
5
  const expectString = expect.any(String);
@@ -220,15 +220,14 @@ test("generate data for embedding from props bound to data source variables", ()
220
220
  {
221
221
  type: "instance",
222
222
  component: "Box1",
223
+ dataSources: {
224
+ showOtherBoxDataSource: { type: "variable", initialValue: false },
225
+ },
223
226
  props: [
224
227
  {
225
- type: "boolean",
228
+ type: "dataSource",
226
229
  name: "showOtherBox",
227
- value: false,
228
- dataSourceRef: {
229
- type: "variable",
230
- name: "showOtherBoxDataSource",
231
- },
230
+ dataSourceName: "showOtherBoxDataSource",
232
231
  },
233
232
  ],
234
233
  children: [],
@@ -238,13 +237,9 @@ test("generate data for embedding from props bound to data source variables", ()
238
237
  component: "Box2",
239
238
  props: [
240
239
  {
241
- type: "boolean",
240
+ type: "dataSource",
242
241
  name: showAttribute,
243
- value: false,
244
- dataSourceRef: {
245
- type: "variable",
246
- name: "showOtherBoxDataSource",
247
- },
242
+ dataSourceName: "showOtherBoxDataSource",
248
243
  },
249
244
  ],
250
245
  children: [],
@@ -302,12 +297,18 @@ test("generate data for embedding from props bound to data source expressions",
302
297
  {
303
298
  type: "instance",
304
299
  component: "Box1",
300
+ dataSources: {
301
+ boxState: { type: "variable", initialValue: "initial" },
302
+ boxStateSuccess: {
303
+ type: "expression",
304
+ code: `boxState === 'success'`,
305
+ },
306
+ },
305
307
  props: [
306
308
  {
307
- type: "string",
309
+ type: "dataSource",
308
310
  name: "state",
309
- value: "initial",
310
- dataSourceRef: { type: "variable", name: "boxState" },
311
+ dataSourceName: "boxState",
311
312
  },
312
313
  ],
313
314
  children: [],
@@ -317,14 +318,9 @@ test("generate data for embedding from props bound to data source expressions",
317
318
  component: "Box2",
318
319
  props: [
319
320
  {
320
- type: "boolean",
321
+ type: "dataSource",
321
322
  name: showAttribute,
322
- value: false,
323
- dataSourceRef: {
324
- type: "expression",
325
- name: "boxStateSuccess",
326
- code: `boxState === 'success'`,
327
- },
323
+ dataSourceName: "boxStateSuccess",
328
324
  },
329
325
  ],
330
326
  children: [],
@@ -389,12 +385,14 @@ test("generate data for embedding from action props", () => {
389
385
  {
390
386
  type: "instance",
391
387
  component: "Box1",
388
+ dataSources: {
389
+ boxState: { type: "variable", initialValue: "initial" },
390
+ },
392
391
  props: [
393
392
  {
394
- type: "string",
393
+ type: "dataSource",
395
394
  name: "state",
396
- value: "initial",
397
- dataSourceRef: { type: "variable", name: "boxState" },
395
+ dataSourceName: "boxState",
398
396
  },
399
397
  ],
400
398
  children: [
@@ -407,6 +405,17 @@ test("generate data for embedding from action props", () => {
407
405
  name: "onClick",
408
406
  value: [{ type: "execute", code: `boxState = 'success'` }],
409
407
  },
408
+ {
409
+ type: "action",
410
+ name: "onChange",
411
+ value: [
412
+ {
413
+ type: "execute",
414
+ args: ["value"],
415
+ code: `boxState = value`,
416
+ },
417
+ ],
418
+ },
410
419
  ],
411
420
  children: [],
412
421
  },
@@ -442,10 +451,24 @@ test("generate data for embedding from action props", () => {
442
451
  value: [
443
452
  {
444
453
  type: "execute",
454
+ args: [],
445
455
  code: expect.stringMatching(/\$ws\$dataSource\$\w+ = 'success'/),
446
456
  },
447
457
  ],
448
458
  },
459
+ {
460
+ id: expectString,
461
+ instanceId: expectString,
462
+ type: "action",
463
+ name: "onChange",
464
+ value: [
465
+ {
466
+ type: "execute",
467
+ args: ["value"],
468
+ code: expect.stringMatching(/\$ws\$dataSource\$\w+ = value/),
469
+ },
470
+ ],
471
+ },
449
472
  ],
450
473
  dataSources: [
451
474
  {
@@ -464,3 +487,67 @@ test("generate data for embedding from action props", () => {
464
487
  styles: [],
465
488
  });
466
489
  });
490
+
491
+ test("add namespace to selected components in embed template", () => {
492
+ expect(
493
+ namespaceMeta(
494
+ {
495
+ type: "container",
496
+ label: "",
497
+ icon: "",
498
+ requiredAncestors: ["Button", "Box"],
499
+ invalidAncestors: ["Tooltip"],
500
+ indexWithinAncestor: "Tooltip",
501
+ template: [
502
+ {
503
+ type: "instance",
504
+ component: "Tooltip",
505
+ children: [
506
+ { type: "text", value: "Some text" },
507
+ {
508
+ type: "instance",
509
+ component: "Box",
510
+ children: [
511
+ {
512
+ type: "instance",
513
+ component: "Button",
514
+ children: [],
515
+ },
516
+ ],
517
+ },
518
+ ],
519
+ },
520
+ ],
521
+ },
522
+ "my-namespace",
523
+ new Set(["Tooltip", "Button"])
524
+ )
525
+ ).toEqual({
526
+ type: "container",
527
+ label: "",
528
+ icon: "",
529
+ requiredAncestors: ["my-namespace:Button", "Box"],
530
+ invalidAncestors: ["my-namespace:Tooltip"],
531
+ indexWithinAncestor: "my-namespace:Tooltip",
532
+ template: [
533
+ {
534
+ type: "instance",
535
+ component: "my-namespace:Tooltip",
536
+ children: [
537
+ { type: "text", value: "Some text" },
538
+ {
539
+ type: "instance",
540
+ component: "Box",
541
+ children: [
542
+ {
543
+ type: "instance",
544
+ component: "my-namespace:Button",
545
+ children: [],
546
+ },
547
+ ],
548
+ },
549
+ ],
550
+ },
551
+ ],
552
+ });
553
+ });
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { nanoid } from "nanoid";
3
3
  import {
4
- type Instance,
4
+ Instance,
5
5
  type InstancesList,
6
6
  PropsList,
7
7
  StyleSourceSelectionsList,
@@ -13,6 +13,7 @@ import {
13
13
  import { StyleValue, type StyleProperty } from "@webstudio-is/css-data";
14
14
  import type { Simplify } from "type-fest";
15
15
  import { encodeDataSourceVariable, validateExpression } from "./expression";
16
+ import type { WsComponentMeta } from "./components/component-meta";
16
17
 
17
18
  const EmbedTemplateText = z.object({
18
19
  type: z.literal("text"),
@@ -21,43 +22,48 @@ const EmbedTemplateText = z.object({
21
22
 
22
23
  type EmbedTemplateText = z.infer<typeof EmbedTemplateText>;
23
24
 
24
- const DataSourceVariableRef = z.object({
25
- type: z.literal("variable"),
26
- name: z.string(),
27
- });
28
-
29
- const DataSourceRef = z.union([
30
- DataSourceVariableRef,
25
+ const EmbedTemplateDataSource = z.union([
26
+ z.object({
27
+ type: z.literal("variable"),
28
+ initialValue: z.union([
29
+ z.string(),
30
+ z.number(),
31
+ z.boolean(),
32
+ z.array(z.string()),
33
+ ]),
34
+ }),
31
35
  z.object({
32
36
  type: z.literal("expression"),
33
- name: z.string(),
34
37
  code: z.string(),
35
38
  }),
36
39
  ]);
37
40
 
41
+ type EmbedTemplateDataSource = z.infer<typeof EmbedTemplateDataSource>;
42
+
38
43
  const EmbedTemplateProp = z.union([
44
+ z.object({
45
+ type: z.literal("dataSource"),
46
+ name: z.string(),
47
+ dataSourceName: z.string(),
48
+ }),
39
49
  z.object({
40
50
  type: z.literal("number"),
41
51
  name: z.string(),
42
- dataSourceRef: z.optional(DataSourceRef),
43
52
  value: z.number(),
44
53
  }),
45
54
  z.object({
46
55
  type: z.literal("string"),
47
56
  name: z.string(),
48
- dataSourceRef: z.optional(DataSourceRef),
49
57
  value: z.string(),
50
58
  }),
51
59
  z.object({
52
60
  type: z.literal("boolean"),
53
61
  name: z.string(),
54
- dataSourceRef: z.optional(DataSourceRef),
55
62
  value: z.boolean(),
56
63
  }),
57
64
  z.object({
58
65
  type: z.literal("string[]"),
59
66
  name: z.string(),
60
- dataSourceRef: z.optional(DataSourceRef),
61
67
  value: z.array(z.string()),
62
68
  }),
63
69
  z.object({
@@ -66,6 +72,7 @@ const EmbedTemplateProp = z.union([
66
72
  value: z.array(
67
73
  z.object({
68
74
  type: z.literal("execute"),
75
+ args: z.optional(z.array(z.string())),
69
76
  code: z.string(),
70
77
  })
71
78
  ),
@@ -94,6 +101,7 @@ export type EmbedTemplateInstance = {
94
101
  type: "instance";
95
102
  component: string;
96
103
  label?: string;
104
+ dataSources?: Record<string, EmbedTemplateDataSource>;
97
105
  props?: EmbedTemplateProp[];
98
106
  styles?: EmbedTemplateStyleDecl[];
99
107
  children: Array<EmbedTemplateInstance | EmbedTemplateText>;
@@ -105,6 +113,7 @@ export const EmbedTemplateInstance: z.ZodType<EmbedTemplateInstance> = z.lazy(
105
113
  type: z.literal("instance"),
106
114
  component: z.string(),
107
115
  label: z.optional(z.string()),
116
+ dataSources: z.optional(z.record(z.string(), EmbedTemplateDataSource)),
108
117
  props: z.optional(z.array(EmbedTemplateProp)),
109
118
  styles: z.optional(z.array(EmbedTemplateStyleDecl)),
110
119
  children: WsEmbedTemplate,
@@ -117,6 +126,25 @@ export const WsEmbedTemplate = z.lazy(() =>
117
126
 
118
127
  export type WsEmbedTemplate = z.infer<typeof WsEmbedTemplate>;
119
128
 
129
+ const getDataSourceValue = (
130
+ value: Extract<EmbedTemplateDataSource, { type: "variable" }>["initialValue"]
131
+ ): Extract<DataSource, { type: "variable" }>["value"] => {
132
+ if (typeof value === "string") {
133
+ return { type: "string", value };
134
+ }
135
+ if (typeof value === "number") {
136
+ return { type: "number", value };
137
+ }
138
+ if (typeof value === "boolean") {
139
+ return { type: "boolean", value };
140
+ }
141
+ if (Array.isArray(value)) {
142
+ return { type: "string[]", value };
143
+ }
144
+ value satisfies never;
145
+ throw Error("Impossible case");
146
+ };
147
+
120
148
  const createInstancesFromTemplate = (
121
149
  treeTemplate: WsEmbedTemplate,
122
150
  instances: InstancesList,
@@ -132,6 +160,38 @@ const createInstancesFromTemplate = (
132
160
  if (item.type === "instance") {
133
161
  const instanceId = nanoid();
134
162
 
163
+ if (item.dataSources) {
164
+ for (const [name, dataSource] of Object.entries(item.dataSources)) {
165
+ if (dataSourceByRef.has(name)) {
166
+ throw Error(`${name} data source already defined`);
167
+ }
168
+ if (dataSource.type === "variable") {
169
+ dataSourceByRef.set(name, {
170
+ type: "variable",
171
+ id: nanoid(),
172
+ scopeInstanceId: instanceId,
173
+ name,
174
+ value: getDataSourceValue(dataSource.initialValue),
175
+ });
176
+ }
177
+ if (dataSource.type === "expression") {
178
+ dataSourceByRef.set(name, {
179
+ type: "expression",
180
+ id: nanoid(),
181
+ scopeInstanceId: instanceId,
182
+ name,
183
+ // replace all references with variable names
184
+ code: validateExpression(dataSource.code, {
185
+ transformIdentifier: (ref) => {
186
+ const id = dataSourceByRef.get(ref)?.id ?? ref;
187
+ return encodeDataSourceVariable(id);
188
+ },
189
+ }),
190
+ });
191
+ }
192
+ }
193
+ }
194
+
135
195
  // populate props
136
196
  if (item.props) {
137
197
  for (const prop of item.props) {
@@ -144,12 +204,18 @@ const createInstancesFromTemplate = (
144
204
  type: "action",
145
205
  name: prop.name,
146
206
  value: prop.value.map((value) => {
207
+ const args = value.args ?? [];
147
208
  return {
148
209
  type: "execute",
210
+ args,
149
211
  // replace all references with variable names
150
212
  code: validateExpression(value.code, {
151
213
  effectful: true,
152
214
  transformIdentifier: (ref) => {
215
+ // bypass arguments without changes
216
+ if (args.includes(ref)) {
217
+ return ref;
218
+ }
153
219
  const id = dataSourceByRef.get(ref)?.id ?? ref;
154
220
  return encodeDataSourceVariable(id);
155
221
  },
@@ -159,51 +225,21 @@ const createInstancesFromTemplate = (
159
225
  });
160
226
  continue;
161
227
  }
162
- if (prop.dataSourceRef === undefined) {
163
- props.push({ id: propId, instanceId, ...prop });
164
- continue;
165
- }
166
- let dataSource = dataSourceByRef.get(prop.dataSourceRef.name);
167
- if (dataSource === undefined) {
168
- const id = nanoid();
169
- const { name: propName, dataSourceRef, ...rest } = prop;
170
- if (dataSourceRef.type === "variable") {
171
- dataSource = {
172
- type: "variable",
173
- id,
174
- // the first instance where data source is appeared in becomes its scope
175
- scopeInstanceId: instanceId,
176
- name: dataSourceRef.name,
177
- value: rest,
178
- };
179
- dataSourceByRef.set(dataSourceRef.name, dataSource);
180
- } else if (dataSourceRef.type === "expression") {
181
- dataSource = {
182
- type: "expression",
183
- id,
184
- scopeInstanceId: instanceId,
185
- name: dataSourceRef.name,
186
- // replace all references with variable names
187
- code: validateExpression(dataSourceRef.code, {
188
- transformIdentifier: (ref) => {
189
- const id = dataSourceByRef.get(ref)?.id ?? ref;
190
- return encodeDataSourceVariable(id);
191
- },
192
- }),
193
- };
194
- dataSourceByRef.set(dataSourceRef.name, dataSource);
195
- } else {
196
- dataSourceRef satisfies never;
197
- continue;
228
+ if (prop.type === "dataSource") {
229
+ const dataSource = dataSourceByRef.get(prop.dataSourceName);
230
+ if (dataSource === undefined) {
231
+ throw Error(`${prop.dataSourceName} data source is not defined`);
198
232
  }
233
+ props.push({
234
+ id: propId,
235
+ instanceId,
236
+ type: "dataSource",
237
+ name: prop.name,
238
+ value: dataSource.id,
239
+ });
240
+ continue;
199
241
  }
200
- props.push({
201
- id: propId,
202
- instanceId,
203
- type: "dataSource",
204
- name: prop.name,
205
- value: dataSource.id,
206
- });
242
+ props.push({ id: propId, instanceId, ...prop });
207
243
  }
208
244
  }
209
245
 
@@ -301,3 +337,60 @@ export const generateDataFromEmbedTemplate = (
301
337
  export type EmbedTemplateData = ReturnType<
302
338
  typeof generateDataFromEmbedTemplate
303
339
  >;
340
+
341
+ const namespaceEmbedTemplateComponents = (
342
+ template: WsEmbedTemplate,
343
+ namespace: string,
344
+ components: Set<EmbedTemplateInstance["component"]>
345
+ ): WsEmbedTemplate => {
346
+ return template.map((item) => {
347
+ if (item.type === "text") {
348
+ return item;
349
+ }
350
+ if (item.type === "instance") {
351
+ const prefix = components.has(item.component) ? `${namespace}:` : "";
352
+ return {
353
+ ...item,
354
+ component: `${prefix}${item.component}`,
355
+ children: namespaceEmbedTemplateComponents(
356
+ item.children,
357
+ namespace,
358
+ components
359
+ ),
360
+ };
361
+ }
362
+ item satisfies never;
363
+ throw Error("Impossible case");
364
+ });
365
+ };
366
+
367
+ export const namespaceMeta = (
368
+ meta: WsComponentMeta,
369
+ namespace: string,
370
+ components: Set<EmbedTemplateInstance["component"]>
371
+ ) => {
372
+ const newMeta = { ...meta };
373
+ if (newMeta.requiredAncestors) {
374
+ newMeta.requiredAncestors = newMeta.requiredAncestors.map((component) =>
375
+ components.has(component) ? `${namespace}:${component}` : component
376
+ );
377
+ }
378
+ if (newMeta.invalidAncestors) {
379
+ newMeta.invalidAncestors = newMeta.invalidAncestors.map((component) =>
380
+ components.has(component) ? `${namespace}:${component}` : component
381
+ );
382
+ }
383
+ if (newMeta.indexWithinAncestor) {
384
+ newMeta.indexWithinAncestor = components.has(newMeta.indexWithinAncestor)
385
+ ? `${namespace}:${newMeta.indexWithinAncestor}`
386
+ : newMeta.indexWithinAncestor;
387
+ }
388
+ if (newMeta.template) {
389
+ newMeta.template = namespaceEmbedTemplateComponents(
390
+ newMeta.template,
391
+ namespace,
392
+ components
393
+ );
394
+ }
395
+ return newMeta;
396
+ };
@@ -4,6 +4,7 @@ import {
4
4
  encodeDataSourceVariable,
5
5
  executeComputingExpressions,
6
6
  executeEffectfulExpression,
7
+ computeExpressionsDependencies,
7
8
  generateComputingExpressions,
8
9
  generateEffectfulExpression,
9
10
  validateExpression,
@@ -186,7 +187,11 @@ test("encode/decode variable names", () => {
186
187
 
187
188
  test("generate effectful expression", () => {
188
189
  expect(
189
- generateEffectfulExpression(`var0 = var0 + var1`, new Set(["var0", "var1"]))
190
+ generateEffectfulExpression(
191
+ `var0 = var0 + var1`,
192
+ new Set(),
193
+ new Set(["var0", "var1"])
194
+ )
190
195
  ).toMatchInlineSnapshot(`
191
196
  "let var0 = _variables.get('var0');
192
197
  let var1 = _variables.get('var1');
@@ -197,7 +202,11 @@ test("generate effectful expression", () => {
197
202
  `);
198
203
 
199
204
  expect(
200
- generateEffectfulExpression(`var0 = var1 + 1`, new Set(["var0", "var1"]))
205
+ generateEffectfulExpression(
206
+ `var0 = var1 + 1`,
207
+ new Set(),
208
+ new Set(["var0", "var1"])
209
+ )
201
210
  ).toMatchInlineSnapshot(`
202
211
  "let var1 = _variables.get('var1');
203
212
  let var0;
@@ -212,6 +221,7 @@ test("add only used variables in effectful expression", () => {
212
221
  expect(
213
222
  generateEffectfulExpression(
214
223
  `var0 = var1 + 1`,
224
+ new Set(),
215
225
  new Set(["var0", "var1", "var2"])
216
226
  )
217
227
  ).toMatchInlineSnapshot(`
@@ -224,10 +234,34 @@ test("add only used variables in effectful expression", () => {
224
234
  `);
225
235
  });
226
236
 
227
- test("forbid not allowed variables in effectful expression", () => {
237
+ test("support effectful expression arguments", () => {
238
+ expect(
239
+ generateEffectfulExpression(
240
+ `var0 = arg0 + 1`,
241
+ new Set(["arg0"]),
242
+ new Set(["var0"])
243
+ )
244
+ ).toMatchInlineSnapshot(`
245
+ "let arg0 = _args.get('arg0');
246
+ let var0;
247
+ var0 = arg0 + 1;
248
+ return new Map([
249
+ ['var0', var0],
250
+ ]);"
251
+ `);
252
+ });
253
+
254
+ test("forbid not allowed variables or arguments in effectful expression", () => {
228
255
  expect(() => {
229
- generateEffectfulExpression(`var0 = var0 + var1`, new Set(["var0"]));
256
+ generateEffectfulExpression(
257
+ `var0 = var0 + var1`,
258
+ new Set(),
259
+ new Set(["var0"])
260
+ );
230
261
  }).toThrowError(/Unknown dependency "var1"/);
262
+ expect(() => {
263
+ generateEffectfulExpression(`var0 = arg0`, new Set(), new Set(["var0"]));
264
+ }).toThrowError(/Unknown dependency "arg0"/);
231
265
  });
232
266
 
233
267
  test("execute effectful expression", () => {
@@ -235,7 +269,41 @@ test("execute effectful expression", () => {
235
269
  ["var0", 2],
236
270
  ["var1", 3],
237
271
  ]);
238
- expect(executeEffectfulExpression(`var0 = var0 + var1`, variables)).toEqual(
239
- new Map([["var0", 5]])
272
+ expect(
273
+ executeEffectfulExpression(`var0 = var0 + var1`, new Map(), variables)
274
+ ).toEqual(new Map([["var0", 5]]));
275
+
276
+ expect(
277
+ executeEffectfulExpression(`arg0 = 5`, new Map([["arg0", 0]]), new Map())
278
+ ).toEqual(new Map());
279
+ });
280
+
281
+ test("compute expressions dependencies", () => {
282
+ const expressions = new Map([
283
+ ["exp1", `var1`],
284
+ ["exp2", `exp1 + exp1`],
285
+ ["exp3", `exp1 + exp2`],
286
+ ["exp4", `var1 + exp1`],
287
+ ]);
288
+ expect(computeExpressionsDependencies(expressions)).toEqual(
289
+ new Map([
290
+ ["exp4", new Set(["var1", "exp1"])],
291
+ ["exp3", new Set(["var1", "exp1", "exp2"])],
292
+ ["exp2", new Set(["var1", "exp1"])],
293
+ ["exp1", new Set(["var1"])],
294
+ ])
295
+ );
296
+ });
297
+
298
+ test("handle cyclic dependencies", () => {
299
+ const expressions = new Map([
300
+ ["exp1", `exp2 + var1`],
301
+ ["exp2", `exp1 + var1`],
302
+ ]);
303
+ expect(computeExpressionsDependencies(expressions)).toEqual(
304
+ new Map([
305
+ ["exp2", new Set(["var1", "exp1", "exp2"])],
306
+ ["exp1", new Set(["var1", "exp1", "exp2"])],
307
+ ])
240
308
  );
241
309
  });