@webstudio-is/react-sdk 0.78.0 → 0.80.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 (40) hide show
  1. package/lib/cjs/context.js +8 -2
  2. package/lib/cjs/css/normalize.js +23 -43
  3. package/lib/cjs/css/presets.js +1 -111
  4. package/lib/cjs/embed-template.js +45 -16
  5. package/lib/cjs/expression.js +137 -22
  6. package/lib/cjs/index.js +6 -2
  7. package/lib/cjs/props.js +32 -2
  8. package/lib/cjs/tree/create-elements-tree.js +7 -2
  9. package/lib/cjs/tree/root.js +15 -7
  10. package/lib/context.js +8 -2
  11. package/lib/css/normalize.js +23 -33
  12. package/lib/css/presets.js +1 -111
  13. package/lib/embed-template.js +45 -16
  14. package/lib/expression.js +137 -22
  15. package/lib/index.js +13 -5
  16. package/lib/props.js +32 -2
  17. package/lib/tree/create-elements-tree.js +10 -3
  18. package/lib/tree/root.js +16 -8
  19. package/lib/types/components/component-meta.d.ts +30 -0
  20. package/lib/types/context.d.ts +5 -2
  21. package/lib/types/css/normalize.d.ts +0 -520
  22. package/lib/types/css/presets.d.ts +0 -282
  23. package/lib/types/embed-template.d.ts +38 -0
  24. package/lib/types/expression.d.ts +12 -4
  25. package/lib/types/index.d.ts +1 -1
  26. package/lib/types/props.d.ts +10 -0
  27. package/lib/types/tree/create-elements-tree.d.ts +6 -5
  28. package/lib/types/tree/root.d.ts +5 -5
  29. package/package.json +17 -16
  30. package/src/context.tsx +17 -4
  31. package/src/css/normalize.ts +23 -32
  32. package/src/css/presets.ts +0 -110
  33. package/src/embed-template.test.ts +84 -4
  34. package/src/embed-template.ts +51 -18
  35. package/src/expression.test.ts +106 -12
  36. package/src/expression.ts +157 -26
  37. package/src/index.ts +6 -2
  38. package/src/props.ts +33 -3
  39. package/src/tree/create-elements-tree.tsx +19 -10
  40. package/src/tree/root.ts +24 -13
package/src/expression.ts CHANGED
@@ -1,27 +1,37 @@
1
1
  import jsep from "jsep";
2
+ import jsepAssignment from "@jsep-plugin/assignment";
3
+ import type {
4
+ UpdateExpression,
5
+ AssignmentExpression,
6
+ } from "@jsep-plugin/assignment";
2
7
 
3
- type TransformIdentifier = (id: string) => string;
8
+ jsep.plugins.register(jsepAssignment);
4
9
 
5
- type Node = jsep.CoreExpression;
10
+ type TransformIdentifier = (id: string, assignee: boolean) => string;
11
+
12
+ type Node = jsep.CoreExpression | UpdateExpression | AssignmentExpression;
6
13
 
