webstudio 0.151.0 → 0.167.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstudio",
3
- "version": "0.151.0",
3
+ "version": "0.167.0",
4
4
  "description": "Webstudio CLI",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -17,53 +17,55 @@
17
17
  ],
18
18
  "license": "AGPL-3.0-or-later",
19
19
  "dependencies": {
20
+ "@clack/prompts": "^0.7.0",
21
+ "change-case": "^5.0.2",
20
22
  "deepmerge": "^4.3.1",
21
23
  "env-paths": "^3.0.0",
22
24
  "execa": "^7.2.0",
23
- "ora": "^7.0.1",
25
+ "parse5": "7.1.2",
24
26
  "p-limit": "^4.0.0",
25
- "picocolors": "^1.0.0",
26
- "prompts": "^2.4.2",
27
+ "picocolors": "^1.0.1",
27
28
  "strip-indent": "^4.0.0",
28
29
  "title-case": "^4.1.0",
29
30
  "yargs": "^17.7.2",
30
31
  "zod": "^3.22.4",
31
- "@webstudio-is/http-client": "0.151.0",
32
- "@webstudio-is/react-sdk": "0.151.0",
33
- "@webstudio-is/image": "0.151.0",
34
- "@webstudio-is/sdk": "0.151.0",
35
- "@webstudio-is/sdk-components-react-radix": "0.151.0",
36
- "@webstudio-is/sdk-components-react-remix": "0.151.0",
37
- "@webstudio-is/sdk-components-react": "0.151.0"
32
+ "@webstudio-is/http-client": "0.167.0",
33
+ "@webstudio-is/image": "0.167.0",
34
+ "@webstudio-is/react-sdk": "0.167.0",
35
+ "@webstudio-is/sdk": "0.167.0",
36
+ "@webstudio-is/sdk-components-react-radix": "0.167.0",
37
+ "@webstudio-is/sdk-components-react": "0.167.0",
38
+ "@webstudio-is/sdk-components-react-remix": "0.167.0"
38
39
  },
39
40
  "devDependencies": {
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",
41
+ "@jest/globals": "^29.7.0",
42
+ "@netlify/remix-adapter": "^2.4.0",
43
+ "@netlify/remix-edge-adapter": "3.3.0",
44
+ "@remix-run/cloudflare": "^2.9.2",
45
+ "@remix-run/cloudflare-pages": "^2.9.2",
46
+ "@remix-run/dev": "^2.9.2",
47
+ "@remix-run/node": "^2.9.2",
48
+ "@remix-run/react": "^2.9.2",
49
+ "@remix-run/server-runtime": "^2.9.2",
48
50
  "@types/node": "^20.12.7",
49
- "@types/prompts": "^2.4.5",
50
51
  "@types/react": "^18.2.70",
51
52
  "@types/react-dom": "^18.2.25",
52
53
  "@types/yargs": "^17.0.32",
53
54
  "react": "18.3.0-canary-14898b6a9-20240318",
54
55
  "react-dom": "18.3.0-canary-14898b6a9-20240318",
55
- "tsx": "^4.7.2",
56
56
  "typescript": "5.4.5",
57
- "vite": "^5.2.11",
57
+ "vite": "^5.2.12",
58
58
  "wrangler": "^3.48.0",
59
- "@webstudio-is/form-handlers": "0.151.0",
60
- "@webstudio-is/tsconfig": "1.0.7"
59
+ "@webstudio-is/form-handlers": "0.167.0",
60
+ "@webstudio-is/tsconfig": "1.0.7",
61
+ "@webstudio-is/jest-config": "1.0.7"
61
62
  },
62
63
  "scripts": {
63
64
  "typecheck": "tsc",
64
65
  "checks": "pnpm typecheck",
65
66
  "build": "rm -rf lib && esbuild src/cli.ts --outdir=lib --bundle --format=esm --packages=external",
66
67
  "local-run": "tsx --no-warnings ./src/bin.ts",
67
- "dev": "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib"
68
+ "dev": "esbuild src/cli.ts --watch --bundle --format=esm --packages=external --outdir=./lib",
69
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest"
68
70
  }
69
71
  }
@@ -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,9 +12,9 @@
12
12
  "build-cf-types": "wrangler types"
13
13
  },
14
14
  "dependencies": {
15
- "@remix-run/cloudflare": "2.9.1",
16
- "@remix-run/cloudflare-pages": "2.9.1",
17
- "isbot": "^4.1.0"
15
+ "@remix-run/cloudflare": "2.9.2",
16
+ "@remix-run/cloudflare-pages": "2.9.2",
17
+ "isbot": "^5.1.8"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@cloudflare/workers-types": "^4.20240405.0",
@@ -8,7 +8,7 @@
8
8
  "**/.client/**/*.tsx"
9
9
  ],
