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.
This commit is contained in:
Ivan Starkov authored and GitHub committed 2024-01-26 14:38:53 +07:00
1 parent abcc046c65
commit 091bd3d9a3
8 files changed
+212 -288

No files matched your search

@@ -50,8 +50,7 @@ export const user: { email: string | null } | undefined = {
};
export const projectId = "0d856812-61d8-4014-a20a-82e01c0eb8ee";
type Params = Record<string, string | undefined>;
const Page = (_props: { params: Params }) => {
const Page = ({ params: PageParams }: { params: any }) => {
return (
<Body
data-ws-id="LW98_-srDnnagkR10lsk4"
@@ -74,6 +73,7 @@ const Page = (_props: { params: Params }) => {
export { Page };
type Params = Record<string, string | undefined>;
export const getRemixParams = ({ ...params }: Params): Params => {
return params;
};
@@ -67,91 +67,6 @@ export const headers = () => {
};
};
export const meta: V2_ServerRuntimeMetaFunction<typeof loader> = ({ data }) => {
const { page, project } = pageData;
const metas: ReturnType<V2_ServerRuntimeMetaFunction> = [];
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;
};
@@ -67,91 +67,6 @@ export const headers = () => {
};
};
export const meta: V2_ServerRuntimeMetaFunction<typeof loader> = ({ data }) => {
const { page, project } = pageData;
const metas: ReturnType<V2_ServerRuntimeMetaFunction> = [];
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;
};
@@ -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<typeof loader>();
return (
<ReactSdkContext.Provider
value={{
imageLoader,
assetBaseUrl,
imageBaseUrl,
pagesPaths,
resources,
}}
>
<Page params={params} />
</ReactSdkContext.Provider>
);
};
export default Outlet;
File renamed without changes.
@@ -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[] = [];
+30 -19
View File
@@ -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);
@@ -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";