@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.
- package/lib/cjs/context.js +5 -1
- package/lib/cjs/css/normalize.js +9 -1
- package/lib/cjs/embed-template.js +68 -3
- package/lib/cjs/expression.js +191 -0
- package/lib/cjs/index.js +7 -1
- package/lib/cjs/props.js +28 -10
- package/lib/cjs/tree/create-elements-tree.js +14 -1
- package/lib/cjs/tree/root.js +55 -0
- package/lib/cjs/tree/webstudio-component.js +9 -2
- package/lib/context.js +5 -1
- package/lib/css/normalize.js +9 -1
- package/lib/embed-template.js +68 -3
- package/lib/expression.js +161 -0
- package/lib/index.js +13 -1
- package/lib/props.js +28 -10
- package/lib/tree/create-elements-tree.js +14 -1
- package/lib/tree/root.js +63 -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 +1316 -0
- package/lib/types/embed-template.d.ts +512 -0
- package/lib/types/expression.d.ts +6 -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 +3 -3
- 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 +9 -1
- package/src/embed-template.test.ts +177 -1
- package/src/embed-template.ts +73 -2
- package/src/expression.test.ts +122 -0
- package/src/expression.ts +183 -0
- package/src/index.ts +7 -0
- package/src/props.ts +29 -10
- package/src/tree/create-elements-tree.tsx +20 -1
- package/src/tree/root.ts +81 -4
- package/src/tree/webstudio-component.tsx +7 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type TransformIdentifier = (id: string) => string;
|
|
2
|
+
export declare const validateExpression: (code: string, transformIdentifier?: TransformIdentifier) => string;
|
|
3
|
+
export declare const executeExpressions: (variables: Map<string, unknown>, expressions: Map<string, string>) => Map<string, unknown>;
|
|
4
|
+
export declare const encodeDataSourceVariable: (id: string) => string;
|
|
5
|
+
export declare const decodeDataSourceVariable: (name: string) => string | undefined;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export * from "./app";
|
|
|
5
5
|
export * from "./components/components-utils";
|
|
6
6
|
export { type WsComponentPropsMeta, type WsComponentMeta, type ComponentState, type PresetStyle, componentCategories, stateCategories, defaultStates, } from "./components/component-meta";
|
|
7
7
|
export * from "./embed-template";
|
|
8
|
-
export { usePropUrl, usePropAsset, getInstanceIdFromComponentProps, } from "./props";
|
|
8
|
+
export { useInstanceProps, usePropUrl, usePropAsset, getInstanceIdFromComponentProps, } from "./props";
|
|
9
9
|
export { type Params, ReactSdkContext } from "./context";
|
|
10
|
+
export { validateExpression, executeExpressions, encodeDataSourceVariable, decodeDataSourceVariable, } from "./expression";
|
package/lib/types/props.d.ts
CHANGED
|
@@ -50,14 +50,15 @@ export declare const getPropsByInstanceId: (props: Map<string, {
|
|
|
50
50
|
id: string;
|
|
51
51
|
instanceId: string;
|
|
52
52
|
required?: boolean | undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
} | {
|
|
54
|
+
name: string;
|
|
55
|
+
type: "dataSource";
|
|
56
|
+
value: string;
|
|
57
|
+
id: string;
|
|
58
58
|
instanceId: string;
|
|
59
|
-
|
|
60
|
-
}
|
|
59
|
+
required?: boolean | undefined;
|
|
60
|
+
}>) => PropsByInstanceId;
|
|
61
|
+
export declare const useInstanceProps: (instanceId: Instance["id"]) => Record<string, unknown>;
|
|
61
62
|
export declare const usePropAsset: (instanceId: Instance["id"], name: string) => {
|
|
62
63
|
name: string;
|
|
63
64
|
type: "font";
|
|
@@ -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 { Instance, Instances } from "@webstudio-is/project-build";
|
|
4
|
+
import type { DataSource, Instance, Instances } from "@webstudio-is/project-build";
|
|
5
5
|
import type { Components } from "../components/components-utils";
|
|
6
6
|
import { type Params } 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, Component, components, }: Params & {
|
|
9
|
+
export declare const createElementsTree: ({ renderer, imageBaseUrl, assetBaseUrl, instances, rootInstanceId, propsByInstanceIdStore, assetsStore, pagesStore, dataSourceValuesStore, onDataSourceUpdate, Component, components, }: Params & {
|
|
10
10
|
instances: Map<string, {
|
|
11
11
|
type: "instance";
|
|
12
12
|
id: string;
|
|
@@ -24,6 +24,8 @@ 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
29
|
Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
|
|
28
30
|
components: Components;
|
|
29
31
|
}) => JSX.Element | null;
|
package/lib/types/tree/root.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
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";
|
|
@@ -12,7 +12,7 @@ export type Data = {
|
|
|
12
12
|
params?: Params;
|
|
13
13
|
};
|
|
14
14
|
export type RootPropsData = Omit<Data, "build"> & {
|
|
15
|
-
build: Pick<Data["build"], "instances" | "props">;
|
|
15
|
+
build: Pick<Data["build"], "instances" | "props" | "dataSources">;
|
|
16
16
|
};
|
|
17
17
|
type RootProps = {
|
|
18
18
|
data: RootPropsData;
|
|
@@ -11,5 +11,6 @@ type WebstudioComponentProps = {
|
|
|
11
11
|
export declare const WebstudioComponent: ({ instance, instanceSelector, children, components, ...rest }: WebstudioComponentProps) => JSX.Element;
|
|
12
12
|
export declare const idAttribute = "data-ws-id";
|
|
13
13
|
export declare const componentAttribute = "data-ws-component";
|
|
14
|
+
export declare const showAttribute = "data-ws-show";
|
|
14
15
|
export declare const collapsedAttribute = "data-ws-collapsed";
|
|
15
16
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webstudio-is/react-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Webstudio JavaScript / TypeScript API",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -30,15 +30,16 @@
|
|
|
30
30
|
"@nanostores/react": "^0.4.1",
|
|
31
31
|
"detect-font": "^0.1.5",
|
|
32
32
|
"html-tags": "^3.2.0",
|
|
33
|
+
"jsep": "^1.3.8",
|
|
33
34
|
"nanoevents": "^7.0.1",
|
|
34
|
-
"nanoid": "^3.
|
|
35
|
+
"nanoid": "^3.3.6",
|
|
35
36
|
"nanostores": "^0.7.1",
|
|
36
|
-
"@webstudio-is/asset-uploader": "^0.
|
|
37
|
-
"@webstudio-is/css-data": "^0.
|
|
38
|
-
"@webstudio-is/css-engine": "^0.
|
|
39
|
-
"@webstudio-is/fonts": "^0.
|
|
40
|
-
"@webstudio-is/generate-arg-types": "^0.
|
|
41
|
-
"@webstudio-is/project-build": "^0.
|
|
37
|
+
"@webstudio-is/asset-uploader": "^0.76.0",
|
|
38
|
+
"@webstudio-is/css-data": "^0.76.0",
|
|
39
|
+
"@webstudio-is/css-engine": "^0.76.0",
|
|
40
|
+
"@webstudio-is/fonts": "^0.76.0",
|
|
41
|
+
"@webstudio-is/generate-arg-types": "^0.76.0",
|
|
42
|
+
"@webstudio-is/project-build": "^0.76.0"
|
|
42
43
|
},
|
|
43
44
|
"exports": {
|
|
44
45
|
".": {
|
|
@@ -69,7 +70,6 @@
|
|
|
69
70
|
"dts": "tsc --project tsconfig.dts.json",
|
|
70
71
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
|
71
72
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
|
|
72
|
-
"
|
|
73
|
-
"checks": "pnpm typecheck && pnpm lint && pnpm test"
|
|
73
|
+
"checks": "pnpm typecheck && pnpm test"
|
|
74
74
|
}
|
|
75
75
|
}
|
package/src/context.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ReadableAtom, atom } from "nanostores";
|
|
2
2
|
import { createContext } from "react";
|
|
3
3
|
import type { Assets } from "@webstudio-is/asset-uploader";
|
|
4
|
+
import type { DataSource, Instance, Prop } from "@webstudio-is/project-build";
|
|
4
5
|
import type { Pages, PropsByInstanceId } from "./props";
|
|
5
6
|
|
|
6
7
|
export type Params = {
|
|
@@ -35,6 +36,12 @@ export const ReactSdkContext = createContext<
|
|
|
35
36
|
propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
|
|
36
37
|
assetsStore: ReadableAtom<Assets>;
|
|
37
38
|
pagesStore: ReadableAtom<Pages>;
|
|
39
|
+
dataSourceValuesStore: ReadableAtom<Map<DataSource["id"], unknown>>;
|
|
40
|
+
setDataSourceValue: (
|
|
41
|
+
instanceId: Instance["id"],
|
|
42
|
+
prop: Prop["name"],
|
|
43
|
+
value: unknown
|
|
44
|
+
) => void;
|
|
38
45
|
}
|
|
39
46
|
>({
|
|
40
47
|
imageBaseUrl: "/",
|
|
@@ -42,4 +49,8 @@ export const ReactSdkContext = createContext<
|
|
|
42
49
|
propsByInstanceIdStore: atom(new Map()),
|
|
43
50
|
assetsStore: atom(new Map()),
|
|
44
51
|
pagesStore: atom(new Map()),
|
|
52
|
+
dataSourceValuesStore: atom(new Map()),
|
|
53
|
+
setDataSourceValue: () => {
|
|
54
|
+
throw Error("React SDK setDataSourceValue is not implemented");
|
|
55
|
+
},
|
|
45
56
|
});
|
package/src/css/normalize.ts
CHANGED
|
@@ -124,9 +124,17 @@ export const body = [
|
|
|
124
124
|
property: "fontFamily",
|
|
125
125
|
value: {
|
|
126
126
|
type: "keyword",
|
|
127
|
-
value:
|
|
127
|
+
value: "Arial, sans-serif",
|
|
128
128
|
},
|
|
129
129
|
},
|
|
130
|
+
{
|
|
131
|
+
property: "fontSize",
|
|
132
|
+
value: { type: "unit", unit: "px", value: 16 },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
property: "lineHeight",
|
|
136
|
+
value: { type: "unit", unit: "number", value: 1.2 },
|
|
137
|
+
},
|
|
130
138
|
boxSizing,
|
|
131
139
|
...borders,
|
|
132
140
|
] satisfies Styles;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { expect, test } from "@jest/globals";
|
|
2
2
|
import { generateDataFromEmbedTemplate } from "./embed-template";
|
|
3
|
+
import { showAttribute } from "./tree";
|
|
3
4
|
|
|
4
|
-
const expectString = expect.any(String)
|
|
5
|
+
const expectString = expect.any(String);
|
|
5
6
|
|
|
6
7
|
const defaultBreakpointId = "base";
|
|
7
8
|
|
|
@@ -44,6 +45,7 @@ test("generate data for embedding from instances and text", () => {
|
|
|
44
45
|
},
|
|
45
46
|
],
|
|
46
47
|
props: [],
|
|
48
|
+
dataSources: [],
|
|
47
49
|
styleSourceSelections: [],
|
|
48
50
|
styleSources: [],
|
|
49
51
|
styles: [],
|
|
@@ -112,6 +114,7 @@ test("generate data for embedding from props", () => {
|
|
|
112
114
|
value: "value3",
|
|
113
115
|
},
|
|
114
116
|
],
|
|
117
|
+
dataSources: [],
|
|
115
118
|
styleSourceSelections: [],
|
|
116
119
|
styleSources: [],
|
|
117
120
|
styles: [],
|
|
@@ -163,6 +166,7 @@ test("generate data for embedding from styles", () => {
|
|
|
163
166
|
},
|
|
164
167
|
],
|
|
165
168
|
props: [],
|
|
169
|
+
dataSources: [],
|
|
166
170
|
styleSourceSelections: [
|
|
167
171
|
{
|
|
168
172
|
instanceId: expectString,
|
|
@@ -208,3 +212,175 @@ test("generate data for embedding from styles", () => {
|
|
|
208
212
|
],
|
|
209
213
|
});
|
|
210
214
|
});
|
|
215
|
+
|
|
216
|
+
test("generate data for embedding from props bound to data source variables", () => {
|
|
217
|
+
expect(
|
|
218
|
+
generateDataFromEmbedTemplate(
|
|
219
|
+
[
|
|
220
|
+
{
|
|
221
|
+
type: "instance",
|
|
222
|
+
component: "Box1",
|
|
223
|
+
props: [
|
|
224
|
+
{
|
|
225
|
+
type: "boolean",
|
|
226
|
+
name: "showOtherBox",
|
|
227
|
+
value: false,
|
|
228
|
+
dataSourceRef: {
|
|
229
|
+
type: "variable",
|
|
230
|
+
name: "showOtherBoxDataSource",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
children: [],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
type: "instance",
|
|
238
|
+
component: "Box2",
|
|
239
|
+
props: [
|
|
240
|
+
{
|
|
241
|
+
type: "boolean",
|
|
242
|
+
name: showAttribute,
|
|
243
|
+
value: false,
|
|
244
|
+
dataSourceRef: {
|
|
245
|
+
type: "variable",
|
|
246
|
+
name: "showOtherBoxDataSource",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
children: [],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
defaultBreakpointId
|
|
254
|
+
)
|
|
255
|
+
).toEqual({
|
|
256
|
+
children: [
|
|
257
|
+
{ type: "id", value: expectString },
|
|
258
|
+
{ type: "id", value: expectString },
|
|
259
|
+
],
|
|
260
|
+
instances: [
|
|
261
|
+
{ type: "instance", id: expectString, component: "Box1", children: [] },
|
|
262
|
+
{ type: "instance", id: expectString, component: "Box2", children: [] },
|
|
263
|
+
],
|
|
264
|
+
props: [
|
|
265
|
+
{
|
|
266
|
+
id: expectString,
|
|
267
|
+
instanceId: expectString,
|
|
268
|
+
type: "dataSource",
|
|
269
|
+
name: "showOtherBox",
|
|
270
|
+
value: expectString,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: expectString,
|
|
274
|
+
instanceId: expectString,
|
|
275
|
+
type: "dataSource",
|
|
276
|
+
name: showAttribute,
|
|
277
|
+
value: expectString,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
dataSources: [
|
|
281
|
+
{
|
|
282
|
+
type: "variable",
|
|
283
|
+
id: expectString,
|
|
284
|
+
scopeInstanceId: expectString,
|
|
285
|
+
name: "showOtherBoxDataSource",
|
|
286
|
+
value: {
|
|
287
|
+
type: "boolean",
|
|
288
|
+
value: false,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
styleSourceSelections: [],
|
|
293
|
+
styleSources: [],
|
|
294
|
+
styles: [],
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("generate data for embedding from props bound to data source expressions", () => {
|
|
299
|
+
expect(
|
|
300
|
+
generateDataFromEmbedTemplate(
|
|
301
|
+
[
|
|
302
|
+
{
|
|
303
|
+
type: "instance",
|
|
304
|
+
component: "Box1",
|
|
305
|
+
props: [
|
|
306
|
+
{
|
|
307
|
+
type: "string",
|
|
308
|
+
name: "state",
|
|
309
|
+
value: "initial",
|
|
310
|
+
dataSourceRef: {
|
|
311
|
+
type: "variable",
|
|
312
|
+
name: "boxState",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
children: [],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: "instance",
|
|
320
|
+
component: "Box2",
|
|
321
|
+
props: [
|
|
322
|
+
{
|
|
323
|
+
type: "boolean",
|
|
324
|
+
name: showAttribute,
|
|
325
|
+
value: false,
|
|
326
|
+
dataSourceRef: {
|
|
327
|
+
type: "expression",
|
|
328
|
+
name: "boxStateSuccess",
|
|
329
|
+
code: `boxState === 'success'`,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
children: [],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
defaultBreakpointId
|
|
337
|
+
)
|
|
338
|
+
).toEqual({
|
|
339
|
+
children: [
|
|
340
|
+
{ type: "id", value: expectString },
|
|
341
|
+
{ type: "id", value: expectString },
|
|
342
|
+
],
|
|
343
|
+
instances: [
|
|
344
|
+
{ type: "instance", id: expectString, component: "Box1", children: [] },
|
|
345
|
+
{ type: "instance", id: expectString, component: "Box2", children: [] },
|
|
346
|
+
],
|
|
347
|
+
props: [
|
|
348
|
+
{
|
|
349
|
+
id: expectString,
|
|
350
|
+
instanceId: expectString,
|
|
351
|
+
type: "dataSource",
|
|
352
|
+
name: "state",
|
|
353
|
+
value: expectString,
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: expectString,
|
|
357
|
+
instanceId: expectString,
|
|
358
|
+
type: "dataSource",
|
|
359
|
+
name: showAttribute,
|
|
360
|
+
value: expectString,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
dataSources: [
|
|
364
|
+
{
|
|
365
|
+
type: "variable",
|
|
366
|
+
id: expectString,
|
|
367
|
+
scopeInstanceId: expectString,
|
|
368
|
+
name: "boxState",
|
|
369
|
+
value: {
|
|
370
|
+
type: "string",
|
|
371
|
+
value: "initial",
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: "expression",
|
|
376
|
+
id: expectString,
|
|
377
|
+
scopeInstanceId: expectString,
|
|
378
|
+
name: "boxStateSuccess",
|
|
379
|
+
code: expect.stringMatching(/\$ws\$dataSource\$\w+ === 'success'/),
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
styleSourceSelections: [],
|
|
383
|
+
styleSources: [],
|
|
384
|
+
styles: [],
|
|
385
|
+
});
|
|
386
|
+
});
|
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,122 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
decodeDataSourceVariable,
|
|
4
|
+
encodeDataSourceVariable,
|
|
5
|
+
executeExpressions,
|
|
6
|
+
validateExpression,
|
|
7
|
+
} from "./expression";
|
|
8
|
+
|
|
9
|
+
test("allow literals and array expressions", () => {
|
|
10
|
+
expect(
|
|
11
|
+
validateExpression(`["", '', 0, true, false, null, undefined]`)
|
|
12
|
+
).toEqual(`["", '', 0, true, false, null, undefined]`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("allow unary and binary expressions", () => {
|
|
16
|
+
expect(validateExpression(`[-1, 1 + 1]`)).toEqual(`[-1, 1 + 1]`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("forbid member expressions", () => {
|
|
20
|
+
expect(() => {
|
|
21
|
+
validateExpression("var1 + obj.param");
|
|
22
|
+
}).toThrowError(/Cannot access "param" of "obj"/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("forbid this expressions", () => {
|
|
26
|
+
expect(() => {
|
|
27
|
+
validateExpression("var1 + this");
|
|
28
|
+
}).toThrowError(/"this" is not supported/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("forbid call expressions", () => {
|
|
32
|
+
expect(() => {
|
|
33
|
+
validateExpression("var1 + fn1()");
|
|
34
|
+
}).toThrowError(/Cannot call "fn1"/);
|
|
35
|
+
expect(() => {
|
|
36
|
+
validateExpression("var1 + this.fn1()");
|
|
37
|
+
}).toThrowError(/Cannot call "this.fn1"/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("forbid ternary", () => {
|
|
41
|
+
expect(() => {
|
|
42
|
+
validateExpression("var1 ? var2 : var3");
|
|
43
|
+
}).toThrowError(/Ternary operator is not supported/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("forbid multiple expressions", () => {
|
|
47
|
+
expect(() => {
|
|
48
|
+
validateExpression("a b");
|
|
49
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
50
|
+
expect(() => {
|
|
51
|
+
validateExpression("a, b");
|
|
52
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
53
|
+
expect(() => {
|
|
54
|
+
validateExpression("a; b");
|
|
55
|
+
}).toThrowError(/Cannot use multiple expressions/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("transform identifiers", () => {
|
|
59
|
+
expect(validateExpression(`a + b`, (id) => `$ws$${id}`)).toEqual(
|
|
60
|
+
`$ws$a + $ws$b`
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("execute expression", () => {
|
|
65
|
+
const variables = new Map();
|
|
66
|
+
const expressions = new Map([["exp1", "1 + 1"]]);
|
|
67
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
68
|
+
new Map([["exp1", 2]])
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("execute expression dependent on variables", () => {
|
|
73
|
+
const variables = new Map([["var1", 5]]);
|
|
74
|
+
const expressions = new Map([["exp1", "var1 + 1"]]);
|
|
75
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
76
|
+
new Map([["exp1", 6]])
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("execute expression dependent on other expressions", () => {
|
|
81
|
+
const variables = new Map([["var1", 3]]);
|
|
82
|
+
const expressions = new Map([
|
|
83
|
+
["exp1", "exp0 + 1"],
|
|
84
|
+
["exp0", "var1 + 2"],
|
|
85
|
+
]);
|
|
86
|
+
expect(executeExpressions(variables, expressions)).toEqual(
|
|
87
|
+
new Map([
|
|
88
|
+
["exp1", 6],
|
|
89
|
+
["exp0", 5],
|
|
90
|
+
])
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("forbid circular expressions", () => {
|
|
95
|
+
const variables = new Map([["var1", 3]]);
|
|
96
|
+
const expressions = new Map([
|
|
97
|
+
["exp0", "exp2 + 1"],
|
|
98
|
+
["exp1", "exp0 + 2"],
|
|
99
|
+
["exp2", "exp1 + 3"],
|
|
100
|
+
]);
|
|
101
|
+
expect(() => {
|
|
102
|
+
executeExpressions(variables, expressions);
|
|
103
|
+
}).toThrowError(/exp2 is not defined/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("make sure dependency exists", () => {
|
|
107
|
+
const variables = new Map();
|
|
108
|
+
const expressions = new Map([["exp1", "var1 + 1"]]);
|
|
109
|
+
expect(() => {
|
|
110
|
+
executeExpressions(variables, expressions);
|
|
111
|
+
}).toThrowError(/Unknown dependency "var1"/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("encode/decode variable names", () => {
|
|
115
|
+
expect(encodeDataSourceVariable("my--id")).toEqual(
|
|
116
|
+
"$ws$dataSource$my__DASH____DASH__id"
|
|
117
|
+
);
|
|
118
|
+
expect(decodeDataSourceVariable(encodeDataSourceVariable("my--id"))).toEqual(
|
|
119
|
+
"my--id"
|
|
120
|
+
);
|
|
121
|
+
expect(decodeDataSourceVariable("myVarName")).toEqual(undefined);
|
|
122
|
+
});
|