From 2096789e91c596138b8defdcd1adaa32a0a7565b Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Thu, 1 Aug 2024 16:12:36 +0300 Subject: [PATCH] experimental: Download ssg (#3846) ## Description Can be broken as saas has other non rebased staging PRs Additionally: - [x] - Download will start even if dialog is closed ## Steps for reproduction image - [x] - Check that vanilla images has usual path - [x] - Check that vercel has vercel.json at root and specific images paths - [x] - Check that netlify has specific images paths ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file --- .../app/builder/features/topbar/publish.tsx | 158 ++++++++++++------ 1 file changed, 110 insertions(+), 48 deletions(-) diff --git a/apps/builder/app/builder/features/topbar/publish.tsx b/apps/builder/app/builder/features/topbar/publish.tsx index bdf8fdfc77..ef2e3b060b 100644 --- a/apps/builder/app/builder/features/topbar/publish.tsx +++ b/apps/builder/app/builder/features/topbar/publish.tsx @@ -5,7 +5,6 @@ import { useState, useOptimistic, useTransition, - useRef, } from "react"; import { useStore } from "@nanostores/react"; import { @@ -60,6 +59,8 @@ import { AddDomain } from "./add-domain"; import { humanizeString } from "~/shared/string-utils"; import { trpcClient, nativeClient } from "~/shared/trpc/trpc-client"; import { isFeatureEnabled } from "@webstudio-is/feature-flags"; +import type { Templates } from "@webstudio-is/sdk"; +import { formatDistance } from "date-fns/formatDistance"; type ProjectData = | { @@ -351,6 +352,37 @@ const Publish = ({ ); }; +const getStaticPublishStatusAndText = ({ + updatedAt, + publishStatus, +}: Pick, "updatedAt" | "publishStatus">) => { + let status = publishStatus; + + const delta = Date.now() - new Date(updatedAt).getTime(); + // Assume build failed after 3 minutes + + if (publishStatus === "PENDING" && delta > PENDING_TIMEOUT) { + status = "FAILED"; + } + + const textStart = + status === "PUBLISHED" + ? "Downloaded" + : status === "FAILED" + ? "Download failed" + : "Download started"; + + const statusText = `${textStart} ${formatDistance( + new Date(updatedAt), + new Date(), + { + addSuffix: true, + } + )}`; + + return { statusText, status }; +}; + const fetchProjectDataStatus = async (projectId: Project["id"]) => { const projectData = await nativeClient.domain.project.query({ projectId, @@ -362,37 +394,51 @@ const fetchProjectDataStatus = async (projectId: Project["id"]) => { if (projectData.project.latestStaticBuild == null) { return { - status: "INITIAL" as const, + status: "LOADED" as const, statusText: "Not published", }; } - const { status, statusText } = getPublishStatusAndText( + const { status, statusText } = getStaticPublishStatusAndText( projectData.project.latestStaticBuild ); return { status, statusText }; }; -type StaticProjectStatus = Awaited>; +type StaticProjectStatus = + | Awaited> + | { status: "INITIAL"; statusText: string }; -const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { +const PublishStatic = ({ + projectId, + templates, +}: { + projectId: Project["id"]; + templates: readonly Templates[]; +}) => { const [_, startTransition] = useTransition(); - const isCanceledRef = useRef(false); - useEffect(() => { - isCanceledRef.current = false; - return () => { - isCanceledRef.current = true; - }; - }, []); - const [projectDataStatus, setProjectDataStatus] = useState({ status: "INITIAL", - statusText: "Not published", + statusText: "Loading", }); + useEffect(() => { + startTransition(async () => { + try { + const projectDataStatus = await fetchProjectDataStatus(projectId); + setProjectDataStatus(projectDataStatus); + } catch (e) { + setProjectDataStatus({ + status: "FAILED", + statusText: e instanceof Error ? e.message : "Unknown error", + }); + } + }); + }, [projectId]); + const [optimisticPendingStatus, setOptimisticPendingStatus] = useOptimistic( projectDataStatus, (_currentState, optimisticValue: StaticProjectStatus) => { @@ -400,18 +446,12 @@ const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { } ); - const isPublishInProgress = optimisticPendingStatus.status === "PENDING"; + const isPublishInProgress = + optimisticPendingStatus.status === "PENDING" || + optimisticPendingStatus.status === "INITIAL"; return ( - + {optimisticPendingStatus.status === "FAILED" && ( {optimisticPendingStatus.statusText} )} @@ -433,7 +473,7 @@ const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { const result = await nativeClient.domain.publish.mutate({ projectId, destination: "static", - templates: ["ssg"], + templates: [...templates], }); if (result.success === false) { @@ -458,16 +498,8 @@ const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { for (let i = 0; i !== repeat; i++) { await new Promise((resolve) => setTimeout(resolve, timeout)); - if (isCanceledRef.current) { - break; - } - projectDataStatus = await fetchProjectDataStatus(projectId); - if (isCanceledRef.current) { - break; - } - if (projectDataStatus.status !== "PENDING") { break; } @@ -478,6 +510,12 @@ const PublishStatic = ({ projectId }: { projectId: Project["id"] }) => { } setProjectDataStatus(projectDataStatus); + + if (projectDataStatus.status === "FAILED") { + // Report if Export failed + toast.error(projectDataStatus.statusText); + } + if (projectDataStatus.status === "PUBLISHED") { window.location.href = `/cgi/static/ssg/${name}`; } @@ -682,9 +720,18 @@ const Content = (props: { }; const deployTargets = { + vanilla: { + command: ` + npm install + npm run dev + `, + docs: "https://remix.run/", + ssgTemplates: ["ssg"], + }, vercel: { command: "npx vercel@latest", docs: "https://vercel.com/docs/cli", + ssgTemplates: ["ssg-vercel"], }, netlify: { command: ` @@ -693,6 +740,7 @@ npx netlify-cli sites:create npx netlify-cli build npx netlify-cli deploy`, docs: "https://docs.netlify.com/cli/get-started/", + ssgTemplates: ["ssg-netlify"], }, } as const; @@ -714,10 +762,36 @@ const ExportContent = (props: { projectId: Project["id"] }) => { marginTop: theme.spacing[5], }} > + +
+ + + Destination + + + humanizeString(value)} - onChange={(value) => { - if (isDeployTargets(value)) { - setDeployTarget(value); - } - }} - /> -