webstudio 0.145.0 → 0.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -68,16 +68,16 @@ var isFileExists = async (filePath) => {
68
68
  return false;
69
69
  }
70
70
  };
71
- var ensureFileInPath = async (filePath, content) => {
71
+ var createFileIfNotExists = async (filePath, content) => {
72
72
  const dir = dirname(filePath);
73
- await ensureFolderExists(dir);
73
+ await createFolderIfNotExists(dir);
74
74
  try {
75
75
  await access(filePath, constants.F_OK);
76
76
  } catch {
77
77
  await writeFile(filePath, content || "", "utf8");
78
78
  }
79
79
  };
80
- var ensureFolderExists = async (folderPath) => {
80
+ var createFolderIfNotExists = async (folderPath) => {
81
81
  try {
82
82
  await access(folderPath, constants.F_OK);
83
83
  } catch {
@@ -142,7 +142,7 @@ var link = async (options) => {
142
142
  const localConfig = {
143
143
  projectId
144
144
  };
145
- await ensureFileInPath(
145
+ await createFileIfNotExists(
146
146
  join2(cwd(), LOCAL_CONFIG_FILE),
147
147
  JSON.stringify(localConfig, null, 2)
148
148
  );
@@ -231,7 +231,7 @@ var sync = async (options) => {
231
231
  project;
232
232
  spinner.text = "Saving project data to config file";
233
233
  const localBuildFilePath = join3(cwd2(), LOCAL_DATA_FILE);
234
- await ensureFileInPath(localBuildFilePath);
234
+ await createFileIfNotExists(localBuildFilePath);
235
235
  await writeFile3(localBuildFilePath, JSON.stringify(project, null, 2), "utf8");
236
236
  spinner.succeed("Project data synced successfully");
237
237
  };
@@ -265,17 +265,17 @@ import {
265
265
  normalizeProps,
266
266
  generateRemixRoute,
267
267
  generateRemixParams,
268
- collectionComponent
268
+ isCoreComponent
269
269
  } from "@webstudio-is/react-sdk";
270
270
  import {
271
271
  createScope,
272
272
  findTreeInstanceIds,
273
273
  getPagePath,
274
274
  parseComponentName,
275
- executeExpression,
276
275
  generateFormsProperties,
277
276
  generateResourcesLoader,
278
- generatePageMeta
277
+ generatePageMeta,
278
+ getStaticSiteMapXml
279
279
  } from "@webstudio-is/sdk";
280
280
  import { createImageLoader } from "@webstudio-is/image";
281
281
  import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas";
@@ -288,7 +288,7 @@ var downloadAsset = async (url, name, assetBaseUrl) => {
288
288
  try {
289
289
  await access2(assetPath);
290
290
  } catch {
291
- await ensureFolderExists(dirname2(assetPath));
291
+ await createFolderIfNotExists(dirname2(assetPath));
292
292
  try {
293
293
  const response = await fetch(url);
294
294
  if (!response.ok) {
@@ -480,7 +480,7 @@ var prebuild = async (options) => {
480
480
  };
481
481
  componentsByPage[page.id] = /* @__PURE__ */ new Set();
482
482
  for (const [_instanceId, instance] of instances) {
483
- if (instance.component === collectionComponent) {
483
+ if (isCoreComponent(instance.component)) {
484
484
  continue;
485
485
  }
486
486
  componentsByPage[page.id].add(instance.component);
@@ -546,6 +546,8 @@ var prebuild = async (options) => {
546
546
  const assets = new Map(siteData.assets.map((asset) => [asset.id, asset]));
547
547
  spinner.text = "Generating css file";
548
548
  const { cssText, classesMap } = generateCss({
549
+ instances: new Map(siteData.build.instances),
550
+ props: new Map(siteData.build.props),
549
551
  assets,
550
552
  breakpoints: new Map(siteData.build?.breakpoints),
551
553
  styles: new Map(siteData.build?.styles),
@@ -555,13 +557,18 @@ var prebuild = async (options) => {
555
557
  assetBaseUrl,
556
558
  atomic: siteData.build.pages.compiler?.atomicStyles ?? true
557
559
  });
558
- await ensureFileInPath(join4(generatedDir, "index.css"), cssText);
560
+ await createFileIfNotExists(join4(generatedDir, "index.css"), cssText);
559
561
  spinner.text = "Generating routes and pages";
560
- const routeTemplatePath = normalize(
561
- join4(cwd3(), "__templates__", "route-template.tsx")
562
+ const routeTemplatesDir = join4(cwd3(), "app/route-templates");
563
+ const routeTemplatePath = normalize(join4(routeTemplatesDir, "html.tsx"));
564
+ const routeXmlTemplatePath = normalize(join4(routeTemplatesDir, "xml.tsx"));
565
+ const defaultSiteMapXmlPath = normalize(
566
+ join4(routeTemplatesDir, "default-sitemap.tsx")
562
567
  );
563
568
  const routeFileTemplate = await readFile4(routeTemplatePath, "utf8");
564
- await rm(dirname2(routeTemplatePath), { recursive: true });
569
+ const routeXmlFileTemplate = await readFile4(routeXmlTemplatePath, "utf8");
570
+ const defaultSiteMapTemplate = await readFile4(defaultSiteMapXmlPath, "utf8");
571
+ await rm(routeTemplatesDir, { recursive: true, force: true });
565
572
  for (const [pageId, pageComponents] of Object.entries(componentsByPage)) {
566
573
  const scope = createScope([
567
574
  // manually maintained list of occupied identifiers
@@ -594,14 +601,36 @@ var prebuild = async (options) => {
594
601
  namespaces.get(namespace)?.add([shortName, component]);
595
602
  }
596
603
  let componentImports = "";
604
+ let xmlPresentationComponents = "";
605
+ const pageData = siteDataByPage[pageId];
606
+ const documentType = pageData.page.meta.documentType ?? "html";
597
607
  for (const [namespace, componentsSet] of namespaces.entries()) {
598
- const specifiers = Array.from(componentsSet).map(
599
- ([shortName, component]) => `${shortName} as ${scope.getName(component, shortName)}`
600
- ).join(", ");
601
- componentImports += `import { ${specifiers} } from "${namespace}";
608
+ switch (documentType) {
609
+ case "html":
610
+ {
611
+ const specifiers = Array.from(componentsSet).map(
612
+ ([shortName, component]) => `${shortName} as ${scope.getName(component, shortName)}`
613
+ ).join(", ");
614
+ componentImports += `import { ${specifiers} } from "${namespace}";
615
+ `;
616
+ }
617
+ break;
618
+ case "xml":
619
+ {
620
+ componentImports = `import { XmlNode } from "@webstudio-is/sdk-components-react";
602
621
  `;
622
+ xmlPresentationComponents += Array.from(componentsSet).map(
623
+ ([shortName, component]) => scope.getName(component, shortName)
624
+ ).filter((scopedName) => scopedName !== "XmlNode").map(
625
+ (scopedName) => scopedName === "Body" ? `const ${scopedName} = (props: any) => props.children;` : `const ${scopedName} = () => null;`
626
+ ).join("\n");
627
+ }
628
+ break;
629
+ default: {
630
+ documentType;
631
+ }
632
+ }
603
633
  }
604
- const pageData = siteDataByPage[pageId];
605
634
  const pageFontAssets = fontAssetsByPage[pageId];
606
635
  const pageBackgroundImageAssets = backgroundImageAssetsByPage[pageId];
607
636
  const rootInstanceId = pageData.page.rootInstanceId;
@@ -633,96 +662,99 @@ var prebuild = async (options) => {
633
662
  )
634
663
  });
635
664
  const projectMeta = siteData.build.pages.meta;
665
+ const contactEmail = (
666
+ // fallback to user email when contact email is empty string
667
+ projectMeta?.contactEmail || siteData.user?.email || void 0
668
+ );
636
669
  const pageMeta = pageData.page.meta;
637
670
  const favIconAsset = assets.get(projectMeta?.faviconAssetId ?? "");
638
671
  const socialImageAsset = assets.get(pageMeta.socialImageAssetId ?? "");
639
672
  const pageExports = `/* eslint-disable */
640
- /* This is a auto generated file for building the project */
673
+ /* This is a auto generated file for building the project */
641
674
 
642
675
 
643
- import { Fragment, useState } from "react";
644
- import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
645
- import { useResource } from "@webstudio-is/react-sdk";
646
- ${componentImports}
676
+ import { Fragment, useState } from "react";
677
+ import type { FontAsset, ImageAsset } from "@webstudio-is/sdk";
678
+ import { useResource } from "@webstudio-is/react-sdk";
679
+ ${componentImports}
647
680
 
648
- export const favIconAsset: ImageAsset | undefined =
649
- ${JSON.stringify(favIconAsset)};
681
+ export const siteName = ${JSON.stringify(projectMeta?.siteName)};
650
682
 
651
- export const socialImageAsset: ImageAsset | undefined =
652
- ${JSON.stringify(socialImageAsset)};
683
+ export const favIconAsset: ImageAsset | undefined =
684
+ ${JSON.stringify(favIconAsset)};
653
685
 
654
- // Font assets on current page (can be preloaded)
655
- export const pageFontAssets: FontAsset[] =
656
- ${JSON.stringify(pageFontAssets)}
686
+ export const socialImageAsset: ImageAsset | undefined =
687
+ ${JSON.stringify(socialImageAsset)};
657
688
 
658
- export const pageBackgroundImageAssets: ImageAsset[] =
659
- ${JSON.stringify(pageBackgroundImageAssets)}
689
+ // Font assets on current page (can be preloaded)
690
+ export const pageFontAssets: FontAsset[] =
691
+ ${JSON.stringify(pageFontAssets)}
660
692
 
693
+ export const pageBackgroundImageAssets: ImageAsset[] =
694
+ ${JSON.stringify(pageBackgroundImageAssets)}
661
695
 
696
+ ${xmlPresentationComponents}
662
697
 
663
- ${pageComponent}
698
+ ${pageComponent}
664
699
 
665
- export { Page }
666
- `;
700
+ export { Page }
701
+ `;
667
702
  const serverExports = `/* eslint-disable */
668
- /* This is a auto generated file for building the project */
703
+ /* This is a auto generated file for building the project */
669
704
 
670
705
 
671
- import type { ProjectMeta, PageMeta } from "@webstudio-is/sdk";
672
- ${generateResourcesLoader({
706
+ import type { PageMeta } from "@webstudio-is/sdk";
707
+ ${generateResourcesLoader({
673
708
  scope,
674
709
  page: pageData.page,
675
710
  dataSources,
676
711
  resources
677
712
  })}
678
713
 
679
- ${generatePageMeta({
714
+ ${generatePageMeta({
680
715
  globalScope: scope,
681
716
  page: pageData.page,
682
717
  dataSources
683
718
  })}
684
719
 
685
- ${generateFormsProperties(props)}
720
+ ${generateFormsProperties(props)}
686
721
 
687
- ${generateRemixParams(pageData.page.path)}
722
+ ${generateRemixParams(pageData.page.path)}
688
723
 
689
- export const projectId = "${siteData.build.projectId}";
724
+ export const projectId = "${siteData.build.projectId}";
690
725
 
691
- export const user: { email: string | null } | undefined =
692
- ${JSON.stringify(siteData.user)};
726
+ export const contactEmail = ${JSON.stringify(contactEmail)};
693
727
 
694
- export const projectMeta: ProjectMeta =
695
- ${JSON.stringify(projectMeta)};
696
- `;
728
+ export const customCode = ${JSON.stringify(
729
+ projectMeta?.code?.trim() ?? ""
730
+ )};
731
+ `;
697
732
  const pagePath = getPagePath(pageData.page.id, siteData.build.pages);
698
733
  const remixRoute = generateRemixRoute(pagePath);
699
734
  const fileName = `${remixRoute}.tsx`;
700
- const routeFileContent = routeFileTemplate.replace(
735
+ const routeFileContent = (documentType === "html" ? routeFileTemplate : routeXmlFileTemplate).replace(
701
736
  /".*\/__generated__\/_index"/,
702
737
  `"../__generated__/${remixRoute}"`
703
738
  ).replace(
704
739
  /".*\/__generated__\/_index.server"/,
705
740
  `"../__generated__/${remixRoute}.server"`
706
741
  );
707
- await ensureFileInPath(join4(routesDir, fileName), routeFileContent);
708
- await ensureFileInPath(join4(generatedDir, fileName), pageExports);
709
- await ensureFileInPath(
742
+ await createFileIfNotExists(join4(routesDir, fileName), routeFileContent);
743
+ await createFileIfNotExists(join4(generatedDir, fileName), pageExports);
744
+ await createFileIfNotExists(
710
745
  join4(generatedDir, `${remixRoute}.server.tsx`),
711
746
  serverExports
712
747
  );
713
748
  }
714
- await writeFile4(
715
- join4(generatedDir, "[sitemap.xml].ts"),
749
+ await createFileIfNotExists(
750
+ join4(routesDir, "[sitemap.xml]._index.tsx"),
751
+ defaultSiteMapTemplate.replace(/".*\/__generated__\//, `"../__generated__/`)
752
+ );
753
+ await createFileIfNotExists(
754
+ join4(generatedDir, "$resources.sitemap.xml.ts"),
716
755
  `
717
756
  export const sitemap = ${JSON.stringify(
718
- {
719
- pages: siteData.pages.filter(
720
- (page) => executeExpression(page.meta.excludePageFromSearch) !== true
721
- ).map((page) => ({
722
- path: page.path,
723
- lastModified: siteData.build.updatedAt
724
- }))
725
- },
757
+ getStaticSiteMapXml(siteData.build.pages, siteData.build.updatedAt),
726
758
  null,
727
759
  2
728
760
  )};
@@ -736,11 +768,11 @@ export const projectMeta: ProjectMeta =
736
768
  const redirectFileName = `${redirectPagePath}.ts`;
737
769
  const content = `import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
738
770
 
739
- export const loader = (arg: LoaderFunctionArgs) => {
740
- return redirect("${redirect.new}", ${redirect.status ?? 301});
741
- };
742
- `;
743
- await ensureFileInPath(join4(routesDir, redirectFileName), content);
771
+ export const loader = (arg: LoaderFunctionArgs) => {
772
+ return redirect("${redirect.new}", ${redirect.status ?? 301});
773
+ };
774
+ `;
775
+ await createFileIfNotExists(join4(routesDir, redirectFileName), content);
744
776
  }
745
777
  }
746
778
  spinner.text = "Downloading fonts and images";
@@ -825,7 +857,7 @@ var initFlow = async (options) => {
825
857
  if (folderName === void 0) {
826
858
  throw new Error("Folder name is required");
827
859
  }
828
- await ensureFolderExists(join5(cwd4(), folderName));
860
+ await createFolderIfNotExists(join5(cwd4(), folderName));
829
861
  chdir(join5(cwd4(), folderName));
830
862
  }
831
863
  const { projectLink } = await prompt({
@@ -917,7 +949,7 @@ import makeCLI from "yargs";
917
949
  // package.json
918
950
  var package_default = {
919
951
  name: "webstudio",
920
- version: "0.145.0",
952
+ version: "0.163.0",
921
953
  description: "Webstudio CLI",
922
954
  author: "Webstudio <github@webstudio.is>",
923
955
  homepage: "https://webstudio.is",
@@ -961,24 +993,26 @@ var package_default = {
961
993
  zod: "^3.22.4"
962
994
  },
963
995
  devDependencies: {
964
- "@netlify/remix-adapter": "^2.3.0",
965
- "@netlify/remix-edge-adapter": "3.2.0",
966
- "@remix-run/cloudflare": "^2.8.1",
967
- "@remix-run/cloudflare-pages": "^2.8.1",
968
- "@remix-run/dev": "^2.8.1",
969
- "@remix-run/node": "^2.8.1",
970
- "@remix-run/react": "^2.8.1",
971
- "@remix-run/server-runtime": "^2.8.1",
972
- "@types/node": "^18.17.1",
996
+ "@netlify/remix-adapter": "^2.3.1",
997
+ "@netlify/remix-edge-adapter": "3.2.2",
998
+ "@remix-run/cloudflare": "^2.9.1",
999
+ "@remix-run/cloudflare-pages": "^2.9.1",
1000
+ "@remix-run/dev": "^2.9.1",
1001
+ "@remix-run/node": "^2.9.1",
1002
+ "@remix-run/react": "^2.9.1",
1003
+ "@remix-run/server-runtime": "^2.9.1",
1004
+ "@types/node": "^20.12.7",
973
1005
  "@types/prompts": "^2.4.5",
974
1006
  "@types/react": "^18.2.70",
975
1007
  "@types/react-dom": "^18.2.25",
976
1008
  "@types/yargs": "^17.0.32",
977
1009
  "@webstudio-is/form-handlers": "workspace:*",
978
1010
  "@webstudio-is/tsconfig": "workspace:*",
1011
+ react: "18.3.0-canary-14898b6a9-20240318",
1012
+ "react-dom": "18.3.0-canary-14898b6a9-20240318",
979
1013
  tsx: "^4.7.2",
980
1014
  typescript: "5.4.5",
981
- "vite-tsconfig-paths": "^4.3.2",
1015
+ vite: "^5.2.11",
982
1016
  wrangler: "^3.48.0"
983
1017
  }
984
1018
  };
@@ -986,7 +1020,7 @@ var package_default = {
986
1020
  // src/cli.ts
987
1021
  var main = async () => {
988
1022
  try {
989
- await ensureFileInPath(GLOBAL_CONFIG_FILE, "{}");
1023
+ await createFileIfNotExists(GLOBAL_CONFIG_FILE, "{}");
990
1024
  const cmd = makeCLI(hideBin(argv)).strict().fail(function(msg, err, yargs) {
991
1025
  if (err) {
992
1026
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.145.0",
3
+ "version": "0.163.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -28,33 +28,35 @@
28
28
  "title-case": "^4.1.0",
29
29
  "yargs": "^17.7.2",
30
30
  "zod": "^3.22.4",
31
- "@webstudio-is/http-client": "0.145.0",
32
- "@webstudio-is/image": "0.145.0",
33
- "@webstudio-is/sdk": "0.145.0",
34
- "@webstudio-is/react-sdk": "0.145.0",
35
- "@webstudio-is/sdk-components-react": "0.145.0",
36
- "@webstudio-is/sdk-components-react-radix": "0.145.0",
37
- "@webstudio-is/sdk-components-react-remix": "0.145.0"
31
+ "@webstudio-is/http-client": "0.163.0",
32
+ "@webstudio-is/react-sdk": "0.163.0",
33
+ "@webstudio-is/image": "0.163.0",
34
+ "@webstudio-is/sdk": "0.163.0",
35
+ "@webstudio-is/sdk-components-react-radix": "0.163.0",
36
+ "@webstudio-is/sdk-components-react": "0.163.0",
37
+ "@webstudio-is/sdk-components-react-remix": "0.163.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@netlify/remix-adapter": "^2.3.0",
41
- "@netlify/remix-edge-adapter": "3.2.0",
42
- "@remix-run/cloudflare": "^2.8.1",
43
- "@remix-run/cloudflare-pages": "^2.8.1",
44
- "@remix-run/dev": "^2.8.1",
45
- "@remix-run/node": "^2.8.1",
46
- "@remix-run/react": "^2.8.1",
47
- "@remix-run/server-runtime": "^2.8.1",
48
- "@types/node": "^18.17.1",
40
+ "@netlify/remix-adapter": "^2.3.1",
41
+ "@netlify/remix-edge-adapter": "3.2.2",
42
+ "@remix-run/cloudflare": "^2.9.1",
43
+ "@remix-run/cloudflare-pages": "^2.9.1",
44
+ "@remix-run/dev": "^2.9.1",
45
+ "@remix-run/node": "^2.9.1",
46
+ "@remix-run/react": "^2.9.1",
47
+ "@remix-run/server-runtime": "^2.9.1",
48
+ "@types/node": "^20.12.7",
49
49
  "@types/prompts": "^2.4.5",
50
50
  "@types/react": "^18.2.70",
51
51
  "@types/react-dom": "^18.2.25",
52
52
  "@types/yargs": "^17.0.32",
53
+ "react": "18.3.0-canary-14898b6a9-20240318",
54
+ "react-dom": "18.3.0-canary-14898b6a9-20240318",
53
55
  "tsx": "^4.7.2",
54
56
  "typescript": "5.4.5",
55
- "vite-tsconfig-paths": "^4.3.2",
57
+ "vite": "^5.2.11",
56
58
  "wrangler": "^3.48.0",
57
- "@webstudio-is/form-handlers": "0.145.0",
59
+ "@webstudio-is/form-handlers": "0.163.0",
58
60
  "@webstudio-is/tsconfig": "1.0.7"
59
61
  },
60
62
  "scripts": {
@@ -2,7 +2,6 @@ import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
4
  // @ts-ignore - the server build file is generated by `remix vite:build`
5
- // eslint-disable-next-line import/no-unresolved
6
5
  import * as build from "../build/server";
7
6
 
8
7
  export const onRequest = createPagesFunctionHandler({ build });
@@ -12,17 +12,13 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
- "@remix-run/cloudflare": "^2.8.1",
16
- "@remix-run/cloudflare-pages": "^2.8.1",
15
+ "@remix-run/cloudflare": "2.9.1",
16
+ "@remix-run/cloudflare-pages": "2.9.1",
17
17
  "isbot": "^4.1.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@cloudflare/workers-types": "^4.20240405.0",
21
- "vite-tsconfig-paths": "^4.2.1",
22
21
  "wrangler": "^3.48.0",
23
22
  "miniflare": "^3.20231030.4"
24
- },
25
- "engines": {
26
- "node": ">=18.0.0"
27
23
  }
28
24
  }
@@ -26,9 +26,6 @@
26
26
  "skipLibCheck": true,
27
27
  "forceConsistentCasingInFileNames": true,
28
28
  "baseUrl": ".",
29
- "paths": {
30
- "~/*": ["./app/*"]
31
- },
32
29
 
33
30
  // Vite takes care of building everything, not tsc.
34
31
  "noEmit": true
@@ -3,13 +3,11 @@ import {
3
3
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
4
4
  } from "@remix-run/dev";
5
5
  import { defineConfig } from "vite";
6
- import tsconfigPaths from "vite-tsconfig-paths";
7
6
 
8
7
  export default defineConfig(({ mode }) => ({
9
8
  plugins: [
10
9
  // without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)
11
10
  mode === "production" ? undefined : remixCloudflareDevProxy(),
12
11
  remix(),
13
- tsconfigPaths(),
14
12
  ].filter(Boolean),
15
13
  }));
@@ -1,5 +1,5 @@
1
1
  import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
2
- import { sitemap } from "../__generated__/[sitemap.xml]";
2
+ import { sitemap } from "../../../../__generated__/$resources.sitemap.xml";
3
3
 
4
4
  export const loader = (arg: LoaderFunctionArgs) => {
5
5
  const host =
@@ -7,7 +7,7 @@ export const loader = (arg: LoaderFunctionArgs) => {
7
7
  arg.request.headers.get("host") ||
8
8
  "";
9
9
 
10
- const urls = sitemap.pages.map((page) => {
10
+ const urls = sitemap.map((page) => {
11
11
  const url = new URL(`https://${host}${page.path}`);
12
12
 
13
13
  return `
@@ -11,34 +11,47 @@ import {
11
11
  } from "@remix-run/server-runtime";
12
12
  import { useLoaderData } from "@remix-run/react";
13
13
  import { ReactSdkContext } from "@webstudio-is/react-sdk";
14
- import { n8nHandler, getFormId } from "@webstudio-is/form-handlers";
14
+ import {
15
+ n8nHandler,
16
+ formIdFieldName,
17
+ formBotFieldName,
18
+ } from "@webstudio-is/form-handlers";
15
19
  import {
16
20
  Page,
21
+ siteName,
17
22
  favIconAsset,
18
23
  socialImageAsset,
19
24
  pageFontAssets,
20
25
  pageBackgroundImageAssets,
21
- } from "../../../__generated__/_index";
26
+ } from "../../../../__generated__/_index";
22
27
  import {
23
28
  formsProperties,
24
29
  loadResources,
25
30
  getPageMeta,
26
31
  getRemixParams,
27
32
  projectId,
28
- user,
29
- projectMeta,
30
- } from "../../../__generated__/_index.server";
33
+ contactEmail,
34
+ } from "../../../../__generated__/_index.server";
31
35
 
32
36
  import css from "../__generated__/index.css?url";
33
- import { assetBaseUrl, imageBaseUrl, imageLoader } from "~/constants.mjs";
37
+ import { assetBaseUrl, imageBaseUrl, imageLoader } from "../constants.mjs";
34
38
 
35
39
  export const loader = async (arg: LoaderFunctionArgs) => {
36
40
  const url = new URL(arg.request.url);
41
+ const host =
42
+ arg.request.headers.get("x-forwarded-host") ||
43
+ arg.request.headers.get("host") ||
44
+ "";
45
+ url.host = host;
46
+ url.protocol = "https";
47
+
37
48
  const params = getRemixParams(arg.params);
38
49
  const system = {
39
50
  params,
40
51
  search: Object.fromEntries(url.searchParams),
52
+ origin: url.origin,
41
53
  };
54
+
42
55
  const resources = await loadResources({ system });
43
56
  const pageMeta = getPageMeta({ system, resources });
44
57
 
@@ -50,14 +63,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
50
63
  return redirect(pageMeta.redirect, status);
51
64
  }
52
65
 
53
- const host =
54
- arg.request.headers.get("x-forwarded-host") ||
55
- arg.request.headers.get("host") ||
56
- "";
57
-
58
- url.host = host;
59
- url.protocol = "https";
60
-
61
66
  // typecheck
62
67
  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;
63
68
 
@@ -69,7 +74,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
69
74
  system,
70
75
  resources,
71
76
  pageMeta,
72
- projectMeta,
73
77
  },
74
78
  // No way for current information to change, so add cache for 10 minutes
75
79
  // In case of CRM Data, this should be set to 0
@@ -95,7 +99,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
95
99
  if (data === undefined) {
96
100
  return metas;
97
101
  }
98
- const { pageMeta, projectMeta } = data;
102
+ const { pageMeta } = data;
99
103
 
100
104
  if (data.url) {
101
105
  metas.push({
@@ -117,16 +121,16 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
117
121
 
118
122
  const origin = `https://${data.host}`;
119
123
 
120
- if (projectMeta?.siteName) {
124
+ if (siteName) {
121
125
  metas.push({
122
126
  property: "og:site_name",
123
- content: projectMeta.siteName,
127
+ content: siteName,
124
128
  });
125
129
  metas.push({
126
130
  "script:ld+json": {
127
131
  "@context": "https://schema.org",
128
132
  "@type": "WebSite",
129
- name: projectMeta.siteName,
133
+ name: siteName,
130
134
  url: origin,
131
135
  },
132
136
  });
@@ -240,68 +244,86 @@ const getMethod = (value: string | undefined) => {
240
244
  }
241
245
  };
242
246
 
243
- export const action = async ({ request, context }: ActionFunctionArgs) => {
244
- const formData = await request.formData();
247
+ export const action = async ({
248
+ request,
249
+ context,
250
+ }: ActionFunctionArgs): Promise<
251
+ { success: true } | { success: false; errors: string[] }
252
+ > => {
253
+ try {
254
+ const formData = await request.formData();
245
255
 
246
- const formId = getFormId(formData);
247
- if (formId === undefined) {
248
- // We're throwing rather than returning { success: false }
249
- // because this isn't supposed to happen normally: bug or malicious user
250
- throw json("Form not found", { status: 404 });
251
- }
256
+ const formId = formData.get(formIdFieldName);
252
257
 
253
- const formProperties = formsProperties.get(formId);
258
+ if (formId == null || typeof formId !== "string") {
259
+ throw new Error("No form id in FormData");
260
+ }
254
261
 
255
- // form properties are not defined when defaults are used
256
- const { action, method } = formProperties ?? {};
262
+ const formBotValue = formData.get(formBotFieldName);
257
263
 
258
- const email = user?.email;
264
+ if (formBotValue == null || typeof formBotValue !== "string") {
265
+ throw new Error("Form bot field not found");
266
+ }
259
267
 
260
- if (email == null) {
261
- return { success: false };
262
- }
268
+ const submitTime = parseInt(formBotValue, 16);
269
+ // Assumes that the difference between the server time and the form submission time,
270
+ // including any client-server time drift, is within a 5-minute range.
271
+ // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
272
+ // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
273
+ if (
274
+ Number.isNaN(submitTime) ||
275
+ Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
276
+ ) {
277
+ throw new Error(`Form bot value invalid ${formBotValue}`);
278
+ }
263
279
 
264
- // wrapped in try/catch just in cases new URL() throws
265
- // (should not happen)
266
- let pageUrl: URL;
267
- try {
268
- pageUrl = new URL(request.url);
280
+ const formProperties = formsProperties.get(formId);
281
+
282
+ // form properties are not defined when defaults are used
283
+ const { action, method } = formProperties ?? {};
284
+
285
+ if (contactEmail === undefined) {
286
+ throw new Error("Contact email not found");
287
+ }
288
+
289
+ const pageUrl = new URL(request.url);
269
290
  pageUrl.host = getRequestHost(request);
270
- } catch {
271
- return { success: false };
272
- }
273
291
 
274
- if (action !== undefined) {
275
- try {
276
- // Test that action is full URL
277
- new URL(action);
278
- } catch {
279
- return json(
280
- {
281
- success: false,
282
- error: "Invalid action URL, must be valid http/https protocol",
283
- },
284
- { status: 200 }
285
- );
292
+ if (action !== undefined) {
293
+ try {
294
+ // Test that action is full URL
295
+ new URL(action);
296
+ } catch {
297
+ throw new Error(
298
+ "Invalid action URL, must be valid http/https protocol"
299
+ );
300
+ }
286
301
  }
287
- }
288
302
 
289
- const formInfo = {
290
- formData,
291
- projectId,
292
- action: action ?? null,
293
- method: getMethod(method),
294
- pageUrl: pageUrl.toString(),
295
- toEmail: email,
296
- fromEmail: pageUrl.hostname + "@webstudio.email",
297
- } as const;
298
-
299
- const result = await n8nHandler({
300
- formInfo,
301
- hookUrl: context.N8N_FORM_EMAIL_HOOK,
302
- });
303
+ const formInfo = {
304
+ formData,
305
+ projectId,
306
+ action: action ?? null,
307
+ method: getMethod(method),
308
+ pageUrl: pageUrl.toString(),
309
+ toEmail: contactEmail,
310
+ fromEmail: pageUrl.hostname + "@webstudio.email",
311
+ } as const;
312
+
313
+ const result = await n8nHandler({
314
+ formInfo,
315
+ hookUrl: context.N8N_FORM_EMAIL_HOOK,
316
+ });
303
317
 
304
- return result;
318
+ return result;
319
+ } catch (error) {
320
+ console.error(error);
321
+
322
+ return {
323
+ success: false,
324
+ errors: [error instanceof Error ? error.message : "Unknown error"],
325
+ };
326
+ }
305
327
  };
306
328
 
307
329
  const Outlet = () => {
@@ -0,0 +1,61 @@
1
+ /* eslint-disable camelcase */
2
+ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
3
+ import { ReactSdkContext } from "@webstudio-is/react-sdk";
4
+ import { Page } from "../../../../__generated__/_index";
5
+ import {
6
+ loadResources,
7
+ getPageMeta,
8
+ getRemixParams,
9
+ } from "../../../../__generated__/_index.server";
10
+
11
+ import { assetBaseUrl, imageBaseUrl, imageLoader } from "../constants.mjs";
12
+ import { renderToString } from "react-dom/server";
13
+
14
+ export const loader = async (arg: LoaderFunctionArgs) => {
15
+ const url = new URL(arg.request.url);
16
+ const host =
17
+ arg.request.headers.get("x-forwarded-host") ||
18
+ arg.request.headers.get("host") ||
19
+ "";
20
+ url.host = host;
21
+ url.protocol = "https";
22
+
23
+ const params = getRemixParams(arg.params);
24
+
25
+ const system = {
26
+ params,
27
+ search: Object.fromEntries(url.searchParams),
28
+ origin: url.origin,
29
+ };
30
+
31
+ const resources = await loadResources({ system });
32
+ const pageMeta = getPageMeta({ system, resources });
33
+
34
+ if (pageMeta.redirect) {
35
+ const status =
36
+ pageMeta.status === 301 || pageMeta.status === 302
37
+ ? pageMeta.status
38
+ : 302;
39
+ return redirect(pageMeta.redirect, status);
40
+ }
41
+
42
+ // typecheck
43
+ arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;
44
+
45
+ const text = renderToString(
46
+ <ReactSdkContext.Provider
47
+ value={{
48
+ imageLoader,
49
+ assetBaseUrl,
50
+ imageBaseUrl,
51
+ resources,
52
+ }}
53
+ >
54
+ <Page system={system} />
55
+ </ReactSdkContext.Provider>
56
+ );
57
+
58
+ return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n${text}`, {
59
+ headers: { "Content-Type": "application/xml" },
60
+ });
61
+ };
@@ -8,28 +8,28 @@
8
8
  "typecheck": "tsc"
9
9
  },
10
10
  "dependencies": {
11
- "@remix-run/react": "^2.8.1",
12
- "@remix-run/server-runtime": "^2.8.1",
13
- "@remix-run/node": "^2.8.1",
14
- "@webstudio-is/react-sdk": "0.145.0",
15
- "@webstudio-is/sdk-components-react-radix": "0.145.0",
16
- "@webstudio-is/sdk-components-react-remix": "0.145.0",
17
- "@webstudio-is/sdk-components-react": "0.145.0",
18
- "@webstudio-is/form-handlers": "0.145.0",
19
- "@webstudio-is/image": "0.145.0",
20
- "@webstudio-is/sdk": "0.145.0",
11
+ "@remix-run/node": "2.9.1",
12
+ "@remix-run/react": "2.9.1",
13
+ "@remix-run/server-runtime": "2.9.1",
14
+ "@webstudio-is/react-sdk": "0.163.0",
15
+ "@webstudio-is/sdk-components-react-radix": "0.163.0",
16
+ "@webstudio-is/sdk-components-react-remix": "0.163.0",
17
+ "@webstudio-is/sdk-components-react": "0.163.0",
18
+ "@webstudio-is/form-handlers": "0.163.0",
19
+ "@webstudio-is/image": "0.163.0",
20
+ "@webstudio-is/sdk": "0.163.0",
21
21
  "isbot": "^3.6.8",
22
22
  "react": "18.3.0-canary-14898b6a9-20240318",
23
23
  "react-dom": "18.3.0-canary-14898b6a9-20240318"
24
24
  },
25
25
  "devDependencies": {
26
- "@remix-run/dev": "^2.8.1",
26
+ "@remix-run/dev": "2.9.1",
27
27
  "@types/react": "^18.2.70",
28
28
  "@types/react-dom": "^18.2.25",
29
29
  "typescript": "5.4.5",
30
- "vite": "^5.2.8"
30
+ "vite": "^5.2.11"
31
31
  },
32
32
  "engines": {
33
- "node": ">=18.0.0"
33
+ "node": ">=20.0.0"
34
34
  }
35
35
  }
@@ -16,11 +16,6 @@
16
16
  "forceConsistentCasingInFileNames": true,
17
17
  "allowImportingTsExtensions": true,
18
18
  "baseUrl": ".",
19
- "paths": {
20
- "~/*": ["./app/*"]
21
- },
22
- "customConditions": ["webstudio"],
23
-
24
19
  // Remix takes care of building everything in `remix build`.
25
20
  "noEmit": true,
26
21
  "skipLibCheck": true
@@ -1,15 +1,6 @@
1
- import { resolve } from "node:path";
2
1
  import { defineConfig } from "vite";
3
2
  import { vitePlugin as remix } from "@remix-run/dev";
4
3
 
5
4
  export default defineConfig({
6
5
  plugins: [remix()],
7
- resolve: {
8
- alias: [
9
- {
10
- find: "~",
11
- replacement: resolve("app"),
12
- },
13
- ],
14
- },
15
6
  });
@@ -0,0 +1,25 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
3
+ "compilerOptions": {
4
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
5
+ "types": ["@remix-run/node", "vite/client"],
6
+ "isolatedModules": true,
7
+ "esModuleInterop": true,
8
+ "jsx": "react-jsx",
9
+ "module": "ESNext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "target": "ES2022",
13
+ "strict": true,
14
+ "allowJs": true,
15
+ "checkJs": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "allowImportingTsExtensions": true,
18
+ "baseUrl": ".",
19
+ "customConditions": ["webstudio"],
20
+
21
+ // Remix takes care of building everything in `remix build`.
22
+ "noEmit": true,
23
+ "skipLibCheck": true
24
+ }
25
+ }
@@ -3,7 +3,7 @@
3
3
  "start": "netlify serve"
4
4
  },
5
5
  "dependencies": {
6
- "@netlify/edge-functions": "^2.3.1",
7
- "@netlify/remix-edge-adapter": "3.2.0"
6
+ "@netlify/edge-functions": "^2.6.0",
7
+ "@netlify/remix-edge-adapter": "^3.2.2"
8
8
  }
9
9
  }
@@ -1,16 +1,7 @@
1
- import { resolve } from "node:path";
2
1
  import { vitePlugin as remix } from "@remix-run/dev";
3
2
  import { defineConfig } from "vite";
4
3
  import { netlifyPlugin } from "@netlify/remix-edge-adapter/plugin";
5
4
 
6
5
  export default defineConfig({
7
6
  plugins: [remix(), netlifyPlugin()],
8
- resolve: {
9
- alias: [
10
- {
11
- find: "~",
12
- replacement: resolve("app"),
13
- },
14
- ],
15
- },
16
7
  });
@@ -4,6 +4,6 @@
4
4
  },
5
5
  "dependencies": {
6
6
  "@netlify/functions": "^2.6.0",
7
- "@netlify/remix-adapter": "^2.3.0"
7
+ "@netlify/remix-adapter": "^2.3.1"
8
8
  }
9
9
  }
@@ -1,16 +1,7 @@
1
- import { resolve } from "node:path";
2
1
  import { vitePlugin as remix } from "@remix-run/dev";
3
2
  import { defineConfig } from "vite";
4
3
  import { netlifyPlugin } from "@netlify/remix-adapter/plugin";
5
4
 
6
5
  export default defineConfig({
7
6
  plugins: [remix(), netlifyPlugin()],
8
- resolve: {
9
- alias: [
10
- {
11
- find: "~",
12
- replacement: resolve("app"),
13
- },
14
- ],
15
- },
16
7
  });
@@ -0,0 +1,34 @@
1
+ {
2
+ "include": [
3
+ "**/*.ts",
4
+ "**/*.tsx",
5
+ "**/.server/**/*.ts",
6
+ "**/.server/**/*.tsx",
7
+ "**/.client/**/*.ts",
8
+ "**/.client/**/*.tsx"
9
+ ],
10
+ "compilerOptions": {
11
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
12
+ "types": [
13
+ "@remix-run/cloudflare",
14
+ "vite/client",
15
+ "@cloudflare/workers-types/2023-07-01"
16
+ ],
17
+ "isolatedModules": true,
18
+ "esModuleInterop": true,
19
+ "jsx": "react-jsx",
20
+ "module": "ESNext",
21
+ "moduleResolution": "Bundler",
22
+ "resolveJsonModule": true,
23
+ "target": "ES2022",
24
+ "strict": true,
25
+ "allowJs": true,
26
+ "skipLibCheck": true,
27
+ "forceConsistentCasingInFileNames": true,
28
+ "baseUrl": ".",
29
+ "customConditions": ["webstudio"],
30
+
31
+ // Vite takes care of building everything, not tsc.
32
+ "noEmit": true
33
+ }
34
+ }
@@ -1,11 +0,0 @@
1
- /**
2
- * The only intent of this file is to support typings inside ../routes/[sitemap.xml].tsx for easier development.
3
- **/
4
- export const sitemap = {
5
- pages: [
6
- {
7
- path: "",
8
- lastModified: "2021-10-13T12:00:00.000Z",
9
- },
10
- ],
11
- };