7
14
  const generateCode = (
8
15
  node: Node,
9
16
  failOnForbidden: boolean,
17
+ effectful: boolean,
10
18
  transformIdentifier: TransformIdentifier
11
19
  ): string => {
12
20
  if (node.type === "Identifier") {
13
- return transformIdentifier(node.name);
21
+ return transformIdentifier(node.name, false);
14
22
  }
15
23
  if (node.type === "MemberExpression") {
16
24
  if (failOnForbidden) {
17
25
  const object = generateCode(
18
26
  node.object as Node,
19
27
  false,
28
+ effectful,
20
29
  transformIdentifier
21
30
  );
22
31
  const property = generateCode(
23
32
  node.property as Node,
24
33
  false,
34
+ effectful,
25
35
  transformIdentifier
26
36
  );
27
37
  throw Error(`Cannot access "${property}" of "${object}"`);
@@ -29,11 +39,13 @@ const generateCode = (
29
39
  const object = generateCode(
30
40
  node.object as Node,
31
41
  failOnForbidden,
42
+ effectful,
32
43
  transformIdentifier
33
44
  );
34
45
  const property = generateCode(
35
46
  node.property as Node,
36
47
  failOnForbidden,
48
+ effectful,
37
49
  transformIdentifier
38
50
  );
39
51
  return `${object}.${property}`;
@@ -45,6 +57,7 @@ const generateCode = (
45
57
  const arg = generateCode(
46
58
  node.argument as Node,
47
59
  failOnForbidden,
60
+ effectful,
48
61
  transformIdentifier
49
62
  );
50
63
  return `${node.operator}${arg}`;
@@ -53,18 +66,25 @@ const generateCode = (
53
66
  const left = generateCode(
54
67
  node.left as Node,
55
68
  failOnForbidden,
69
+ effectful,
56
70
  transformIdentifier
57
71
  );
58
72
  const right = generateCode(
59
73
  node.right as Node,
60
74
  failOnForbidden,
75
+ effectful,
61
76
  transformIdentifier
62
77
  );
63
78
  return `${left} ${node.operator} ${right}`;
64
79
  }
65
80
  if (node.type === "ArrayExpression") {
66
81
  const elements = node.elements.map((element) =>
67
- generateCode(element as Node, failOnForbidden, transformIdentifier)
82
+ generateCode(
83
+ element as Node,
84
+ failOnForbidden,
85
+ effectful,
86
+ transformIdentifier
87
+ )
68
88
  );
69
89
  return `[${elements.join(", ")}]`;
70
90
  }
@@ -73,6 +93,7 @@ const generateCode = (
73
93
  const callee = generateCode(
74
94
  node.callee as Node,
75
95
  false,
96
+ effectful,
76
97
  transformIdentifier
77
98
  );
78
99
  throw Error(`Cannot call "${callee}"`);
@@ -80,10 +101,11 @@ const generateCode = (
80
101
  const callee = generateCode(
81
102
  node.callee as Node,
82
103
  failOnForbidden,
104
+ effectful,
83
105
  transformIdentifier
84
106
  );
85
107
  const args = node.arguments.map((arg) =>
86
- generateCode(arg as Node, failOnForbidden, transformIdentifier)
108
+ generateCode(arg as Node, failOnForbidden, effectful, transformIdentifier)
87
109
  );
88
110
  return `${callee}(${args.join(", ")})`;
89
111
  }
@@ -99,16 +121,43 @@ const generateCode = (
99
121
  if (node.type === "Compound") {
100
122
  throw Error("Cannot use multiple expressions");
101
123
  }
124
+ if (node.type === "AssignmentExpression") {
125
+ if (node.operator !== "=") {
126
+ throw Error(`Only "=" assignment operator is supported`);
127
+ }
128
+ if (effectful === false) {
129
+ throw Error(`Cannot use assignment in this expression`);
130
+ }
131
+ const left = generateCode(
132
+ node.left as Node,
133
+ failOnForbidden,
134
+ effectful,
135
+ // override and mark all identifiers inside of left expression as assignee
136
+ (id) => transformIdentifier(id, true)
137
+ );
138
+ const right = generateCode(
139
+ node.right as Node,
140
+ failOnForbidden,
141
+ effectful,
142
+ transformIdentifier
143
+ );
144
+ return `${left} ${node.operator} ${right}`;
145
+ }
146
+ if (node.type === "UpdateExpression") {
147
+ throw Error(`"${node.operator}" operator is not supported`);
148
+ }
102
149
  node satisfies never;
103
150
  return "";
104
151
  };
105
152
 
106
153
  export const validateExpression = (
107
154
  code: string,
108
- transformIdentifier: TransformIdentifier = (id) => id
155
+ options?: { effectful?: boolean; transformIdentifier?: TransformIdentifier }
109
156
  ) => {
157
+ const { effectful = false, transformIdentifier = (id: string) => id } =
158
+ options ?? {};
110
159
  const expression = jsep(code) as Node;
111
- return generateCode(expression, true, transformIdentifier);
160
+ return generateCode(expression, true, effectful, transformIdentifier);
112
161
  };
113
162
 
114
163
  const sortTopologically = (
@@ -135,22 +184,26 @@ const sortTopologically = (
135
184
  * Generates a function body expecting map as _variables argument
136
185
  * and outputing map of results
137
186
  */
138
- export const generateExpressionsComputation = (
139
- variables: Set<string>,
140
- expressions: Map<string, string>
187
+ export const generateComputingExpressions = (
188
+ expressions: Map<string, string>,
189
+ allowedVariables: Set<string>
141
190
  ) => {
142
191
  const depsById = new Map<string, Set<string>>();
192
+ const inputVariables = new Set<string>();
143
193
  for (const [id, code] of expressions) {
144
194
  const deps = new Set<string>();
145
- validateExpression(code, (identifier) => {
146
- if (variables.has(identifier)) {
147
- return identifier;
148
- }
149
- if (expressions.has(identifier)) {
150
- deps.add(identifier);
151
- return identifier;
152
- }
153
- throw Error(`Unknown dependency "${identifier}"`);
195
+ validateExpression(code, {
196
+ transformIdentifier: (identifier) => {
197
+ if (allowedVariables.has(identifier)) {
198
+ inputVariables.add(identifier);
199
+ return identifier;
200
+ }
201
+ if (expressions.has(identifier)) {
202
+ deps.add(identifier);
203
+ return identifier;
204
+ }
205
+ throw Error(`Unknown dependency "${identifier}"`);
206
+ },
154
207
  });
155
208
  depsById.set(id, deps);
156
209
  }
@@ -163,7 +216,7 @@ export const generateExpressionsComputation = (
163
216
  // generate code computing all expressions
164
217
  let generatedCode = "";
165
218
 
166
- for (const id of variables) {
219
+ for (const id of inputVariables) {
167
220
  generatedCode += `const ${id} = _variables.get('${id}');\n`;
168
221
  }
169
222
 
@@ -184,19 +237,78 @@ export const generateExpressionsComputation = (
184
237
  return generatedCode;
185
238
  };
186
239
 
187
- export const executeExpressions = (
188
- variables: Map<string, unknown>,
189
- expressions: Map<string, string>
240
+ export const executeComputingExpressions = (
241
+ expressions: Map<string, string>,
242
+ variables: Map<string, unknown>
190
243
  ) => {
191
- const generatedCode = generateExpressionsComputation(
192
- new Set(variables.keys()),
193
- expressions
244
+ const generatedCode = generateComputingExpressions(
245
+ expressions,
246
+ new Set(variables.keys())
194
247
  );
195
248
  const executeFn = new Function("_variables", generatedCode);
196
249
  const values = executeFn(variables) as Map<string, unknown>;
197
250
  return values;
198
251
  };
199
252
 
253
+ export const generateEffectfulExpression = (
254
+ code: string,
255
+ allowedVariables: Set<string>
256
+ ) => {
257
+ const inputVariables = new Set<string>();
258
+ const outputVariables = new Set<string>();
259
+ validateExpression(code, {
260
+ effectful: true,
261
+ transformIdentifier: (identifier, assignee) => {
262
+ if (allowedVariables.has(identifier)) {
263
+ if (assignee) {
264
+ outputVariables.add(identifier);
265
+ } else {
266
+ inputVariables.add(identifier);
267
+ }
268
+ return identifier;
269
+ }
270
+ throw Error(`Unknown dependency "${identifier}"`);
271
+ },
272
+ });
273
+
274
+ // generate code computing all expressions
275
+ let generatedCode = "";
276
+
277
+ for (const id of inputVariables) {
278
+ generatedCode += `let ${id} = _variables.get('${id}');\n`;
279
+ }
280
+ for (const id of outputVariables) {
281
+ if (inputVariables.has(id) === false) {
282
+ generatedCode += `let ${id};\n`;
283
+ }
284
+ }
285
+
286
+ generatedCode += `${code};\n`;
287
+
288
+ generatedCode += `return new Map([\n`;
289
+ for (const id of outputVariables) {
290
+ generatedCode += ` ['${id}', ${id}],\n`;
291
+ }
292
+ generatedCode += `]);`;
293
+
294
+ return generatedCode;
295
+ };
296
+
297
+ export const executeEffectfulExpression = (
298
+ code: string,
299
+ variables: Map<string, unknown>
300
+ ) => {
301
+ const generatedCode = generateEffectfulExpression(
302
+ code,
303
+ new Set(variables.keys())
304
+ );
305
+ const executeFn = new Function("_variables", generatedCode);
306
+ const values = executeFn(variables) as Map<string, unknown>;
307
+ return values;
308
+ };
309
+
310
+ type Values = Map<string, unknown>;
311
+
200
312
  const dataSourceVariablePrefix = "$ws$dataSource$";
201
313
 
202
314
  // data source id is generated with nanoid which has "-" in alphabeta
@@ -208,6 +320,14 @@ export const encodeDataSourceVariable = (id: string) => {
208
320
  return `${dataSourceVariablePrefix}${encoded}`;
209
321
  };
210
322
 
323
+ export const encodeVariablesMap = (values: Values) => {
324
+ const encodedValues: Values = new Map();
325
+ for (const [id, value] of values) {
326
+ encodedValues.set(encodeDataSourceVariable(id), value);
327
+ }
328
+ return encodedValues;
329
+ };
330
+
211
331
  export const decodeDataSourceVariable = (name: string) => {
212
332
  if (name.startsWith(dataSourceVariablePrefix)) {
213
333
  const encoded = name.slice(dataSourceVariablePrefix.length);
@@ -215,3 +335,14 @@ export const decodeDataSourceVariable = (name: string) => {
215
335
  }
216
336
  return;
217
337
  };
338
+
339
+ export const decodeVariablesMap = (values: Values) => {
340
+ const decodedValues: Values = new Map();
341
+ for (const [name, value] of values) {
342
+ const id = decodeDataSourceVariable(name);
343
+ if (id !== undefined) {
344
+ decodedValues.set(id, value);
345
+ }
346
+ }
347
+ return decodedValues;
348
+ };
package/src/index.ts CHANGED
@@ -22,8 +22,12 @@ export {
22
22
  export { type Params, ReactSdkContext } from "./context";
23
23
  export {
24
24
  validateExpression,
25
- generateExpressionsComputation,
26
- executeExpressions,
25
+ generateComputingExpressions,
26
+ executeComputingExpressions,
27
+ generateEffectfulExpression,
28
+ executeEffectfulExpression,
27
29
  encodeDataSourceVariable,
30
+ encodeVariablesMap,
28
31
  decodeDataSourceVariable,
32
+ decodeVariablesMap,
29
33
  } from "./expression";
package/src/props.ts CHANGED
@@ -26,8 +26,13 @@ export const getPropsByInstanceId = (props: Props) => {
26
26
  // this utility is be used only for preview with static props
27
27
  // so there is no need to use computed to optimize rerenders
28
28
  export const useInstanceProps = (instanceId: Instance["id"]) => {
29
- const { propsByInstanceIdStore, dataSourceValuesStore } =
30
- useContext(ReactSdkContext);
29
+ const {
30
+ propsByInstanceIdStore,
31
+ dataSourceValuesStore,
32
+ executeEffectfulExpression,
33
+ setDataSourceValues,
34
+ renderer,
35
+ } = useContext(ReactSdkContext);
31
36
  const instancePropsObjectStore = useMemo(() => {
32
37
  return computed(
33
38
  [propsByInstanceIdStore, dataSourceValuesStore],
@@ -49,12 +54,37 @@ export const useInstanceProps = (instanceId: Instance["id"]) => {
49
54
  }
50
55
  continue;
51
56
  }
57
+ if (prop.type === "action") {
58
+ instancePropsObject[prop.name] = () => {
59
+ // prevent all actions in canvas mode
60
+ if (renderer === "canvas") {
61
+ return;
62
+ }
63
+ for (const value of prop.value) {
64
+ if (value.type === "execute") {
65
+ const newValues = executeEffectfulExpression(
66
+ value.code,
67
+ dataSourceValues
68
+ );
69
+ setDataSourceValues(newValues);
70
+ }
71
+ }
72
+ };
73
+ continue;
74
+ }
52
75
  instancePropsObject[prop.name] = prop.value;
53
76
  }
54
77
  return instancePropsObject;
55
78
  }
56
79
  );
57
- }, [propsByInstanceIdStore, dataSourceValuesStore, instanceId]);
80
+ }, [
81
+ propsByInstanceIdStore,
82
+ dataSourceValuesStore,
83
+ instanceId,
84
+ renderer,
85
+ executeEffectfulExpression,
86
+ setDataSourceValues,
87
+ ]);
58
88
  const instancePropsObject = useStore(instancePropsObjectStore);
59
89
  return instancePropsObject;
60
90
  };
@@ -2,13 +2,13 @@ import { type ComponentProps, Fragment } from "react";
2
2
  import type { ReadableAtom } from "nanostores";
3
3
  import { Scripts, ScrollRestoration } from "@remix-run/react";
4
4
  import type { Assets } from "@webstudio-is/asset-uploader";
5
- import type {
6
- DataSource,
7
- Instance,
8
- Instances,
9
- } from "@webstudio-is/project-build";
5
+ import type { Instance, Instances } from "@webstudio-is/project-build";
10
6
  import type { Components } from "../components/components-utils";
11
- import { ReactSdkContext, type Params } from "../context";
7
+ import {
8
+ type Params,
9
+ type DataSourceValues,
10
+ ReactSdkContext,
11
+ } from "../context";
12
12
  import type { Pages, PropsByInstanceId } from "../props";
13
13
  import type { WebstudioComponent } from "./webstudio-component";
14
14
 
@@ -24,6 +24,7 @@ export const createElementsTree = ({
24
24
  assetsStore,
25
25
  pagesStore,
26
26
  dataSourceValuesStore,
27
+ executeEffectfulExpression,
27
28
  onDataSourceUpdate,
28
29
  Component,
29
30
  components,
@@ -33,8 +34,12 @@ export const createElementsTree = ({
33
34
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
34
35
  assetsStore: ReadableAtom<Assets>;
35
36
  pagesStore: ReadableAtom<Pages>;
36
- dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
37
- onDataSourceUpdate: (dataSourceId: DataSource["id"], value: unknown) => void;
37
+ executeEffectfulExpression: (
38
+ expression: string,
39
+ values: DataSourceValues
40
+ ) => DataSourceValues;
41
+ dataSourceValuesStore: ReadableAtom<DataSourceValues>;
42
+ onDataSourceUpdate: (newValues: DataSourceValues) => void;
38
43
  Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
39
44
  components: Components;
40
45
  }) => {
@@ -74,7 +79,9 @@ export const createElementsTree = ({
74
79
  renderer,
75
80
  imageBaseUrl,
76
81
  assetBaseUrl,
77
- setDataSourceValue: (instanceId, propName, value) => {
82
+ executeEffectfulExpression,
83
+ setDataSourceValues: onDataSourceUpdate,
84
+ setBoundDataSourceValue: (instanceId, propName, value) => {
78
85
  const propsByInstanceId = propsByInstanceIdStore.get();
79
86
  const props = propsByInstanceId.get(instanceId);
80
87
  const prop = props?.find((prop) => prop.name === propName);
@@ -82,7 +89,9 @@ export const createElementsTree = ({
82
89
  throw Error(`${propName} is not data source`);
83
90
  }
84
91
  const dataSourceId = prop.value;
85
- onDataSourceUpdate(dataSourceId, value);
92
+ const newValues = new Map();
93
+ newValues.set(dataSourceId, value);
94
+ onDataSourceUpdate(newValues);
86
95
  },
87
96
  }}
88
97
  >
package/src/tree/root.ts CHANGED
@@ -1,17 +1,17 @@
1
- import { useRef, type ComponentProps } from "react";
1
+ import { useRef, type ComponentProps, useCallback } from "react";
2
2
  import {
3
3
  atom,
4
4
  computed,
5
5
  type ReadableAtom,
6
6
  type WritableAtom,
7
7
  } from "nanostores";
8
- import { type Build, type Page, DataSource } from "@webstudio-is/project-build";
8
+ import { type Build, type Page } from "@webstudio-is/project-build";
9
9
  import type { Asset } from "@webstudio-is/asset-uploader";
10
10
  import { createElementsTree } from "./create-elements-tree";
11
11
  import { WebstudioComponent } from "./webstudio-component";
12
12
  import { getPropsByInstanceId } from "../props";
13
13
  import type { Components } from "../components/components-utils";
14
- import type { Params } from "../context";
14
+ import type { Params, DataSourceValues } from "../context";
15
15
 
16
16
  export type Data = {
17
17
  page: Page;
@@ -25,18 +25,21 @@ export type RootPropsData = Omit<Data, "build"> & {
25
25
  build: Pick<Data["build"], "instances" | "props" | "dataSources">;
26
26
  };
27
27
 
28
- type DataSourceValues = Map<DataSource["id"], unknown>;
29
-
30
28
  type RootProps = {
31
29
  data: RootPropsData;
32
- computeExpressions: (values: DataSourceValues) => DataSourceValues;
30
+ executeComputingExpressions: (values: DataSourceValues) => DataSourceValues;
31
+ executeEffectfulExpression: (
32
+ expression: string,
33
+ values: DataSourceValues
34
+ ) => DataSourceValues;
33
35
  Component?: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
34
36
  components: Components;
35
37
  };
36
38
 
37
39
  export const InstanceRoot = ({
38
40
  data,
39
- computeExpressions,
41
+ executeComputingExpressions,
42
+ executeEffectfulExpression,
40
43
  Component,
41
44
  components,
42
45
  }: RootProps): JSX.Element | null => {
@@ -67,7 +70,7 @@ export const InstanceRoot = ({
67
70
 
68
71
  // set expression values
69
72
  try {
70
- const result = computeExpressions(dataSourceValues);
73
+ const result = executeComputingExpressions(dataSourceValues);
71
74
  for (const [id, value] of result) {
72
75
  dataSourceValues.set(id, value);
73
76
  }
@@ -82,6 +85,17 @@ export const InstanceRoot = ({
82
85
  }
83
86
  const dataSourceValuesStore = dataSourceValuesStoreRef.current;
84
87
 
88
+ const onDataSourceUpdate = useCallback(
89
+ (newValues: DataSourceValues) => {
90
+ const dataSourceVariables = new Map(dataSourceVariablesStore.get());
91
+ for (const [dataSourceId, value] of newValues) {
92
+ dataSourceVariables.set(dataSourceId, value);
93
+ }
94
+ dataSourceVariablesStore.set(dataSourceVariables);
95
+ },
96
+ [dataSourceVariablesStore]
97
+ );
98
+
85
99
  return createElementsTree({
86
100
  imageBaseUrl: data.params?.imageBaseUrl ?? "/",
87
101
  assetBaseUrl: data.params?.assetBaseUrl ?? "/",
@@ -92,12 +106,9 @@ export const InstanceRoot = ({
92
106
  ),
93
107
  assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
94
108
  pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
109
+ executeEffectfulExpression,
95
110
  dataSourceValuesStore,
96
- onDataSourceUpdate: (dataSourceId, value) => {
97
- const dataSourceVariables = new Map(dataSourceVariablesStore.get());
98
- dataSourceVariables.set(dataSourceId, value);
99
- dataSourceVariablesStore.set(dataSourceVariables);
100
- },
111
+ onDataSourceUpdate,
101
112
  Component: Component ?? WebstudioComponent,
102
113
  components,
103
114
  });