@webstudio-is/react-sdk 0.75.0 → 0.76.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 (41) hide show
  1. package/lib/cjs/context.js +5 -1
  2. package/lib/cjs/css/normalize.js +9 -1
  3. package/lib/cjs/embed-template.js +68 -3
  4. package/lib/cjs/expression.js +191 -0
  5. package/lib/cjs/index.js +7 -1
  6. package/lib/cjs/props.js +28 -10
  7. package/lib/cjs/tree/create-elements-tree.js +14 -1
  8. package/lib/cjs/tree/root.js +55 -0
  9. package/lib/cjs/tree/webstudio-component.js +9 -2
  10. package/lib/context.js +5 -1
  11. package/lib/css/normalize.js +9 -1
  12. package/lib/embed-template.js +68 -3
  13. package/lib/expression.js +161 -0
  14. package/lib/index.js +13 -1
  15. package/lib/props.js +28 -10
  16. package/lib/tree/create-elements-tree.js +14 -1
  17. package/lib/tree/root.js +63 -1
  18. package/lib/tree/webstudio-component.js +9 -2
  19. package/lib/types/components/component-meta.d.ts +112 -0
  20. package/lib/types/context.d.ts +3 -0
  21. package/lib/types/css/normalize.d.ts +1316 -0
  22. package/lib/types/embed-template.d.ts +512 -0
  23. package/lib/types/expression.d.ts +6 -0
  24. package/lib/types/expression.test.d.ts +1 -0
  25. package/lib/types/index.d.ts +2 -1
  26. package/lib/types/props.d.ts +8 -7
  27. package/lib/types/tree/create-elements-tree.d.ts +4 -2
  28. package/lib/types/tree/root.d.ts +3 -3
  29. package/lib/types/tree/webstudio-component.d.ts +1 -0
  30. package/package.json +10 -10
  31. package/src/context.tsx +11 -0
  32. package/src/css/normalize.ts +9 -1
  33. package/src/embed-template.test.ts +177 -1
  34. package/src/embed-template.ts +73 -2
  35. package/src/expression.test.ts +122 -0
  36. package/src/expression.ts +183 -0
  37. package/src/index.ts +7 -0
  38. package/src/props.ts +29 -10
  39. package/src/tree/create-elements-tree.tsx +20 -1
  40. package/src/tree/root.ts +81 -4
  41. package/src/tree/webstudio-component.tsx +7 -1
@@ -1,29 +1,45 @@
1
1
  import { z } from "zod";
2
2
  import { nanoid } from "nanoid";
3
3
  import { StyleValue } from "@webstudio-is/css-data";
4
+ import { encodeDataSourceVariable, validateExpression } from "./expression";
4
5
  const EmbedTemplateText = z.object({
5
6
  type: z.literal("text"),
6
7
  value: z.string()
7
8
  });
9
+ const DataSourceRef = z.union([
10
+ z.object({
11
+ type: z.literal("variable"),
12
+ name: z.string()
13
+ }),
14
+ z.object({
15
+ type: z.literal("expression"),
16
+ name: z.string(),
17
+ code: z.string()
18
+ })
19
+ ]);
8
20
  const EmbedTemplateProp = z.union([
9
21
  z.object({
10
22
  type: z.literal("number"),
11
23
  name: z.string(),
24
+ dataSourceRef: z.optional(DataSourceRef),
12
25
  value: z.number()
13
26
  }),
14
27
  z.object({
15
28
  type: z.literal("string"),
16
29
  name: z.string(),
30
+ dataSourceRef: z.optional(DataSourceRef),
17
31
  value: z.string()
18
32
  }),
19
33
  z.object({
20
34
  type: z.literal("boolean"),
21
35
  name: z.string(),
36
+ dataSourceRef: z.optional(DataSourceRef),
22
37
  value: z.boolean()
23
38
  }),
24
39
  z.object({
25
40
  type: z.literal("string[]"),
26
41
  name: z.string(),
42
+ dataSourceRef: z.optional(DataSourceRef),
27
43
  value: z.array(z.string())
28
44
  })
29
45
  ]);
