diff --git a/apps/builder/app/shared/instance-utils.test.tsx b/apps/builder/app/shared/instance-utils.test.tsx index 5bed7153430a..231e3b9727f8 100644 --- a/apps/builder/app/shared/instance-utils.test.tsx +++ b/apps/builder/app/shared/instance-utils.test.tsx @@ -5,7 +5,7 @@ import { createDefaultPages } from "@webstudio-is/project-build"; import { $, ws, - ExpressionValue, + expression, renderTemplate, renderData, } from "@webstudio-is/template"; @@ -2793,9 +2793,7 @@ describe("find closest insertable", () => { const { instances } = renderData( <$.Body ws:id="bodyId"> <$.Box ws:id="box1Id"> - <$.Paragraph ws:id="paragraphId"> - {new ExpressionValue(`"bla"`)} - + <$.Paragraph ws:id="paragraphId">{expression`"bla"`} <$.Box ws:id="box2Id"> ); diff --git a/apps/builder/app/shared/matcher.test.tsx b/apps/builder/app/shared/matcher.test.tsx index c84c5f4dd590..544c928d4894 100644 --- a/apps/builder/app/shared/matcher.test.tsx +++ b/apps/builder/app/shared/matcher.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, test, vi } from "vitest"; import { $, - ExpressionValue, + expression, renderTemplate, renderData, } from "@webstudio-is/template"; @@ -947,9 +947,7 @@ describe("find closest container", () => { ...renderData( <$.Body ws:id="body"> <$.Box ws:id="box"> - <$.Box ws:id="box-with-expr"> - {new ExpressionValue("1 + 1")} - + <$.Box ws:id="box-with-expr">{expression`1 + 1`} ), @@ -1009,9 +1007,7 @@ describe("find closest non textual container", () => { ...renderData( <$.Body ws:id="body"> <$.Box ws:id="box"> - <$.Box ws:id="box-with-expr"> - {new ExpressionValue("1 + 1")} - + <$.Box ws:id="box-with-expr">{expression`1 + 1`} ), diff --git a/packages/react-sdk/src/component-generator.test.tsx b/packages/react-sdk/src/component-generator.test.tsx index 90adf5bd974b..2367cf829d28 100644 --- a/packages/react-sdk/src/component-generator.test.tsx +++ b/packages/react-sdk/src/component-generator.test.tsx @@ -892,6 +892,16 @@ return (condition) && }); test("generate resource prop", () => { + const myResource = new ResourceValue("myResource", { + url: expression`"https://my-url.com?with-secret"`, + method: "get", + headers: [], + }); + const anotherResource = new ResourceValue("anotherResource", { + url: expression`"https://another-url.com?with-secret"`, + method: "get", + headers: [], + }); expect( generateWebstudioComponent({ classesMap: new Map(), @@ -902,14 +912,8 @@ test("generate resource prop", () => { indexesWithinAncestors: new Map(), ...renderData( <$.Body ws:id="body"> - <$.Form - ws:id="form1" - action={new ResourceValue("https://my-url.com?with-secret")} - > - <$.Form - ws:id="form2" - action={new ResourceValue("https://another-url.com?with-secret")} - > + <$.Form ws:id="form1" action={myResource}> + <$.Form ws:id="form2" action={anotherResource}> ), }) diff --git a/packages/template/src/jsx.test.tsx b/packages/template/src/jsx.test.tsx index f8d7da3f90d9..d779cc43370d 100644 --- a/packages/template/src/jsx.test.tsx +++ b/packages/template/src/jsx.test.tsx @@ -5,12 +5,11 @@ import { ActionValue, AssetValue, expression, - ExpressionValue, PageValue, Parameter, - ParameterValue, PlaceholderValue, renderTemplate, + ResourceValue, Variable, } from "./jsx"; import { css } from "./css"; @@ -154,10 +153,7 @@ test("render literal props", () => { test("render defined props", () => { const { props } = renderTemplate( - <$.Body - data-expression={new ExpressionValue("1 + 1")} - data-parameter={new ParameterValue("parameterId")} - > + <$.Body> <$.Box data-asset={new AssetValue("assetId")} data-page={new PageValue("pageId")} @@ -166,20 +162,6 @@ test("render defined props", () => { ); expect(props).toEqual([ - { - id: "0:data-expression", - instanceId: "0", - name: "data-expression", - type: "expression", - value: "1 + 1", - }, - { - id: "0:data-parameter", - instanceId: "0", - name: "data-parameter", - type: "parameter", - value: "parameterId", - }, { id: "1:data-asset", instanceId: "1", @@ -493,6 +475,86 @@ test("render parameter bound to prop expression", () => { ]); }); +test("render resource variable", () => { + const value = new Variable("value", "value"); + const myResource = new ResourceValue("myResource", { + url: expression`"https://my-url.com/" + ${value}`, + method: "get", + headers: [{ name: "auth", value: expression`${value}` }], + body: expression`${value}`, + }); + const { dataSources, resources } = renderTemplate( + <$.Body ws:id="body">{expression`${myResource}.title`} + ); + expect(dataSources).toEqual([ + { + id: "1", + name: "value", + scopeInstanceId: "body", + type: "variable", + value: { type: "string", value: "value" }, + }, + { + id: "0", + scopeInstanceId: "body", + name: "myResource", + type: "resource", + resourceId: "resource:0", + }, + ]); + expect(resources).toEqual([ + { + id: "resource:0", + name: "myResource", + url: `"https://my-url.com/" + $ws$dataSource$1`, + method: "get", + headers: [{ name: "auth", value: `$ws$dataSource$1` }], + body: `$ws$dataSource$1`, + }, + ]); +}); + +test("render resource prop", () => { + const value = new Variable("value", "value"); + const myResource = new ResourceValue("myResource", { + url: expression`"https://my-url.com/" + ${value}`, + method: "get", + headers: [{ name: "auth", value: expression`${value}` }], + body: expression`${value}`, + }); + const { props, dataSources, resources } = renderTemplate( + <$.Body ws:id="body" action={myResource}> + ); + expect(props).toEqual([ + { + id: "body:action", + instanceId: "body", + name: "action", + type: "resource", + value: "resource:0", + }, + ]); + expect(dataSources).toEqual([ + { + id: "1", + name: "value", + scopeInstanceId: "body", + type: "variable", + value: { type: "string", value: "value" }, + }, + ]); + expect(resources).toEqual([ + { + id: "resource:0", + name: "myResource", + url: `"https://my-url.com/" + $ws$dataSource$1`, + method: "get", + headers: [{ name: "auth", value: `$ws$dataSource$1` }], + body: `$ws$dataSource$1`, + }, + ]); +}); + test("render ws:show attribute", () => { const { props } = renderTemplate( <$.Body ws:id="body" ws:show={true}> diff --git a/packages/template/src/jsx.ts b/packages/template/src/jsx.ts index 90e9aee2ef66..fdb527de0de8 100644 --- a/packages/template/src/jsx.ts +++ b/packages/template/src/jsx.ts @@ -5,6 +5,7 @@ import type { DataSource, Instance, Prop, + Resource, StyleDecl, StyleSource, StyleSourceSelection, @@ -30,17 +31,32 @@ export class Parameter { } } +type ResourceConfig = { + url: Expression; + method: Resource["method"]; + headers: Array<{ name: string; value: Expression }>; + body?: Expression; +}; + +export class ResourceValue { + name: string; + config: ResourceConfig; + constructor(name: string, config: ResourceConfig) { + this.name = name; + this.config = config; + } +} + class Expression { chunks: string[]; - variables: Array; - constructor(chunks: string[], variables: Array) { + variables: Array; + constructor( + chunks: string[], + variables: Array + ) { this.chunks = chunks; this.variables = variables; } - serialize(variableIds: string[]): string { - const values = variableIds.map(encodeDataSourceVariable); - return String.raw({ raw: this.chunks }, ...values); - } } export const expression = ( @@ -50,27 +66,6 @@ export const expression = ( return new Expression(Array.from(chunks), variables); }; -export class ExpressionValue { - value: string; - constructor(expression: string) { - this.value = expression; - } -} - -export class ParameterValue { - value: string; - constructor(dataSourceId: string) { - this.value = dataSourceId; - } -} - -export class ResourceValue { - value: string; - constructor(resourceId: string) { - this.value = resourceId; - } -} - export class ActionValue { args: string[]; expression: Expression; @@ -112,7 +107,6 @@ export class PlaceholderValue { const isChildValue = (child: unknown) => typeof child === "string" || child instanceof PlaceholderValue || - child instanceof ExpressionValue || child instanceof Expression; const traverseJsx = ( @@ -157,6 +151,7 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { const styleSourceSelections: StyleSourceSelection[] = []; const styles: StyleDecl[] = []; const dataSources = new Map(); + const resources = new Map(); const ids = new Map(); const getId = (key: unknown) => { let id = ids.get(key); @@ -202,6 +197,44 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { name: variable.name, }); } + if (variable instanceof ResourceValue) { + dataSources.set(variable, { + type: "resource", + scopeInstanceId: instanceId, + id, + name: variable.name, + resourceId: getResourceId(instanceId, variable), + }); + } + return id; + }; + const compileExpression = (instanceId: string, expression: Expression) => { + const values = expression.variables.map((variable) => + getVariableId(instanceId, variable) + ); + return String.raw( + { raw: expression.chunks }, + ...values.map(encodeDataSourceVariable) + ); + }; + const getResourceId = (instanceId: string, resourceValue: ResourceValue) => { + const id = `resource:${getId(resourceValue)}`; + if (resources.has(resourceValue)) { + return id; + } + resources.set(resourceValue, { + id, + name: resourceValue.name, + url: compileExpression(instanceId, resourceValue.config.url), + method: resourceValue.config.method, + headers: resourceValue.config.headers.map(({ name, value }) => ({ + name, + value: compileExpression(instanceId, value), + })), + body: resourceValue.config.body + ? compileExpression(instanceId, resourceValue.config.body) + : undefined, + }); return id; }; // lazily create breakpoint @@ -250,11 +283,11 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { const propId = `${instanceId}:${name}`; const base = { id: propId, instanceId, name }; if (value instanceof Expression) { - const values = value.variables.map((variable) => - getVariableId(instanceId, variable) - ); - const expression = value.serialize(values); - props.push({ ...base, type: "expression", value: expression }); + props.push({ + ...base, + type: "expression", + value: compileExpression(instanceId, value), + }); continue; } if (value instanceof Parameter) { @@ -265,24 +298,13 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { }); continue; } - if (value instanceof ExpressionValue) { - props.push({ ...base, type: "expression", value: value.value }); - continue; - } - if (value instanceof ParameterValue) { - props.push({ ...base, type: "parameter", value: value.value }); - continue; - } if (value instanceof ResourceValue) { - props.push({ ...base, type: "resource", value: value.value }); + const resourceId = getResourceId(instanceId, value); + props.push({ ...base, type: "resource", value: resourceId }); continue; } if (value instanceof ActionValue) { - const code = value.expression.serialize( - value.expression.variables.map((variable) => - getVariableId(instanceId, variable) - ) - ); + const code = compileExpression(instanceId, value.expression); const action = { type: "execute" as const, args: value.args, code }; props.push({ ...base, type: "action", value: [action] }); continue; @@ -325,15 +347,9 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { return { type: "text", value: child.value, placeholder: true }; } if (child instanceof Expression) { - const values = child.variables.map((variable) => - getVariableId(instanceId, variable) - ); - const expression = child.serialize(values); + const expression = compileExpression(instanceId, child); return { type: "expression", value: expression }; } - if (child instanceof ExpressionValue) { - return { type: "expression", value: child.value }; - } return { type: "id", value: child.props?.["ws:id"] ?? getId(child) }; }), }; @@ -349,7 +365,7 @@ export const renderTemplate = (root: JSX.Element): WebstudioFragment => { styleSourceSelections, styles, dataSources: Array.from(dataSources.values()), - resources: [], + resources: Array.from(resources.values()), assets: [], }; }; @@ -387,7 +403,7 @@ type ComponentProps = Record & "ws:label"?: string; "ws:style"?: TemplateStyleDecl[]; "ws:show"?: boolean | Expression; - children?: ReactNode | ExpressionValue | Expression | PlaceholderValue; + children?: ReactNode | Expression | PlaceholderValue; }; type Component = { displayName: string } & ((