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/lib/cli.js +427 -245
- package/package.json +27 -25
- package/templates/cloudflare/functions/[[path]].ts +0 -1
- package/templates/cloudflare/package.json +3 -3
- package/templates/cloudflare/tsconfig.json +1 -1
- package/templates/defaults/app/root.tsx +20 -2
- package/templates/defaults/app/{routes/[sitemap.xml].tsx → route-templates/default-sitemap.tsx} +1 -1
- package/templates/defaults/app/{routes/template.tsx → route-templates/html.tsx} +75 -53
- package/templates/defaults/app/route-templates/xml.tsx +61 -0
- package/templates/defaults/package.json +13 -13
- package/templates/defaults/tsconfig.json +1 -1
- package/templates/internal/tsconfig.json +1 -1
- package/templates/netlify-edge-functions/package.json +2 -2
- package/templates/netlify-functions/package.json +2 -2
- package/templates/saas-helpers/tsconfig.json +1 -1
- package/templates/defaults/app/__generated__/[sitemap.xml].ts +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstudio",
|
|
3
|
-
"version": "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
|
-
"
|
|
25
|
+
"parse5": "7.1.2",
|
|
24
26
|
"p-limit": "^4.0.0",
|
|
25
|
-
"picocolors": "^1.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.
|
|
32
|
-
"@webstudio-is/
|
|
33
|
-
"@webstudio-is/
|
|
34
|
-
"@webstudio-is/sdk": "0.
|
|
35
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
36
|
-
"@webstudio-is/sdk-components-react
|
|
37
|
-
"@webstudio-is/sdk-components-react": "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
|
-
"@
|
|
41
|
-
"@netlify/remix-
|
|
42
|
-
"@remix-
|
|
43
|
-
"@remix-run/cloudflare
|
|
44
|
-
"@remix-run/
|
|
45
|
-
"@remix-run/
|
|
46
|
-
"@remix-run/
|
|
47
|
-
"@remix-run/
|
|
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.
|
|
57
|
+
"vite": "^5.2.12",
|
|
58
58
|
"wrangler": "^3.48.0",
|
|
59
|
-
"@webstudio-is/form-handlers": "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.
|
|
16
|
-
"@remix-run/cloudflare-pages": "2.9.
|
|
17
|
-
"isbot": "^
|
|
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",
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
|
|
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=
|
|
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>
|
package/templates/defaults/app/{routes/[sitemap.xml].tsx → route-templates/default-sitemap.tsx}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
|
2
|
-
import { sitemap } from "
|
|
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 {
|
|
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 ({
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
if (formId == null || typeof formId !== "string") {
|
|
257
|
+
throw new Error("No form id in FormData");
|
|
258
|
+
}
|
|
254
259
|
|
|
255
|
-
|
|
256
|
-
const { action, method } = formProperties ?? {};
|
|
260
|
+
const formBotValue = formData.get(formBotFieldName);
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
if (formBotValue == null || typeof formBotValue !== "string") {
|
|
263
|
+
throw new Error("Form bot field not found");
|
|
264
|
+
}
|
|
261
265
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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.
|
|
12
|
-
"@remix-run/react": "2.9.
|
|
13
|
-
"@remix-run/server-runtime": "2.9.
|
|
14
|
-
"@webstudio-is/react-sdk": "0.
|
|
15
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
16
|
-
"@webstudio-is/sdk-components-react-remix": "0.
|
|
17
|
-
"@webstudio-is/sdk-components-react": "0.
|
|
18
|
-
"@webstudio-is/form-handlers": "0.
|
|
19
|
-
"@webstudio-is/image": "0.
|
|
20
|
-
"@webstudio-is/sdk": "0.
|
|
21
|
-
"isbot": "^
|
|
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.
|
|
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.
|
|
30
|
+
"vite": "^5.2.12"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=20.0.0"
|