10
10
  "compilerOptions": {
11
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
11
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
12
12
  "types": [
13
13
  "@remix-run/cloudflare",
14
14
  "vite/client",
@@ -1,13 +1,31 @@
1
- import { Links, Meta, Outlet } from "@remix-run/react";
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+
3
+ import { Links, Meta, Outlet, useMatches } from "@remix-run/react";
4
+ // @todo think about how to make __generated__ typeable
5
+ // @ts-ignore
6
+ import { CustomCode } from "./__generated__/_index";
2
7
 
3
8
  const Root = () => {
9
+ // Get language from matches
10
+ const matches = useMatches();
11
+
12
+ const lastMatchWithLanguage = matches.findLast((match) => {
13
+ // @ts-ignore
14
+ const language = match?.data?.pageMeta?.language;
15
+ return language != null;
16
+ });
17
+
18
+ // @ts-ignore
19
+ const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? "en";
20
+
4
21
  return (
5
- <html lang="en">
22
+ <html lang={lang}>
6
23
  <head>
7
24
  <meta charSet="utf-8" />
8
25
  <meta name="viewport" content="width=device-width,initial-scale=1" />
9
26
  <Meta />
10
27
  <Links />
28
+ <CustomCode />
11
29
  </head>
12
30
  <Outlet />
13
31
  </html>
@@ -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 =
@@ -11,7 +11,11 @@ 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,
17
21
  siteName,
@@ -77,7 +81,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
77
81
  status: pageMeta.status,
78
82
  headers: {
79
83
  "Cache-Control": "public, max-age=600",
80
- "x-ws-language": pageMeta.language ?? "en",
81
84
  },
82
85
  }
83
86
  );
@@ -86,7 +89,6 @@ export const loader = async (arg: LoaderFunctionArgs) => {
86
89
  export const headers: HeadersFunction = ({ loaderHeaders }) => {
87
90
  return {
88
91
  "Cache-Control": "public, max-age=0, must-revalidate",
89
- "x-ws-language": loaderHeaders.get("x-ws-language") ?? "",
90
92
  };
91
93
  };
92
94
 
@@ -240,66 +242,86 @@ const getMethod = (value: string | undefined) => {
240
242
  }
241
243
  };
242
244
 
243
- export const action = async ({ request, context }: ActionFunctionArgs) => {
244
- const formData = await request.formData();
245
+ export const action = async ({
246
+ request,
247
+ context,
248
+ }: ActionFunctionArgs): Promise<
249
+ { success: true } | { success: false; errors: string[] }
250
+ > => {
251
+ try {
252
+ const formData = await request.formData();
245
253
 
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
- }
254
+ const formId = formData.get(formIdFieldName);
252
255
 
253
- const formProperties = formsProperties.get(formId);
256
+ if (formId == null || typeof formId !== "string") {
257
+ throw new Error("No form id in FormData");
258
+ }
254
259
 
255
- // form properties are not defined when defaults are used
256
- const { action, method } = formProperties ?? {};
260
+ const formBotValue = formData.get(formBotFieldName);
257
261
 
258
- if (contactEmail === undefined) {
259
- return { success: false };
260
- }
262
+ if (formBotValue == null || typeof formBotValue !== "string") {
263
+ throw new Error("Form bot field not found");
264
+ }
261
265
 
262
- // wrapped in try/catch just in cases new URL() throws
263
- // (should not happen)
264
- let pageUrl: URL;
265
- try {
266
- pageUrl = new URL(request.url);
266
+ const submitTime = parseInt(formBotValue, 16);
267
+ // Assumes that the difference between the server time and the form submission time,
268
+ // including any client-server time drift, is within a 5-minute range.
269
+ // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.
270
+ // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`
271
+ if (
272
+ Number.isNaN(submitTime) ||
273
+ Math.abs(Date.now() - submitTime) > 1000 * 60 * 5
274
+ ) {
275
+ throw new Error(`Form bot value invalid ${formBotValue}`);
276
+ }
277
+
278
+ const formProperties = formsProperties.get(formId);
279
+
280
+ // form properties are not defined when defaults are used
281
+ const { action, method } = formProperties ?? {};
282
+
283
+ if (contactEmail === undefined) {
284
+ throw new Error("Contact email not found");
285
+ }
286
+
287
+ const pageUrl = new URL(request.url);
267
288
  pageUrl.host = getRequestHost(request);
268
- } catch {
269
- return { success: false };
270
- }
271
289
 
272
- if (action !== undefined) {
273
- try {
274
- // Test that action is full URL
275
- new URL(action);
276
- } catch {
277
- return json(
278
- {
279
- success: false,
280
- error: "Invalid action URL, must be valid http/https protocol",
281
- },
282
- { status: 200 }
283
- );
290
+ if (action !== undefined) {
291
+ try {
292
+ // Test that action is full URL
293
+ new URL(action);
294
+ } catch {
295
+ throw new Error(
296
+ "Invalid action URL, must be valid http/https protocol"
297
+ );
298
+ }
284
299
  }
285
- }
286
300
 
287
- const formInfo = {
288
- formData,
289
- projectId,
290
- action: action ?? null,
291
- method: getMethod(method),
292
- pageUrl: pageUrl.toString(),
293
- toEmail: contactEmail,
294
- fromEmail: pageUrl.hostname + "@webstudio.email",
295
- } as const;
296
-
297
- const result = await n8nHandler({
298
- formInfo,
299
- hookUrl: context.N8N_FORM_EMAIL_HOOK,
300
- });
301
+ const formInfo = {
302
+ formData,
303
+ projectId,
304
+ action: action ?? null,
305
+ method: getMethod(method),
306
+ pageUrl: pageUrl.toString(),
307
+ toEmail: contactEmail,
308
+ fromEmail: pageUrl.hostname + "@webstudio.email",
309
+ } as const;
310
+
311
+ const result = await n8nHandler({
312
+ formInfo,
313
+ hookUrl: context.N8N_FORM_EMAIL_HOOK,
314
+ });
301
315
 
302
- return result;
316
+ return result;
317
+ } catch (error) {
318
+ console.error(error);
319
+
320
+ return {
321
+ success: false,
322
+ errors: [error instanceof Error ? error.message : "Unknown error"],
323
+ };
324
+ }
303
325
  };
304
326
 
305
327
  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,26 +8,26 @@
8
8
  "typecheck": "tsc"
9
9
  },
10
10
  "dependencies": {
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.151.0",
15
- "@webstudio-is/sdk-components-react-radix": "0.151.0",
16
- "@webstudio-is/sdk-components-react-remix": "0.151.0",
17
- "@webstudio-is/sdk-components-react": "0.151.0",
18
- "@webstudio-is/form-handlers": "0.151.0",
19
- "@webstudio-is/image": "0.151.0",
20
- "@webstudio-is/sdk": "0.151.0",
21
- "isbot": "^3.6.8",
11
+ "@remix-run/node": "2.9.2",
12
+ "@remix-run/react": "2.9.2",
13
+ "@remix-run/server-runtime": "2.9.2",
14
+ "@webstudio-is/react-sdk": "0.167.0",
15
+ "@webstudio-is/sdk-components-react-radix": "0.167.0",
16
+ "@webstudio-is/sdk-components-react-remix": "0.167.0",
17
+ "@webstudio-is/sdk-components-react": "0.167.0",
18
+ "@webstudio-is/form-handlers": "0.167.0",
19
+ "@webstudio-is/image": "0.167.0",
20
+ "@webstudio-is/sdk": "0.167.0",
21
+ "isbot": "^5.1.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.9.1",
26
+ "@remix-run/dev": "2.9.2",
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.11"
30
+ "vite": "^5.2.12"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=20.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
3
3
  "compilerOptions": {
4
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
4
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
5
  "types": ["@remix-run/node", "vite/client"],
6
6
  "isolatedModules": true,
7
7
  "esModuleInterop": true,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
3
3
  "compilerOptions": {
4
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
4
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
5
5
  "types": ["@remix-run/node", "vite/client"],
6
6
  "isolatedModules": true,
7
7
  "esModuleInterop": true,
@@ -3,7 +3,7 @@
3
3
  "start": "netlify serve"
4
4
  },
5
5
  "dependencies": {
6
- "@netlify/edge-functions": "^2.6.0",
7
- "@netlify/remix-edge-adapter": "^3.2.2"
6
+ "@netlify/edge-functions": "^2.8.1",
7
+ "@netlify/remix-edge-adapter": "^3.3.0"
8
8
  }
9
9
  }
@@ -3,7 +3,7 @@
3
3
  "start": "netlify serve"
4
4
  },
5
5
  "dependencies": {
6
- "@netlify/functions": "^2.6.0",
7
- "@netlify/remix-adapter": "^2.3.1"
6
+ "@netlify/functions": "^2.7.0",
7
+ "@netlify/remix-adapter": "^2.4.0"
8
8
  }
9
9
  }
@@ -8,7 +8,7 @@
8
8
  "**/.client/**/*.tsx"
9
9
  ],
10
10
  "compilerOptions": {
11
- "lib": ["DOM", "DOM.Iterable", "ES2022"],
11
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
12
12
  "types": [
13
13
  "@remix-run/cloudflare",
14
14
  "vite/client",
@@ -1,9 +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
- {
6
- path: "",
7
- lastModified: "2021-10-13T12:00:00.000Z",
8
- },
9
- ];