@webstudio-is/react-sdk 0.75.0 → 0.77.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/lib/cjs/context.js +5 -1
- package/lib/cjs/css/normalize.js +51 -23
- package/lib/cjs/css/presets.js +111 -1
- package/lib/cjs/embed-template.js +68 -3
- package/lib/cjs/expression.js +213 -0
- package/lib/cjs/index.js +8 -1
- package/lib/cjs/props.js +28 -10
- package/lib/cjs/tree/create-elements-tree.js +14 -1
- package/lib/cjs/tree/root.js +38 -0
- package/lib/cjs/tree/webstudio-component.js +9 -2
- package/lib/context.js +5 -1
- package/lib/css/normalize.js +41 -23
- package/lib/css/presets.js +111 -1
- package/lib/embed-template.js +68 -3
- package/lib/expression.js +183 -0
- package/lib/index.js +15 -1
- package/lib/props.js +28 -10
- package/lib/tree/create-elements-tree.js +14 -1
- package/lib/tree/root.js +42 -1
- package/lib/tree/webstudio-component.js +9 -2
- package/lib/types/components/component-meta.d.ts +112 -0
- package/lib/types/context.d.ts +3 -0
- package/lib/types/css/normalize.d.ts +1836 -0
- package/lib/types/css/presets.d.ts +282 -0
- package/lib/types/embed-template.d.ts +512 -0
- package/lib/types/expression.d.ts +11 -0
- package/lib/types/expression.test.d.ts +1 -0
- package/lib/types/index.d.ts +2 -1
- package/lib/types/props.d.ts +8 -7
- package/lib/types/tree/create-elements-tree.d.ts +4 -2
- package/lib/types/tree/root.d.ts +6 -4
- package/lib/types/tree/webstudio-component.d.ts +1 -0
- package/package.json +10 -10
- package/src/context.tsx +11 -0
- package/src/css/normalize.ts +40 -23
- package/src/css/presets.ts +110 -0
- package/src/embed-template.test.ts +177 -1
- package/src/embed-template.ts +73 -2
- package/src/expression.test.ts +147 -0
- package/src/expression.ts +217 -0
- package/src/index.ts +8 -0
- package/src/props.ts +29 -10
- package/src/tree/create-elements-tree.tsx +20 -1
- package/src/tree/root.ts +61 -4
- package/src/tree/webstudio-component.tsx +7 -1
package/src/embed-template.ts
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
StyleSourcesList,
|
|
9
9
|
StylesList,
|
|
10
10
|
Breakpoint,
|
|
11
|
+
DataSource,
|
|
11
12
|
} from "@webstudio-is/project-build";
|
|
12
13
|
import { StyleValue, type StyleProperty } from "@webstudio-is/css-data";
|
|
13
14
|
import type { Simplify } from "type-fest";
|
|
15
|
+
import { encodeDataSourceVariable, validateExpression } from "./expression";
|
|
14
16
|
|
|
15
17
|
const EmbedTemplateText = z.object({
|
|
16
18
|
type: z.literal("text"),
|
|
@@ -19,25 +21,41 @@ const EmbedTemplateText = z.object({
|
|
|
19
21
|
|
|
20
22
|
type EmbedTemplateText = z.infer<typeof EmbedTemplateText>;
|
|
21
23
|
|
|
24
|
+
const DataSourceRef = z.union([
|
|
25
|
+
z.object({
|
|
26
|
+
type: z.literal("variable"),
|
|
27
|
+
name: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
z.object({
|
|
30
|
+
type: z.literal("expression"),
|
|
31
|
+
name: z.string(),
|
|
32
|
+
code: z.string(),
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
|
|
22
36
|
const EmbedTemplateProp = z.union([
|
|
23
37
|
z.object({
|
|
24
38
|
type: z.literal("number"),
|
|
25
39
|
name: z.string(),
|
|
40
|
+
dataSourceRef: z.optional(DataSourceRef),
|
|
26
41
|
value: z.number(),
|
|
27
42
|
}),
|
|
28
43
|
z.object({
|
|
29
44
|
type: z.literal("string"),
|
|
30
45
|
name: z.string(),
|
|
46
|
+
dataSourceRef: z.optional(DataSourceRef),
|
|
31
47
|
value: z.string(),
|
|
32
48
|
}),
|
|
33
49
|
z.object({
|
|
34
50
|
type: z.literal("boolean"),
|
|
35
51
|
name: z.string(),
|
|
52
|
+
dataSourceRef: z.optional(DataSourceRef),
|
|
36
53
|
value: z.boolean(),
|
|
37
54
|
}),
|
|
38
55
|
z.object({
|
|
39
56
|
type: z.literal("string[]"),
|
|
40
57
|
name: z.string(),
|
|
58
|
+
dataSourceRef: z.optional(DataSourceRef),
|
|
41
59
|
value: z.array(z.string()),
|
|
42
60
|
}),
|
|
43
61
|
]);
|
|
@@ -91,6 +109,7 @@ const createInstancesFromTemplate = (
|
|
|
91
109
|
treeTemplate: WsEmbedTemplate,
|
|
92
110
|
instances: InstancesList,
|
|
93
111
|
props: PropsList,
|
|
112
|
+
dataSourceByRef: Map<string, DataSource>,
|
|
94
113
|
styleSourceSelections: StyleSourceSelectionsList,
|
|
95
114
|
styleSources: StyleSourcesList,
|
|
96
115
|
styles: StylesList,
|
|
@@ -104,10 +123,45 @@ const createInstancesFromTemplate = (
|
|
|
104
123
|
// populate props
|
|
105
124
|
if (item.props) {
|
|
106
125
|
for (const prop of item.props) {
|
|
126
|
+
const propId = nanoid();
|
|
127
|
+
if (prop.dataSourceRef === undefined) {
|
|
128
|
+
props.push({ id: propId, instanceId, ...prop });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let dataSource = dataSourceByRef.get(prop.dataSourceRef.name);
|
|
132
|
+
if (dataSource === undefined) {
|
|
133
|
+
const id = nanoid();
|
|
134
|
+
const { name: propName, dataSourceRef, ...rest } = prop;
|
|
135
|
+
if (dataSourceRef.type === "variable") {
|
|
136
|
+
dataSource = {
|
|
137
|
+
type: "variable",
|
|
138
|
+
id,
|
|
139
|
+
// the first instance where data source is appeared in becomes its scope
|
|
140
|
+
scopeInstanceId: instanceId,
|
|
141
|
+
name: dataSourceRef.name,
|
|
142
|
+
value: rest,
|
|
143
|
+
};
|
|
144
|
+
dataSourceByRef.set(dataSourceRef.name, dataSource);
|
|
145
|
+
} else if (dataSourceRef.type === "expression") {
|
|
146
|
+
dataSource = {
|
|
147
|
+
type: "expression",
|
|
148
|
+
id,
|
|
149
|
+
scopeInstanceId: instanceId,
|
|
150
|
+
name: dataSourceRef.name,
|
|
151
|
+
code: dataSourceRef.code,
|
|
152
|
+
};
|
|
153
|
+
dataSourceByRef.set(dataSourceRef.name, dataSource);
|
|
154
|
+
} else {
|
|
155
|
+
dataSourceRef satisfies never;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
107
159
|
props.push({
|
|
108
|
-
id:
|
|
160
|
+
id: propId,
|
|
109
161
|
instanceId,
|
|
110
|
-
|
|
162
|
+
type: "dataSource",
|
|
163
|
+
name: prop.name,
|
|
164
|
+
value: dataSource.id,
|
|
111
165
|
});
|
|
112
166
|
}
|
|
113
167
|
}
|
|
@@ -148,6 +202,7 @@ const createInstancesFromTemplate = (
|
|
|
148
202
|
item.children,
|
|
149
203
|
instances,
|
|
150
204
|
props,
|
|
205
|
+
dataSourceByRef,
|
|
151
206
|
styleSourceSelections,
|
|
152
207
|
styleSources,
|
|
153
208
|
styles,
|
|
@@ -175,6 +230,7 @@ export const generateDataFromEmbedTemplate = (
|
|
|
175
230
|
) => {
|
|
176
231
|
const instances: InstancesList = [];
|
|
177
232
|
const props: PropsList = [];
|
|
233
|
+
const dataSourceByRef = new Map<string, DataSource>();
|
|
178
234
|
const styleSourceSelections: StyleSourceSelectionsList = [];
|
|
179
235
|
const styleSources: StyleSourcesList = [];
|
|
180
236
|
const styles: StylesList = [];
|
|
@@ -183,15 +239,30 @@ export const generateDataFromEmbedTemplate = (
|
|
|
183
239
|
treeTemplate,
|
|
184
240
|
instances,
|
|
185
241
|
props,
|
|
242
|
+
dataSourceByRef,
|
|
186
243
|
styleSourceSelections,
|
|
187
244
|
styleSources,
|
|
188
245
|
styles,
|
|
189
246
|
defaultBreakpointId
|
|
190
247
|
);
|
|
248
|
+
|
|
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
|
+
|
|
191
261
|
return {
|
|
192
262
|
children,
|
|
193
263
|
instances,
|
|
194
264
|
props,
|
|
265
|
+
dataSources,
|
|
195
266
|
styleSourceSelections,
|
|
196
267
|
styleSources,
|
|
197
268
|
styles,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
decodeDataSourceVariable,
|
|
4
|
+
encodeDataSourceVariable,
|
|
5
|
+
executeExpressions,
|
|
6
|
+
generateExpressionsComputation,
|
|
7
|
+
validateExpression,
|
|
8
|
+
} from "./expression";
|
|
9
|
+
|
|
10
|
+
test("allow literals and array expressions", () => {
|
|
11
|
+
expect(
|
|
12
|
+
validateExpression(`["", '', 0, true, false, null, undefined]`)
|
|
13
|
+
).toEqual(`["", '', 0, true, false, null, undefined]`);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("allow unary and binary expressions", () => {
|
|
17
|
+
expect(validateExpression(`[-1, 1 + 1]`)).toEqual(`[-1, 1 + 1]`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("forbid member expressions", () => {
|
|
21
|
+
expect(() => {
|
|
22
|
+
validateExpression("var1 + obj.param");
|
|
23
|
+
}).toThrowError(/Cannot access "param" of "obj"/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("forbid this expressions", () => {
|
|
27
|
+
expect(() => {
|
|
28
|
+
validateExpression("var1 + this");
|
|
29
|
+
}).toThrowError(/"this" is not supported/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("forbid call expressions", () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
validateExpression("var1 + fn1()");
|
|
35
|
+
}).toThrowError(/Cannot call "fn1"/);
|
|
36
|
+
expect(() => {
|
|
37
|
+
validateExpression("var1 + this.fn1()");
|
|
38
|
+
}).toThrowError(/Cannot call "this.fn1"/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("forbid ternary", () => {
|
|
42
|
+
expect(() => {
|
|
43
|
+
validateExpression("var1 ? var2 : var3");
|
|
44
|
+
}).toThrowError(/Ternary operator is not supported/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("forbid multiple expressions", () => {
|
|
48
|
+
expect(() => {
|
|
49
|
+
validateExpression("a b");
|
|
50
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
51
|
+
expect(() => {
|
|
52
|
+
validateExpression("a, b");
|
|
53
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
54
|
+
expect(() => {
|
|
55
|
+
validateExpression("a; b");
|
|
56
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("transform identifiers", () => {
|
|
60
|
+
expect(validateExpression(`a + b`, (id) => `$ws$${id}`)).toEqual(
|
|
61
|
+
`$ws$a + $ws$b`
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("generate expressions computation", () => {
|
|
66
|
+
const variables = new Set(["var0"]);
|
|
67
|
+
const expressions = new Map([
|
|
68
|
+
["exp3", "exp2 + exp1"],
|
|
69
|
+
["exp1", "var0"],
|
|
70
|
+
["exp2", "exp1"],
|
|
71
|
+
["exp4", "exp2"],
|
|
72
|
+
]);
|
|
73
|
+
expect(generateExpressionsComputation(variables, expressions))
|
|
74
|
+
.toMatchInlineSnapshot(`
|
|
75
|
+
"const var0 = _variables.get('var0');
|
|
76
|
+
const exp1 = (var0);
|
|
77
|
+
const exp2 = (exp1);
|
|
78
|
+
const exp3 = (exp2 + exp1);
|
|
79
|
+
const exp4 = (exp2);
|
|
80
|
+
return new Map([
|
|
81
|
+
['exp1', exp1],
|
|
82
|
+
['exp2', exp2],
|
|
83
|
+
['exp3', exp3],
|
|
84
|
+
['exp4', exp4],
|
|
85
|
+
]);"
|
|
86
|
+
`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("execute expression", () => {
|
|
90
|
+
const variables = new Map();
|
|
91
|
+
const expressions = new Map([["exp1", "1 + 1"]]);
|
|
92
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
93
|
+
new Map([["exp1", 2]])
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("execute expression dependent on variables", () => {
|
|
98
|
+
const variables = new Map([["var1", 5]]);
|
|
99
|
+
const expressions = new Map([["exp1", "var1 + 1"]]);
|
|
100
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
101
|
+
new Map([["exp1", 6]])
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("execute expression dependent on another expressions", () => {
|
|
106
|
+
const variables = new Map([["var1", 3]]);
|
|
107
|
+
const expressions = new Map([
|
|
108
|
+
["exp1", "exp0 + 1"],
|
|
109
|
+
["exp0", "var1 + 2"],
|
|
110
|
+
]);
|
|
111
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
112
|
+
new Map([
|
|
113
|
+
["exp1", 6],
|
|
114
|
+
["exp0", 5],
|
|
115
|
+
])
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("forbid circular expressions", () => {
|
|
120
|
+
const variables = new Map([["var1", 3]]);
|
|
121
|
+
const expressions = new Map([
|
|
122
|
+
["exp0", "exp2 + 1"],
|
|
123
|
+
["exp1", "exp0 + 2"],
|
|
124
|
+
["exp2", "exp1 + 3"],
|
|
125
|
+
]);
|
|
126
|
+
expect(() => {
|
|
127
|
+
executeExpressions(variables, expressions);
|
|
128
|
+
}).toThrowError(/Cannot access 'exp0' before initialization/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("make sure dependency exists", () => {
|
|
132
|
+
const variables = new Map();
|
|
133
|
+
const expressions = new Map([["exp1", "var1 + 1"]]);
|
|
134
|
+
expect(() => {
|
|
135
|
+
executeExpressions(variables, expressions);
|
|
136
|
+
}).toThrowError(/Unknown dependency "var1"/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("encode/decode variable names", () => {
|
|
140
|
+
expect(encodeDataSourceVariable("my--id")).toEqual(
|
|
141
|
+
"$ws$dataSource$my__DASH____DASH__id"
|
|
142
|
+
);
|
|
143
|
+
expect(decodeDataSourceVariable(encodeDataSourceVariable("my--id"))).toEqual(
|
|
144
|
+
"my--id"
|
|
145
|
+
);
|
|
146
|
+
expect(decodeDataSourceVariable("myVarName")).toEqual(undefined);
|
|
147
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import jsep from "jsep";
|
|
2
|
+
|
|
3
|
+
type TransformIdentifier = (id: string) => string;
|
|
4
|
+
|
|
5
|
+
type Node = jsep.CoreExpression;
|
|
6
|
+
|
|
7
|
+
const generateCode = (
|
|
8
|
+
node: Node,
|
|
9
|
+
failOnForbidden: boolean,
|
|
10
|
+
transformIdentifier: TransformIdentifier
|
|
11
|
+
): string => {
|
|
12
|
+
if (node.type === "Identifier") {
|
|
13
|
+
return transformIdentifier(node.name);
|
|
14
|
+
}
|
|
15
|
+
if (node.type === "MemberExpression") {
|
|
16
|
+
if (failOnForbidden) {
|
|
17
|
+
const object = generateCode(
|
|
18
|
+
node.object as Node,
|
|
19
|
+
false,
|
|
20
|
+
transformIdentifier
|
|
21
|
+
);
|
|
22
|
+
const property = generateCode(
|
|
23
|
+
node.property as Node,
|
|
24
|
+
false,
|
|
25
|
+
transformIdentifier
|
|
26
|
+
);
|
|
27
|
+
throw Error(`Cannot access "${property}" of "${object}"`);
|
|
28
|
+
}
|
|
29
|
+
const object = generateCode(
|
|
30
|
+
node.object as Node,
|
|
31
|
+
failOnForbidden,
|
|
32
|
+
transformIdentifier
|
|
33
|
+
);
|
|
34
|
+
const property = generateCode(
|
|
35
|
+
node.property as Node,
|
|
36
|
+
failOnForbidden,
|
|
37
|
+
transformIdentifier
|
|
38
|
+
);
|
|
39
|
+
return `${object}.${property}`;
|
|
40
|
+
}
|
|
41
|
+
if (node.type === "Literal") {
|
|
42
|
+
return node.raw;
|
|
43
|
+
}
|
|
44
|
+
if (node.type === "UnaryExpression") {
|
|
45
|
+
const arg = generateCode(
|
|
46
|
+
node.argument as Node,
|
|
47
|
+
failOnForbidden,
|
|
48
|
+
transformIdentifier
|
|
49
|
+
);
|
|
50
|
+
return `${node.operator}${arg}`;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === "BinaryExpression") {
|
|
53
|
+
const left = generateCode(
|
|
54
|
+
node.left as Node,
|
|
55
|
+
failOnForbidden,
|
|
56
|
+
transformIdentifier
|
|
57
|
+
);
|
|
58
|
+
const right = generateCode(
|
|
59
|
+
node.right as Node,
|
|
60
|
+
failOnForbidden,
|
|
61
|
+
transformIdentifier
|
|
62
|
+
);
|
|
63
|
+
return `${left} ${node.operator} ${right}`;
|
|
64
|
+
}
|
|
65
|
+
if (node.type === "ArrayExpression") {
|
|
66
|
+
const elements = node.elements.map((element) =>
|
|
67
|
+
generateCode(element as Node, failOnForbidden, transformIdentifier)
|
|
68
|
+
);
|
|
69
|
+
return `[${elements.join(", ")}]`;
|
|
70
|
+
}
|
|
71
|
+
if (node.type === "CallExpression") {
|
|
72
|
+
if (failOnForbidden) {
|
|
73
|
+
const callee = generateCode(
|
|
74
|
+
node.callee as Node,
|
|
75
|
+
false,
|
|
76
|
+
transformIdentifier
|
|
77
|
+
);
|
|
78
|
+
throw Error(`Cannot call "${callee}"`);
|
|
79
|
+
}
|
|
80
|
+
const callee = generateCode(
|
|
81
|
+
node.callee as Node,
|
|
82
|
+
failOnForbidden,
|
|
83
|
+
transformIdentifier
|
|
84
|
+
);
|
|
85
|
+
const args = node.arguments.map((arg) =>
|
|
86
|
+
generateCode(arg as Node, failOnForbidden, transformIdentifier)
|
|
87
|
+
);
|
|
88
|
+
return `${callee}(${args.join(", ")})`;
|
|
89
|
+
}
|
|
90
|
+
if (node.type === "ThisExpression") {
|
|
91
|
+
if (failOnForbidden) {
|
|
92
|
+
throw Error(`"this" is not supported`);
|
|
93
|
+
}
|
|
94
|
+
return "this";
|
|
95
|
+
}
|
|
96
|
+
if (node.type === "ConditionalExpression") {
|
|
97
|
+
throw Error("Ternary operator is not supported");
|
|
98
|
+
}
|
|
99
|
+
if (node.type === "Compound") {
|
|
100
|
+
throw Error("Cannot use multiple expressions");
|
|
101
|
+
}
|
|
102
|
+
node satisfies never;
|
|
103
|
+
return "";
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const validateExpression = (
|
|
107
|
+
code: string,
|
|
108
|
+
transformIdentifier: TransformIdentifier = (id) => id
|
|
109
|
+
) => {
|
|
110
|
+
const expression = jsep(code) as Node;
|
|
111
|
+
return generateCode(expression, true, transformIdentifier);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const sortTopologically = (
|
|
115
|
+
list: Set<string>,
|
|
116
|
+
depsById: Map<string, Set<string>>,
|
|
117
|
+
explored = new Set<string>(),
|
|
118
|
+
sorted: string[] = []
|
|
119
|
+
) => {
|
|
120
|
+
for (const id of list) {
|
|
121
|
+
if (explored.has(id)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
explored.add(id);
|
|
125
|
+
const deps = depsById.get(id);
|
|
126
|
+
if (deps) {
|
|
127
|
+
sortTopologically(deps, depsById, explored, sorted);
|
|
128
|
+
}
|
|
129
|
+
sorted.push(id);
|
|
130
|
+
}
|
|
131
|
+
return sorted;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generates a function body expecting map as _variables argument
|
|
136
|
+
* and outputing map of results
|
|
137
|
+
*/
|
|
138
|
+
export const generateExpressionsComputation = (
|
|
139
|
+
variables: Set<string>,
|
|
140
|
+
expressions: Map<string, string>
|
|
141
|
+
) => {
|
|
142
|
+
const depsById = new Map<string, Set<string>>();
|
|
143
|
+
for (const [id, code] of expressions) {
|
|
144
|
+
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}"`);
|
|
154
|
+
});
|
|
155
|
+
depsById.set(id, deps);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sortedExpressions = sortTopologically(
|
|
159
|
+
new Set(expressions.keys()),
|
|
160
|
+
depsById
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// generate code computing all expressions
|
|
164
|
+
let generatedCode = "";
|
|
165
|
+
|
|
166
|
+
for (const id of variables) {
|
|
167
|
+
generatedCode += `const ${id} = _variables.get('${id}');\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const id of sortedExpressions) {
|
|
171
|
+
const code = expressions.get(id);
|
|
172
|
+
if (code === undefined) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
generatedCode += `const ${id} = (${code});\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
generatedCode += `return new Map([\n`;
|
|
179
|
+
for (const id of sortedExpressions) {
|
|
180
|
+
generatedCode += ` ['${id}', ${id}],\n`;
|
|
181
|
+
}
|
|
182
|
+
generatedCode += `]);`;
|
|
183
|
+
|
|
184
|
+
return generatedCode;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const executeExpressions = (
|
|
188
|
+
variables: Map<string, unknown>,
|
|
189
|
+
expressions: Map<string, string>
|
|
190
|
+
) => {
|
|
191
|
+
const generatedCode = generateExpressionsComputation(
|
|
192
|
+
new Set(variables.keys()),
|
|
193
|
+
expressions
|
|
194
|
+
);
|
|
195
|
+
const executeFn = new Function("_variables", generatedCode);
|
|
196
|
+
const values = executeFn(variables) as Map<string, unknown>;
|
|
197
|
+
return values;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const dataSourceVariablePrefix = "$ws$dataSource$";
|
|
201
|
+
|
|
202
|
+
// data source id is generated with nanoid which has "-" in alphabeta
|
|
203
|
+
// here "-" is encoded with "__DASH__' in variable name
|
|
204
|
+
// https://github.com/ai/nanoid/blob/047686abad8f15aff05f3a2eeedb7c98b6847392/url-alphabet/index.js
|
|
205
|
+
|
|
206
|
+
export const encodeDataSourceVariable = (id: string) => {
|
|
207
|
+
const encoded = id.replaceAll("-", "__DASH__");
|
|
208
|
+
return `${dataSourceVariablePrefix}${encoded}`;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const decodeDataSourceVariable = (name: string) => {
|
|
212
|
+
if (name.startsWith(dataSourceVariablePrefix)) {
|
|
213
|
+
const encoded = name.slice(dataSourceVariablePrefix.length);
|
|
214
|
+
return encoded.replaceAll("__DASH__", "-");
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -14,8 +14,16 @@ export {
|
|
|
14
14
|
} from "./components/component-meta";
|
|
15
15
|
export * from "./embed-template";
|
|
16
16
|
export {
|
|
17
|
+
useInstanceProps,
|
|
17
18
|
usePropUrl,
|
|
18
19
|
usePropAsset,
|
|
19
20
|
getInstanceIdFromComponentProps,
|
|
20
21
|
} from "./props";
|
|
21
22
|
export { type Params, ReactSdkContext } from "./context";
|
|
23
|
+
export {
|
|
24
|
+
validateExpression,
|
|
25
|
+
generateExpressionsComputation,
|
|
26
|
+
executeExpressions,
|
|
27
|
+
encodeDataSourceVariable,
|
|
28
|
+
decodeDataSourceVariable,
|
|
29
|
+
} from "./expression";
|
package/src/props.ts
CHANGED
|
@@ -26,17 +26,36 @@ 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 } =
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const { propsByInstanceIdStore, dataSourceValuesStore } =
|
|
30
|
+
useContext(ReactSdkContext);
|
|
31
|
+
const instancePropsObjectStore = useMemo(() => {
|
|
32
|
+
return computed(
|
|
33
|
+
[propsByInstanceIdStore, dataSourceValuesStore],
|
|
34
|
+
(propsByInstanceId, dataSourceValues) => {
|
|
35
|
+
const instancePropsObject: Record<Prop["name"], unknown> = {};
|
|
36
|
+
const instanceProps = propsByInstanceId.get(instanceId);
|
|
37
|
+
if (instanceProps === undefined) {
|
|
38
|
+
return instancePropsObject;
|
|
39
|
+
}
|
|
40
|
+
for (const prop of instanceProps) {
|
|
41
|
+
if (prop.type === "asset" || prop.type === "page") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (prop.type === "dataSource") {
|
|
45
|
+
const dataSourceId = prop.value;
|
|
46
|
+
const value = dataSourceValues.get(dataSourceId);
|
|
47
|
+
if (value !== undefined) {
|
|
48
|
+
instancePropsObject[prop.name] = value;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
instancePropsObject[prop.name] = prop.value;
|
|
53
|
+
}
|
|
54
|
+
return instancePropsObject;
|
|
37
55
|
}
|
|
38
|
-
|
|
39
|
-
}
|
|
56
|
+
);
|
|
57
|
+
}, [propsByInstanceIdStore, dataSourceValuesStore, instanceId]);
|
|
58
|
+
const instancePropsObject = useStore(instancePropsObjectStore);
|
|
40
59
|
return instancePropsObject;
|
|
41
60
|
};
|
|
42
61
|
|
|
@@ -2,7 +2,11 @@ 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 {
|
|
5
|
+
import type {
|
|
6
|
+
DataSource,
|
|
7
|
+
Instance,
|
|
8
|
+
Instances,
|
|
9
|
+
} from "@webstudio-is/project-build";
|
|
6
10
|
import type { Components } from "../components/components-utils";
|
|
7
11
|
import { ReactSdkContext, type Params } from "../context";
|
|
8
12
|
import type { Pages, PropsByInstanceId } from "../props";
|
|
@@ -19,6 +23,8 @@ export const createElementsTree = ({
|
|
|
19
23
|
propsByInstanceIdStore,
|
|
20
24
|
assetsStore,
|
|
21
25
|
pagesStore,
|
|
26
|
+
dataSourceValuesStore,
|
|
27
|
+
onDataSourceUpdate,
|
|
22
28
|
Component,
|
|
23
29
|
components,
|
|
24
30
|
}: Params & {
|
|
@@ -27,6 +33,8 @@ export const createElementsTree = ({
|
|
|
27
33
|
propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
|
|
28
34
|
assetsStore: ReadableAtom<Assets>;
|
|
29
35
|
pagesStore: ReadableAtom<Pages>;
|
|
36
|
+
dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
|
|
37
|
+
onDataSourceUpdate: (dataSourceId: DataSource["id"], value: unknown) => void;
|
|
30
38
|
Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
|
|
31
39
|
components: Components;
|
|
32
40
|
}) => {
|
|
@@ -62,9 +70,20 @@ export const createElementsTree = ({
|
|
|
62
70
|
propsByInstanceIdStore,
|
|
63
71
|
assetsStore,
|
|
64
72
|
pagesStore,
|
|
73
|
+
dataSourceValuesStore,
|
|
65
74
|
renderer,
|
|
66
75
|
imageBaseUrl,
|
|
67
76
|
assetBaseUrl,
|
|
77
|
+
setDataSourceValue: (instanceId, propName, value) => {
|
|
78
|
+
const propsByInstanceId = propsByInstanceIdStore.get();
|
|
79
|
+
const props = propsByInstanceId.get(instanceId);
|
|
80
|
+
const prop = props?.find((prop) => prop.name === propName);
|
|
81
|
+
if (prop?.type !== "dataSource") {
|
|
82
|
+
throw Error(`${propName} is not data source`);
|
|
83
|
+
}
|
|
84
|
+
const dataSourceId = prop.value;
|
|
85
|
+
onDataSourceUpdate(dataSourceId, value);
|
|
86
|
+
},
|
|
68
87
|
}}
|
|
69
88
|
>
|
|
70
89
|
{root}
|