webstudio 0.267.0 → 0.269.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 +628 -50
- package/package.json +15 -14
- package/templates/cloudflare/package.json +1 -0
- package/templates/defaults/app/redirect-url.ts +21 -0
- package/templates/defaults/app/root.tsx +9 -0
- package/templates/defaults/app/route-templates/html.tsx +56 -12
- package/templates/defaults/app/route-templates/redirect.tsx +4 -3
- package/templates/defaults/app/route-templates/text.tsx +107 -0
- package/templates/defaults/app/route-templates/xml.tsx +28 -2
- package/templates/defaults/package.json +8 -7
- package/templates/internal/package.json +2 -1
- package/templates/react-router/app/redirect-url.ts +21 -0
- package/templates/react-router/app/root.tsx +15 -1
- package/templates/react-router/app/route-templates/html.tsx +56 -12
- package/templates/react-router/app/route-templates/redirect.tsx +4 -3
- package/templates/react-router/app/route-templates/text.tsx +107 -0
- package/templates/react-router/app/route-templates/xml.tsx +28 -2
- package/templates/react-router/package.json +8 -7
- package/templates/react-router-cloudflare/package.json +1 -0
- package/templates/ssg/app/route-templates/html/+Page.tsx +34 -11
- package/templates/ssg/package.json +6 -6
- package/templates/ssg/renderer/+onRenderClient.tsx +3 -2
- package/templates/ssg/public/favicon.ico +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstudio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.269.0",
|
|
4
4
|
"description": "Webstudio CLI",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"warn-once": "^0.1.1",
|
|
44
44
|
"yargs": "^17.7.2",
|
|
45
45
|
"zod": "^3.24.2",
|
|
46
|
-
"@webstudio-is/project-migrations": "0.
|
|
47
|
-
"@webstudio-is/trpc-interface": "0.
|
|
46
|
+
"@webstudio-is/project-migrations": "0.269.0",
|
|
47
|
+
"@webstudio-is/trpc-interface": "0.269.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@cloudflare/vite-plugin": "^1.1.0",
|
|
@@ -74,17 +74,18 @@
|
|
|
74
74
|
"vite": "^6.3.4",
|
|
75
75
|
"vitest": "^3.1.2",
|
|
76
76
|
"wrangler": "^3.63.2",
|
|
77
|
-
"@webstudio-is/
|
|
78
|
-
"@webstudio-is/
|
|
79
|
-
"@webstudio-is/react-sdk": "0.
|
|
80
|
-
"@webstudio-is/
|
|
81
|
-
"@webstudio-is/
|
|
82
|
-
"@webstudio-is/sdk
|
|
83
|
-
"@webstudio-is/sdk-components-react": "0.
|
|
84
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
85
|
-
"@webstudio-is/sdk-components-react-
|
|
86
|
-
"@webstudio-is/sdk-components-react-
|
|
87
|
-
"@webstudio-is/tsconfig": "1.0.7"
|
|
77
|
+
"@webstudio-is/http-client": "0.269.0",
|
|
78
|
+
"@webstudio-is/image": "0.269.0",
|
|
79
|
+
"@webstudio-is/react-sdk": "0.269.0",
|
|
80
|
+
"@webstudio-is/sdk-components-animation": "0.269.0",
|
|
81
|
+
"@webstudio-is/css-engine": "0.269.0",
|
|
82
|
+
"@webstudio-is/sdk": "0.269.0",
|
|
83
|
+
"@webstudio-is/sdk-components-react": "0.269.0",
|
|
84
|
+
"@webstudio-is/sdk-components-react-radix": "0.269.0",
|
|
85
|
+
"@webstudio-is/sdk-components-react-remix": "0.269.0",
|
|
86
|
+
"@webstudio-is/sdk-components-react-router": "0.269.0",
|
|
87
|
+
"@webstudio-is/tsconfig": "1.0.7",
|
|
88
|
+
"@webstudio-is/wsauth": "0.269.0"
|
|
88
89
|
},
|
|
89
90
|
"scripts": {
|
|
90
91
|
"typecheck": "tsgo --noEmit",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createPath, generatePath, parsePath } from "@remix-run/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expands route params in local redirect targets.
|
|
5
|
+
* External and protocol-relative URLs are returned unchanged because route params
|
|
6
|
+
* only apply to app paths.
|
|
7
|
+
*/
|
|
8
|
+
export const generateRedirectUrl = (
|
|
9
|
+
url: string,
|
|
10
|
+
params: Record<string, string | undefined>
|
|
11
|
+
) => {
|
|
12
|
+
if (url.startsWith("/") === false || url.startsWith("//")) {
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const path = parsePath(url);
|
|
17
|
+
return createPath({
|
|
18
|
+
...path,
|
|
19
|
+
pathname: generatePath(path.pathname ?? "/", params),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
2
|
|
|
3
3
|
import { Links, Meta, Outlet, useMatches } from "@remix-run/react";
|
|
4
|
+
import type { HeadersFunction } from "@remix-run/server-runtime";
|
|
4
5
|
// @todo think about how to make __generated__ typeable
|
|
5
6
|
// @ts-ignore
|
|
6
7
|
import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
|
|
7
8
|
|
|
9
|
+
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
10
|
+
if (errorHeaders) {
|
|
11
|
+
return errorHeaders;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {};
|
|
15
|
+
};
|
|
16
|
+
|
|
8
17
|
const Root = () => {
|
|
9
18
|
// Get language from matches
|
|
10
19
|
const matches = useMatches();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComponentProps, memo, useMemo } from "react";
|
|
1
2
|
import {
|
|
2
3
|
type ServerRuntimeMetaFunction as MetaFunction,
|
|
3
4
|
type LinksFunction,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
formIdFieldName,
|
|
18
19
|
formBotFieldName,
|
|
19
20
|
} from "@webstudio-is/sdk/runtime";
|
|
21
|
+
import { authenticateRequest } from "@webstudio-is/wsauth";
|
|
20
22
|
import {
|
|
21
23
|
ReactSdkContext,
|
|
22
24
|
PageSettingsMeta,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
} from "@webstudio-is/react-sdk/runtime";
|
|
26
28
|
import {
|
|
27
29
|
projectId,
|
|
30
|
+
projectDomain,
|
|
28
31
|
Page,
|
|
29
32
|
siteName,
|
|
30
33
|
favIconAsset,
|
|
@@ -42,6 +45,25 @@ import * as constants from "__CONSTANTS__";
|
|
|
42
45
|
import css from "__CSS__?url";
|
|
43
46
|
import { sitemap } from "__SITEMAP__";
|
|
44
47
|
import { assets } from "__ASSETS__";
|
|
48
|
+
import { authRoutes } from "__AUTH__";
|
|
49
|
+
|
|
50
|
+
const authenticateProductionRequest = (request: Request) => {
|
|
51
|
+
const host =
|
|
52
|
+
request.headers.get("x-forwarded-host") ||
|
|
53
|
+
request.headers.get("host") ||
|
|
54
|
+
"";
|
|
55
|
+
|
|
56
|
+
const requestHost = host.split(":")[0];
|
|
57
|
+
if (
|
|
58
|
+
projectDomain !== undefined &&
|
|
59
|
+
(requestHost === projectDomain ||
|
|
60
|
+
requestHost.startsWith(`${projectDomain}.`))
|
|
61
|
+
) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return authenticateRequest(request, authRoutes);
|
|
66
|
+
};
|
|
45
67
|
|
|
46
68
|
const customFetch: typeof fetch = (input, init) => {
|
|
47
69
|
if (typeof input !== "string") {
|
|
@@ -83,6 +105,8 @@ const customFetch: typeof fetch = (input, init) => {
|
|
|
83
105
|
};
|
|
84
106
|
|
|
85
107
|
export const loader = async (arg: LoaderFunctionArgs) => {
|
|
108
|
+
const authRoute = authenticateProductionRequest(arg.request);
|
|
109
|
+
|
|
86
110
|
const url = new URL(arg.request.url);
|
|
87
111
|
const host =
|
|
88
112
|
arg.request.headers.get("x-forwarded-host") ||
|
|
@@ -133,13 +157,18 @@ export const loader = async (arg: LoaderFunctionArgs) => {
|
|
|
133
157
|
{
|
|
134
158
|
status: pageMeta.status,
|
|
135
159
|
headers: {
|
|
136
|
-
"Cache-Control":
|
|
160
|
+
"Cache-Control":
|
|
161
|
+
authRoute === undefined ? "public, max-age=600" : "private, no-store",
|
|
137
162
|
},
|
|
138
163
|
}
|
|
139
164
|
);
|
|
140
165
|
};
|
|
141
166
|
|
|
142
|
-
export const headers: HeadersFunction = () => {
|
|
167
|
+
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
168
|
+
if (errorHeaders) {
|
|
169
|
+
return errorHeaders;
|
|
170
|
+
}
|
|
171
|
+
|
|
143
172
|
return {
|
|
144
173
|
"Cache-Control": "public, max-age=0, must-revalidate",
|
|
145
174
|
};
|
|
@@ -220,6 +249,8 @@ export const action = async ({
|
|
|
220
249
|
}: ActionFunctionArgs): Promise<
|
|
221
250
|
{ success: true } | { success: false; errors: string[] }
|
|
222
251
|
> => {
|
|
252
|
+
authenticateProductionRequest(request);
|
|
253
|
+
|
|
223
254
|
try {
|
|
224
255
|
const url = new URL(request.url);
|
|
225
256
|
url.host = getRequestHost(request);
|
|
@@ -298,20 +329,33 @@ export const action = async ({
|
|
|
298
329
|
}
|
|
299
330
|
};
|
|
300
331
|
|
|
332
|
+
const PageBoundary = memo(
|
|
333
|
+
({ url, system }: ComponentProps<typeof Page> & { url: string }) => {
|
|
334
|
+
// Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
|
|
335
|
+
return <Page key={url} system={system} />;
|
|
336
|
+
},
|
|
337
|
+
// React Router can rerender the current route while the next route loaders are
|
|
338
|
+
// still pending. Keep the generated page out of that pending-navigation render
|
|
339
|
+
// path, but let URL changes remount it.
|
|
340
|
+
(prevProps, nextProps) => prevProps.url === nextProps.url
|
|
341
|
+
);
|
|
342
|
+
|
|
301
343
|
const Outlet = () => {
|
|
302
344
|
const { system, resources, url, pageMeta, host } =
|
|
303
345
|
useLoaderData<typeof loader>();
|
|
346
|
+
const sdkContext = useMemo(
|
|
347
|
+
() => ({
|
|
348
|
+
...constants,
|
|
349
|
+
resources,
|
|
350
|
+
breakpoints,
|
|
351
|
+
onError: console.error,
|
|
352
|
+
}),
|
|
353
|
+
[resources]
|
|
354
|
+
);
|
|
355
|
+
|
|
304
356
|
return (
|
|
305
|
-
<ReactSdkContext.Provider
|
|
306
|
-
|
|
307
|
-
...constants,
|
|
308
|
-
resources,
|
|
309
|
-
breakpoints,
|
|
310
|
-
onError: console.error,
|
|
311
|
-
}}
|
|
312
|
-
>
|
|
313
|
-
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
|
314
|
-
<Page key={url} system={system} />
|
|
357
|
+
<ReactSdkContext.Provider value={sdkContext}>
|
|
358
|
+
<PageBoundary url={url} system={system} />
|
|
315
359
|
<PageSettingsMeta
|
|
316
360
|
url={url}
|
|
317
361
|
pageMeta={pageMeta}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { redirect } from "@remix-run/server-runtime";
|
|
1
|
+
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
|
2
|
+
import { generateRedirectUrl } from "../redirect-url";
|
|
2
3
|
import { url, status } from "__REDIRECT__";
|
|
3
4
|
|
|
4
|
-
export const loader = () => {
|
|
5
|
-
return redirect(url, status);
|
|
5
|
+
export const loader = (arg: LoaderFunctionArgs) => {
|
|
6
|
+
return redirect(generateRedirectUrl(url, arg.params), status);
|
|
6
7
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
|
2
|
+
import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
|
|
3
|
+
import { authenticateRequest } from "@webstudio-is/wsauth";
|
|
4
|
+
import { projectDomain } from "__CLIENT__";
|
|
5
|
+
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
|
6
|
+
import { sitemap } from "__SITEMAP__";
|
|
7
|
+
import { assets } from "__ASSETS__";
|
|
8
|
+
import { authRoutes } from "__AUTH__";
|
|
9
|
+
|
|
10
|
+
const authenticateProductionRequest = (request: Request) => {
|
|
11
|
+
const host =
|
|
12
|
+
request.headers.get("x-forwarded-host") ||
|
|
13
|
+
request.headers.get("host") ||
|
|
14
|
+
"";
|
|
15
|
+
|
|
16
|
+
const requestHost = host.split(":")[0];
|
|
17
|
+
if (
|
|
18
|
+
projectDomain !== undefined &&
|
|
19
|
+
(requestHost === projectDomain ||
|
|
20
|
+
requestHost.startsWith(`${projectDomain}.`))
|
|
21
|
+
) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return authenticateRequest(request, authRoutes);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const customFetch: typeof fetch = (input, init) => {
|
|
29
|
+
if (typeof input !== "string") {
|
|
30
|
+
return fetch(input, init);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isLocalResource(input, "sitemap.xml")) {
|
|
34
|
+
const response = new Response(JSON.stringify(sitemap));
|
|
35
|
+
response.headers.set("content-type", "application/json; charset=utf-8");
|
|
36
|
+
return Promise.resolve(response);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isLocalResource(input, "current-date")) {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const startOfDay = new Date(
|
|
42
|
+
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
|
43
|
+
);
|
|
44
|
+
const data = {
|
|
45
|
+
iso: startOfDay.toISOString(),
|
|
46
|
+
year: startOfDay.getUTCFullYear(),
|
|
47
|
+
month: startOfDay.getUTCMonth() + 1,
|
|
48
|
+
day: startOfDay.getUTCDate(),
|
|
49
|
+
timestamp: startOfDay.getTime(),
|
|
50
|
+
};
|
|
51
|
+
const response = new Response(JSON.stringify(data));
|
|
52
|
+
response.headers.set("content-type", "application/json; charset=utf-8");
|
|
53
|
+
return Promise.resolve(response);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isLocalResource(input, "assets")) {
|
|
57
|
+
const response = new Response(JSON.stringify(assets));
|
|
58
|
+
response.headers.set("content-type", "application/json; charset=utf-8");
|
|
59
|
+
return Promise.resolve(response);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return fetch(input, init);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const loader = async (arg: LoaderFunctionArgs) => {
|
|
66
|
+
const authRoute = authenticateProductionRequest(arg.request);
|
|
67
|
+
|
|
68
|
+
const url = new URL(arg.request.url);
|
|
69
|
+
const host =
|
|
70
|
+
arg.request.headers.get("x-forwarded-host") ||
|
|
71
|
+
arg.request.headers.get("host") ||
|
|
72
|
+
"";
|
|
73
|
+
url.host = host;
|
|
74
|
+
url.protocol = "https";
|
|
75
|
+
|
|
76
|
+
const params = getRemixParams(arg.params);
|
|
77
|
+
|
|
78
|
+
const system = {
|
|
79
|
+
params,
|
|
80
|
+
search: Object.fromEntries(url.searchParams),
|
|
81
|
+
origin: url.origin,
|
|
82
|
+
pathname: url.pathname,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const resources = await loadResources(
|
|
86
|
+
customFetch,
|
|
87
|
+
getResources({ system }).data
|
|
88
|
+
);
|
|
89
|
+
const pageMeta = getPageMeta({ system, resources });
|
|
90
|
+
|
|
91
|
+
if (pageMeta.redirect) {
|
|
92
|
+
const status =
|
|
93
|
+
pageMeta.status === 301 || pageMeta.status === 302
|
|
94
|
+
? pageMeta.status
|
|
95
|
+
: 302;
|
|
96
|
+
return redirect(pageMeta.redirect, status);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Response(pageMeta.content ?? "", {
|
|
100
|
+
status: pageMeta.status,
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
103
|
+
"Cache-Control":
|
|
104
|
+
authRoute === undefined ? "public, max-age=600" : "private, no-store",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
};
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import { renderToString } from "react-dom/server";
|
|
2
2
|
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
|
3
3
|
import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime";
|
|
4
|
+
import { authenticateRequest } from "@webstudio-is/wsauth";
|
|
4
5
|
import {
|
|
5
6
|
ReactSdkContext,
|
|
6
7
|
xmlNodeTagSuffix,
|
|
7
8
|
} from "@webstudio-is/react-sdk/runtime";
|
|
8
|
-
import { Page, breakpoints } from "__CLIENT__";
|
|
9
|
+
import { Page, breakpoints, projectDomain } from "__CLIENT__";
|
|
9
10
|
import { getPageMeta, getRemixParams, getResources } from "__SERVER__";
|
|
10
11
|
import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
|
|
11
12
|
import { sitemap } from "__SITEMAP__";
|
|
12
13
|
import { assets } from "__ASSETS__";
|
|
14
|
+
import { authRoutes } from "__AUTH__";
|
|
15
|
+
|
|
16
|
+
const authenticateProductionRequest = (request: Request) => {
|
|
17
|
+
const host =
|
|
18
|
+
request.headers.get("x-forwarded-host") ||
|
|
19
|
+
request.headers.get("host") ||
|
|
20
|
+
"";
|
|
21
|
+
|
|
22
|
+
const requestHost = host.split(":")[0];
|
|
23
|
+
if (
|
|
24
|
+
projectDomain !== undefined &&
|
|
25
|
+
(requestHost === projectDomain ||
|
|
26
|
+
requestHost.startsWith(`${projectDomain}.`))
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return authenticateRequest(request, authRoutes);
|
|
32
|
+
};
|
|
13
33
|
|
|
14
34
|
const customFetch: typeof fetch = (input, init) => {
|
|
15
35
|
if (typeof input !== "string") {
|
|
@@ -51,6 +71,8 @@ const customFetch: typeof fetch = (input, init) => {
|
|
|
51
71
|
};
|
|
52
72
|
|
|
53
73
|
export const loader = async (arg: LoaderFunctionArgs) => {
|
|
74
|
+
const authRoute = authenticateProductionRequest(arg.request);
|
|
75
|
+
|
|
54
76
|
const url = new URL(arg.request.url);
|
|
55
77
|
const host =
|
|
56
78
|
arg.request.headers.get("x-forwarded-host") ||
|
|
@@ -104,6 +126,10 @@ export const loader = async (arg: LoaderFunctionArgs) => {
|
|
|
104
126
|
text = text.replaceAll(xmlNodeTagSuffix, "");
|
|
105
127
|
|
|
106
128
|
return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n${text}`, {
|
|
107
|
-
headers: {
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/xml",
|
|
131
|
+
"Cache-Control":
|
|
132
|
+
authRoute === undefined ? "public, max-age=600" : "private, no-store",
|
|
133
|
+
},
|
|
108
134
|
});
|
|
109
135
|
};
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
"@remix-run/node": "2.16.5",
|
|
12
12
|
"@remix-run/react": "2.16.5",
|
|
13
13
|
"@remix-run/server-runtime": "2.16.5",
|
|
14
|
-
"@webstudio-is/image": "0.
|
|
15
|
-
"@webstudio-is/react-sdk": "0.
|
|
16
|
-
"@webstudio-is/sdk": "0.
|
|
17
|
-
"@webstudio-is/sdk-components-react": "0.
|
|
18
|
-
"@webstudio-is/sdk-components-animation": "0.
|
|
19
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
20
|
-
"@webstudio-is/sdk-components-react-remix": "0.
|
|
14
|
+
"@webstudio-is/image": "0.269.0",
|
|
15
|
+
"@webstudio-is/react-sdk": "0.269.0",
|
|
16
|
+
"@webstudio-is/sdk": "0.269.0",
|
|
17
|
+
"@webstudio-is/sdk-components-react": "0.269.0",
|
|
18
|
+
"@webstudio-is/sdk-components-animation": "0.269.0",
|
|
19
|
+
"@webstudio-is/sdk-components-react-radix": "0.269.0",
|
|
20
|
+
"@webstudio-is/sdk-components-react-remix": "0.269.0",
|
|
21
|
+
"@webstudio-is/wsauth": "0.269.0",
|
|
21
22
|
"isbot": "^5.1.25",
|
|
22
23
|
"react": "18.3.0-canary-14898b6a9-20240318",
|
|
23
24
|
"react-dom": "18.3.0-canary-14898b6a9-20240318"
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"@webstudio-is/sdk-components-animation": "workspace:*",
|
|
7
7
|
"@webstudio-is/sdk-components-react": "workspace:*",
|
|
8
8
|
"@webstudio-is/sdk-components-react-radix": "workspace:*",
|
|
9
|
-
"@webstudio-is/sdk-components-react-remix": "workspace:*"
|
|
9
|
+
"@webstudio-is/sdk-components-react-remix": "workspace:*",
|
|
10
|
+
"@webstudio-is/wsauth": "workspace:*"
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createPath, generatePath, parsePath } from "react-router";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expands route params in local redirect targets.
|
|
5
|
+
* External and protocol-relative URLs are returned unchanged because route params
|
|
6
|
+
* only apply to app paths.
|
|
7
|
+
*/
|
|
8
|
+
export const generateRedirectUrl = (
|
|
9
|
+
url: string,
|
|
10
|
+
params: Record<string, string | undefined>
|
|
11
|
+
) => {
|
|
12
|
+
if (url.startsWith("/") === false || url.startsWith("//")) {
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const path = parsePath(url);
|
|
17
|
+
return createPath({
|
|
18
|
+
...path,
|
|
19
|
+
pathname: generatePath(path.pathname ?? "/", params),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type HeadersFunction,
|
|
5
|
+
Links,
|
|
6
|
+
Meta,
|
|
7
|
+
Outlet,
|
|
8
|
+
useMatches,
|
|
9
|
+
} from "react-router";
|
|
4
10
|
// @todo think about how to make __generated__ typeable
|
|
5
11
|
// @ts-ignore
|
|
6
12
|
import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
|
|
7
13
|
|
|
14
|
+
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
15
|
+
if (errorHeaders) {
|
|
16
|
+
return errorHeaders;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {};
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
const Root = () => {
|
|
9
23
|
// Get language from matches
|
|
10
24
|
const matches = useMatches();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComponentProps, memo, useMemo } from "react";
|
|
1
2
|
import {
|
|
2
3
|
type MetaFunction,
|
|
3
4
|
type LinksFunction,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
formBotFieldName,
|
|
18
19
|
cachedFetch,
|
|
19
20
|
} from "@webstudio-is/sdk/runtime";
|
|
21
|
+
import { authenticateRequest } from "@webstudio-is/wsauth";
|
|
20
22
|
import {
|
|
21
23
|
ReactSdkContext,
|
|
22
24
|
PageSettingsMeta,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
} from "@webstudio-is/react-sdk/runtime";
|
|
25
27
|
import {
|
|
26
28
|
projectId,
|
|
29
|
+
projectDomain,
|
|
27
30
|
Page,
|
|
28
31
|
siteName,
|
|
29
32
|
favIconAsset,
|
|
@@ -41,6 +44,25 @@ import * as constants from "__CONSTANTS__";
|
|
|
41
44
|
import css from "__CSS__?url";
|
|
42
45
|
import { sitemap } from "__SITEMAP__";
|
|
43
46
|
import { assets } from "__ASSETS__";
|
|
47
|
+
import { authRoutes } from "__AUTH__";
|
|
48
|
+
|
|
49
|
+
const authenticateProductionRequest = (request: Request) => {
|
|
50
|
+
const host =
|
|
51
|
+
request.headers.get("x-forwarded-host") ||
|
|
52
|
+
request.headers.get("host") ||
|
|
53
|
+
"";
|
|
54
|
+
|
|
55
|
+
const requestHost = host.split(":")[0];
|
|
56
|
+
if (
|
|
57
|
+
projectDomain !== undefined &&
|
|
58
|
+
(requestHost === projectDomain ||
|
|
59
|
+
requestHost.startsWith(`${projectDomain}.`))
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return authenticateRequest(request, authRoutes);
|
|
65
|
+
};
|
|
44
66
|
|
|
45
67
|
const customFetch: typeof fetch = (input, init) => {
|
|
46
68
|
if (typeof input !== "string") {
|
|
@@ -82,6 +104,8 @@ const customFetch: typeof fetch = (input, init) => {
|
|
|
82
104
|
};
|
|
83
105
|
|
|
84
106
|
export const loader = async (arg: LoaderFunctionArgs) => {
|
|
107
|
+
const authRoute = authenticateProductionRequest(arg.request);
|
|
108
|
+
|
|
85
109
|
const url = new URL(arg.request.url);
|
|
86
110
|
const host =
|
|
87
111
|
arg.request.headers.get("x-forwarded-host") ||
|
|
@@ -132,13 +156,18 @@ export const loader = async (arg: LoaderFunctionArgs) => {
|
|
|
132
156
|
{
|
|
133
157
|
status: pageMeta.status,
|
|
134
158
|
headers: {
|
|
135
|
-
"Cache-Control":
|
|
159
|
+
"Cache-Control":
|
|
160
|
+
authRoute === undefined ? "public, max-age=600" : "private, no-store",
|
|
136
161
|
},
|
|
137
162
|
}
|
|
138
163
|
);
|
|
139
164
|
};
|
|
140
165
|
|
|
141
|
-
export const headers: HeadersFunction = () => {
|
|
166
|
+
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
167
|
+
if (errorHeaders) {
|
|
168
|
+
return errorHeaders;
|
|
169
|
+
}
|
|
170
|
+
|
|
142
171
|
return {
|
|
143
172
|
"Cache-Control": "public, max-age=0, must-revalidate",
|
|
144
173
|
};
|
|
@@ -219,6 +248,8 @@ export const action = async ({
|
|
|
219
248
|
}: ActionFunctionArgs): Promise<
|
|
220
249
|
{ success: true } | { success: false; errors: string[] }
|
|
221
250
|
> => {
|
|
251
|
+
authenticateProductionRequest(request);
|
|
252
|
+
|
|
222
253
|
try {
|
|
223
254
|
const url = new URL(request.url);
|
|
224
255
|
url.host = getRequestHost(request);
|
|
@@ -297,20 +328,33 @@ export const action = async ({
|
|
|
297
328
|
}
|
|
298
329
|
};
|
|
299
330
|
|
|
331
|
+
const PageBoundary = memo(
|
|
332
|
+
({ url, system }: ComponentProps<typeof Page> & { url: string }) => {
|
|
333
|
+
// Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
|
|
334
|
+
return <Page key={url} system={system} />;
|
|
335
|
+
},
|
|
336
|
+
// React Router can rerender the current route while the next route loaders are
|
|
337
|
+
// still pending. Keep the generated page out of that pending-navigation render
|
|
338
|
+
// path, but let URL changes remount it.
|
|
339
|
+
(prevProps, nextProps) => prevProps.url === nextProps.url
|
|
340
|
+
);
|
|
341
|
+
|
|
300
342
|
const Outlet = () => {
|
|
301
343
|
const { system, resources, url, pageMeta, host } =
|
|
302
344
|
useLoaderData<typeof loader>();
|
|
345
|
+
const sdkContext = useMemo(
|
|
346
|
+
() => ({
|
|
347
|
+
...constants,
|
|
348
|
+
resources,
|
|
349
|
+
breakpoints,
|
|
350
|
+
onError: console.error,
|
|
351
|
+
}),
|
|
352
|
+
[resources]
|
|
353
|
+
);
|
|
354
|
+
|
|
303
355
|
return (
|
|
304
|
-
<ReactSdkContext.Provider
|
|
305
|
-
|
|
306
|
-
...constants,
|
|
307
|
-
resources,
|
|
308
|
-
breakpoints,
|
|
309
|
-
onError: console.error,
|
|
310
|
-
}}
|
|
311
|
-
>
|
|
312
|
-
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
|
313
|
-
<Page key={url} system={system} />
|
|
356
|
+
<ReactSdkContext.Provider value={sdkContext}>
|
|
357
|
+
<PageBoundary url={url} system={system} />
|
|
314
358
|
<PageSettingsMeta
|
|
315
359
|
url={url}
|
|
316
360
|
pageMeta={pageMeta}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { redirect } from "react-router";
|
|
1
|
+
import { type LoaderFunctionArgs, redirect } from "react-router";
|
|
2
|
+
import { generateRedirectUrl } from "../redirect-url";
|
|
2
3
|
import { url, status } from "__REDIRECT__";
|
|
3
4
|
|
|
4
|
-
export const loader = () => {
|
|
5
|
-
throw redirect(url, status);
|
|
5
|
+
export const loader = (arg: LoaderFunctionArgs) => {
|
|
6
|
+
throw redirect(generateRedirectUrl(url, arg.params), status);
|
|
6
7
|
};
|