mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-06 12:04:00 +09:00
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 <img width="371" alt="image" src="https://github.com/user-attachments/assets/fffc2062-b7b0-4ae9-b8dd-d6acde6bd6fb"> - [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
This commit is contained in:
1 parent
0b72f64b80
commit
2096789e91
1 file changed
+110
-48
@@ -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<NonNullable<Domain["latestBuid"]>, "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<ReturnType<typeof fetchProjectDataStatus>>;
|
||||
type StaticProjectStatus =
|
||||
| Awaited<ReturnType<typeof fetchProjectDataStatus>>
|
||||
| { 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<StaticProjectStatus>({
|
||||
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 (
|
||||
<Flex
|
||||
css={{
|
||||
paddingBottom: theme.spacing[5],
|
||||
paddingTop: theme.spacing[5],
|
||||
}}
|
||||
gap={2}
|
||||
shrink={false}
|
||||
direction={"column"}
|
||||
>
|
||||
<Flex gap={2} shrink={false} direction={"column"}>
|
||||
{optimisticPendingStatus.status === "FAILED" && (
|
||||
<Text color="destructive">{optimisticPendingStatus.statusText}</Text>
|
||||
)}
|
||||
@@ -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],
|
||||
}}
|
||||
>
|
||||
<Grid columns={1} gap={2}>
|
||||
<div />
|
||||
<Grid columns={2} gap={2} align={"center"}>
|
||||
<Text color="main" variant="labelsTitleCase">
|
||||
Destination
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
fullWidth
|
||||
value={deployTarget}
|
||||
options={Object.keys(deployTargets)}
|
||||
getLabel={(value) => humanizeString(value)}
|
||||
onChange={(value) => {
|
||||
if (isDeployTargets(value)) {
|
||||
setDeployTarget(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid columns={1} gap={1}>
|
||||
{isFeatureEnabled("staticExport") && (
|
||||
<>
|
||||
<PublishStatic projectId={props.projectId} />
|
||||
<PublishStatic
|
||||
projectId={props.projectId}
|
||||
templates={deployTargets[deployTarget].ssgTemplates}
|
||||
/>
|
||||
<div />
|
||||
<div />
|
||||
<Grid
|
||||
gap={2}
|
||||
align={"center"}
|
||||
@@ -726,7 +800,7 @@ const ExportContent = (props: { projectId: Project["id"] }) => {
|
||||
}}
|
||||
>
|
||||
<Separator css={{ alignSelf: "unset" }} />
|
||||
<Text color="main">OR</Text>
|
||||
<Text color="main">CLI</Text>
|
||||
<Separator css={{ alignSelf: "unset" }} />
|
||||
</Grid>
|
||||
</>
|
||||
@@ -811,18 +885,6 @@ const ExportContent = (props: { projectId: Project["id"] }) => {
|
||||
</Text>
|
||||
</Grid>
|
||||
|
||||
<Select
|
||||
fullWidth
|
||||
value={deployTarget}
|
||||
options={Object.keys(deployTargets)}
|
||||
getLabel={(value) => humanizeString(value)}
|
||||
onChange={(value) => {
|
||||
if (isDeployTargets(value)) {
|
||||
setDeployTarget(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex gap={2} align="end">
|
||||
<TextArea
|
||||
css={{ flex: 1 }}
|
||||
@@ -854,7 +916,7 @@ const ExportContent = (props: { projectId: Project["id"] }) => {
|
||||
<Link
|
||||
variant="inherit"
|
||||
color="inherit"
|
||||
href="https://github.com/webstudio-is/webstudio/tree/main/packages/cli"
|
||||
href="https://wstd.us/cli"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user