diff --git a/.github/workflows/cli-r2-static.yaml b/.github/workflows/cli-r2-static.yaml new file mode 100644 index 0000000000..9c92a25f9b --- /dev/null +++ b/.github/workflows/cli-r2-static.yaml @@ -0,0 +1,104 @@ +name: CLI R2 SSG + +on: + push: + branches: + - "*.staging" + +# cancel in-progress runs on new commits to same PR (gitub.event.number) +concurrency: + group: vercel-cli-r2-static-${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + statuses: write # This is required for the GitHub Script createCommitStatus to work + packages: write + +jobs: + build: + env: + COMPATIBILITY_DATE: 2024-04-10 + + environment: + name: "staging" + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} # HEAD commit instead of merge commit + + - uses: pnpm/action-setup@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: pnpm instal + run: pnpm install --ignore-scripts + + - name: pnpm build + run: pnpm --filter 'ssg^...' run build + + # Ideally, execute 'pnpm deploy --prod', but @remix-run/dev doesn't support this flag. + # Despite being listed as a dependency, @remix-run/dev does not install the remix cli. + # TODO: Minimize artefact size due to frequent downloads on each publish. + - name: pnpm deploy + run: pnpm --filter 'ssg' deploy "${{ github.workspace }}/../ssg-template" + + - name: Make archive + run: | + tar --use-compress-program="zstd -19" -cf ssg-template.tar.zst ssg-template + working-directory: ${{ github.workspace }}/.. + + - name: Copy artifact + run: | + # For staging + aws s3 cp ssg-template.tar.zst "s3://${ARTEFACT_BUCKET_NAME}/public/ssg-template/${{ github.ref_name }}.tar.zst" + + # For production can be cached forever + aws s3 cp \ + --metadata-directive REPLACE --cache-control "public,max-age=31536102,immutable" \ + ssg-template.tar.zst "s3://${ARTEFACT_BUCKET_NAME}/public/ssg-template/${{ github.sha }}.tar.zst" + + working-directory: ${{ github.workspace }}/.. + env: + AWS_ENDPOINT_URL_S3: ${{ secrets.AWS_ENDPOINT_URL_S3 }} + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ARTEFACT_BUCKET_NAME: ${{ secrets.ARTEFACT_BUCKET_NAME }} + + checks: + environment: + name: "staging" + + runs-on: ubuntu-latest + + needs: build + + steps: + - uses: pnpm/action-setup@v3 + with: + version: "9" + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Copy atrifact via http + run: curl -o ssg-template.tar.zst ${{ secrets.ARTEFACT_BUCKET_URL }}/public/ssg-template/${{ github.ref_name }}.tar.zst + + - name: Extract archive + run: tar --use-compress-program="zstd -d" -xf ssg-template.tar.zst -C . + + - name: Webstudio Build + run: pnpm webstudio build --template ssg --template internal --preview + working-directory: ${{ github.workspace }}/ssg-template + + - name: Build + run: pnpm build + working-directory: ${{ github.workspace }}/ssg-template diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx index 26bcdcd3d9..6610c7cdd4 100644 --- a/apps/builder/app/builder/features/topbar/domains.tsx +++ b/apps/builder/app/builder/features/topbar/domains.tsx @@ -83,7 +83,8 @@ export const getStatus = (projectDomain: Domain) => ? (`VERIFIED_${projectDomain.domain.status}` as const) : `UNVERIFIED`; -export const PENDING_TIMEOUT = 60 * 3 * 1000; +export const PENDING_TIMEOUT = + process.env.NODE_ENV === "production" ? 60 * 3 * 1000 : 35000; export const getPublishStatusAndText = ({ updatedAt, diff --git a/apps/builder/app/builder/features/topbar/publish.tsx b/apps/builder/app/builder/features/topbar/publish.tsx index 1797a00797..fa1481e100 100644 --- a/apps/builder/app/builder/features/topbar/publish.tsx +++ b/apps/builder/app/builder/features/topbar/publish.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + useOptimistic, + useTransition, + useRef, +} from "react"; import { useStore } from "@nanostores/react"; import { Button, @@ -25,6 +33,7 @@ import { Link, PanelBanner, buttonStyle, + toast, } from "@webstudio-is/design-system"; import stripIndent from "strip-indent"; import { @@ -49,7 +58,8 @@ import { } from "@webstudio-is/icons"; import { AddDomain } from "./add-domain"; import { humanizeString } from "~/shared/string-utils"; -import { trpcClient } from "~/shared/trpc/trpc-client"; +import { trpcClient, nativeClient } from "~/shared/trpc/trpc-client"; +import { isFeatureEnabled } from "@webstudio-is/feature-flags"; type ProjectData = | { @@ -326,6 +336,7 @@ const Publish = ({ domains: domainsToPublish.map( (projectDomain) => projectDomain.domain.domain ), + destination: "saas", }, () => { refresh(); @@ -340,6 +351,148 @@ const Publish = ({ ); }; +const fetchProjectDataStatus = async (projectId: Project["id"]) => { + const projectData = await nativeClient.domain.project.query({ + projectId, + }); + + if (projectData.success === false) { + throw new Error(projectData.error); + } + + if (projectData.project.latestStaticBuild == null) { + return { + status: "INITIAL" as const, + statusText: "Not published", + }; + } + + const { status, statusText } = getPublishStatusAndText( + projectData.project.latestStaticBuild + ); + + return { status, statusText }; +}; + +type StaticProjectStatus = Awaited>; + +const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { + const [_, startTransition] = useTransition(); + + const isCanceledRef = useRef(false); + useEffect(() => { + isCanceledRef.current = false; + return () => { + isCanceledRef.current = true; + }; + }, []); + + const [projectDataStatus, setProjectDataStatus] = + useState({ + status: "INITIAL", + statusText: "Not published", + }); + + const [optimisticPendingStatus, setOptimisticPendingStatus] = useOptimistic( + projectDataStatus, + (_currentState, optimisticValue: StaticProjectStatus) => { + return optimisticValue; + } + ); + + const isPublishInProgress = optimisticPendingStatus.status === "PENDING"; + + return ( + + {optimisticPendingStatus.status === "FAILED" && ( + {optimisticPendingStatus.statusText} + )} + + + + + + ); +}; + const ErrorText = ({ children }: { children: string }) => ( void; }) => { const [newDomains, setNewDomains] = useState(new Set()); + const { data: domainsResult, load: domainRefresh, @@ -487,11 +641,9 @@ const Content = (props: { {domainsResult.error} )} - - - - {projectData?.success === true ? ( + {projectData?.success === true && ( - ) : ( + )} + {projectData?.success !== true && ( )} @@ -543,7 +695,7 @@ type DeployTargets = keyof typeof deployTargets; const isDeployTargets = (value: string): value is DeployTargets => Object.keys(deployTargets).includes(value); -const ExportContent = () => { +const ExportContent = (props: { projectId: Project["id"] }) => { const npxCommand = "npx webstudio@latest"; const [deployTarget, setDeployTarget] = useState("vercel"); @@ -557,6 +709,23 @@ const ExportContent = () => { }} > + {isFeatureEnabled("staticExport") && ( + <> + + + + OR + + + + )} + Step 1 @@ -750,7 +919,7 @@ export const PublishButton = ({ projectId }: PublishProps) => { {dialogContentType === "export" && ( <> Export - + )} diff --git a/apps/builder/app/shared/db/canvas.server.ts b/apps/builder/app/shared/db/canvas.server.ts index 31bc5faf1c..05ee5309f2 100644 --- a/apps/builder/app/shared/db/canvas.server.ts +++ b/apps/builder/app/shared/db/canvas.server.ts @@ -35,14 +35,17 @@ export const loadProductionCanvasData = async ( // Check that build deployment domains are still active and verified // for examle: redeploy created few days later - const domains = deployment.domains.filter((domain) => - currentProjectDomains.some( - (projectDomain) => - projectDomain.domain.domain === domain && - projectDomain.domain.status === "ACTIVE" && - projectDomain.verified - ) - ); + const domains = + deployment.destination === "static" + ? [] + : deployment.domains.filter((domain) => + currentProjectDomains.some( + (projectDomain) => + projectDomain.domain.domain === domain && + projectDomain.domain.status === "ACTIVE" && + projectDomain.verified + ) + ); const page = findPageByIdOrPath("/", build.pages); diff --git a/apps/builder/app/shared/trpc/trpc-client.ts b/apps/builder/app/shared/trpc/trpc-client.ts index 27504f5d36..bc8ad6f92e 100644 --- a/apps/builder/app/shared/trpc/trpc-client.ts +++ b/apps/builder/app/shared/trpc/trpc-client.ts @@ -11,7 +11,7 @@ import { createRecursiveProxy } from "@trpc/server/shared"; import { useMemo, useState } from "react"; import { $authToken } from "../nano-states"; -const client = createTRPCProxyClient({ +export const nativeClient = createTRPCProxyClient({ links: [ httpBatchLink({ url: "/trpc", @@ -99,9 +99,9 @@ export const trpcClient: { // await new Promise((resolve) => setTimeout(resolve, 1000)); // } - const result = await (client as unknown as Client)[namespace][method][ - hook === "useMutation" ? "mutate" : "query" - ](input); + const result = await (nativeClient as unknown as Client)[namespace][ + method + ][hook === "useMutation" ? "mutate" : "query"](input); // Newer request has been made, ignore this one // In the future to support optimistic updates we can call onComplete with the requestId order diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json index 8537753492..b89dccd766 100644 --- a/apps/builder/tsconfig.json +++ b/apps/builder/tsconfig.json @@ -3,7 +3,7 @@ "include": ["app", "**/*.ts", "**/*.tsx", "../../@types/**/*.d.ts"], "compilerOptions": { - "types": ["@remix-run/node", "vite/client"], + "types": ["@remix-run/node", "vite/client", "react/canary"], "paths": { "~/*": ["app/*"] }, diff --git a/fixtures/ssg/.webstudio/data.json b/fixtures/ssg/.webstudio/data.json index ccea62eda4..f6dd4a99ba 100644 --- a/fixtures/ssg/.webstudio/data.json +++ b/fixtures/ssg/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "2b8a9d28-3a0e-48c9-9aa2-b26db7926a22", + "id": "a2e8de30-03d5-4514-a3a6-406b3266a3af", "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", - "version": 33, - "createdAt": "2024-07-26T13:42:42.816", - "updatedAt": "2024-07-26T13:42:42.816", + "version": 38, + "createdAt": "2024-07-29T12:50:07.515", + "updatedAt": "2024-07-29T12:50:07.515", "pages": { "meta": { "siteName": "", @@ -25,6 +25,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { @@ -252,6 +253,16 @@ "type": "asset", "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" } + ], + [ + "CAkmmL8-JAgokmeopoFXh", + { + "id": "CAkmmL8-JAgokmeopoFXh", + "instanceId": "2sIE8GxbKRBaav_zdhaZ1", + "name": "src", + "type": "string", + "value": "https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale" + } ] ], "dataSources": [ @@ -298,6 +309,10 @@ { "type": "id", "value": "uHB3Fjb7-NELG-bnH7bXB" + }, + { + "type": "id", + "value": "2sIE8GxbKRBaav_zdhaZ1" } ] } @@ -308,6 +323,7 @@ "type": "instance", "id": "MYDt0guk1-vzc7yzqyN6A", "component": "Heading", + "label": "xD", "children": [ { "type": "text", @@ -380,6 +396,15 @@ "component": "Image", "children": [] } + ], + [ + "2sIE8GxbKRBaav_zdhaZ1", + { + "type": "instance", + "id": "2sIE8GxbKRBaav_zdhaZ1", + "component": "Image", + "children": [] + } ] ], "deployment": { @@ -411,6 +436,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { diff --git a/fixtures/ssg/app/__generated__/$resources.sitemap.xml.ts b/fixtures/ssg/app/__generated__/$resources.sitemap.xml.ts index f8e2489bc3..9b3f4dfa6d 100644 --- a/fixtures/ssg/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/ssg/app/__generated__/$resources.sitemap.xml.ts @@ -1,6 +1,6 @@ export const sitemap = [ { path: "/", - lastModified: "2024-07-26", + lastModified: "2024-07-29", }, ]; diff --git a/fixtures/ssg/app/__generated__/_index.tsx b/fixtures/ssg/app/__generated__/_index.tsx index c10ae2e1fa..62ef9f959f 100644 --- a/fixtures/ssg/app/__generated__/_index.tsx +++ b/fixtures/ssg/app/__generated__/_index.tsx @@ -41,6 +41,10 @@ const Page = ({}: { system: any }) => { height={16} className="w-image c161qeci" /> + ); }; diff --git a/fixtures/ssg/app/constants.mjs b/fixtures/ssg/app/constants.mjs index 24fd396edf..8c020c0df1 100644 --- a/fixtures/ssg/app/constants.mjs +++ b/fixtures/ssg/app/constants.mjs @@ -9,5 +9,9 @@ export const imageBaseUrl = "/assets/"; * @type {import("@webstudio-is/image").ImageLoader} */ export const imageLoader = ({ src }) => { + if (URL.canParse(src)) { + return src; + } + return imageBaseUrl + src; }; diff --git a/fixtures/ssg/package.json b/fixtures/ssg/package.json index f4681947bb..d85531f695 100644 --- a/fixtures/ssg/package.json +++ b/fixtures/ssg/package.json @@ -26,6 +26,7 @@ "@types/react": "^18.2.70", "@types/react-dom": "^18.2.25", "@vitejs/plugin-react": "^4.3.1", + "prettier": "3.3.2", "typescript": "5.5.2", "vite": "^5.3.4" }, diff --git a/fixtures/webstudio-cloudflare-template/.webstudio/data.json b/fixtures/webstudio-cloudflare-template/.webstudio/data.json index ccea62eda4..f6dd4a99ba 100644 --- a/fixtures/webstudio-cloudflare-template/.webstudio/data.json +++ b/fixtures/webstudio-cloudflare-template/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "2b8a9d28-3a0e-48c9-9aa2-b26db7926a22", + "id": "a2e8de30-03d5-4514-a3a6-406b3266a3af", "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", - "version": 33, - "createdAt": "2024-07-26T13:42:42.816", - "updatedAt": "2024-07-26T13:42:42.816", + "version": 38, + "createdAt": "2024-07-29T12:50:07.515", + "updatedAt": "2024-07-29T12:50:07.515", "pages": { "meta": { "siteName": "", @@ -25,6 +25,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { @@ -252,6 +253,16 @@ "type": "asset", "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" } + ], + [ + "CAkmmL8-JAgokmeopoFXh", + { + "id": "CAkmmL8-JAgokmeopoFXh", + "instanceId": "2sIE8GxbKRBaav_zdhaZ1", + "name": "src", + "type": "string", + "value": "https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale" + } ] ], "dataSources": [ @@ -298,6 +309,10 @@ { "type": "id", "value": "uHB3Fjb7-NELG-bnH7bXB" + }, + { + "type": "id", + "value": "2sIE8GxbKRBaav_zdhaZ1" } ] } @@ -308,6 +323,7 @@ "type": "instance", "id": "MYDt0guk1-vzc7yzqyN6A", "component": "Heading", + "label": "xD", "children": [ { "type": "text", @@ -380,6 +396,15 @@ "component": "Image", "children": [] } + ], + [ + "2sIE8GxbKRBaav_zdhaZ1", + { + "type": "instance", + "id": "2sIE8GxbKRBaav_zdhaZ1", + "component": "Image", + "children": [] + } ] ], "deployment": { @@ -411,6 +436,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-cloudflare-template/app/__generated__/$resources.sitemap.xml.ts index f8e2489bc3..9b3f4dfa6d 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/$resources.sitemap.xml.ts @@ -1,6 +1,6 @@ export const sitemap = [ { path: "/", - lastModified: "2024-07-26", + lastModified: "2024-07-29", }, ]; diff --git a/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx b/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx index a6c2d25781..ac33ac2b85 100644 --- a/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx +++ b/fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx @@ -43,6 +43,10 @@ const Page = ({}: { system: any }) => { height={16} className="w-image c161qeci" /> + ); }; diff --git a/fixtures/webstudio-remix-netlify-edge-functions/.webstudio/data.json b/fixtures/webstudio-remix-netlify-edge-functions/.webstudio/data.json index ccea62eda4..f6dd4a99ba 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/.webstudio/data.json +++ b/fixtures/webstudio-remix-netlify-edge-functions/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "2b8a9d28-3a0e-48c9-9aa2-b26db7926a22", + "id": "a2e8de30-03d5-4514-a3a6-406b3266a3af", "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", - "version": 33, - "createdAt": "2024-07-26T13:42:42.816", - "updatedAt": "2024-07-26T13:42:42.816", + "version": 38, + "createdAt": "2024-07-29T12:50:07.515", + "updatedAt": "2024-07-29T12:50:07.515", "pages": { "meta": { "siteName": "", @@ -25,6 +25,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { @@ -252,6 +253,16 @@ "type": "asset", "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" } + ], + [ + "CAkmmL8-JAgokmeopoFXh", + { + "id": "CAkmmL8-JAgokmeopoFXh", + "instanceId": "2sIE8GxbKRBaav_zdhaZ1", + "name": "src", + "type": "string", + "value": "https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale" + } ] ], "dataSources": [ @@ -298,6 +309,10 @@ { "type": "id", "value": "uHB3Fjb7-NELG-bnH7bXB" + }, + { + "type": "id", + "value": "2sIE8GxbKRBaav_zdhaZ1" } ] } @@ -308,6 +323,7 @@ "type": "instance", "id": "MYDt0guk1-vzc7yzqyN6A", "component": "Heading", + "label": "xD", "children": [ { "type": "text", @@ -380,6 +396,15 @@ "component": "Image", "children": [] } + ], + [ + "2sIE8GxbKRBaav_zdhaZ1", + { + "type": "instance", + "id": "2sIE8GxbKRBaav_zdhaZ1", + "component": "Image", + "children": [] + } ] ], "deployment": { @@ -411,6 +436,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { diff --git a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/$resources.sitemap.xml.ts index f8e2489bc3..9b3f4dfa6d 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/$resources.sitemap.xml.ts @@ -1,6 +1,6 @@ export const sitemap = [ { path: "/", - lastModified: "2024-07-26", + lastModified: "2024-07-29", }, ]; diff --git a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx index a6c2d25781..ac33ac2b85 100644 --- a/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx +++ b/fixtures/webstudio-remix-netlify-edge-functions/app/__generated__/_index.tsx @@ -43,6 +43,10 @@ const Page = ({}: { system: any }) => { height={16} className="w-image c161qeci" /> + ); }; diff --git a/fixtures/webstudio-remix-netlify-functions/.webstudio/data.json b/fixtures/webstudio-remix-netlify-functions/.webstudio/data.json index ccea62eda4..f6dd4a99ba 100644 --- a/fixtures/webstudio-remix-netlify-functions/.webstudio/data.json +++ b/fixtures/webstudio-remix-netlify-functions/.webstudio/data.json @@ -1,10 +1,10 @@ { "build": { - "id": "2b8a9d28-3a0e-48c9-9aa2-b26db7926a22", + "id": "a2e8de30-03d5-4514-a3a6-406b3266a3af", "projectId": "d845c167-ea07-4875-b08d-83e97c09dcce", - "version": 33, - "createdAt": "2024-07-26T13:42:42.816", - "updatedAt": "2024-07-26T13:42:42.816", + "version": 38, + "createdAt": "2024-07-29T12:50:07.515", + "updatedAt": "2024-07-29T12:50:07.515", "pages": { "meta": { "siteName": "", @@ -25,6 +25,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { @@ -252,6 +253,16 @@ "type": "asset", "value": "1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee" } + ], + [ + "CAkmmL8-JAgokmeopoFXh", + { + "id": "CAkmmL8-JAgokmeopoFXh", + "instanceId": "2sIE8GxbKRBaav_zdhaZ1", + "name": "src", + "type": "string", + "value": "https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale" + } ] ], "dataSources": [ @@ -298,6 +309,10 @@ { "type": "id", "value": "uHB3Fjb7-NELG-bnH7bXB" + }, + { + "type": "id", + "value": "2sIE8GxbKRBaav_zdhaZ1" } ] } @@ -308,6 +323,7 @@ "type": "instance", "id": "MYDt0guk1-vzc7yzqyN6A", "component": "Heading", + "label": "xD", "children": [ { "type": "text", @@ -380,6 +396,15 @@ "component": "Image", "children": [] } + ], + [ + "2sIE8GxbKRBaav_zdhaZ1", + { + "type": "instance", + "id": "2sIE8GxbKRBaav_zdhaZ1", + "component": "Image", + "children": [] + } ] ], "deployment": { @@ -411,6 +436,7 @@ "id": "WPPAbLFyJD_02vhjRd8P4", "name": "Another page", "title": "\"Another page\"", + "history": ["/another-page"], "rootInstanceId": "n_VBMr7klpx25buS0NV7R", "systemDataSourceId": "tdXe9gFf83hSo9BLWU6xl", "meta": { diff --git a/fixtures/webstudio-remix-netlify-functions/app/__generated__/$resources.sitemap.xml.ts b/fixtures/webstudio-remix-netlify-functions/app/__generated__/$resources.sitemap.xml.ts index f8e2489bc3..9b3f4dfa6d 100644 --- a/fixtures/webstudio-remix-netlify-functions/app/__generated__/$resources.sitemap.xml.ts +++ b/fixtures/webstudio-remix-netlify-functions/app/__generated__/$resources.sitemap.xml.ts @@ -1,6 +1,6 @@ export const sitemap = [ { path: "/", - lastModified: "2024-07-26", + lastModified: "2024-07-29", }, ]; diff --git a/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx b/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx index a6c2d25781..ac33ac2b85 100644 --- a/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx +++ b/fixtures/webstudio-remix-netlify-functions/app/__generated__/_index.tsx @@ -43,6 +43,10 @@ const Page = ({}: { system: any }) => { height={16} className="w-image c161qeci" /> + ); }; diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index 85ff5e8383..db2f801438 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -296,11 +296,6 @@ export const prebuild = async (options: { ); } - const domain = siteData.build.deployment?.projectDomain; - if (domain === undefined) { - throw new Error(`Project domain is missing from the project data`); - } - // collect all possible component metas const metas = new Map(); const componentSources = new Map(); @@ -448,6 +443,15 @@ export const prebuild = async (options: { const assetsToDownload: Promise[] = []; const appDomain = options.preview ? "wstd.work" : "wstd.io"; + + const domain = + siteData.build.deployment?.destination === "static" + ? siteData.build.deployment?.assetsDomain + : siteData.build.deployment?.projectDomain; + if (domain === undefined) { + throw new Error(`Project domain is missing from the project data`); + } + const assetBuildUrl = `https://${domain}.${appDomain}/cgi/asset/`; const imageLoader = createImageLoader({ diff --git a/packages/cli/templates/ssg/app/constants.mjs b/packages/cli/templates/ssg/app/constants.mjs index 24fd396edf..8c020c0df1 100644 --- a/packages/cli/templates/ssg/app/constants.mjs +++ b/packages/cli/templates/ssg/app/constants.mjs @@ -9,5 +9,9 @@ export const imageBaseUrl = "/assets/"; * @type {import("@webstudio-is/image").ImageLoader} */ export const imageLoader = ({ src }) => { + if (URL.canParse(src)) { + return src; + } + return imageBaseUrl + src; }; diff --git a/packages/domain/package.json b/packages/domain/package.json index 13693652ec..4af5fe9037 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -13,6 +13,7 @@ "@webstudio-is/project": "workspace:*", "@webstudio-is/project-build": "workspace:*", "@webstudio-is/trpc-interface": "workspace:*", + "nanoid": "^5.0.1", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/domain/src/trpc/domain.ts b/packages/domain/src/trpc/domain.ts index 14d58349a2..2db36992a9 100644 --- a/packages/domain/src/trpc/domain.ts +++ b/packages/domain/src/trpc/domain.ts @@ -4,6 +4,7 @@ import { db as projectDb } from "@webstudio-is/project/index.server"; import { db } from "../db"; import { createProductionBuild } from "@webstudio-is/project-build/index.server"; import { router, procedure } from "@webstudio-is/trpc-interface/index.server"; +import { nanoid } from "nanoid"; export const domainRouter = router({ getEntriToken: procedure.query(async ({ ctx }) => { @@ -41,35 +42,65 @@ export const domainRouter = router({ } }), publish: procedure - .input(z.object({ projectId: z.string(), domains: z.array(z.string()) })) + .input( + z.discriminatedUnion("destination", [ + z.object({ + projectId: z.string(), + domains: z.array(z.string()), + destination: z.literal("saas"), + }), + z.object({ + projectId: z.string(), + destination: z.literal("static"), + }), + ]) + ) .mutation(async ({ input, ctx }) => { try { const project = await projectDb.project.loadById(input.projectId, ctx); + const name = `${project.id}-${nanoid()}.zip`; + const build = await createProductionBuild( { projectId: input.projectId, - deployment: { - domains: input.domains, - projectDomain: project.domain, - }, + deployment: + input.destination === "saas" + ? { + destination: input.destination, + domains: input.domains, + projectDomain: project.domain, + } + : { + destination: input.destination, + name, + assetsDomain: project.domain, + templates: ["ssg"], + }, }, ctx ); const { deploymentTrpc, env } = ctx.deployment; - const result = deploymentTrpc.publish.mutate({ + console.info("input.destination", input.destination); + + const result = await deploymentTrpc.publish.mutate({ // used to load build data from the builder see routes/rest.build.$buildId.ts builderOrigin: env.BUILDER_ORIGIN, githubSha: env.GITHUB_SHA, buildId: build.id, // preview support branchName: env.GITHUB_REF_NAME, + destination: input.destination, // action log helper (not used for deployment, but for action logs readablity) projectDomainName: project.domain, }); + if (input.destination === "static" && result.success) { + return { success: true as const, name }; + } + return result; } catch (error) { return { diff --git a/packages/feature-flags/src/flags.ts b/packages/feature-flags/src/flags.ts index f528adf748..d72384dc2b 100644 --- a/packages/feature-flags/src/flags.ts +++ b/packages/feature-flags/src/flags.ts @@ -8,3 +8,4 @@ export const cssVars = false; export const filters = false; export const xmlElement = false; export const transforms = false; +export const staticExport = false; diff --git a/packages/postgrest/src/__generated__/db-types.ts b/packages/postgrest/src/__generated__/db-types.ts index 06a6086f22..313b97c45e 100644 --- a/packages/postgrest/src/__generated__/db-types.ts +++ b/packages/postgrest/src/__generated__/db-types.ts @@ -679,6 +679,30 @@ export type Database = { }, ]; }; + LatestStaticBuildPerProject: { + Row: { + buildId: string | null; + projectId: string | null; + publishStatus: Database["public"]["Enums"]["PublishStatus"] | null; + updatedAt: string | null; + }; + Relationships: [ + { + foreignKeyName: "Build_projectId_fkey"; + columns: ["projectId"]; + isOneToOne: false; + referencedRelation: "Project"; + referencedColumns: ["id"]; + }, + { + foreignKeyName: "Build_projectId_fkey"; + columns: ["projectId"]; + isOneToOne: false; + referencedRelation: "DashboardProject"; + referencedColumns: ["id"]; + }, + ]; + }; ProjectWithDomain: { Row: { cname: string | null; diff --git a/packages/prisma-client/prisma/migrations/20240723144019_latest-build/migration.sql b/packages/prisma-client/prisma/migrations/20240723144019_latest-build/migration.sql new file mode 100644 index 0000000000..d2632b8bc9 --- /dev/null +++ b/packages/prisma-client/prisma/migrations/20240723144019_latest-build/migration.sql @@ -0,0 +1,86 @@ +CREATE OR REPLACE VIEW "LatestBuildPerProject" AS +WITH lb AS ( + SELECT DISTINCT ON ("projectId") + bld.id AS "buildId", + bld."projectId", + bld."updatedAt" + FROM + "Build" bld + WHERE + bld.deployment IS NOT NULL + AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL + ORDER BY + bld."projectId", + bld."createdAt" DESC, + "buildId" +) +SELECT DISTINCT ON ("projectId", "domain") + bld.id AS "buildId", + bld."projectId", + deployment::jsonb ->> 'projectDomain' AS domain, + coalesce(bld."updatedAt" = lb."updatedAt", FALSE) AS "isLatestBuild", + bld."updatedAt", + bld."publishStatus" +FROM + "Build" bld, + lb +WHERE + bld.deployment IS NOT NULL + AND lb."projectId" = bld."projectId" + AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL +ORDER BY + bld."projectId", + "domain", + bld."createdAt" DESC, + "buildId"; + + +CREATE OR REPLACE VIEW "LatestBuildPerProjectDomain" AS +WITH lbd AS ( + SELECT DISTINCT ON ("projectId", + "domain") + jsonb_array_elements_text(deployment::jsonb -> 'domains') AS "domain", + bld.id AS "buildId", + bld."projectId", + bld."updatedAt", + bld."publishStatus" + FROM + "Build" bld + WHERE + bld.deployment IS NOT NULL + ORDER BY + bld."projectId", + "domain", + bld."createdAt" DESC, + "buildId" +), +lb AS ( + SELECT DISTINCT ON ("projectId") + bld.id AS "buildId", + bld."projectId", + bld."updatedAt", + bld."publishStatus" + FROM + "Build" bld + WHERE + bld.deployment IS NOT NULL + AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL + ORDER BY + bld."projectId", + bld."createdAt" DESC, + "buildId" +) +SELECT + d.id AS "domainId", + lbd."projectId", + lbd."buildId", + coalesce(lbd."updatedAt" = lb."updatedAt", FALSE) AS "isLatestBuild", + lbd."publishStatus", + lbd."updatedAt" +FROM + lbd, + lb, + "Domain" d +WHERE + lbd.domain = d.domain + AND lb."projectId" = lbd."projectId"; \ No newline at end of file diff --git a/packages/prisma-client/prisma/migrations/20240723150501_latest-static-build/migration.sql b/packages/prisma-client/prisma/migrations/20240723150501_latest-static-build/migration.sql new file mode 100644 index 0000000000..4efae32b9e --- /dev/null +++ b/packages/prisma-client/prisma/migrations/20240723150501_latest-static-build/migration.sql @@ -0,0 +1,17 @@ + +CREATE OR REPLACE VIEW "LatestStaticBuildPerProject" AS +SELECT DISTINCT ON ("projectId") + bld.id AS "buildId", + bld."projectId", + bld."updatedAt", + bld."publishStatus" +FROM + "Build" bld +WHERE + bld.deployment IS NOT NULL + AND bld.deployment::jsonb ->> 'destination'::text = 'static' +ORDER BY + bld."projectId", + bld."createdAt" DESC, + "buildId"; + diff --git a/packages/prisma-client/prisma/schema.prisma b/packages/prisma-client/prisma/schema.prisma index 31a100c00e..df8caec175 100644 --- a/packages/prisma-client/prisma/schema.prisma +++ b/packages/prisma-client/prisma/schema.prisma @@ -139,21 +139,22 @@ view UserProduct { } model Project { - id String @id @default(uuid()) - createdAt DateTime @default(now()) + id String @id @default(uuid()) + createdAt DateTime @default(now()) title String - domain String @unique - user User? @relation(fields: [userId], references: [id]) + domain String @unique + user User? @relation(fields: [userId], references: [id]) userId String? build Build[] - isDeleted Boolean @default(false) + isDeleted Boolean @default(false) files File[] projectDomain ProjectDomain[] latestBuild LatestBuildPerProject? authorizationToken AuthorizationToken[] - previewImageAsset Asset? @relation(fields: [previewImageAssetId, id], references: [id, projectId]) + previewImageAsset Asset? @relation(fields: [previewImageAssetId, id], references: [id, projectId]) previewImageAssetId String? - marketplaceApprovalStatus MarketplaceApprovalStatus @default(UNLISTED) + marketplaceApprovalStatus MarketplaceApprovalStatus @default(UNLISTED) + latestStaticBuild LatestStaticBuildPerProject? @@unique([id, isDeleted]) @@unique([domain, isDeleted]) @@ -199,7 +200,8 @@ model Build { deployment String? publishStatus PublishStatus @default(PENDING) - latestBuildPerProject LatestBuildPerProject? + latestBuildPerProject LatestBuildPerProject? + latestStaticBuildPerProject LatestStaticBuildPerProject? @@id([id, projectId]) // ApprovedMarketplaceProduct view performance index @@ -322,6 +324,20 @@ view LatestBuildPerProject { @@unique([buildId, projectId]) } +view LatestStaticBuildPerProject { + buildId String + build Build @relation(fields: [buildId, projectId], references: [id, projectId]) + + projectId String + project Project @relation(fields: [projectId], references: [id]) + + publishStatus PublishStatus + updatedAt DateTime + + @@id([projectId]) + @@unique([buildId, projectId]) +} + // Dashboard view DashboardProject { id String @id @default(uuid()) diff --git a/packages/project/src/db/project.ts b/packages/project/src/db/project.ts index 9fe400699e..348c7d9b20 100644 --- a/packages/project/src/db/project.ts +++ b/packages/project/src/db/project.ts @@ -25,6 +25,7 @@ export const loadById = async ( where: { id_isDeleted: { id: projectId, isDeleted: false } }, include: { latestBuild: true, + latestStaticBuild: true, previewImageAsset: true, }, }); diff --git a/packages/project/src/shared/schema.ts b/packages/project/src/shared/schema.ts index 860ef37eaf..7ddbb9b856 100644 --- a/packages/project/src/shared/schema.ts +++ b/packages/project/src/shared/schema.ts @@ -43,6 +43,15 @@ export const Project = z.object({ }) .nullable() ), + latestStaticBuild: z.optional( + z + .object({ + buildId: z.string(), + publishStatus: z.enum(["PENDING", "PUBLISHED", "FAILED"]), + updatedAt: z.date().transform((date) => date.toISOString()), + }) + .nullable() + ), marketplaceApprovalStatus: MarketplaceApprovalStatus, }); diff --git a/packages/sdk-components-react-remix/src/remix-form.tsx b/packages/sdk-components-react-remix/src/remix-form.tsx index 454deaf7bd..2200dbd2c8 100644 --- a/packages/sdk-components-react-remix/src/remix-form.tsx +++ b/packages/sdk-components-react-remix/src/remix-form.tsx @@ -17,19 +17,19 @@ export const RemixForm = forwardRef< // resulting in our UI displaying a list that encompasses both variants. method?: Lowercase>; } ->((props, ref) => { +>(({ action, ...props }, ref) => { const { renderer } = useContext(ReactSdkContext); // remix casts action to relative url if ( - props.action === undefined || - props.action === "" || - props.action?.startsWith("/") + action === undefined || + action === "" || + (typeof action === "string" && action?.startsWith("/")) ) { // remix forms specifies own action instead of provided one // which makes it hard to handle intercepted submit events // render remix form only in published sites if (renderer !== "canvas" && renderer !== "preview") { - return
; + return ; } } return ; diff --git a/packages/sdk-components-react-remix/src/webhook-form.tsx b/packages/sdk-components-react-remix/src/webhook-form.tsx index 71b1fd1e03..765b0b8311 100644 --- a/packages/sdk-components-react-remix/src/webhook-form.tsx +++ b/packages/sdk-components-react-remix/src/webhook-form.tsx @@ -120,7 +120,11 @@ export const WebhookForm = forwardRef< ref={ref} onSubmit={handleSubmitAndAddHiddenJsField} > - + {children} ); diff --git a/packages/sdk-components-react-remix/tsconfig.json b/packages/sdk-components-react-remix/tsconfig.json index d2e5040f8f..fb43dde600 100644 --- a/packages/sdk-components-react-remix/tsconfig.json +++ b/packages/sdk-components-react-remix/tsconfig.json @@ -1,3 +1,6 @@ { - "extends": "@webstudio-is/tsconfig/base.json" + "extends": "@webstudio-is/tsconfig/base.json", + "compilerOptions": { + "types": ["react/canary", "@types/node"] + } } diff --git a/packages/sdk-components-react/src/image.tsx b/packages/sdk-components-react/src/image.tsx index eeb27f20ab..5da5d4e728 100644 --- a/packages/sdk-components-react/src/image.tsx +++ b/packages/sdk-components-react/src/image.tsx @@ -65,6 +65,15 @@ export const Image = forwardRef< assetName = src.slice(assetBaseUrl.length); } + console.info( + "assetName", + assetName, + "src", + src, + "assetBaseUrl", + assetBaseUrl + ); + return ( ; diff --git a/packages/trpc-interface/src/shared/deployment.ts b/packages/trpc-interface/src/shared/deployment.ts index f4f5958c1a..6e24ad5ce7 100644 --- a/packages/trpc-interface/src/shared/deployment.ts +++ b/packages/trpc-interface/src/shared/deployment.ts @@ -7,6 +7,7 @@ const PublishInput = z.object({ builderOrigin: z.string(), githubSha: z.string().optional(), + destination: z.enum(["saas", "static"]), // preview support branchName: z.string(), // action log helper (not used for deployment, but for action logs readablity) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74edd139f9..a6bbccf38f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -492,6 +492,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.1 version: 4.3.1(vite@5.3.4(@types/node@20.12.7)) + prettier: + specifier: 3.3.2 + version: 3.3.2 typescript: specifier: 5.5.2 version: 5.5.2 @@ -1399,6 +1402,9 @@ importers: '@webstudio-is/trpc-interface': specifier: workspace:* version: link:../trpc-interface + nanoid: + specifier: ^5.0.1 + version: 5.0.1 zod: specifier: ^3.22.4 version: 3.22.4