diff --git a/apps/builder/app/shared/copy-paste.test.tsx b/apps/builder/app/shared/copy-paste.test.tsx index 2d23b1da2b47..0c9fe34dbdeb 100644 --- a/apps/builder/app/shared/copy-paste.test.tsx +++ b/apps/builder/app/shared/copy-paste.test.tsx @@ -1,16 +1,36 @@ -import { expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import stripIndent from "strip-indent"; import { createRegularStyleSheet } from "@webstudio-is/css-engine"; import { createDefaultPages } from "@webstudio-is/project-build"; -import { ROOT_INSTANCE_ID, type WebstudioData } from "@webstudio-is/sdk"; -import { $, ws, css, renderData } from "@webstudio-is/template"; +import { + getStyleDeclKey, + ROOT_INSTANCE_ID, + type WebstudioData, +} from "@webstudio-is/sdk"; +import { + $, + ws, + css, + renderData, + type TemplateStyleDecl, + expression, + Variable, + ResourceValue, + ActionValue, +} from "@webstudio-is/template"; import type { Project } from "@webstudio-is/project"; import { extractWebstudioFragment, + findAvailableDataSources, insertWebstudioFragmentCopy, } from "./instance-utils"; import { $project } from "./nano-states"; +const pages = createDefaultPages({ + rootInstanceId: "bodyId", + systemDataSourceId: "", +}); + $project.set({ id: "current_project" } as Project); const createStub = (element: JSX.Element) => { @@ -39,12 +59,14 @@ const toCss = (data: WebstudioData) => { if (name) { const rule = sheet.addNestingRule(name); for (const styleDecl of data.styles.values()) { - rule.setDeclaration({ - breakpoint: styleDecl.breakpointId, - selector: styleDecl.state ?? "", - property: styleDecl.property, - value: styleDecl.value, - }); + if (styleDecl.styleSourceId === styleSourceId) { + rule.setDeclaration({ + breakpoint: styleDecl.breakpointId, + selector: styleDecl.state ?? "", + property: styleDecl.property, + value: styleDecl.value, + }); + } } } } @@ -52,6 +74,23 @@ const toCss = (data: WebstudioData) => { return sheet.cssText; }; +const insertStyles = ({ + data, + breakpointId, + styleSourceId, + style, +}: { + data: WebstudioData; + breakpointId: string; + styleSourceId: string; + style: TemplateStyleDecl[]; +}) => { + for (const styleDecl of style) { + const newStyleDecl = { breakpointId, styleSourceId, ...styleDecl }; + data.styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl); + } +}; + test("should add :root local styles", () => { const oldProject = createStub( { }); expect(toCss(newProject)).toEqual( stripIndent(` - @media all { - :root:local { - color: red - } + @media all { + :root:local { + color: red } - `).trim() + } + `).trim() ); }); @@ -132,25 +171,333 @@ test("should copy local styles of duplicated instance", () => { ); const fragment = extractWebstudioFragment(project, "boxId"); - expect(Array.from(project.styles.values())).toEqual([ - expect.objectContaining({ - property: "color", - value: { type: "keyword", value: "red" }, - }), - ]); insertWebstudioFragmentCopy({ data: project, fragment, availableDataSources: new Set(), }); - expect(Array.from(project.styles.values())).toEqual([ - expect.objectContaining({ - property: "color", - value: { type: "keyword", value: "red" }, - }), - expect.objectContaining({ - property: "color", - value: { type: "keyword", value: "red" }, - }), - ]); + const newInstanceId = Array.from(project.instances.keys()).at(-1); + expect(toCss(project)).toEqual( + stripIndent(` + @media all { + boxId:local { + color: red + } + ${newInstanceId}:local { + color: red + } + } + `).trim() + ); + // modify original style + insertStyles({ + data: project, + breakpointId: "base", + styleSourceId: project.styleSourceSelections.get("boxId")?.values[0] ?? "", + style: css` + font-size: medium; + `, + }); + expect(toCss(project)).toEqual( + stripIndent(` + @media all { + boxId:local { + color: red; + font-size: medium + } + ${newInstanceId}:local { + color: red + } + } + `).trim() + ); +}); + +describe("variables", () => { + test("extract variable", () => { + const boxVariable = new Variable("Box Variable", ""); + const data = renderData( + <$.Body ws:id="bodyId"> + <$.Box ws:id="boxId" vars={expression`${boxVariable}`}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([ + expect.objectContaining({ id: "0", type: "variable" }), + ]); + expect(fragment.props).toEqual([ + expect.objectContaining({ + instanceId: "boxId", + value: "$ws$dataSource$0", + }), + ]); + }); + + test("unset variable outside of scope", () => { + const bodyVariable = new Variable("Body Variable", ""); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box + ws:id="boxId" + vars={expression`${bodyVariable}`} + action={ + new ActionValue(["state"], expression`${bodyVariable} = state`) + } + > + {expression`${bodyVariable}`} + + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([]); + expect(fragment.props).toEqual([ + expect.objectContaining({ + instanceId: "boxId", + value: "Body$32$Variable", + }), + expect.objectContaining({ + instanceId: "boxId", + value: [ + { + type: "execute", + args: ["state"], + code: "Body$32$Variable = state", + }, + ], + }), + ]); + expect(fragment.instances).toEqual([ + expect.objectContaining({ + id: "boxId", + children: [{ type: "expression", value: "Body$32$Variable" }], + }), + ]); + }); + + test("restore unset variables when insert fragment", () => { + const bodyVariable = new Variable("Body Variable", ""); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box + ws:id="boxId" + vars={expression`${bodyVariable} + unknownVariable`} + action={ + new ActionValue(["state"], expression`${bodyVariable} = state`) + } + > + {expression`${bodyVariable}`} + + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + insertWebstudioFragmentCopy({ + data: { pages, ...data }, + fragment, + availableDataSources: findAvailableDataSources( + data.dataSources, + data.instances, + ["bodyId"] + ), + }); + const newInstanceId = Array.from(data.instances.keys()).at(-1) ?? ""; + expect(newInstanceId).not.toEqual("boxId"); + expect(data.instances.get(newInstanceId)?.children).toEqual([ + { type: "expression", value: "$ws$dataSource$0" }, + ]); + expect( + Array.from(data.props.values()).filter( + (item) => item.instanceId === newInstanceId + ) + ).toEqual([ + expect.objectContaining({ + value: "$ws$dataSource$0 + unknownVariable", + }), + expect.objectContaining({ + value: [expect.objectContaining({ code: "$ws$dataSource$0 = state" })], + }), + ]); + }); +}); + +describe("resources", () => { + test("extract resource variable with dependant variables", () => { + const boxVariable = new Variable("Box Variable", ""); + const resourceVariable = new ResourceValue("Box Resource", { + url: expression`${boxVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${boxVariable}` }], + body: expression`${boxVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId"> + <$.Box ws:id="boxId" vars={expression`${resourceVariable}`}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([ + expect.objectContaining({ id: "1", type: "variable" }), + expect.objectContaining({ id: "0", type: "resource" }), + ]); + expect(fragment.resources).toEqual([ + expect.objectContaining({ + url: "$ws$dataSource$1", + headers: [{ name: "auth", value: "$ws$dataSource$1" }], + body: "$ws$dataSource$1", + }), + ]); + }); + + test("extract resource variable and unset variables outside of scope", () => { + const bodyVariable = new Variable("Body Variable", ""); + const resourceVariable = new ResourceValue("Box Resource", { + url: expression`${bodyVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${bodyVariable}` }], + body: expression`${bodyVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box ws:id="boxId" vars={expression`${resourceVariable}`}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([ + expect.objectContaining({ id: "1", type: "resource" }), + ]); + expect(fragment.resources).toEqual([ + expect.objectContaining({ + url: "Body$32$Variable", + headers: [{ name: "auth", value: "Body$32$Variable" }], + body: "Body$32$Variable", + }), + ]); + }); + + test("restore unset variables in resource variable", () => { + const bodyVariable = new Variable("Body Variable", ""); + const resourceVariable = new ResourceValue("Box Resource", { + url: expression`${bodyVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${bodyVariable}` }], + body: expression`${bodyVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box ws:id="boxId" vars={expression`${resourceVariable}`}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + insertWebstudioFragmentCopy({ + data: { pages, ...data }, + fragment, + availableDataSources: findAvailableDataSources( + data.dataSources, + data.instances, + ["bodyId"] + ), + }); + const newInstanceId = Array.from(data.instances.keys()).at(-1); + expect(newInstanceId).not.toEqual("boxId"); + expect(Array.from(data.resources.values())).toEqual([ + expect.objectContaining({ + url: "$ws$dataSource$0", + headers: [{ name: "auth", value: "$ws$dataSource$0" }], + body: "$ws$dataSource$0", + }), + expect.objectContaining({ + url: "$ws$dataSource$0", + headers: [{ name: "auth", value: "$ws$dataSource$0" }], + body: "$ws$dataSource$0", + }), + ]); + }); + + test("extract resource prop with dependant variables", () => { + const boxVariable = new Variable("Box Variable", ""); + const resourceProp = new ResourceValue("Box Resource", { + url: expression`${boxVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${boxVariable}` }], + body: expression`${boxVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId"> + <$.Box ws:id="boxId" resource={resourceProp}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([ + expect.objectContaining({ id: "1", type: "variable" }), + ]); + expect(fragment.resources).toEqual([ + expect.objectContaining({ + url: "$ws$dataSource$1", + headers: [{ name: "auth", value: "$ws$dataSource$1" }], + body: "$ws$dataSource$1", + }), + ]); + }); + + test("extract resource prop and unset variables outside of scope", () => { + const bodyVariable = new Variable("Body Variable", ""); + const resourceProp = new ResourceValue("Box Resource", { + url: expression`${bodyVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${bodyVariable}` }], + body: expression`${bodyVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box ws:id="boxId" resource={resourceProp}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + expect(fragment.dataSources).toEqual([]); + expect(fragment.resources).toEqual([ + expect.objectContaining({ + url: "Body$32$Variable", + headers: [{ name: "auth", value: "Body$32$Variable" }], + body: "Body$32$Variable", + }), + ]); + }); + + test("restore unset variables in resource prop", () => { + const bodyVariable = new Variable("Body Variable", ""); + const resourceProp = new ResourceValue("Box Resource", { + url: expression`${bodyVariable}`, + method: "get", + headers: [{ name: "auth", value: expression`${bodyVariable}` }], + body: expression`${bodyVariable}`, + }); + const data = renderData( + <$.Body ws:id="bodyId" vars={expression`${bodyVariable}`}> + <$.Box ws:id="boxId" resource={resourceProp}> + + ); + const fragment = extractWebstudioFragment({ pages, ...data }, "boxId"); + insertWebstudioFragmentCopy({ + data: { pages, ...data }, + fragment, + availableDataSources: findAvailableDataSources( + data.dataSources, + data.instances, + ["bodyId"] + ), + }); + const newInstanceId = Array.from(data.instances.keys()).at(-1); + expect(newInstanceId).not.toEqual("boxId"); + expect(Array.from(data.resources.values())).toEqual([ + expect.objectContaining({ + url: "$ws$dataSource$0", + headers: [{ name: "auth", value: "$ws$dataSource$0" }], + body: "$ws$dataSource$0", + }), + expect.objectContaining({ + url: "$ws$dataSource$0", + headers: [{ name: "auth", value: "$ws$dataSource$0" }], + body: "$ws$dataSource$0", + }), + ]); + }); }); diff --git a/apps/builder/app/shared/copy-paste/plugin-instance.test.ts b/apps/builder/app/shared/copy-paste/plugin-instance.test.ts index fe4246323cef..446027f13cb1 100644 --- a/apps/builder/app/shared/copy-paste/plugin-instance.test.ts +++ b/apps/builder/app/shared/copy-paste/plugin-instance.test.ts @@ -25,7 +25,7 @@ import { $props, $registeredComponentMetas, } from "../nano-states"; -import { onCopy, onCut, onPaste } from "./plugin-instance"; +import { onCopy, onPaste } from "./plugin-instance"; import { createDefaultPages } from "@webstudio-is/project-build"; import { $awareness, selectInstance } from "../awareness"; @@ -255,53 +255,6 @@ describe("data sources", () => { ); }); - test("are inlined into props when not scoped to copied instances or depends on not scoped data source", () => { - $instances.set(instances); - $props.set(props); - $dataSources.set(dataSources); - selectInstance(["box2", "box1", "body0"]); - const clipboardData = onCopy() ?? ""; - selectInstance(["body0"]); - onPaste(clipboardData); - - const instancesDifference = getMapDifference(instances, $instances.get()); - const [newBox2] = instancesDifference.keys(); - - const dataSourcesDifference = getMapDifference( - dataSources, - $dataSources.get() - ); - expect(dataSourcesDifference).toEqual(new Map()); - - const propsDifference = getMapDifference(props, $props.get()); - const [newProp1, newProp2, newProp3] = propsDifference.keys(); - expect(propsDifference).toEqual( - toMap([ - { - id: newProp1, - instanceId: newBox2, - name: "state", - type: "expression", - value: `"initial"`, - }, - { - id: newProp2, - instanceId: newBox2, - name: "show", - type: "expression", - value: `"initial" === 'initial'`, - }, - { - id: newProp3, - instanceId: newBox2, - type: "action", - name: "onChange", - value: [], - }, - ]) - ); - }); - test("preserve data sources outside of scope when pasted within their scope", () => { $instances.set(instances); $props.set(props); @@ -594,66 +547,3 @@ test("insert into portal fragment when portal is a target", () => { ]) ); }); - -test("inline data source not available in portal when copy paste inside the portal", () => { - const instances = toMap([ - createInstance("body", "Body", [ - { type: "id", value: "box" }, - { type: "id", value: "portal" }, - ]), - createInstance("box", "Box", []), - createInstance("portal", portalComponent, [ - { type: "id", value: "fragment" }, - ]), - createInstance("fragment", "Fragment", []), - ]); - $instances.set(instances); - const dataSources = toMap([ - { - id: "variableId", - scopeInstanceId: "body", - name: "variableName", - type: "variable", - value: { type: "string", value: "value" }, - }, - ]); - $dataSources.set(dataSources); - const props = toMap([ - { - id: "propId", - instanceId: "box", - name: "data-value", - type: "expression", - value: "$ws$dataSource$variableId", - }, - ]); - $props.set(props); - selectInstance(["box", "body"]); - const clipboardData = onCut() ?? ""; - selectInstance(["fragment", "portal", "body"]); - onPaste(clipboardData); - expect($instances.get()).toEqual( - toMap([ - createInstance("body", "Body", [{ type: "id", value: "portal" }]), - createInstance("portal", portalComponent, [ - { type: "id", value: "fragment" }, - ]), - createInstance("fragment", "Fragment", [ - { type: "id", value: expectString }, - ]), - createInstance(expectString, "Box", []), - ]) - ); - expect($dataSources.get()).toEqual(dataSources); - expect($props.get()).toEqual( - toMap([ - { - id: expectString, - instanceId: expectString, - name: "data-value", - type: "expression", - value: `"value"`, - }, - ]) - ); -}); diff --git a/apps/builder/app/shared/instance-utils.test.tsx b/apps/builder/app/shared/instance-utils.test.tsx index 231e3b9727f8..dd070ff6c7c9 100644 --- a/apps/builder/app/shared/instance-utils.test.tsx +++ b/apps/builder/app/shared/instance-utils.test.tsx @@ -1048,268 +1048,6 @@ describe("extract webstudio fragment", () => { ]); }); - test("collect data sources used in expression props within instances", () => { - // body - // box1 - // box2 - $instances.set( - toMap([ - createInstance("body", "Body", [{ type: "id", value: "box1" }]), - createInstance("box1", "Box", [{ type: "id", value: "box2" }]), - createInstance("box2", "Box", []), - ]) - ); - $dataSources.set( - toMap([ - { - id: "box1$state", - scopeInstanceId: "box1", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - ]) - ); - $props.set( - toMap([ - { - id: "box1$stateProp", - instanceId: "box1", - name: "state", - type: "expression", - value: "$ws$dataSource$box1$state", - }, - { - id: "box2$stateProp", - instanceId: "box2", - name: "state", - type: "expression", - value: "$ws$dataSource$box1$state", - }, - { - id: "box2$showProp", - instanceId: "box2", - name: "show", - type: "expression", - value: `$ws$dataSource$box1$state === 'initial'`, - }, - { - id: "box2$trueProp", - instanceId: "box2", - name: "bool-prop", - type: "expression", - value: `true`, - }, - ]) - ); - const { props, dataSources } = extractWebstudioFragment( - getWebstudioData(), - "box2" - ); - - expect(dataSources).toEqual([ - { - id: "box1$state", - scopeInstanceId: "box1", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - ]); - expect(props).toEqual([ - { - id: "box2$stateProp", - instanceId: "box2", - name: "state", - type: "expression", - value: "$ws$dataSource$box1$state", - }, - { - id: "box2$showProp", - instanceId: "box2", - name: "show", - type: "expression", - value: `$ws$dataSource$box1$state === 'initial'`, - }, - { - id: "box2$trueProp", - instanceId: "box2", - name: "bool-prop", - type: "expression", - value: `true`, - }, - ]); - }); - - test("collect data sources used in actions within instances", () => { - // body - // box1 - // box2 - $instances.set( - toMap([ - createInstance("body", "Body", [{ type: "id", value: "box1" }]), - createInstance("box1", "Box", [{ type: "id", value: "box2" }]), - createInstance("box2", "Box", []), - ]) - ); - $dataSources.set( - toMap([ - { - id: "box1$state", - scopeInstanceId: "box1", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - { - id: "box2$state", - scopeInstanceId: "box2", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - ]) - ); - $props.set( - toMap([ - { - id: "box2$onChange1", - instanceId: "box2", - type: "action", - name: "onChange", - value: [ - { - type: "execute", - args: ["value"], - code: `$ws$dataSource$box1$state = value`, - }, - ], - }, - { - id: "box2$onChange2", - instanceId: "box2", - type: "action", - name: "onChange", - value: [ - { - type: "execute", - args: ["value"], - code: `$ws$dataSource$box2$state = value`, - }, - ], - }, - ]) - ); - const { props, dataSources } = extractWebstudioFragment( - getWebstudioData(), - "box2" - ); - - expect(dataSources).toEqual([ - { - id: "box1$state", - scopeInstanceId: "box1", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - { - id: "box2$state", - scopeInstanceId: "box2", - type: "variable", - name: "state", - value: { type: "string", value: "initial" }, - }, - ]); - expect(props).toEqual([ - { - id: "box2$onChange1", - instanceId: "box2", - type: "action", - name: "onChange", - value: [ - { - args: ["value"], - code: "$ws$dataSource$box1$state = value", - type: "execute", - }, - ], - }, - { - id: "box2$onChange2", - instanceId: "box2", - type: "action", - name: "onChange", - value: [ - { - type: "execute", - args: ["value"], - code: `$ws$dataSource$box2$state = value`, - }, - ], - }, - ]); - }); - - test("collect data sources used in expression children within instances", () => { - // body - // box - $instances.set( - toMap([ - createInstance("body", "Body", [{ type: "id", value: "box" }]), - createInstance("box", "Box", [ - { - type: "expression", - value: "$ws$dataSource$body + $ws$dataSource$box", - }, - ]), - ]) - ); - $dataSources.set( - toMap([ - { - id: "body", - scopeInstanceId: "body", - type: "variable", - name: "body", - value: { type: "string", value: "body" }, - }, - { - id: "bodyUnused", - scopeInstanceId: "body", - type: "variable", - name: "bodyUnused", - value: { type: "string", value: "bodyUnused" }, - }, - { - id: "box", - scopeInstanceId: "box", - type: "variable", - name: "box", - value: { type: "string", value: "box" }, - }, - ]) - ); - const { dataSources } = extractWebstudioFragment(getWebstudioData(), "box"); - - expect(dataSources).toEqual([ - { - id: "body", - scopeInstanceId: "body", - type: "variable", - name: "body", - value: { type: "string", value: "body" }, - }, - { - id: "box", - scopeInstanceId: "box", - type: "variable", - name: "box", - value: { type: "string", value: "box" }, - }, - ]); - }); - test("collect resources within instances", () => { // body // box1 @@ -1412,123 +1150,6 @@ describe("extract webstudio fragment", () => { }, ]); }); - - test("collect data sources used in resources", () => { - // body - // box - $instances.set( - toMap([ - createInstance("body", "Body", [{ type: "id", value: "box" }]), - createInstance("box", "Box", []), - ]) - ); - $resources.set( - toMap([ - { - id: "resourceId", - name: "resourceName", - url: `$ws$dataSource$bodyUrl`, - method: "post", - headers: [ - { - name: "Authorization", - value: `"Token " + $ws$dataSource$bodyToken`, - }, - ], - body: `$ws$dataSource$boxBody`, - }, - ]) - ); - $dataSources.set( - toMap([ - { - id: "boxData", - scopeInstanceId: "box", - name: "data", - type: "resource", - resourceId: "resourceId", - }, - { - id: "boxBody", - scopeInstanceId: "box", - name: "token", - type: "variable", - value: { type: "string", value: "body" }, - }, - { - id: "bodyUrl", - scopeInstanceId: "body", - name: "url", - type: "variable", - value: { type: "string", value: "url" }, - }, - { - id: "bodyToken", - scopeInstanceId: "body", - name: "token", - type: "variable", - value: { type: "string", value: "token" }, - }, - { - id: "bodyAnotherUrl", - scopeInstanceId: "body", - name: "anotherUrl", - type: "variable", - value: { type: "string", value: "anotherUrl" }, - }, - ]) - ); - const { resources, dataSources } = extractWebstudioFragment( - getWebstudioData(), - "box" - ); - - expect(resources).toEqual([ - { - id: "resourceId", - name: "resourceName", - url: `$ws$dataSource$bodyUrl`, - method: "post", - headers: [ - { - name: "Authorization", - value: `"Token " + $ws$dataSource$bodyToken`, - }, - ], - body: `$ws$dataSource$boxBody`, - }, - ]); - expect(dataSources).toEqual([ - { - id: "boxData", - scopeInstanceId: "box", - name: "data", - type: "resource", - resourceId: "resourceId", - }, - { - id: "boxBody", - scopeInstanceId: "box", - name: "token", - type: "variable", - value: { type: "string", value: "body" }, - }, - { - id: "bodyUrl", - scopeInstanceId: "body", - name: "url", - type: "variable", - value: { type: "string", value: "url" }, - }, - { - id: "bodyToken", - scopeInstanceId: "body", - name: "token", - type: "variable", - value: { type: "string", value: "token" }, - }, - ]); - }); }); describe("insert webstudio fragment copy", () => { @@ -1972,102 +1593,6 @@ describe("insert webstudio fragment copy", () => { ]); }); - test("inline data sources when not available in scope", () => { - const data = getWebstudioDataStub(); - insertWebstudioFragmentCopy({ - data, - fragment: { - ...emptyFragment, - dataSources: [ - { - id: "outsideVariableId", - scopeInstanceId: "outsideInstanceId", - type: "variable", - name: "myOutsideVariable", - value: { type: "string", value: "outside" }, - }, - { - id: "insideVariableId", - scopeInstanceId: "insideInstanceId", - type: "variable", - name: "myInsideVariable", - value: { type: "string", value: "inside" }, - }, - ], - instances: [ - createInstance("body", "Body", [ - { - type: "expression", - value: - "$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId", - }, - ]), - ], - props: [ - { - id: "expressionId", - instanceId: "body", - name: "myProp1", - type: "expression", - value: - "$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId", - }, - { - id: "actionId", - instanceId: "body", - name: "myProp2", - type: "action", - value: [ - { - type: "execute", - args: [], - code: `$ws$dataSource$outsideVariableId = "outside"`, - }, - { - type: "execute", - args: [], - code: `$ws$dataSource$insideVariableId = "inside"`, - }, - ], - }, - ], - }, - availableDataSources: new Set(["insideVariableId"]), - }); - const [newInstanceId] = data.instances.keys(); - expect(data.dataSources).toEqual(new Map()); - expect(Array.from(data.instances.values())).toEqual([ - createInstance(newInstanceId, "Body", [ - { - type: "expression", - value: `"outside" + $ws$dataSource$insideVariableId`, - }, - ]), - ]); - expect(Array.from(data.props.values())).toEqual([ - { - id: expect.not.stringMatching("expressionId"), - instanceId: newInstanceId, - name: "myProp1", - type: "expression", - value: `"outside" + $ws$dataSource$insideVariableId`, - }, - { - id: expect.not.stringMatching("actionId"), - instanceId: newInstanceId, - name: "myProp2", - type: "action", - value: [ - { - type: "execute", - args: [], - code: `$ws$dataSource$insideVariableId = "inside"`, - }, - ], - }, - ]); - }); - test("insert data sources from portals with old ids", () => { const data = getWebstudioDataStub(); insertWebstudioFragmentCopy({ @@ -2179,106 +1704,6 @@ describe("insert webstudio fragment copy", () => { ]); }); - test("inline data sources from portals when not available in scope", () => { - const data = getWebstudioDataStub(); - insertWebstudioFragmentCopy({ - data, - fragment: { - ...emptyFragment, - // portal - // fragment - instances: [ - createInstance("portal", portalComponent, [ - { type: "id", value: "fragment" }, - ]), - createInstance("fragment", "Fragment", [ - { - type: "expression", - value: - "$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId", - }, - ]), - ], - dataSources: [ - { - id: "outsideVariableId", - scopeInstanceId: "outsideInstanceId", - type: "variable", - name: "myOutsideVariable", - value: { type: "string", value: "outside" }, - }, - { - id: "insideVariableId", - scopeInstanceId: "insideInstanceId", - type: "variable", - name: "myInsideVariable", - value: { type: "string", value: "inside" }, - }, - ], - props: [ - { - id: "expressionId", - instanceId: "fragment", - name: "myProp1", - type: "expression", - value: - "$ws$dataSource$outsideVariableId + $ws$dataSource$insideVariableId", - }, - { - id: "actionId", - instanceId: "fragment", - name: "myProp2", - type: "action", - value: [ - { - type: "execute", - args: [], - code: `$ws$dataSource$outsideVariableId = "outside"`, - }, - { - type: "execute", - args: [], - code: `$ws$dataSource$insideVariableId = "inside"`, - }, - ], - }, - ], - }, - availableDataSources: new Set(["insideVariableId"]), - }); - expect(data.dataSources).toEqual(new Map()); - expect(data.instances.get("fragment")).toEqual( - createInstance("fragment", "Fragment", [ - { - type: "expression", - value: `"outside" + $ws$dataSource$insideVariableId`, - }, - ]) - ); - expect(Array.from(data.props.values())).toEqual([ - { - id: "expressionId", - instanceId: "fragment", - name: "myProp1", - type: "expression", - value: `"outside" + $ws$dataSource$insideVariableId`, - }, - { - id: "actionId", - instanceId: "fragment", - name: "myProp2", - type: "action", - value: [ - { - type: "execute", - args: [], - code: `$ws$dataSource$insideVariableId = "inside"`, - }, - ], - }, - ]); - }); - test("insert local styles with new ids and use merged breakpoint ids", () => { const breakpoints = toMap([{ id: "base", label: "base" }]); const data = getWebstudioDataStub({ breakpoints }); @@ -2469,104 +1894,6 @@ describe("insert webstudio fragment copy", () => { ]); }); - test("inline data sources into resources when not available in the scope", () => { - const data = getWebstudioDataStub(); - insertWebstudioFragmentCopy({ - data, - fragment: { - ...emptyFragment, - resources: [ - { - id: "resourceId", - name: "", - url: `$ws$dataSource$paramVariableId`, - method: "post", - headers: [ - { name: "auth", value: "$ws$dataSource$paramVariableId" }, - ], - body: `$ws$dataSource$paramVariableId`, - }, - ], - dataSources: [ - { - id: "paramVariableId", - scopeInstanceId: "outside", - name: "myParam", - type: "variable", - value: { type: "string", value: "myParam" }, - }, - { - id: "variableId", - scopeInstanceId: "body", - name: "myResource", - type: "resource", - resourceId: "resourceId", - }, - ], - }, - availableDataSources: new Set(), - }); - const [newResourceId] = data.resources.keys(); - expect(Array.from(data.resources.values())).toEqual([ - { - id: newResourceId, - name: "", - url: `"myParam"`, - method: "post", - headers: [{ name: "auth", value: `"myParam"` }], - body: `"myParam"`, - }, - ]); - }); - - test("inline resource variables when not available in scope", () => { - const data = getWebstudioDataStub(); - insertWebstudioFragmentCopy({ - data, - fragment: { - ...emptyFragment, - resources: [ - { - id: "resourceId", - name: "", - url: `""`, - method: "get", - headers: [], - }, - ], - dataSources: [ - { - id: "variableId", - scopeInstanceId: "outside", - name: "myResource", - type: "resource", - resourceId: "resourceId", - }, - ], - props: [ - { - id: "expressionId", - instanceId: "body", - name: "myProp", - type: "expression", - value: "$ws$dataSource$variableId", - }, - ], - }, - availableDataSources: new Set(), - }); - const [newInstanceId] = data.instances.keys(); - expect(Array.from(data.props.values())).toEqual([ - { - id: expect.not.stringMatching("expressionId"), - instanceId: newInstanceId, - name: "myProp", - type: "expression", - value: "{}", - }, - ]); - }); - test("insert resources from portals with old ids", () => { const data = getWebstudioDataStub(); insertWebstudioFragmentCopy({ @@ -2622,64 +1949,6 @@ describe("insert webstudio fragment copy", () => { ]); }); - test("inline data sources into resources from portals when not available in scope", () => { - const data = getWebstudioDataStub(); - insertWebstudioFragmentCopy({ - data, - fragment: { - ...emptyFragment, - // portal - // fragment - instances: [ - createInstance("portal", portalComponent, [ - { type: "id", value: "fragment" }, - ]), - createInstance("fragment", "Fragment", []), - ], - resources: [ - { - id: "resourceId", - name: "", - url: `$ws$dataSource$paramVariableId`, - method: "post", - headers: [ - { name: "auth", value: "$ws$dataSource$paramVariableId" }, - ], - body: `$ws$dataSource$paramVariableId`, - }, - ], - dataSources: [ - { - id: "paramVariableId", - scopeInstanceId: "outside", - name: "myParam", - type: "variable", - value: { type: "string", value: "myParam" }, - }, - { - id: "variableId", - scopeInstanceId: "fragment", - name: "myResource", - type: "resource", - resourceId: "resourceId", - }, - ], - }, - availableDataSources: new Set(), - }); - const [newResourceId] = data.resources.keys(); - expect(Array.from(data.resources.values())).toEqual([ - { - id: newResourceId, - name: "", - url: `"myParam"`, - method: "post", - headers: [{ name: "auth", value: `"myParam"` }], - body: `"myParam"`, - }, - ]); - }); - test("insert instances with multiple roots", () => { const data = getWebstudioDataStub(); const { newInstanceIds } = insertWebstudioFragmentCopy({ diff --git a/apps/builder/app/shared/instance-utils.ts b/apps/builder/app/shared/instance-utils.ts index dc2c153de4f0..da4e8be4ca15 100644 --- a/apps/builder/app/shared/instance-utils.ts +++ b/apps/builder/app/shared/instance-utils.ts @@ -11,7 +11,6 @@ import { type StyleSources, type Breakpoints, type DataSources, - type Props, type DataSource, type Breakpoint, type WebstudioFragment, @@ -24,10 +23,10 @@ import { decodeDataSourceVariable, encodeDataSourceVariable, transpileExpression, - getExpressionIdentifiers, ROOT_INSTANCE_ID, portalComponent, collectionComponent, + Prop, } from "@webstudio-is/sdk"; import { generateDataFromEmbedTemplate } from "@webstudio-is/react-sdk"; import { @@ -64,6 +63,18 @@ import { findClosestNonTextualContainer, findClosestInstanceMatchingFragment, } from "./matcher"; +import { + restoreExpressionVariables, + unsetExpressionVariables, +} from "./data-variables"; +import { current, isDraft } from "immer"; + +/** + * structuredClone can be invoked on draft and throw error + * extract current snapshot before cloning + */ +const unwrap = (value: Value) => + isDraft(value) ? current(value) : value; export const updateWebstudioData = (mutate: (data: WebstudioData) => void) => { serverSyncStore.createTransaction( @@ -507,19 +518,6 @@ const traverseStyleValue = ( value satisfies never; }; -const collectUsedDataSources = ( - expression: string, - usedDataSourceIds: Set -) => { - const identifiers = getExpressionIdentifiers(expression); - for (const identifier of identifiers) { - const id = decodeDataSourceVariable(identifier); - if (id !== undefined) { - usedDataSourceIds.add(id); - } - } -}; - export const extractWebstudioFragment = ( data: WebstudioData, rootInstanceId: string @@ -538,19 +536,13 @@ export const extractWebstudioFragment = ( // collect the instance by id and all its descendants including portal instances const fragmentInstanceIds = findTreeInstanceIds(instances, rootInstanceId); - const fragmentInstances: Instance[] = []; + let fragmentInstances: Instance[] = []; const fragmentStyleSourceSelections: StyleSourceSelection[] = []; const fragmentStyleSources: StyleSources = new Map(); - const usedDataSourceIds = new Set(); for (const instanceId of fragmentInstanceIds) { const instance = instances.get(instanceId); if (instance) { fragmentInstances.push(instance); - for (const child of instance.children) { - if (child.type === "expression") { - collectUsedDataSources(child.value, usedDataSourceIds); - } - } } // collect all style sources bound to these instances @@ -605,77 +597,100 @@ export const extractWebstudioFragment = ( }); } + // collect variables scoped to fragment instances + // and variables outside of scope to unset + const fragmentDataSources: DataSources = new Map(); + const fragmentResourceIds = new Set(); + const unsetNameById = new Map(); + for (const dataSource of dataSources.values()) { + if (fragmentInstanceIds.has(dataSource.scopeInstanceId)) { + fragmentDataSources.set(dataSource.id, dataSource); + if (dataSource.type === "resource") { + fragmentResourceIds.add(dataSource.resourceId); + } + } else { + unsetNameById.set(dataSource.id, dataSource.name); + } + } + + // unset variables outside of scope + fragmentInstances = fragmentInstances.map((instance) => { + instance = structuredClone(unwrap(instance)); + for (const child of instance.children) { + if (child.type === "expression") { + const expression = child.value; + child.value = unsetExpressionVariables({ expression, unsetNameById }); + } + } + return instance; + }); + // collect props bound to these instances - const fragmentProps: Props = new Map(); + // and unset variables outside of scope + const fragmentProps: Prop[] = []; for (const prop of props.values()) { if (fragmentInstanceIds.has(prop.instanceId) === false) { continue; } - fragmentProps.set(prop.id, prop); - if (prop.type === "expression") { - collectUsedDataSources(prop.value, usedDataSourceIds); + const newProp = structuredClone(unwrap(prop)); + const expression = prop.value; + newProp.value = unsetExpressionVariables({ expression, unsetNameById }); + fragmentProps.push(newProp); + continue; } if (prop.type === "action") { - for (const value of prop.value) { - if (value.type === "execute") { - collectUsedDataSources(value.code, usedDataSourceIds); - } + const newProp = structuredClone(unwrap(prop)); + for (const value of newProp.value) { + const expression = value.code; + value.code = unsetExpressionVariables({ expression, unsetNameById }); } + fragmentProps.push(newProp); + continue; } + fragmentProps.push(prop); + // collect assets if (prop.type === "asset") { fragmentAssetIds.add(prop.value); } - } - // collect variables scoped to fragment instances - // or used by expressions or actions even outside of the tree - // such variables can be bound to fragment root on paste - const fragmentDataSources: DataSources = new Map(); - const fragmentResourceIds = new Set(); - for (const dataSource of dataSources.values()) { - if ( - // check if data source itself can be copied - (dataSource.scopeInstanceId !== undefined && - fragmentInstanceIds.has(dataSource.scopeInstanceId)) || - usedDataSourceIds.has(dataSource.id) - ) { - fragmentDataSources.set(dataSource.id, dataSource); - if (dataSource.type === "resource") { - fragmentResourceIds.add(dataSource.resourceId); - } + // collect resources from props + if (prop.type === "resource") { + fragmentResourceIds.add(prop.value); } } // collect resources bound to all fragment data sources - // and then collect data sources used in these resources - // it creates some recursive behavior but since resources - // cannot depend on other resources all left data sources - // can be collected just once + // and unset variables which are defined outside of scope + // and used in resource const fragmentResources: Resource[] = []; - const dataSourceIdsUsedInResources = new Set(); for (const resourceId of fragmentResourceIds) { const resource = resources.get(resourceId); if (resource === undefined) { continue; } - fragmentResources.push(resource); - collectUsedDataSources(resource.url, dataSourceIdsUsedInResources); - for (const { value } of resource.headers) { - collectUsedDataSources(value, dataSourceIdsUsedInResources); - } - if (resource.body) { - collectUsedDataSources(resource.body, dataSourceIdsUsedInResources); + const newResource = structuredClone(unwrap(resource)); + newResource.url = unsetExpressionVariables({ + expression: newResource.url, + unsetNameById, + }); + for (const header of newResource.headers) { + header.value = unsetExpressionVariables({ + expression: header.value, + unsetNameById, + }); } - } - for (const dataSource of dataSources.values()) { - if (dataSourceIdsUsedInResources.has(dataSource.id)) { - fragmentDataSources.set(dataSource.id, dataSource); + if (newResource.body) { + newResource.body = unsetExpressionVariables({ + expression: newResource.body, + unsetNameById, + }); } + fragmentResources.push(newResource); } const fragmentAssets: Asset[] = []; @@ -697,7 +712,7 @@ export const extractWebstudioFragment = ( styles: fragmentStyles, dataSources: Array.from(fragmentDataSources.values()), resources: fragmentResources, - props: Array.from(fragmentProps.values()), + props: fragmentProps, assets: fragmentAssets, }; }; @@ -725,41 +740,6 @@ export const findAvailableDataSources = ( return availableDataSources; }; -const inlineUnavailableDataSources = ({ - code, - availableDataSources, - dataSources, -}: { - code: string; - availableDataSources: Set; - dataSources: DataSources; -}) => { - let isDiscarded = false; - const newCode = transpileExpression({ - expression: code, - replaceVariable: (identifier, assignee) => { - const dataSourceId = decodeDataSourceVariable(identifier); - if ( - dataSourceId === undefined || - availableDataSources.has(dataSourceId) - ) { - return; - } - // left operand of assign operator cannot be inlined - if (assignee) { - isDiscarded = true; - } - const dataSource = dataSources.get(dataSourceId); - // inline variable not scoped to portal content instances - if (dataSource?.type === "variable") { - return JSON.stringify(dataSource.value.value); - } - return "{}"; - }, - }); - return { code: newCode, isDiscarded }; -}; - const replaceDataSources = ( code: string, replacements: Map @@ -909,15 +889,10 @@ export const insertWebstudioFragmentCopy = ({ continue; } - const availablePortalDataSources = new Set(availableDataSources); const usedResourceIds = new Set(); for (const dataSource of fragment.dataSources) { // insert only data sources within portal content - if ( - dataSource.scopeInstanceId && - instanceIds.has(dataSource.scopeInstanceId) - ) { - availablePortalDataSources.add(dataSource.id); + if (instanceIds.has(dataSource.scopeInstanceId)) { dataSources.set(dataSource.id, dataSource); if (dataSource.type === "resource") { usedResourceIds.add(dataSource.resourceId); @@ -926,93 +901,21 @@ export const insertWebstudioFragmentCopy = ({ } for (const resource of fragment.resources) { - if (usedResourceIds.has(resource.id) === false) { - continue; + if (usedResourceIds.has(resource.id)) { + resources.set(resource.id, resource); } - const newUrl = inlineUnavailableDataSources({ - code: resource.url, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }).code; - const newHeaders = resource.headers.map((header) => ({ - name: header.name, - value: inlineUnavailableDataSources({ - code: header.value, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }).code, - })); - const newBody = - resource.body === undefined - ? undefined - : inlineUnavailableDataSources({ - code: resource.body, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }).code; - resources.set(resource.id, { - ...resource, - url: newUrl, - headers: newHeaders, - body: newBody, - }); } for (const instance of fragment.instances) { if (instanceIds.has(instance.id)) { - instances.set(instance.id, { - ...instance, - children: instance.children.map((child) => { - if (child.type === "expression") { - const { code } = inlineUnavailableDataSources({ - code: child.value, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }); - return { - type: "expression", - value: code, - }; - } - return child; - }), - }); + instances.set(instance.id, instance); } } - for (let prop of fragment.props) { - if (instanceIds.has(prop.instanceId) === false) { - continue; - } - // inline data sources not available in scope into expressions - if (prop.type === "expression") { - const { code } = inlineUnavailableDataSources({ - code: prop.value, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }); - prop = { ...prop, value: code }; - } - if (prop.type === "action") { - prop = { - ...prop, - value: prop.value.flatMap((value) => { - if (value.type !== "execute") { - return [value]; - } - const { code, isDiscarded } = inlineUnavailableDataSources({ - code: value.code, - availableDataSources: availablePortalDataSources, - dataSources: fragmentDataSources, - }); - if (isDiscarded) { - return []; - } - return [{ ...value, code }]; - }), - }; + for (const prop of fragment.props) { + if (instanceIds.has(prop.instanceId)) { + props.set(prop.id, prop); } - props.set(prop.id, prop); } // insert local style sources with their styles @@ -1070,110 +973,30 @@ export const insertWebstudioFragmentCopy = ({ fragmentInstanceIds.add(ROOT_INSTANCE_ID); newInstanceIds.set(ROOT_INSTANCE_ID, ROOT_INSTANCE_ID); - const availableFragmentDataSources = new Set(availableDataSources); + const maskedIdByName = new Map(); + for (const dataSourceId of availableDataSources) { + const dataSource = dataSources.get(dataSourceId); + if (dataSource) { + maskedIdByName.set(dataSource.name, dataSource.id); + } + } const newResourceIds = new Map(); - const usedResourceIds = new Set(); - for (const dataSource of fragment.dataSources) { + for (let dataSource of fragment.dataSources) { const { scopeInstanceId } = dataSource; // insert only data sources within portal content - if (scopeInstanceId && fragmentInstanceIds.has(scopeInstanceId)) { - availableFragmentDataSources.add(dataSource.id); + if (fragmentInstanceIds.has(scopeInstanceId)) { const newDataSourceId = nanoid(); newDataSourceIds.set(dataSource.id, newDataSourceId); + dataSource = structuredClone(unwrap(dataSource)); + dataSource.id = newDataSourceId; + dataSource.scopeInstanceId = + newInstanceIds.get(scopeInstanceId) ?? scopeInstanceId; if (dataSource.type === "resource") { const newResourceId = nanoid(); newResourceIds.set(dataSource.resourceId, newResourceId); - usedResourceIds.add(dataSource.resourceId); - dataSources.set(newDataSourceId, { - ...dataSource, - id: newDataSourceId, - scopeInstanceId: - newInstanceIds.get(scopeInstanceId) ?? scopeInstanceId, - resourceId: newResourceId, - }); - } else { - dataSources.set(newDataSourceId, { - ...dataSource, - id: newDataSourceId, - scopeInstanceId: - newInstanceIds.get(scopeInstanceId) ?? scopeInstanceId, - }); + dataSource.resourceId = newResourceId; } - } - } - - for (const resource of fragment.resources) { - if (usedResourceIds.has(resource.id) === false) { - continue; - } - const newResourceId = newResourceIds.get(resource.id) ?? resource.id; - - const newUrl = replaceDataSources( - inlineUnavailableDataSources({ - code: resource.url, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, - }).code, - newDataSourceIds - ); - const newHeaders = resource.headers.map((header) => ({ - name: header.name, - value: replaceDataSources( - inlineUnavailableDataSources({ - code: header.value, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, - }).code, - newDataSourceIds - ), - })); - const newBody = - resource.body === undefined - ? undefined - : replaceDataSources( - inlineUnavailableDataSources({ - code: resource.body, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, - }).code, - newDataSourceIds - ); - resources.set(newResourceId, { - ...resource, - id: newResourceId, - url: newUrl, - headers: newHeaders, - body: newBody, - }); - } - - for (const instance of fragment.instances) { - if (fragmentInstanceIds.has(instance.id)) { - const newId = newInstanceIds.get(instance.id) ?? instance.id; - instances.set(newId, { - ...instance, - id: newId, - children: instance.children.map((child) => { - if (child.type === "id") { - return { - type: "id", - value: newInstanceIds.get(child.value) ?? child.value, - }; - } - if (child.type === "expression") { - const { code } = inlineUnavailableDataSources({ - code: child.value, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, - }); - return { - type: "expression", - value: replaceDataSources(code, newDataSourceIds), - }; - } - return child; - }), - }); + dataSources.set(dataSource.id, dataSource); } } @@ -1181,48 +1004,82 @@ export const insertWebstudioFragmentCopy = ({ if (fragmentInstanceIds.has(prop.instanceId) === false) { continue; } - // inline data sources not available in scope into expressions + prop = structuredClone(unwrap(prop)); + prop.id = nanoid(); + prop.instanceId = newInstanceIds.get(prop.instanceId) ?? prop.instanceId; if (prop.type === "expression") { - const { code } = inlineUnavailableDataSources({ - code: prop.value, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, + prop.value = restoreExpressionVariables({ + expression: prop.value, + maskedIdByName, }); - prop = { ...prop, value: replaceDataSources(code, newDataSourceIds) }; + prop.value = replaceDataSources(prop.value, newDataSourceIds); } if (prop.type === "action") { - prop = { - ...prop, - value: prop.value.flatMap((value) => { - if (value.type !== "execute") { - return [value]; - } - const { code, isDiscarded } = inlineUnavailableDataSources({ - code: value.code, - availableDataSources: availableFragmentDataSources, - dataSources: fragmentDataSources, - }); - if (isDiscarded) { - return []; - } - return [ - { ...value, code: replaceDataSources(code, newDataSourceIds) }, - ]; - }), - }; + for (const value of prop.value) { + value.code = restoreExpressionVariables({ + expression: value.code, + maskedIdByName, + }); + value.code = replaceDataSources(value.code, newDataSourceIds); + } } if (prop.type === "parameter") { - prop = { - ...prop, - value: newDataSourceIds.get(prop.value) ?? prop.value, - }; + prop.value = newDataSourceIds.get(prop.value) ?? prop.value; + } + if (prop.type === "resource") { + const newResourceId = nanoid(); + newResourceIds.set(prop.value, newResourceId); + prop.value = newResourceId; } - const newId = nanoid(); - props.set(newId, { - ...prop, - id: newId, - instanceId: newInstanceIds.get(prop.instanceId) ?? prop.instanceId, + props.set(prop.id, prop); + } + + for (let resource of fragment.resources) { + if (newResourceIds.has(resource.id) === false) { + continue; + } + resource = structuredClone(unwrap(resource)); + resource.id = newResourceIds.get(resource.id) ?? resource.id; + resource.url = restoreExpressionVariables({ + expression: resource.url, + maskedIdByName, }); + resource.url = replaceDataSources(resource.url, newDataSourceIds); + for (const header of resource.headers) { + header.value = restoreExpressionVariables({ + expression: header.value, + maskedIdByName, + }); + header.value = replaceDataSources(header.value, newDataSourceIds); + } + if (resource.body) { + resource.body = restoreExpressionVariables({ + expression: resource.body, + maskedIdByName, + }); + resource.body = replaceDataSources(resource.body, newDataSourceIds); + } + resources.set(resource.id, resource); + } + + for (let instance of fragment.instances) { + if (fragmentInstanceIds.has(instance.id)) { + instance = structuredClone(unwrap(instance)); + instance.id = newInstanceIds.get(instance.id) ?? instance.id; + for (const child of instance.children) { + if (child.type === "id") { + child.value = newInstanceIds.get(child.value) ?? child.value; + } + if (child.type === "expression") { + child.value = restoreExpressionVariables({ + expression: child.value, + maskedIdByName, + }); + child.value = replaceDataSources(child.value, newDataSourceIds); + } + } + instances.set(instance.id, instance); + } } // insert local styles with new ids