@@ -47,17 +63,52 @@ const EmbedTemplateInstance = z.lazy(
47
63
  const WsEmbedTemplate = z.lazy(
48
64
  () => z.array(z.union([EmbedTemplateInstance, EmbedTemplateText]))
49
65
  );
50
- const createInstancesFromTemplate = (treeTemplate, instances, props, styleSourceSelections, styleSources, styles, defaultBreakpointId) => {
66
+ const createInstancesFromTemplate = (treeTemplate, instances, props, dataSourceByRef, styleSourceSelections, styleSources, styles, defaultBreakpointId) => {
51
67
  const parentChildren = [];
52
68
  for (const item of treeTemplate) {
53
69
  if (item.type === "instance") {
54
70
  const instanceId = nanoid();
55
71
  if (item.props) {
56
72
  for (const prop of item.props) {
73
+ const propId = nanoid();
74
+ if (prop.dataSourceRef === void 0) {
75
+ props.push({ id: propId, instanceId, ...prop });
76
+ continue;
77
+ }
78
+ let dataSource = dataSourceByRef.get(prop.dataSourceRef.name);
79
+ if (dataSource === void 0) {
80
+ const id = nanoid();
81
+ const { name: propName, dataSourceRef, ...rest } = prop;
82
+ if (dataSourceRef.type === "variable") {
83
+ dataSource = {
84
+ type: "variable",
85
+ id,
86
+ // the first instance where data source is appeared in becomes its scope
87
+ scopeInstanceId: instanceId,
88
+ name: dataSourceRef.name,
89
+ value: rest
90
+ };
91
+ dataSourceByRef.set(dataSourceRef.name, dataSource);
92
+ } else if (dataSourceRef.type === "expression") {
93
+ dataSource = {
94
+ type: "expression",
95
+ id,
96
+ scopeInstanceId: instanceId,
97
+ name: dataSourceRef.name,
98
+ code: dataSourceRef.code
99
+ };
100
+ dataSourceByRef.set(dataSourceRef.name, dataSource);
101
+ } else {
102
+ dataSourceRef;
103
+ continue;
104
+ }
105
+ }
57
106
  props.push({
58
- id: nanoid(),
107
+ id: propId,
59
108
  instanceId,
60
- ...prop
109
+ type: "dataSource",
110
+ name: prop.name,
111
+ value: dataSource.id
61
112
  });
62
113
  }
63
114
  }
@@ -93,6 +144,7 @@ const createInstancesFromTemplate = (treeTemplate, instances, props, styleSource
93
144
  item.children,
94
145
  instances,
95
146
  props,
147
+ dataSourceByRef,
96
148
  styleSourceSelections,
97
149
  styleSources,
98
150
  styles,
@@ -115,6 +167,7 @@ const createInstancesFromTemplate = (treeTemplate, instances, props, styleSource
115
167
  const generateDataFromEmbedTemplate = (treeTemplate, defaultBreakpointId) => {
116
168
  const instances = [];
117
169
  const props = [];
170
+ const dataSourceByRef = /* @__PURE__ */ new Map();
118
171
  const styleSourceSelections = [];
119
172
  const styleSources = [];
120
173
  const styles = [];
@@ -122,15 +175,27 @@ const generateDataFromEmbedTemplate = (treeTemplate, defaultBreakpointId) => {
122
175
  treeTemplate,
123
176
  instances,
124
177
  props,
178
+ dataSourceByRef,
125
179
  styleSourceSelections,
126
180
  styleSources,
127
181
  styles,
128
182
  defaultBreakpointId
129
183
  );
184
+ const dataSources = [];
185
+ for (const dataSource of dataSourceByRef.values()) {
186
+ if (dataSource.type === "expression") {
187
+ dataSource.code = validateExpression(dataSource.code, (ref) => {
188
+ const id = dataSourceByRef.get(ref)?.id ?? ref;
189
+ return encodeDataSourceVariable(id);
190
+ });
191
+ }
192
+ dataSources.push(dataSource);
193
+ }
130
194
  return {
131
195
  children,
132
196
  instances,
133
197
  props,
198
+ dataSources,
134
199
  styleSourceSelections,
135
200
  styleSources,
136
201
  styles
@@ -0,0 +1,161 @@
1
+ import jsep from "jsep";
2
+ const generateCode = (node, failOnForbidden, transformIdentifier) => {
3
+ if (node.type === "Identifier") {
4
+ return transformIdentifier(node.name);
5
+ }
6
+ if (node.type === "MemberExpression") {
7
+ if (failOnForbidden) {
8
+ const object2 = generateCode(
9
+ node.object,
10
+ false,
11
+ transformIdentifier
12
+ );
13
+ const property2 = generateCode(
14
+ node.property,
15
+ false,
16
+ transformIdentifier
17
+ );
18
+ throw Error(`Cannot access "${property2}" of "${object2}"`);
19
+ }
20
+ const object = generateCode(
21
+ node.object,
22
+ failOnForbidden,
23
+ transformIdentifier
24
+ );
25
+ const property = generateCode(
26
+ node.property,
27
+ failOnForbidden,
28
+ transformIdentifier
29
+ );
30
+ return `${object}.${property}`;
31
+ }
32
+ if (node.type === "Literal") {
33
+ return node.raw;
34
+ }
35
+ if (node.type === "UnaryExpression") {
36
+ const arg = generateCode(
37
+ node.argument,
38
+ failOnForbidden,
39
+ transformIdentifier
40
+ );
41
+ return `${node.operator}${arg}`;
42
+ }
43
+ if (node.type === "BinaryExpression") {
44
+ const left = generateCode(
45
+ node.left,
46
+ failOnForbidden,
47
+ transformIdentifier
48
+ );
49
+ const right = generateCode(
50
+ node.right,
51
+ failOnForbidden,
52
+ transformIdentifier
53
+ );
54
+ return `${left} ${node.operator} ${right}`;
55
+ }
56
+ if (node.type === "ArrayExpression") {
57
+ const elements = node.elements.map(
58
+ (element) => generateCode(element, failOnForbidden, transformIdentifier)
59
+ );
60
+ return `[${elements.join(", ")}]`;
61
+ }
62
+ if (node.type === "CallExpression") {
63
+ if (failOnForbidden) {
64
+ const callee2 = generateCode(
65
+ node.callee,
66
+ false,
67
+ transformIdentifier
68
+ );
69
+ throw Error(`Cannot call "${callee2}"`);
70
+ }
71
+ const callee = generateCode(
72
+ node.callee,
73
+ failOnForbidden,
74
+ transformIdentifier
75
+ );
76
+ const args = node.arguments.map(
77
+ (arg) => generateCode(arg, failOnForbidden, transformIdentifier)
78
+ );
79
+ return `${callee}(${args.join(", ")})`;
80
+ }
81
+ if (node.type === "ThisExpression") {
82
+ if (failOnForbidden) {
83
+ throw Error(`"this" is not supported`);
84
+ }
85
+ return "this";
86
+ }
87
+ if (node.type === "ConditionalExpression") {
88
+ throw Error("Ternary operator is not supported");
89
+ }
90
+ if (node.type === "Compound") {
91
+ throw Error("Cannot use multiple expressions");
92
+ }
93
+ node;
94
+ return "";
95
+ };
96
+ const validateExpression = (code, transformIdentifier = (id) => id) => {
97
+ const expression = jsep(code);
98
+ return generateCode(expression, true, transformIdentifier);
99
+ };
100
+ const executeExpressions = (variables, expressions) => {
101
+ const depsById = /* @__PURE__ */ new Map();
102
+ for (const [id, code] of expressions) {
103
+ const deps = /* @__PURE__ */ new Set();
104
+ validateExpression(code, (identifier) => {
105
+ if (variables.has(identifier) || expressions.has(identifier)) {
106
+ deps.add(identifier);
107
+ return identifier;
108
+ }
109
+ throw Error(`Unknown dependency "${identifier}"`);
110
+ });
111
+ depsById.set(id, deps);
112
+ }
113
+ const sortedExpressions = Array.from(expressions.keys()).sort(
114
+ (left, right) => {
115
+ if (depsById.get(left)?.has(right)) {
116
+ return 1;
117
+ }
118
+ if (depsById.get(right)?.has(left)) {
119
+ return -1;
120
+ }
121
+ return 0;
122
+ }
123
+ );
124
+ let header = "";
125
+ for (const [id, value] of variables) {
126
+ header += `const ${id} = ${JSON.stringify(value)};
127
+ `;
128
+ }
129
+ const values = /* @__PURE__ */ new Map();
130
+ for (const id of sortedExpressions) {
131
+ const code = expressions.get(id);
132
+ if (code === void 0) {
133
+ continue;
134
+ }
135
+ const executeFn = new Function(`${header}
136
+ return (${code});`);
137
+ const value = executeFn();
138
+ header += `const ${id} = ${JSON.stringify(value)};
139
+ `;
140
+ values.set(id, value);
141
+ }
142
+ return values;
143
+ };
144
+ const dataSourceVariablePrefix = "$ws$dataSource$";
145
+ const encodeDataSourceVariable = (id) => {
146
+ const encoded = id.replaceAll("-", "__DASH__");
147
+ return `${dataSourceVariablePrefix}${encoded}`;
148
+ };
149
+ const decodeDataSourceVariable = (name) => {
150
+ if (name.startsWith(dataSourceVariablePrefix)) {
151
+ const encoded = name.slice(dataSourceVariablePrefix.length);
152
+ return encoded.replaceAll("__DASH__", "-");
153
+ }
154
+ return;
155
+ };
156
+ export {
157
+ decodeDataSourceVariable,
158
+ encodeDataSourceVariable,
159
+ executeExpressions,
160
+ validateExpression
161
+ };
package/lib/index.js CHANGED
@@ -10,17 +10,29 @@ import {
10
10
  } from "./components/component-meta";
11
11
  export * from "./embed-template";
12
12
  import {
13
+ useInstanceProps,
13
14
  usePropUrl,
14
15
  usePropAsset,
15
16
  getInstanceIdFromComponentProps
16
17
  } from "./props";
17
18
  import { ReactSdkContext } from "./context";
19
+ import {
20
+ validateExpression,
21
+ executeExpressions,
22
+ encodeDataSourceVariable,
23
+ decodeDataSourceVariable
24
+ } from "./expression";
18
25
  export {
19
26
  ReactSdkContext,
20
27
  componentCategories,
28
+ decodeDataSourceVariable,
21
29
  defaultStates,
30
+ encodeDataSourceVariable,
31
+ executeExpressions,
22
32
  getInstanceIdFromComponentProps,
23
33
  stateCategories,
34
+ useInstanceProps,
24
35
  usePropAsset,
25
- usePropUrl
36
+ usePropUrl,
37
+ validateExpression
26
38
  };
package/lib/props.js CHANGED
@@ -16,17 +16,35 @@ const getPropsByInstanceId = (props) => {
16
16
  return propsByInstanceId;
17
17
  };
18
18
  const useInstanceProps = (instanceId) => {
19
- const { propsByInstanceIdStore } = useContext(ReactSdkContext);
20
- const propsByInstanceId = useStore(propsByInstanceIdStore);
21
- const instanceProps = propsByInstanceId.get(instanceId);
22
- const instancePropsObject = {};
23
- if (instanceProps) {
24
- for (const prop of instanceProps) {
25
- if (prop.type !== "asset" && prop.type !== "page") {
26
- instancePropsObject[prop.name] = prop.value;
19
+ const { propsByInstanceIdStore, dataSourceValuesStore } = useContext(ReactSdkContext);
20
+ const instancePropsObjectStore = useMemo(() => {
21
+ return computed(
22
+ [propsByInstanceIdStore, dataSourceValuesStore],
23
+ (propsByInstanceId, dataSourceValues) => {
24
+ const instancePropsObject2 = {};
25
+ const instanceProps = propsByInstanceId.get(instanceId);
26
+ if (instanceProps === void 0) {
27
+ return instancePropsObject2;
28
+ }
29
+ for (const prop of instanceProps) {
30
+ if (prop.type === "asset" || prop.type === "page") {
31
+ continue;
32
+ }
33
+ if (prop.type === "dataSource") {
34
+ const dataSourceId = prop.value;
35
+ const value = dataSourceValues.get(dataSourceId);
36
+ if (value !== void 0) {
37
+ instancePropsObject2[prop.name] = value;
38
+ }
39
+ continue;
40
+ }
41
+ instancePropsObject2[prop.name] = prop.value;
42
+ }
43
+ return instancePropsObject2;
27
44
  }
28
- }
29
- }
45
+ );
46
+ }, [propsByInstanceIdStore, dataSourceValuesStore, instanceId]);
47
+ const instancePropsObject = useStore(instancePropsObjectStore);
30
48
  return instancePropsObject;
31
49
  };
32
50
  const usePropAsset = (instanceId, name) => {
@@ -11,6 +11,8 @@ const createElementsTree = ({
11
11
  propsByInstanceIdStore,
12
12
  assetsStore,
13
13
  pagesStore,
14
+ dataSourceValuesStore,
15
+ onDataSourceUpdate,
14
16
  Component,
15
17
  components
16
18
  }) => {
@@ -46,9 +48,20 @@ const createElementsTree = ({
46
48
  propsByInstanceIdStore,
47
49
  assetsStore,
48
50
  pagesStore,
51
+ dataSourceValuesStore,
49
52
  renderer,
50
53
  imageBaseUrl,
51
- assetBaseUrl
54
+ assetBaseUrl,
55
+ setDataSourceValue: (instanceId, propName, value) => {
56
+ const propsByInstanceId = propsByInstanceIdStore.get();
57
+ const props = propsByInstanceId.get(instanceId);
58
+ const prop = props?.find((prop2) => prop2.name === propName);
59
+ if (prop?.type !== "dataSource") {
60
+ throw Error(`${propName} is not data source`);
61
+ }
62
+ const dataSourceId = prop.value;
63
+ onDataSourceUpdate(dataSourceId, value);
64
+ }
52
65
  },
53
66
  children: root
54
67
  }
package/lib/tree/root.js CHANGED
@@ -1,12 +1,68 @@
1
- import { atom } from "nanostores";
1
+ import { useRef } from "react";
2
+ import {
3
+ atom,
4
+ computed
5
+ } from "nanostores";
2
6
  import { createElementsTree } from "./create-elements-tree";
3
7
  import { WebstudioComponent } from "./webstudio-component";
4
8
  import { getPropsByInstanceId } from "../props";
9
+ import {
10
+ executeExpressions,
11
+ encodeDataSourceVariable,
12
+ decodeDataSourceVariable
13
+ } from "../expression";
14
+ const computeExpressions = (dataSources, dataSourceValues) => {
15
+ const outputValues = /* @__PURE__ */ new Map();
16
+ const variables = /* @__PURE__ */ new Map();
17
+ const expressions = /* @__PURE__ */ new Map();
18
+ for (const [dataSourceId, dataSource] of dataSources) {
19
+ const name = encodeDataSourceVariable(dataSourceId);
20
+ if (dataSource.type === "variable") {
21
+ const value = dataSourceValues.get(dataSourceId) ?? dataSource.value.value;
22
+ variables.set(name, value);
23
+ outputValues.set(dataSourceId, value);
24
+ }
25
+ if (dataSource.type === "expression") {
26
+ expressions.set(name, dataSource.code);
27
+ }
28
+ }
29
+ try {
30
+ const outputVariables = executeExpressions(variables, expressions);
31
+ for (const [name, value] of outputVariables) {
32
+ const id = decodeDataSourceVariable(name);
33
+ if (id !== void 0) {
34
+ outputValues.set(id, value);
35
+ }
36
+ }
37
+ } catch (error) {
38
+ console.error(error);
39
+ }
40
+ return outputValues;
41
+ };
5
42
  const InstanceRoot = ({
6
43
  data,
7
44
  Component,
8
45
  components
9
46
  }) => {
47
+ const dataSourceVariablesStoreRef = useRef(void 0);
48
+ if (dataSourceVariablesStoreRef.current === void 0) {
49
+ const dataSourceVariables = /* @__PURE__ */ new Map();
50
+ for (const [dataSourceId, dataSource] of data.build.dataSources) {
51
+ dataSourceVariables.set(dataSourceId, dataSource);
52
+ }
53
+ dataSourceVariablesStoreRef.current = atom(dataSourceVariables);
54
+ }
55
+ const dataSourceVariablesStore = dataSourceVariablesStoreRef.current;
56
+ const dataSourceValuesStoreRef = useRef(void 0);
57
+ if (dataSourceValuesStoreRef.current === void 0) {
58
+ dataSourceValuesStoreRef.current = computed(
59
+ dataSourceVariablesStore,
60
+ (dataSourceVariables) => {
61
+ return computeExpressions(data.build.dataSources, dataSourceVariables);
62
+ }
63
+ );
64
+ }
65
+ const dataSourceValuesStore = dataSourceValuesStoreRef.current;
10
66
  return createElementsTree({
11
67
  imageBaseUrl: data.params?.imageBaseUrl ?? "/",
12
68
  assetBaseUrl: data.params?.assetBaseUrl ?? "/",
@@ -17,6 +73,12 @@ const InstanceRoot = ({
17
73
  ),
18
74
  assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
19
75
  pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
76
+ dataSourceValuesStore,
77
+ onDataSourceUpdate: (dataSourceId, value) => {
78
+ const dataSourceVariables = new Map(dataSourceVariablesStore.get());
79
+ dataSourceVariables.set(dataSourceId, value);
80
+ dataSourceVariablesStore.set(dataSourceVariables);
81
+ },
20
82
  Component: Component ?? WebstudioComponent,
21
83
  components
22
84
  });
@@ -23,13 +23,18 @@ const WebstudioComponent = ({
23
23
  components,
24
24
  ...rest
25
25
  }) => {
26
- const instanceProps = useInstanceProps(instance.id);
26
+ const { [showAttribute]: show = true, ...instanceProps } = useInstanceProps(
27
+ instance.id
28
+ );
27
29
  const props = {
28
30
  ...instanceProps,
29
31
  ...rest,
30
32
  [idAttribute]: instance.id,
31
33
  [componentAttribute]: instance.component
32
34
  };
35
+ if (show === false) {
36
+ return /* @__PURE__ */ jsx(Fragment, {});
37
+ }
33
38
  const Component = components.get(instance.component);
34
39
  if (Component === void 0) {
35
40
  return /* @__PURE__ */ jsx(Fragment, {});
@@ -38,11 +43,13 @@ const WebstudioComponent = ({
38
43
  };
39
44
  const idAttribute = "data-ws-id";
40
45
  const componentAttribute = "data-ws-component";
46
+ const showAttribute = "data-ws-show";
41
47
  const collapsedAttribute = "data-ws-collapsed";
42
48
  export {
43
49
  WebstudioComponent,
44
50
  collapsedAttribute,
45
51
  componentAttribute,
46
52
  idAttribute,
47
- renderWebstudioComponentChildren
53
+ renderWebstudioComponentChildren,
54
+ showAttribute
48
55
  };