diff --git a/fixtures/webstudio-features/.template/vite.config.ts b/fixtures/webstudio-features/.template/vite.config.ts index b33951009b77..0249ae1afda2 100644 --- a/fixtures/webstudio-features/.template/vite.config.ts +++ b/fixtures/webstudio-features/.template/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vite"; import { reactRouter } from "@react-router/dev/vite"; // @ts-ignore import { dedupeMeta } from "./proxy-emulator/dedupe-meta"; -import { existsSync, readdirSync } from "fs"; +import { existsSync } from "fs"; // @ts-ignore import path from "path"; // @ts-ignore diff --git a/fixtures/webstudio-features/.webstudio/data.json b/fixtures/webstudio-features/.webstudio/data.json index ffdef2f45829..ad9334d05dd2 100644 --- a/fixtures/webstudio-features/.webstudio/data.json +++ b/fixtures/webstudio-features/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "fce42d58-8a67-4f4b-8427-ea7500132e28", + "id": "6f14cdae-073c-4f2c-b535-da3f404f3e36", "projectId": "cddc1d44-af37-4cb6-a430-d300cf6f932d", - "version": 477, - "createdAt": "2025-02-28T14:52:36.082+00:00", - "updatedAt": "2025-02-28T14:52:36.082+00:00", + "version": 479, + "createdAt": "2025-03-04T16:25:33.097+00:00", + "updatedAt": "2025-03-04T16:25:33.097+00:00", "pages": { "meta": { "siteName": "KittyGuardedZone", @@ -2867,7 +2867,7 @@ "instanceId": "SVI6fI342JAxCvwsg4Oc6", "name": "xmlns:xhtml", "type": "string", - "value": "http://www.w3.org/1999/xhtml" + "value": "http://www.w3.org/TR/xhtml11/xhtml11_schema.html" } ], [ diff --git a/fixtures/webstudio-features/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-features/app/__generated__/$resources.sitemap.xml.ts index 7b8438c2278b..3a0bc1fc6f04 100644 --- a/fixtures/webstudio-features/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/webstudio-features/app/__generated__/$resources.sitemap.xml.ts @@ -1,26 +1,26 @@ export const sitemap = [ { path: "/", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, { path: "/_route_with_symbols_", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, { path: "/form", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, { path: "/heading-with-id", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, { path: "/resources", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, { path: "/nested/nested-page", - lastModified: "2025-02-28", + lastModified: "2025-03-04", }, ]; diff --git a/fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.tsx b/fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.tsx index aef811376761..bf4b5c88d034 100644 --- a/fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.tsx +++ b/fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.tsx @@ -4,7 +4,10 @@ import { Fragment, useState } from "react"; import type { FontAsset, ImageAsset } from "@webstudio-is/sdk"; import { useResource, useVariableState } from "@webstudio-is/react-sdk/runtime"; -import { XmlNode, XmlTime } from "@webstudio-is/sdk-components-react"; +import { + XmlNode as XmlNode, + XmlTime as XmlTime, +} from "@webstudio-is/sdk-components-react"; export const siteName = "KittyGuardedZone"; @@ -25,62 +28,54 @@ export const pageFontAssets: FontAsset[] = []; export const pageBackgroundImageAssets: ImageAsset[] = []; -const Body = (props: any) => {props.children}; -const Heading = (props: any) => null; - const Page = (_props: { system: any }) => { const system = _props.system; return ( - - - {[ - { - path: "/", - lastModified: "2024-05-07", - }, - { - path: "/olegs-test", - lastModified: "2024-05-07", - }, - ]?.map?.((url: any, index: number) => ( - - - - {`${system?.origin ?? "${ORIGIN}"}${url?.path}`} - - {url?.lastModified} - + + {[ + { + path: "/", + lastModified: "2024-05-07", + }, + { + path: "/olegs-test", + lastModified: "2024-05-07", + }, + ]?.map?.((url: any, index: number) => ( + + + + {`${system?.origin ?? "${ORIGIN}"}${url?.path}`} - - ))} - - {"Below is custom section"} - - - {"custom-hand-made-location"} - - + {url?.lastModified} + - - {"Hello"} - {"https://webstudio.is"} + + ))} + + {"custom-hand-made-location"} + + + + {"Hello"} + {"https://webstudio.is"} - + ); }; diff --git a/fixtures/webstudio-features/package.json b/fixtures/webstudio-features/package.json index 30d942db54c9..e7de728d614d 100644 --- a/fixtures/webstudio-features/package.json +++ b/fixtures/webstudio-features/package.json @@ -6,7 +6,7 @@ "dev": "react-router dev", "cli": "NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio", "fixtures:link": "pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'", - "fixtures:sync": "pnpm cli sync --buildId fce42d58-8a67-4f4b-8427-ea7500132e28 && pnpm prettier --write ./.webstudio/", + "fixtures:sync": "pnpm cli sync --buildId 6f14cdae-073c-4f2c-b535-da3f404f3e36 && pnpm prettier --write ./.webstudio/", "fixtures:build": "pnpm cli build --template react-router --template ./.template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json" }, "private": true, diff --git a/fixtures/webstudio-features/proxy-emulator/dedupe-meta.ts b/fixtures/webstudio-features/proxy-emulator/dedupe-meta.ts index 0b1a1c4b2e5f..7951329ac846 100644 --- a/fixtures/webstudio-features/proxy-emulator/dedupe-meta.ts +++ b/fixtures/webstudio-features/proxy-emulator/dedupe-meta.ts @@ -17,18 +17,19 @@ export const dedupeMeta: Plugin = { const originalWrite = res.write; const originalEnd = res.end; - let body = ""; + const buffers: Buffer[] = []; res.write = (chunk) => { - body += chunk.toString(); + buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); return true; }; res.end = (chunk) => { if (chunk) { - body += chunk.toString(); + buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } + const body = Buffer.concat(buffers).toString("utf8"); const response = new Response(body); const metasSet = new Set(); diff --git a/fixtures/webstudio-features/vite.config.ts b/fixtures/webstudio-features/vite.config.ts index b33951009b77..0249ae1afda2 100644 --- a/fixtures/webstudio-features/vite.config.ts +++ b/fixtures/webstudio-features/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vite"; import { reactRouter } from "@react-router/dev/vite"; // @ts-ignore import { dedupeMeta } from "./proxy-emulator/dedupe-meta"; -import { existsSync, readdirSync } from "fs"; +import { existsSync } from "fs"; // @ts-ignore import path from "path"; // @ts-ignore diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index f8ee7dc3c395..d12b65dc79d8 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -64,10 +64,6 @@ import { createFramework as createVikeSsgFramework } from "./framework-vike-ssg" const limit = pLimit(10); -type ComponentsByPage = { - [id: Page["id"]]: Set; -}; - type SiteDataByPage = { [id: Page["id"]]: { page: Page; @@ -300,10 +296,9 @@ export const prebuild = async (options: { } } - const projectMetas = new Map( + const usedMetas = new Map( Object.entries(coreMetas) ); - const componentsByPage: ComponentsByPage = {}; const siteDataByPage: SiteDataByPage = {}; const fontAssetsByPage: Record = {}; const backgroundImageAssetsByPage: Record = {}; @@ -369,15 +364,10 @@ export const prebuild = async (options: { assets: siteData.assets, }; - componentsByPage[page.id] = new Set(); for (const [_instanceId, instance] of instances) { - if (isCoreComponent(instance.component)) { - continue; - } - componentsByPage[page.id].add(instance.component); const meta = metas.get(instance.component); if (meta) { - projectMetas.set(instance.component, meta); + usedMetas.set(instance.component, meta); } } @@ -480,14 +470,14 @@ export const prebuild = async (options: { styles: new Map(siteData.build?.styles), styleSourceSelections: new Map(siteData.build?.styleSourceSelections), // pass only used metas to not generate unused preset styles - componentMetas: projectMetas, + componentMetas: usedMetas, assetBaseUrl, atomic: siteData.build.pages.compiler?.atomicStyles ?? true, }); await createFileIfNotExists(join(generatedDir, "index.css"), cssText); - for (const [pageId, pageComponents] of Object.entries(componentsByPage)) { + for (const page of Object.values(siteData.pages)) { const scope = createScope([ // manually maintained list of occupied identifiers "useState", @@ -498,6 +488,34 @@ export const prebuild = async (options: { "_props", ]); + const pageData = siteDataByPage[page.id]; + const instances = new Map(pageData.build.instances); + const documentType = page.meta.documentType ?? "html"; + let rootInstanceId = page.rootInstanceId; + + // cleanup xml markup + if (documentType === "xml") { + // treat first body child as root + const bodyInstance = instances.get(rootInstanceId); + if (bodyInstance?.children?.[0].type === "id") { + rootInstanceId = bodyInstance.children[0].value; + } + // remove all unexpected components + for (const instance of instances.values()) { + if (isCoreComponent(instance.component)) { + continue; + } + if (usedMetas.get(instance.component)?.category === "xml") { + continue; + } + instances.delete(instance.id); + } + } + + const pageComponents = new Set(); + for (const instance of instances.values()) { + pageComponents.add(instance.component); + } const namespaces = new Map< string, Set<[shortName: string, componentName: string]> @@ -518,61 +536,19 @@ export const prebuild = async (options: { } let componentImports = ""; - let xmlPresentationComponents = ""; - - const pageData = siteDataByPage[pageId]; - const documentType = pageData.page.meta.documentType ?? "html"; - for (const [namespace, componentsSet] of namespaces.entries()) { - switch (documentType) { - case "html": - { - const specifiers = Array.from(componentsSet) - .map( - ([shortName, component]) => - `${shortName} as ${scope.getName(component, shortName)}` - ) - .join(", "); - componentImports += `import { ${specifiers} } from "${namespace}";\n`; - } - break; - - case "xml": - { - // In case of xml it's the only component we are supporting - componentImports = `import { XmlNode, XmlTime } from "@webstudio-is/sdk-components-react";\n`; - - xmlPresentationComponents += Array.from(componentsSet) - .map(([shortName, component]) => - scope.getName(component, shortName) - ) - .filter( - (scopedName) => - scopedName !== "XmlNode" && scopedName !== "XmlTime" - ) - .map((scopedName) => - scopedName === "Body" - ? // Using prevents React from hoisting elements like , , and - // out of their intended scope during rendering. - // More details: https://github.com/facebook/react/blob/7c8e5e7ab8bb63de911637892392c5efd8ce1d0f/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3083 - `const ${scopedName} = (props: any) => {props.children};` - : // Do not render all other components - `const ${scopedName} = (props: any) => null;` - ) - .join("\n"); - } - break; - default: { - documentType satisfies never; - } - } + const specifiers = Array.from(componentsSet) + .map( + ([shortName, component]) => + `${shortName} as ${scope.getName(component, shortName)}` + ) + .join(", "); + componentImports += `import { ${specifiers} } from "${namespace}";\n`; } - const pageFontAssets = fontAssetsByPage[pageId]; - const pageBackgroundImageAssets = backgroundImageAssetsByPage[pageId]; + const pageFontAssets = fontAssetsByPage[page.id]; + const pageBackgroundImageAssets = backgroundImageAssetsByPage[page.id]; - const rootInstanceId = pageData.page.rootInstanceId; - const instances = new Map(pageData.build.instances); const props = new Map(pageData.build.props); const dataSources = new Map(pageData.build.dataSources); const resources = new Map(pageData.build.resources); @@ -591,7 +567,7 @@ export const prebuild = async (options: { instanceId: "", name: "system", type: "parameter", - value: pageData.page.systemDataSourceId ?? "", + value: page.systemDataSourceId ?? "", }, { id: "global-system", @@ -605,7 +581,7 @@ export const prebuild = async (options: { props, dataSources, classesMap: classes, - metas: projectMetas, + metas: usedMetas, }); const projectMeta = siteData.build.pages.meta; @@ -614,7 +590,7 @@ export const prebuild = async (options: { projectMeta?.contactEmail || siteData.user?.email || undefined; const favIconAsset = assets.get(projectMeta?.faviconAssetId ?? ""); - const pagePath = getPagePath(pageData.page.id, siteData.build.pages); + const pagePath = getPagePath(page.id, siteData.build.pages); // MARK: - TODO: XML GENERATION const pageExports = `/* eslint-disable */ @@ -668,8 +644,6 @@ export const prebuild = async (options: { : "" } - ${xmlPresentationComponents} - ${pageComponent} export { Page } @@ -681,7 +655,7 @@ export const prebuild = async (options: { import type { PageMeta } from "@webstudio-is/sdk"; ${generateResources({ scope, - page: pageData.page, + page, dataSources, props, resources, @@ -689,12 +663,12 @@ export const prebuild = async (options: { ${generatePageMeta({ globalScope: scope, - page: pageData.page, + page, dataSources, assets, })} - ${generateRemixParams(pageData.page.path)} + ${generateRemixParams(page.path)} export const projectId = "${siteData.build.projectId}"; diff --git a/packages/cli/templates/defaults/app/route-templates/xml.tsx b/packages/cli/templates/defaults/app/route-templates/xml.tsx index 15df66571509..9d0bd38f1aee 100644 --- a/packages/cli/templates/defaults/app/route-templates/xml.tsx +++ b/packages/cli/templates/defaults/app/route-templates/xml.tsx @@ -71,10 +71,6 @@ export const loader = async (arg: LoaderFunctionArgs) => { ); - // Xml is wrapped with to prevent React from hoisting elements like , , and out of their intended scope during rendering. - // More details: https://github.com/facebook/react/blob/7c8e5e7ab8bb63de911637892392c5efd8ce1d0f/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3083 - text = text.replace(/^/g, "").replace(/<\/svg>$/g, ""); - // React has issues rendering certain elements, such as errors when a element has children. // To render XML, we wrap it with an tag and add a suffix to avoid React's default behavior on these elements. text = text.replaceAll(xmlNodeTagSuffix, "");