From 091bd3d9a34f95d5a497ab09baca006c19b21de4 Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Fri, 26 Jan 2024 10:38:53 +0300 Subject: [PATCH] feat: Allow change route template in cli custom templates (#2827) Allow change route template, so various tests in saas become easier. Also now each template can have own route template. --- .../__generated__/[script-test]._index.tsx | 4 +- .../app/routes/[script-test]._index.tsx | 132 ------------- .../app/routes/_index.tsx | 132 ------------- .../__templates__/route-template.tsx | 177 ++++++++++++++++++ .../{index.server.tsx => _index.server.tsx} | 0 .../__generated__/{index.tsx => _index.tsx} | 2 +- packages/cli/src/prebuild.ts | 49 +++-- .../__templates__}/route-template.tsx | 4 +- 8 files changed, 212 insertions(+), 288 deletions(-) create mode 100644 fixtures/webstudio-custom-template/custom-template/__templates__/route-template.tsx rename packages/cli/__generated__/{index.server.tsx => _index.server.tsx} (100%) rename packages/cli/__generated__/{index.tsx => _index.tsx} (92%) rename packages/cli/templates/{ => defaults/__templates__}/route-template.tsx (98%) diff --git a/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx b/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx index ff55483bb6..cb28e28bbe 100644 --- a/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx +++ b/fixtures/webstudio-custom-template/app/__generated__/[script-test]._index.tsx @@ -50,8 +50,7 @@ export const user: { email: string | null } | undefined = { }; export const projectId = "0d856812-61d8-4014-a20a-82e01c0eb8ee"; -type Params = Record; -const Page = (_props: { params: Params }) => { +const Page = ({ params: PageParams }: { params: any }) => { return ( { export { Page }; +type Params = Record; export const getRemixParams = ({ ...params }: Params): Params => { return params; }; diff --git a/fixtures/webstudio-custom-template/app/routes/[script-test]._index.tsx b/fixtures/webstudio-custom-template/app/routes/[script-test]._index.tsx index 96541e80af..941d67b564 100644 --- a/fixtures/webstudio-custom-template/app/routes/[script-test]._index.tsx +++ b/fixtures/webstudio-custom-template/app/routes/[script-test]._index.tsx @@ -67,91 +67,6 @@ export const headers = () => { }; }; -export const meta: V2_ServerRuntimeMetaFunction = ({ data }) => { - const { page, project } = pageData; - - const metas: ReturnType = []; - - if (data?.url) { - metas.push({ - property: "og:url", - content: data.url, - }); - } - - if (page.title) { - metas.push({ title: page.title }); - - metas.push({ - property: "og:title", - content: page.title, - }); - } - - metas.push({ property: "og:type", content: "website" }); - - const origin = `https://${data?.host}`; - - if (project?.siteName) { - metas.push({ - property: "og:site_name", - content: project.siteName, - }); - metas.push({ - "script:ld+json": { - "@context": "https://schema.org", - "@type": "WebSite", - name: project.siteName, - url: origin, - }, - }); - } - - if (page.meta.excludePageFromSearch || data?.excludeFromSearch) { - metas.push({ - name: "robots", - content: "noindex, nofollow", - }); - } - - if (page.meta.description) { - metas.push({ - name: "description", - content: page.meta.description, - }); - metas.push({ - property: "og:description", - content: page.meta.description, - }); - } - - if (page.meta.socialImageAssetId) { - const imageAsset = imageAssets.find( - (asset) => asset.id === page.meta.socialImageAssetId - ); - - if (imageAsset) { - metas.push({ - property: "og:image", - content: `https://${data?.host}${imageLoader({ - src: imageAsset.name, - // Do not transform social image (not enough information do we need to do this) - format: "raw", - })}`, - }); - } - } - - for (const customMeta of page.meta.custom ?? []) { - if (customMeta.property.trim().length === 0) { - continue; - } - metas.push(customMeta); - } - - return metas; -}; - export const links: LinksFunction = () => { const result: LinkDescriptor[] = []; @@ -159,53 +74,6 @@ export const links: LinksFunction = () => { rel: "stylesheet", href: css, }); - - const { project } = pageData; - - if (project?.faviconAssetId) { - const imageAsset = imageAssets.find( - (asset) => asset.id === project.faviconAssetId - ); - - if (imageAsset) { - result.push({ - rel: "icon", - href: imageLoader({ - src: imageAsset.name, - width: 128, - quality: 100, - format: "auto", - }), - type: undefined, - }); - } - } else { - result.push({ - rel: "icon", - href: "/favicon.ico", - type: "image/x-icon", - }); - - result.push({ - rel: "shortcut icon", - href: "/favicon.ico", - type: "image/x-icon", - }); - } - - for (const asset of fontAssets) { - if (asset.type === "font") { - result.push({ - rel: "preload", - href: assetBaseUrl + asset.name, - as: "font", - crossOrigin: "anonymous", - // @todo add mimeType - // type: asset.mimeType, - }); - } - } - return result; }; diff --git a/fixtures/webstudio-custom-template/app/routes/_index.tsx b/fixtures/webstudio-custom-template/app/routes/_index.tsx index eb83492a62..0b67cb29cf 100644 --- a/fixtures/webstudio-custom-template/app/routes/_index.tsx +++ b/fixtures/webstudio-custom-template/app/routes/_index.tsx @@ -67,91 +67,6 @@ export const headers = () => { }; }; -export const meta: V2_ServerRuntimeMetaFunction = ({ data }) => { - const { page, project } = pageData; - - const metas: ReturnType = []; - - if (data?.url) { - metas.push({ - property: "og:url", - content: data.url, - }); - } - - if (page.title) { - metas.push({ title: page.title }); - - metas.push({ - property: "og:title", - content: page.title, - }); - } - - metas.push({ property: "og:type", content: "website" }); - - const origin = `https://${data?.host}`; - - if (project?.siteName) { - metas.push({ - property: "og:site_name", - content: project.siteName, - }); - metas.push({ - "script:ld+json": { - "@context": "https://schema.org", - "@type": "WebSite", - name: project.siteName, - url: origin, - }, - }); - } - - if (page.meta.excludePageFromSearch || data?.excludeFromSearch) { - metas.push({ - name: "robots", - content: "noindex, nofollow", - }); - } - - if (page.meta.description) { - metas.push({ - name: "description", - content: page.meta.description, - }); - metas.push({ - property: "og:description", - content: page.meta.description, - }); - } - - if (page.meta.socialImageAssetId) { - const imageAsset = imageAssets.find( - (asset) => asset.id === page.meta.socialImageAssetId - ); - - if (imageAsset) { - metas.push({ - property: "og:image", - content: `https://${data?.host}${imageLoader({ - src: imageAsset.name, - // Do not transform social image (not enough information do we need to do this) - format: "raw", - })}`, - }); - } - } - - for (const customMeta of page.meta.custom ?? []) { - if (customMeta.property.trim().length === 0) { - continue; - } - metas.push(customMeta); - } - - return metas; -}; - export const links: LinksFunction = () => { const result: LinkDescriptor[] = []; @@ -159,53 +74,6 @@ export const links: LinksFunction = () => { rel: "stylesheet", href: css, }); - - const { project } = pageData; - - if (project?.faviconAssetId) { - const imageAsset = imageAssets.find( - (asset) => asset.id === project.faviconAssetId - ); - - if (imageAsset) { - result.push({ - rel: "icon", - href: imageLoader({ - src: imageAsset.name, - width: 128, - quality: 100, - format: "auto", - }), - type: undefined, - }); - } - } else { - result.push({ - rel: "icon", - href: "/favicon.ico", - type: "image/x-icon", - }); - - result.push({ - rel: "shortcut icon", - href: "/favicon.ico", - type: "image/x-icon", - }); - } - - for (const asset of fontAssets) { - if (asset.type === "font") { - result.push({ - rel: "preload", - href: assetBaseUrl + asset.name, - as: "font", - crossOrigin: "anonymous", - // @todo add mimeType - // type: asset.mimeType, - }); - } - } - return result; }; diff --git a/fixtures/webstudio-custom-template/custom-template/__templates__/route-template.tsx b/fixtures/webstudio-custom-template/custom-template/__templates__/route-template.tsx new file mode 100644 index 0000000000..512bb39f81 --- /dev/null +++ b/fixtures/webstudio-custom-template/custom-template/__templates__/route-template.tsx @@ -0,0 +1,177 @@ +/* eslint-disable camelcase */ +import { + type V2_ServerRuntimeMetaFunction, + type LinksFunction, + type LinkDescriptor, + type ActionArgs, + type LoaderArgs, + json, +} from "@remix-run/server-runtime"; +import { useLoaderData } from "@remix-run/react"; +import type { Page as PageType, ProjectMeta } from "@webstudio-is/sdk"; +import { ReactSdkContext } from "@webstudio-is/react-sdk"; +import { n8nHandler, getFormId } from "@webstudio-is/form-handlers"; +import { + fontAssets, + pageData, + user, + projectId, + pagesPaths, + formsProperties, + Page, + imageAssets, + getRemixParams, +} from "../../app/__generated__/_index"; +import { loadResources } from "../../app/__generated__/_index.server"; +import css from "../__generated__/index.css"; +import { assetBaseUrl, imageBaseUrl, imageLoader } from "~/constants.mjs"; + +export type PageData = { + project?: ProjectMeta; + page: PageType; +}; + +export const loader = async (arg: LoaderArgs) => { + const params = getRemixParams(arg.params); + const resources = await loadResources({ params }); + + const host = + arg.request.headers.get("x-forwarded-host") || + arg.request.headers.get("host") || + ""; + + const url = new URL(arg.request.url); + url.host = host; + url.protocol = "https"; + + // typecheck + arg.context.EXCLUDE_FROM_SEARCH satisfies boolean; + + return json( + { + host, + url: url.href, + excludeFromSearch: arg.context.EXCLUDE_FROM_SEARCH, + params, + resources, + }, + // No way for current information to change, so add cache for 10 minutes + // In case of CRM Data, this should be set to 0 + { headers: { "Cache-Control": "public, max-age=600" } } + ); +}; + +export const headers = () => { + return { + "Cache-Control": "public, max-age=0, must-revalidate", + }; +}; + +export const links: LinksFunction = () => { + const result: LinkDescriptor[] = []; + + result.push({ + rel: "stylesheet", + href: css, + }); + return result; +}; + +const getRequestHost = (request: Request): string => + request.headers.get("x-forwarded-host") || request.headers.get("host") || ""; + +const getMethod = (value: string | undefined) => { + if (value === undefined) { + return "post"; + } + + switch (value.toLowerCase()) { + case "get": + return "get"; + default: + return "post"; + } +}; + +export const action = async ({ request, context }: ActionArgs) => { + const formData = await request.formData(); + + const formId = getFormId(formData); + if (formId === undefined) { + // We're throwing rather than returning { success: false } + // because this isn't supposed to happen normally: bug or malicious user + throw json("Form not found", { status: 404 }); + } + + const formProperties = formsProperties.get(formId); + + // form properties are not defined when defaults are used + const { action, method } = formProperties ?? {}; + + const email = user?.email; + + if (email == null) { + return { success: false }; + } + + // wrapped in try/catch just in cases new URL() throws + // (should not happen) + let pageUrl: URL; + try { + pageUrl = new URL(request.url); + pageUrl.host = getRequestHost(request); + } catch { + return { success: false }; + } + + if (action !== undefined) { + try { + // Test that action is full URL + new URL(action); + } catch { + return json( + { + success: false, + error: "Invalid action URL, must be valid http/https protocol", + }, + { status: 200 } + ); + } + } + + const formInfo = { + formData, + projectId, + action: action ?? null, + method: getMethod(method), + pageUrl: pageUrl.toString(), + toEmail: email, + fromEmail: pageUrl.hostname + "@webstudio.email", + } as const; + + const result = await n8nHandler({ + formInfo, + hookUrl: context.N8N_FORM_EMAIL_HOOK, + }); + + return result; +}; + +const Outlet = () => { + const { params, resources } = useLoaderData(); + return ( + + + + ); +}; + +export default Outlet; diff --git a/packages/cli/__generated__/index.server.tsx b/packages/cli/__generated__/_index.server.tsx similarity index 100% rename from packages/cli/__generated__/index.server.tsx rename to packages/cli/__generated__/_index.server.tsx diff --git a/packages/cli/__generated__/index.tsx b/packages/cli/__generated__/_index.tsx similarity index 92% rename from packages/cli/__generated__/index.tsx rename to packages/cli/__generated__/_index.tsx index 9756c56743..4d22371549 100644 --- a/packages/cli/__generated__/index.tsx +++ b/packages/cli/__generated__/_index.tsx @@ -2,7 +2,7 @@ * The only intent of this file is to support typings inside ../templates/route-template for easier development. **/ import type { Asset, ImageAsset } from "@webstudio-is/sdk"; -import type { PageData } from "../templates/route-template"; +import type { PageData } from "../templates/defaults/__templates__/route-template"; export const fontAssets: Asset[] = []; export const imageAssets: ImageAsset[] = []; diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index a8f3ece990..7d55863272 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -59,7 +59,7 @@ import { isFileExists, } from "./fs-utils"; import type * as sharedConstants from "~/constants.mjs"; -import type { PageData } from "../templates/route-template"; +import type { PageData } from "../templates/defaults/__templates__/route-template"; const limit = pLimit(10); @@ -146,6 +146,9 @@ const mergeJsonFiles = async (sourcePath: string, destinationPath: string) => { await writeFile(destinationPath, content, "utf8"); }; +/** + * Check if template is internal cli template or external path + */ const isCliTemplate = async (template: string) => { const currentPath = fileURLToPath(new URL(import.meta.url)); @@ -163,23 +166,32 @@ const isCliTemplate = async (template: string) => { return false; }; -const copyTemplates = async (template: string = "defaults") => { +/** + * template can be internal cli template or external path + */ +const getTemplatePath = async (template: string) => { const currentPath = fileURLToPath(new URL(import.meta.url)); - const templatesPath = (await isCliTemplate(template)) + const templatePath = (await isCliTemplate(template)) ? normalize(join(dirname(currentPath), "..", "templates", template)) : template; - await cp(templatesPath, cwd(), { + return templatePath; +}; + +const copyTemplates = async (template: string = "defaults") => { + const templatePath = await getTemplatePath(template); + + await cp(templatePath, cwd(), { recursive: true, filter: (source) => { return basename(source) !== "package.json"; }, }); - if ((await isFileExists(join(templatesPath, "package.json"))) === true) { + if ((await isFileExists(join(templatePath, "package.json"))) === true) { await mergeJsonFiles( - join(templatesPath, "package.json"), + join(templatePath, "package.json"), join(cwd(), "package.json") ); } @@ -418,18 +430,14 @@ export const prebuild = async (options: { spinner.text = "Generating routes and pages"; - const routeFileTemplate = await readFile( - normalize( - join( - dirname(fileURLToPath(new URL(import.meta.url))), - "..", - "templates", - "route-template.tsx" - ) - ), - "utf8" + const routeTemplatePath = normalize( + join(cwd(), "__templates__", "route-template.tsx") ); + const routeFileTemplate = await readFile(routeTemplatePath, "utf8"); + + await rm(dirname(routeTemplatePath), { recursive: true }); + for (const [pageId, pageComponents] of Object.entries(componentsByPage)) { const scope = createScope([ // manually maintained list of occupied identifiers @@ -578,10 +586,13 @@ ${utilsExport} const fileName = `${remixRoute}.tsx`; const routeFileContent = routeFileTemplate - .replace('../__generated__/index"', `../__generated__/${remixRoute}"`) .replace( - '../__generated__/index.server"', - `../__generated__/${remixRoute}.server"` + /".*\/__generated__\/_index"/, + `"../__generated__/${remixRoute}"` + ) + .replace( + /".*\/__generated__\/_index.server"/, + `"../__generated__/${remixRoute}.server"` ); await ensureFileInPath(join(routesDir, fileName), routeFileContent); diff --git a/packages/cli/templates/route-template.tsx b/packages/cli/templates/defaults/__templates__/route-template.tsx similarity index 98% rename from packages/cli/templates/route-template.tsx rename to packages/cli/templates/defaults/__templates__/route-template.tsx index 04f30f207b..79a69c6559 100644 --- a/packages/cli/templates/route-template.tsx +++ b/packages/cli/templates/defaults/__templates__/route-template.tsx @@ -21,8 +21,8 @@ import { Page, imageAssets, getRemixParams, -} from "../__generated__/index"; -import { loadResources } from "../__generated__/index.server"; +} from "../../../__generated__/_index"; +import { loadResources } from "../../../__generated__/_index.server"; import css from "../__generated__/index.css"; import { assetBaseUrl, imageBaseUrl, imageLoader } from "~/constants.mjs";