@webstudio-is/react-sdk 0.79.0 → 0.81.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.
@@ -1,12 +1,12 @@
1
1
  import { type ComponentProps } from "react";
2
2
  import type { ReadableAtom } from "nanostores";
3
3
  import type { Assets } from "@webstudio-is/asset-uploader";
4
- import type { DataSource, Instance, Instances } from "@webstudio-is/project-build";
4
+ import type { Instance, Instances } from "@webstudio-is/project-build";
5
5
  import type { Components } from "../components/components-utils";
6
- import { type Params } from "../context";
6
+ import { type Params, type DataSourceValues } from "../context";
7
7
  import type { Pages, PropsByInstanceId } from "../props";
8
8
  import type { WebstudioComponent } from "./webstudio-component";
9
- export declare const createElementsTree: ({ renderer, imageBaseUrl, assetBaseUrl, instances, rootInstanceId, propsByInstanceIdStore, assetsStore, pagesStore, dataSourceValuesStore, onDataSourceUpdate, Component, components, }: Params & {
9
+ export declare const createElementsTree: ({ renderer, imageBaseUrl, assetBaseUrl, instances, rootInstanceId, propsByInstanceIdStore, assetsStore, pagesStore, dataSourceValuesStore, executeEffectfulExpression, onDataSourceUpdate, Component, components, }: Params & {
10
10
  instances: Map<string, {
11
11
  type: "instance";
12
12
  id: string;
@@ -24,8 +24,9 @@ export declare const createElementsTree: ({ renderer, imageBaseUrl, assetBaseUrl
24
24
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
25
25
  assetsStore: ReadableAtom<Assets>;
26
26
  pagesStore: ReadableAtom<Pages>;
27
- dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
28
- onDataSourceUpdate: (dataSourceId: DataSource["id"], value: unknown) => void;
27
+ executeEffectfulExpression: (expression: string, values: DataSourceValues) => DataSourceValues;
28
+ dataSourceValuesStore: ReadableAtom<DataSourceValues>;
29
+ onDataSourceUpdate: (newValues: DataSourceValues) => void;
29
30
  Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
30
31
  components: Components;
31
32
  }) => JSX.Element | null;
@@ -1,9 +1,9 @@
1
1
  import { type ComponentProps } from "react";
2
- import { type Build, type Page, DataSource } from "@webstudio-is/project-build";
2
+ import { type Build, type Page } from "@webstudio-is/project-build";
3
3
  import type { Asset } from "@webstudio-is/asset-uploader";
4
4
  import { WebstudioComponent } from "./webstudio-component";
5
5
  import type { Components } from "../components/components-utils";
6
- import type { Params } from "../context";
6
+ import type { Params, DataSourceValues } from "../context";
7
7
  export type Data = {
8
8
  page: Page;
9
9
  pages: Array<Page>;
@@ -14,12 +14,12 @@ export type Data = {
14
14
  export type RootPropsData = Omit<Data, "build"> & {
15
15
  build: Pick<Data["build"], "instances" | "props" | "dataSources">;
16
16
  };
17
- type DataSourceValues = Map<DataSource["id"], unknown>;
18
17
  type RootProps = {
19
18
  data: RootPropsData;
20
- computeExpressions: (values: DataSourceValues) => DataSourceValues;
19
+ executeComputingExpressions: (values: DataSourceValues) => DataSourceValues;
20
+ executeEffectfulExpression: (expression: string, values: DataSourceValues) => DataSourceValues;
21
21
  Component?: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
22
22
  components: Components;
23
23
  };
24
- export declare const InstanceRoot: ({ data, computeExpressions, Component, components, }: RootProps) => JSX.Element | null;
24
+ export declare const InstanceRoot: ({ data, executeComputingExpressions, executeEffectfulExpression, Component, components, }: RootProps) => JSX.Element | null;
25
25
  export {};
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@webstudio-is/react-sdk",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "Webstudio JavaScript / TypeScript API",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
7
7
  "type": "module",
8
8
  "devDependencies": {
9
- "@jest/globals": "^29.3.1",
10
- "@remix-run/react": "^1.15.0",
9
+ "@jest/globals": "^29.6.1",
10
+ "@remix-run/react": "^1.18.1",
11
11
  "@types/react": "^18.0.35",
12
12
  "@types/react-dom": "^18.0.11",
13
- "jest": "^29.3.1",
13
+ "jest": "^29.6.1",
14
14
  "react": "^18.2.0",
15
15
  "react-dom": "^18.2.0",
16
16
  "type-fest": "^3.7.1",
@@ -21,25 +21,26 @@
21
21
  "@webstudio-is/tsconfig": "^1.0.6"
22
22
  },
23
23
  "peerDependencies": {
24
- "@remix-run/react": "^1.15.0",
24
+ "@remix-run/react": "^1.18.0",
25
25
  "react": "^18.2.0",
26
26
  "react-dom": "^18.2.0",
27
27
  "zod": "^3.19.1"
28
28
  },
29
29
  "dependencies": {
30
- "@nanostores/react": "^0.4.1",
30
+ "@jsep-plugin/assignment": "^1.2.1",
31
+ "@nanostores/react": "^0.7.1",
31
32
  "detect-font": "^0.1.5",
32
- "html-tags": "^3.2.0",
33
+ "html-tags": "^3.3.1",
33
34
  "jsep": "^1.3.8",
34
- "nanoevents": "^7.0.1",
35
- "nanoid": "^3.3.6",
36
- "nanostores": "^0.7.1",
37
- "@webstudio-is/asset-uploader": "^0.79.0",
38
- "@webstudio-is/css-data": "^0.79.0",
39
- "@webstudio-is/css-engine": "^0.79.0",
40
- "@webstudio-is/fonts": "^0.79.0",
41
- "@webstudio-is/generate-arg-types": "^0.79.0",
42
- "@webstudio-is/project-build": "^0.79.0"
35
+ "nanoevents": "^8.0.0",
36
+ "nanoid": "^4.0.2",
37
+ "nanostores": "^0.9.3",
38
+ "@webstudio-is/asset-uploader": "^0.81.0",
39
+ "@webstudio-is/css-data": "^0.81.0",
40
+ "@webstudio-is/css-engine": "^0.81.0",
41
+ "@webstudio-is/fonts": "^0.81.0",
42
+ "@webstudio-is/generate-arg-types": "^0.81.0",
43
+ "@webstudio-is/project-build": "^0.81.0"
43
44
  },
44
45
  "exports": {
45
46
  ".": {
package/src/context.tsx CHANGED
@@ -31,13 +31,20 @@ export type Params = {
31
31
  assetBaseUrl: string;
32
32
  };
33
33
 
34
+ export type DataSourceValues = Map<DataSource["id"], unknown>;
35
+
34
36
  export const ReactSdkContext = createContext<
35
37
  Params & {
36
38
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
37
39
  assetsStore: ReadableAtom<Assets>;
38
40
  pagesStore: ReadableAtom<Pages>;
39
- dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
40
- setDataSourceValue: (
41
+ dataSourceValuesStore: ReadableAtom<DataSourceValues>;
42
+ executeEffectfulExpression: (
43
+ expression: string,
44
+ values: DataSourceValues
45
+ ) => DataSourceValues;
46
+ setDataSourceValues: (newValues: DataSourceValues) => void;
47
+ setBoundDataSourceValue: (
41
48
  instanceId: Instance["id"],
42
49
  prop: Prop["name"],
43
50
  value: unknown
@@ -50,7 +57,13 @@ export const ReactSdkContext = createContext<
50
57
  assetsStore: atom(new Map()),
51
58
  pagesStore: atom(new Map()),
52
59
  dataSourceValuesStore: atom(new Map()),
53
- setDataSourceValue: () => {
54
- throw Error("React SDK setDataSourceValue is not implemented");
60
+ executeEffectfulExpression: () => {
61
+ throw Error("React SDK executeEffectfulExpression is not implemented");
62
+ },
63
+ setDataSourceValues: () => {
64
+ throw Error("React SDK setBoundDataSourceValue is not implemented");
65
+ },
66
+ setBoundDataSourceValue: () => {
67
+ throw Error("React SDK setBoundDataSourceValue is not implemented");
55
68
  },
56
69
  });
@@ -124,7 +124,7 @@ export const body = [
124
124
  property: "fontFamily",
125
125
  value: {
126
126
  type: "keyword",
127
- value: "Arial, sans-serif",
127
+ value: "Arial, Roboto, sans-serif",
128
128
  },
129
129
  },
130
130
  {
@@ -307,10 +307,7 @@ test("generate data for embedding from props bound to data source expressions",
307
307
  type: "string",
308
308
  name: "state",
309
309
  value: "initial",
310
- dataSourceRef: {
311
- type: "variable",
312
- name: "boxState",
313
- },
310
+ dataSourceRef: { type: "variable", name: "boxState" },
314
311
  },
315
312
  ],
316
313
  children: [],
@@ -384,3 +381,86 @@ test("generate data for embedding from props bound to data source expressions",
384
381
  styles: [],
385
382
  });
386
383
  });
384
+
385
+ test("generate data for embedding from action props", () => {
386
+ expect(
387
+ generateDataFromEmbedTemplate(
388
+ [
389
+ {
390
+ type: "instance",
391
+ component: "Box1",
392
+ props: [
393
+ {
394
+ type: "string",
395
+ name: "state",
396
+ value: "initial",
397
+ dataSourceRef: { type: "variable", name: "boxState" },
398
+ },
399
+ ],
400
+ children: [
401
+ {
402
+ type: "instance",
403
+ component: "Box2",
404
+ props: [
405
+ {
406
+ type: "action",
407
+ name: "onClick",
408
+ value: [{ type: "execute", code: `boxState = 'success'` }],
409
+ },
410
+ ],
411
+ children: [],
412
+ },
413
+ ],
414
+ },
415
+ ],
416
+ defaultBreakpointId
417
+ )
418
+ ).toEqual({
419
+ children: [{ type: "id", value: expectString }],
420
+ instances: [
421
+ {
422
+ type: "instance",
423
+ id: expectString,
424
+ component: "Box1",
425
+ children: [{ type: "id", value: expectString }],
426
+ },
427
+ { type: "instance", id: expectString, component: "Box2", children: [] },
428
+ ],
429
+ props: [
430
+ {
431
+ id: expectString,
432
+ instanceId: expectString,
433
+ type: "dataSource",
434
+ name: "state",
435
+ value: expectString,
436
+ },
437
+ {
438
+ id: expectString,
439
+ instanceId: expectString,
440
+ type: "action",
441
+ name: "onClick",
442
+ value: [
443
+ {
444
+ type: "execute",
445
+ code: expect.stringMatching(/\$ws\$dataSource\$\w+ = 'success'/),
446
+ },
447
+ ],
448
+ },
449
+ ],
450
+ dataSources: [
451
+ {
452
+ type: "variable",
453
+ id: expectString,
454
+ scopeInstanceId: expectString,
455
+ name: "boxState",
456
+ value: {
457
+ type: "string",
458
+ value: "initial",
459
+ },
460
+ },
461
+ ],
462
+ styleSourceSelections: [],
463
+ styleSources: [],
464
+ styles: [],
465
+ });
466
+ });
@@ -21,11 +21,13 @@ const EmbedTemplateText = z.object({
21
21
 
22
22
  type EmbedTemplateText = z.infer<typeof EmbedTemplateText>;
23
23
 
24
+ const DataSourceVariableRef = z.object({
25
+ type: z.literal("variable"),
26
+ name: z.string(),
27
+ });
28
+
24
29
  const DataSourceRef = z.union([
25
- z.object({
26
- type: z.literal("variable"),
27
- name: z.string(),
28
- }),
30
+ DataSourceVariableRef,
29
31
  z.object({
30
32
  type: z.literal("expression"),
31
33
  name: z.string(),
@@ -58,6 +60,16 @@ const EmbedTemplateProp = z.union([
58
60
  dataSourceRef: z.optional(DataSourceRef),
59
61
  value: z.array(z.string()),
60
62
  }),
63
+ z.object({
64
+ type: z.literal("action"),
65
+ name: z.string(),
66
+ value: z.array(
67
+ z.object({
68
+ type: z.literal("execute"),
69
+ code: z.string(),
70
+ })
71
+ ),
72
+ }),
61
73
  ]);
62
74
 
63
75
  type EmbedTemplateProp = z.infer<typeof EmbedTemplateProp>;
@@ -124,6 +136,29 @@ const createInstancesFromTemplate = (
124
136
  if (item.props) {
125
137
  for (const prop of item.props) {
126
138
  const propId = nanoid();
139
+ // action cannot be bound to data source
140
+ if (prop.type === "action") {
141
+ props.push({
142
+ id: propId,
143
+ instanceId,
144
+ type: "action",
145
+ name: prop.name,
146
+ value: prop.value.map((value) => {
147
+ return {
148
+ type: "execute",
149
+ // replace all references with variable names
150
+ code: validateExpression(value.code, {
151
+ effectful: true,
152
+ transformIdentifier: (ref) => {
153
+ const id = dataSourceByRef.get(ref)?.id ?? ref;
154
+ return encodeDataSourceVariable(id);
155
+ },
156
+ }),
157
+ };
158
+ }),
159
+ });
160
+ continue;
161
+ }
127
162
  if (prop.dataSourceRef === undefined) {
128
163
  props.push({ id: propId, instanceId, ...prop });
129
164
  continue;
@@ -148,7 +183,13 @@ const createInstancesFromTemplate = (
148
183
  id,
149
184
  scopeInstanceId: instanceId,
150
185
  name: dataSourceRef.name,
151
- code: dataSourceRef.code,
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
+ }),
152
193
  };
153
194
  dataSourceByRef.set(dataSourceRef.name, dataSource);
154
195
  } else {
@@ -246,25 +287,17 @@ export const generateDataFromEmbedTemplate = (
246
287
  defaultBreakpointId
247
288
  );
248
289
 
249
- // replace all references with variable names
250
- const dataSources: DataSource[] = [];
251
- for (const dataSource of dataSourceByRef.values()) {
252
- if (dataSource.type === "expression") {
253
- dataSource.code = validateExpression(dataSource.code, (ref) => {
254
- const id = dataSourceByRef.get(ref)?.id ?? ref;
255
- return encodeDataSourceVariable(id);
256
- });
257
- }
258
- dataSources.push(dataSource);
259
- }
260
-
261
290
  return {
262
291
  children,
263
292
  instances,
264
293
  props,
265
- dataSources,
294
+ dataSources: Array.from(dataSourceByRef.values()),
266
295
  styleSourceSelections,
267
296
  styleSources,
268
297
  styles,
269
298
  };
270
299
  };
300
+
301
+ export type EmbedTemplateData = ReturnType<
302
+ typeof generateDataFromEmbedTemplate
303
+ >;
@@ -2,8 +2,10 @@ import { expect, test } from "@jest/globals";
2
2
  import {
3
3
  decodeDataSourceVariable,
4
4
  encodeDataSourceVariable,
5
- executeExpressions,
6
- generateExpressionsComputation,
5
+ executeComputingExpressions,
6
+ executeEffectfulExpression,
7
+ generateComputingExpressions,
8
+ generateEffectfulExpression,
7
9
  validateExpression,
8
10
  } from "./expression";
9
11
 
@@ -17,6 +19,13 @@ test("allow unary and binary expressions", () => {
17
19
  expect(validateExpression(`[-1, 1 + 1]`)).toEqual(`[-1, 1 + 1]`);
18
20
  });
19
21
 
22
+ test("optionally allow assignment expressions", () => {
23
+ expect(() => {
24
+ validateExpression(`a = 2`);
25
+ }).toThrowError(/Cannot use assignment in this expression/);
26
+ expect(validateExpression(`a = 2`, { effectful: true })).toEqual(`a = 2`);
27
+ });
28
+
20
29
  test("forbid member expressions", () => {
21
30
  expect(() => {
22
31
  validateExpression("var1 + obj.param");
@@ -44,6 +53,21 @@ test("forbid ternary", () => {
44
53
  }).toThrowError(/Ternary operator is not supported/);
45
54
  });
46
55
 
56
+ test("forbid increment and decrement", () => {
57
+ expect(() => {
58
+ validateExpression("++var1");
59
+ }).toThrowError(/"\+\+" operator is not supported/);
60
+ expect(() => {
61
+ validateExpression("var1++");
62
+ }).toThrowError(/"\+\+" operator is not supported/);
63
+ expect(() => {
64
+ validateExpression("--var1");
65
+ }).toThrowError(/"--" operator is not supported/);
66
+ expect(() => {
67
+ validateExpression("var1--");
68
+ }).toThrowError(/"--" operator is not supported/);
69
+ });
70
+
47
71
  test("forbid multiple expressions", () => {
48
72
  expect(() => {
49
73
  validateExpression("a b");
@@ -57,12 +81,14 @@ test("forbid multiple expressions", () => {
57
81
  });
58
82
 
59
83
  test("transform identifiers", () => {
60
- expect(validateExpression(`a + b`, (id) => `$ws$${id}`)).toEqual(
61
- `$ws$a + $ws$b`
62
- );
84
+ expect(
85
+ validateExpression(`a + b`, {
86
+ transformIdentifier: (id) => `$ws$${id}`,
87
+ })
88
+ ).toEqual(`$ws$a + $ws$b`);
63
89
  });
64
90
 
65
- test("generate expressions computation", () => {
91
+ test("generate computing expressions", () => {
66
92
  const variables = new Set(["var0"]);
67
93
  const expressions = new Map([
68
94
  ["exp3", "exp2 + exp1"],
@@ -70,7 +96,7 @@ test("generate expressions computation", () => {
70
96
  ["exp2", "exp1"],
71
97
  ["exp4", "exp2"],
72
98
  ]);
73
- expect(generateExpressionsComputation(variables, expressions))
99
+ expect(generateComputingExpressions(expressions, variables))
74
100
  .toMatchInlineSnapshot(`
75
101
  "const var0 = _variables.get('var0');
76
102
  const exp1 = (var0);
@@ -86,10 +112,22 @@ test("generate expressions computation", () => {
86
112
  `);
87
113
  });
88
114
 
115
+ test("add only used variables in computing expression", () => {
116
+ const expressions = new Map([["exp1", "var0"]]);
117
+ expect(generateComputingExpressions(expressions, new Set(["var0", "var1"])))
118
+ .toMatchInlineSnapshot(`
119
+ "const var0 = _variables.get('var0');
120
+ const exp1 = (var0);
121
+ return new Map([
122
+ ['exp1', exp1],
123
+ ]);"
124
+ `);
125
+ });
126
+
89
127
  test("execute expression", () => {
90
128
  const variables = new Map();
91
129
  const expressions = new Map([["exp1", "1 + 1"]]);
92
- expect(executeExpressions(variables, expressions)).toEqual(
130
+ expect(executeComputingExpressions(expressions, variables)).toEqual(
93
131
  new Map([["exp1", 2]])
94
132
  );
95
133
  });
@@ -97,7 +135,7 @@ test("execute expression", () => {
97
135
  test("execute expression dependent on variables", () => {
98
136
  const variables = new Map([["var1", 5]]);
99
137
  const expressions = new Map([["exp1", "var1 + 1"]]);
100
- expect(executeExpressions(variables, expressions)).toEqual(
138
+ expect(executeComputingExpressions(expressions, variables)).toEqual(
101
139
  new Map([["exp1", 6]])
102
140
  );
103
141
  });
@@ -108,7 +146,7 @@ test("execute expression dependent on another expressions", () => {
108
146
  ["exp1", "exp0 + 1"],
109
147
  ["exp0", "var1 + 2"],
110
148
  ]);
111
- expect(executeExpressions(variables, expressions)).toEqual(
149
+ expect(executeComputingExpressions(expressions, variables)).toEqual(
112
150
  new Map([
113
151
  ["exp1", 6],
114
152
  ["exp0", 5],
@@ -124,7 +162,7 @@ test("forbid circular expressions", () => {
124
162
  ["exp2", "exp1 + 3"],
125
163
  ]);
126
164
  expect(() => {
127
- executeExpressions(variables, expressions);
165
+ executeComputingExpressions(expressions, variables);
128
166
  }).toThrowError(/Cannot access 'exp0' before initialization/);
129
167
  });
130
168
 
@@ -132,7 +170,7 @@ test("make sure dependency exists", () => {
132
170
  const variables = new Map();
133
171
  const expressions = new Map([["exp1", "var1 + 1"]]);
134
172
  expect(() => {
135
- executeExpressions(variables, expressions);
173
+ executeComputingExpressions(expressions, variables);
136
174
  }).toThrowError(/Unknown dependency "var1"/);
137
175
  });
138
176
 
@@ -145,3 +183,59 @@ test("encode/decode variable names", () => {
145
183
  );
146
184
  expect(decodeDataSourceVariable("myVarName")).toEqual(undefined);
147
185
  });
186
+
187
+ test("generate effectful expression", () => {
188
+ expect(
189
+ generateEffectfulExpression(`var0 = var0 + var1`, new Set(["var0", "var1"]))
190
+ ).toMatchInlineSnapshot(`
191
+ "let var0 = _variables.get('var0');
192
+ let var1 = _variables.get('var1');
193
+ var0 = var0 + var1;
194
+ return new Map([
195
+ ['var0', var0],
196
+ ]);"
197
+ `);
198
+
199
+ expect(
200
+ generateEffectfulExpression(`var0 = var1 + 1`, new Set(["var0", "var1"]))
201
+ ).toMatchInlineSnapshot(`
202
+ "let var1 = _variables.get('var1');
203
+ let var0;
204
+ var0 = var1 + 1;
205
+ return new Map([
206
+ ['var0', var0],
207
+ ]);"
208
+ `);
209
+ });
210
+
211
+ test("add only used variables in effectful expression", () => {
212
+ expect(
213
+ generateEffectfulExpression(
214
+ `var0 = var1 + 1`,
215
+ new Set(["var0", "var1", "var2"])
216
+ )
217
+ ).toMatchInlineSnapshot(`
218
+ "let var1 = _variables.get('var1');
219
+ let var0;
220
+ var0 = var1 + 1;
221
+ return new Map([
222
+ ['var0', var0],
223
+ ]);"
224
+ `);
225
+ });
226
+
227
+ test("forbid not allowed variables in effectful expression", () => {
228
+ expect(() => {
229
+ generateEffectfulExpression(`var0 = var0 + var1`, new Set(["var0"]));
230
+ }).toThrowError(/Unknown dependency "var1"/);
231
+ });
232
+
233
+ test("execute effectful expression", () => {
234
+ const variables = new Map([
235
+ ["var0", 2],
236
+ ["var1", 3],
237
+ ]);
238
+ expect(executeEffectfulExpression(`var0 = var0 + var1`, variables)).toEqual(
239
+ new Map([["var0", 5]])
240
+ );
241
+ });