mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
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:
1 parent
abcc046c65
commit
091bd3d9a3
8 files changed
+212
-288
No files matched your search
+2
-2
@@ -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;
|
||||
Generated
File renamed without changes.
+1
-1
@@ -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[] = [];
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user