webstudio 0.268.0 → 0.270.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 +2451 -5103
- package/package.json +15 -15
- package/templates/cloudflare/package.json +1 -1
- package/templates/defaults/app/redirect-url.ts +167 -0
- package/templates/defaults/app/root.tsx +16 -1
- package/templates/defaults/app/route-templates/html.tsx +24 -10
- package/templates/defaults/package.json +8 -8
- package/templates/react-router/app/redirect-url.ts +167 -0
- package/templates/react-router/app/root.tsx +13 -0
- package/templates/react-router/app/route-templates/html.tsx +24 -10
- package/templates/react-router/package.json +8 -8
- package/templates/react-router-cloudflare/package.json +1 -1
- 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/defaults/app/route-templates/redirect.tsx +0 -6
- package/templates/react-router/app/route-templates/redirect.tsx +0 -6
- 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.270.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/
|
|
47
|
-
"@webstudio-is/
|
|
46
|
+
"@webstudio-is/project-migrations": "0.270.0",
|
|
47
|
+
"@webstudio-is/trpc-interface": "0.270.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@cloudflare/vite-plugin": "^1.1.0",
|
|
@@ -74,18 +74,18 @@
|
|
|
74
74
|
"vite": "^6.3.4",
|
|
75
75
|
"vitest": "^3.1.2",
|
|
76
76
|
"wrangler": "^3.63.2",
|
|
77
|
-
"@webstudio-is/css-engine": "0.
|
|
78
|
-
"@webstudio-is/
|
|
79
|
-
"@webstudio-is/
|
|
80
|
-
"@webstudio-is/react-sdk": "0.
|
|
81
|
-
"@webstudio-is/sdk": "0.
|
|
82
|
-
"@webstudio-is/sdk-components-animation": "0.
|
|
83
|
-
"@webstudio-is/sdk-components-react
|
|
84
|
-
"@webstudio-is/sdk-components-react-
|
|
85
|
-
"@webstudio-is/sdk-components-react-
|
|
86
|
-
"@webstudio-is/
|
|
87
|
-
"@webstudio-is/
|
|
88
|
-
"@webstudio-is/
|
|
77
|
+
"@webstudio-is/css-engine": "0.270.0",
|
|
78
|
+
"@webstudio-is/image": "0.270.0",
|
|
79
|
+
"@webstudio-is/http-client": "0.270.0",
|
|
80
|
+
"@webstudio-is/react-sdk": "0.270.0",
|
|
81
|
+
"@webstudio-is/sdk": "0.270.0",
|
|
82
|
+
"@webstudio-is/sdk-components-animation": "0.270.0",
|
|
83
|
+
"@webstudio-is/sdk-components-react": "0.270.0",
|
|
84
|
+
"@webstudio-is/sdk-components-react-radix": "0.270.0",
|
|
85
|
+
"@webstudio-is/sdk-components-react-remix": "0.270.0",
|
|
86
|
+
"@webstudio-is/tsconfig": "1.0.7",
|
|
87
|
+
"@webstudio-is/sdk-components-react-router": "0.270.0",
|
|
88
|
+
"@webstudio-is/wsauth": "0.270.0"
|
|
89
89
|
},
|
|
90
90
|
"scripts": {
|
|
91
91
|
"typecheck": "tsgo --noEmit",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPath,
|
|
3
|
+
generatePath,
|
|
4
|
+
matchPath,
|
|
5
|
+
parsePath,
|
|
6
|
+
} from "@remix-run/react";
|
|
7
|
+
import { redirect } from "@remix-run/server-runtime";
|
|
8
|
+
|
|
9
|
+
type RedirectItem = {
|
|
10
|
+
old: string;
|
|
11
|
+
new: string;
|
|
12
|
+
status?: number | string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type MatchedRedirect = {
|
|
16
|
+
url: string;
|
|
17
|
+
status: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Expands route params in local redirect targets.
|
|
22
|
+
* External and protocol-relative URLs are returned unchanged because route params
|
|
23
|
+
* only apply to app paths.
|
|
24
|
+
*/
|
|
25
|
+
export const generateRedirectUrl = (
|
|
26
|
+
url: string,
|
|
27
|
+
params: Record<string, string | undefined>
|
|
28
|
+
) => {
|
|
29
|
+
if (
|
|
30
|
+
/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url) ||
|
|
31
|
+
url.startsWith("//") ||
|
|
32
|
+
url.startsWith("?") ||
|
|
33
|
+
url.startsWith("#")
|
|
34
|
+
) {
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const targetParams = Object.fromEntries(
|
|
39
|
+
Object.entries(params).map(([name, value]) => [
|
|
40
|
+
name,
|
|
41
|
+
name === "*" ? value : value?.replaceAll("/", "%2F"),
|
|
42
|
+
])
|
|
43
|
+
);
|
|
44
|
+
const path = parsePath(url);
|
|
45
|
+
try {
|
|
46
|
+
return createPath({
|
|
47
|
+
...path,
|
|
48
|
+
pathname: generatePath(path.pathname ?? "/", targetParams),
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const stripHash = (source: string) => {
|
|
56
|
+
const hashIndex = source.indexOf("#");
|
|
57
|
+
return hashIndex === -1 ? source : source.slice(0, hashIndex);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getRedirectStatus = (status: RedirectItem["status"]) => {
|
|
61
|
+
return Number(status) === 302 ? 302 : 301;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const decodePathname = (pathname: string) => {
|
|
65
|
+
try {
|
|
66
|
+
return decodeURI(pathname);
|
|
67
|
+
} catch {
|
|
68
|
+
return pathname;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getPathnameVariants = (pathname: string) => {
|
|
73
|
+
return Array.from(new Set([pathname, decodePathname(pathname)]));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const isOptionalSegmentMarker = (source: string, index: number) => {
|
|
77
|
+
const nextChar = source[index + 1];
|
|
78
|
+
if (nextChar !== undefined && nextChar !== "/") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const segmentStart = source.lastIndexOf("/", index - 1);
|
|
83
|
+
const segment = source.slice(segmentStart + 1, index);
|
|
84
|
+
return segment !== "";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getSourceSearchIndex = (source: string) => {
|
|
88
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
89
|
+
if (
|
|
90
|
+
source[index] === "?" &&
|
|
91
|
+
isOptionalSegmentMarker(source, index) === false
|
|
92
|
+
) {
|
|
93
|
+
return index;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return -1;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isRedirectPattern = (source: string) => {
|
|
100
|
+
return (
|
|
101
|
+
/(^|\/):[^/]+/.test(source) ||
|
|
102
|
+
/(^|\/)\*(?=\/|$)/.test(source) ||
|
|
103
|
+
/(^|\/)[^/]+\?(?=\/|$)/.test(source)
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const matchRedirect = (
|
|
108
|
+
requestUrl: string,
|
|
109
|
+
redirects: RedirectItem[]
|
|
110
|
+
): MatchedRedirect | undefined => {
|
|
111
|
+
const url = new URL(requestUrl);
|
|
112
|
+
const requestPathnames = getPathnameVariants(url.pathname);
|
|
113
|
+
|
|
114
|
+
for (const redirect of redirects) {
|
|
115
|
+
const source = stripHash(redirect.old);
|
|
116
|
+
if (getSourceSearchIndex(source) !== -1) {
|
|
117
|
+
const exactMatch = requestPathnames.some(
|
|
118
|
+
(requestPathname) => source === `${requestPathname}${url.search}`
|
|
119
|
+
);
|
|
120
|
+
if (exactMatch) {
|
|
121
|
+
return {
|
|
122
|
+
url: generateRedirectUrl(redirect.new, {}),
|
|
123
|
+
status: getRedirectStatus(redirect.status),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isRedirectPattern(source) === false) {
|
|
130
|
+
if (requestPathnames.includes(source)) {
|
|
131
|
+
return {
|
|
132
|
+
url: generateRedirectUrl(redirect.new, {}),
|
|
133
|
+
status: getRedirectStatus(redirect.status),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const match = requestPathnames
|
|
140
|
+
.map((requestPathname) =>
|
|
141
|
+
matchPath(
|
|
142
|
+
{ path: source, caseSensitive: true, end: true },
|
|
143
|
+
requestPathname
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
.find((match) => match !== null);
|
|
147
|
+
if (match === undefined) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
url: generateRedirectUrl(redirect.new, match.params),
|
|
153
|
+
status: getRedirectStatus(redirect.status),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const redirectRequest = (
|
|
159
|
+
request: Request,
|
|
160
|
+
redirects: RedirectItem[]
|
|
161
|
+
): Response | undefined => {
|
|
162
|
+
const matchedRedirect = matchRedirect(request.url, redirects);
|
|
163
|
+
if (matchedRedirect === undefined) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return redirect(matchedRedirect.url, matchedRedirect.status);
|
|
167
|
+
};
|
|
@@ -1,10 +1,25 @@
|
|
|
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 {
|
|
4
|
+
import type {
|
|
5
|
+
HeadersFunction,
|
|
6
|
+
LoaderFunctionArgs,
|
|
7
|
+
} from "@remix-run/server-runtime";
|
|
8
|
+
import { redirectRequest } from "./redirect-url";
|
|
5
9
|
// @todo think about how to make __generated__ typeable
|
|
6
10
|
// @ts-ignore
|
|
7
11
|
import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
import { redirects } from "./__generated__/$resources.redirects";
|
|
14
|
+
|
|
15
|
+
export const loader = ({ request }: LoaderFunctionArgs) => {
|
|
16
|
+
const redirectResponse = redirectRequest(request, redirects);
|
|
17
|
+
if (redirectResponse !== undefined) {
|
|
18
|
+
return redirectResponse;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
8
23
|
|
|
9
24
|
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
10
25
|
if (errorHeaders) {
|
|
@@ -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,
|
|
@@ -328,20 +329,33 @@ export const action = async ({
|
|
|
328
329
|
}
|
|
329
330
|
};
|
|
330
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
|
+
|
|
331
343
|
const Outlet = () => {
|
|
332
344
|
const { system, resources, url, pageMeta, host } =
|
|
333
345
|
useLoaderData<typeof loader>();
|
|
346
|
+
const sdkContext = useMemo(
|
|
347
|
+
() => ({
|
|
348
|
+
...constants,
|
|
349
|
+
resources,
|
|
350
|
+
breakpoints,
|
|
351
|
+
onError: console.error,
|
|
352
|
+
}),
|
|
353
|
+
[resources]
|
|
354
|
+
);
|
|
355
|
+
|
|
334
356
|
return (
|
|
335
|
-
<ReactSdkContext.Provider
|
|
336
|
-
|
|
337
|
-
...constants,
|
|
338
|
-
resources,
|
|
339
|
-
breakpoints,
|
|
340
|
-
onError: console.error,
|
|
341
|
-
}}
|
|
342
|
-
>
|
|
343
|
-
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
|
344
|
-
<Page key={url} system={system} />
|
|
357
|
+
<ReactSdkContext.Provider value={sdkContext}>
|
|
358
|
+
<PageBoundary url={url} system={system} />
|
|
345
359
|
<PageSettingsMeta
|
|
346
360
|
url={url}
|
|
347
361
|
pageMeta={pageMeta}
|
|
@@ -11,14 +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.
|
|
21
|
-
"@webstudio-is/wsauth": "0.
|
|
14
|
+
"@webstudio-is/image": "0.270.0",
|
|
15
|
+
"@webstudio-is/react-sdk": "0.270.0",
|
|
16
|
+
"@webstudio-is/sdk": "0.270.0",
|
|
17
|
+
"@webstudio-is/sdk-components-react": "0.270.0",
|
|
18
|
+
"@webstudio-is/sdk-components-animation": "0.270.0",
|
|
19
|
+
"@webstudio-is/sdk-components-react-radix": "0.270.0",
|
|
20
|
+
"@webstudio-is/sdk-components-react-remix": "0.270.0",
|
|
21
|
+
"@webstudio-is/wsauth": "0.270.0",
|
|
22
22
|
"isbot": "^5.1.25",
|
|
23
23
|
"react": "18.3.0-canary-14898b6a9-20240318",
|
|
24
24
|
"react-dom": "18.3.0-canary-14898b6a9-20240318"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPath,
|
|
3
|
+
generatePath,
|
|
4
|
+
matchPath,
|
|
5
|
+
parsePath,
|
|
6
|
+
redirect,
|
|
7
|
+
} from "react-router";
|
|
8
|
+
|
|
9
|
+
type RedirectItem = {
|
|
10
|
+
old: string;
|
|
11
|
+
new: string;
|
|
12
|
+
status?: number | string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type MatchedRedirect = {
|
|
16
|
+
url: string;
|
|
17
|
+
status: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Expands route params in local redirect targets.
|
|
22
|
+
* External and protocol-relative URLs are returned unchanged because route params
|
|
23
|
+
* only apply to app paths.
|
|
24
|
+
*/
|
|
25
|
+
export const generateRedirectUrl = (
|
|
26
|
+
url: string,
|
|
27
|
+
params: Record<string, string | undefined>
|
|
28
|
+
) => {
|
|
29
|
+
if (
|
|
30
|
+
/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(url) ||
|
|
31
|
+
url.startsWith("//") ||
|
|
32
|
+
url.startsWith("?") ||
|
|
33
|
+
url.startsWith("#")
|
|
34
|
+
) {
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const targetParams = Object.fromEntries(
|
|
39
|
+
Object.entries(params).map(([name, value]) => [
|
|
40
|
+
name,
|
|
41
|
+
name === "*" ? value : value?.replaceAll("/", "%2F"),
|
|
42
|
+
])
|
|
43
|
+
);
|
|
44
|
+
const path = parsePath(url);
|
|
45
|
+
try {
|
|
46
|
+
return createPath({
|
|
47
|
+
...path,
|
|
48
|
+
pathname: generatePath(path.pathname ?? "/", targetParams),
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const stripHash = (source: string) => {
|
|
56
|
+
const hashIndex = source.indexOf("#");
|
|
57
|
+
return hashIndex === -1 ? source : source.slice(0, hashIndex);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getRedirectStatus = (status: RedirectItem["status"]) => {
|
|
61
|
+
return Number(status) === 302 ? 302 : 301;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const decodePathname = (pathname: string) => {
|
|
65
|
+
try {
|
|
66
|
+
return decodeURI(pathname);
|
|
67
|
+
} catch {
|
|
68
|
+
return pathname;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getPathnameVariants = (pathname: string) => {
|
|
73
|
+
return Array.from(new Set([pathname, decodePathname(pathname)]));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const isOptionalSegmentMarker = (source: string, index: number) => {
|
|
77
|
+
const nextChar = source[index + 1];
|
|
78
|
+
if (nextChar !== undefined && nextChar !== "/") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const segmentStart = source.lastIndexOf("/", index - 1);
|
|
83
|
+
const segment = source.slice(segmentStart + 1, index);
|
|
84
|
+
return segment !== "";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getSourceSearchIndex = (source: string) => {
|
|
88
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
89
|
+
if (
|
|
90
|
+
source[index] === "?" &&
|
|
91
|
+
isOptionalSegmentMarker(source, index) === false
|
|
92
|
+
) {
|
|
93
|
+
return index;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return -1;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isRedirectPattern = (source: string) => {
|
|
100
|
+
return (
|
|
101
|
+
/(^|\/):[^/]+/.test(source) ||
|
|
102
|
+
/(^|\/)\*(?=\/|$)/.test(source) ||
|
|
103
|
+
/(^|\/)[^/]+\?(?=\/|$)/.test(source)
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const matchRedirect = (
|
|
108
|
+
requestUrl: string,
|
|
109
|
+
redirects: RedirectItem[]
|
|
110
|
+
): MatchedRedirect | undefined => {
|
|
111
|
+
const url = new URL(requestUrl);
|
|
112
|
+
const requestPathnames = getPathnameVariants(url.pathname);
|
|
113
|
+
|
|
114
|
+
for (const redirect of redirects) {
|
|
115
|
+
const source = stripHash(redirect.old);
|
|
116
|
+
if (getSourceSearchIndex(source) !== -1) {
|
|
117
|
+
const exactMatch = requestPathnames.some(
|
|
118
|
+
(requestPathname) => source === `${requestPathname}${url.search}`
|
|
119
|
+
);
|
|
120
|
+
if (exactMatch) {
|
|
121
|
+
return {
|
|
122
|
+
url: generateRedirectUrl(redirect.new, {}),
|
|
123
|
+
status: getRedirectStatus(redirect.status),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isRedirectPattern(source) === false) {
|
|
130
|
+
if (requestPathnames.includes(source)) {
|
|
131
|
+
return {
|
|
132
|
+
url: generateRedirectUrl(redirect.new, {}),
|
|
133
|
+
status: getRedirectStatus(redirect.status),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const match = requestPathnames
|
|
140
|
+
.map((requestPathname) =>
|
|
141
|
+
matchPath(
|
|
142
|
+
{ path: source, caseSensitive: true, end: true },
|
|
143
|
+
requestPathname
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
.find((match) => match !== null);
|
|
147
|
+
if (match === undefined) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
url: generateRedirectUrl(redirect.new, match.params),
|
|
153
|
+
status: getRedirectStatus(redirect.status),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const redirectRequest = (
|
|
159
|
+
request: Request,
|
|
160
|
+
redirects: RedirectItem[]
|
|
161
|
+
): Response | undefined => {
|
|
162
|
+
const matchedRedirect = matchRedirect(request.url, redirects);
|
|
163
|
+
if (matchedRedirect === undefined) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return redirect(matchedRedirect.url, matchedRedirect.status);
|
|
167
|
+
};
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type HeadersFunction,
|
|
5
|
+
type LoaderFunctionArgs,
|
|
5
6
|
Links,
|
|
6
7
|
Meta,
|
|
7
8
|
Outlet,
|
|
8
9
|
useMatches,
|
|
9
10
|
} from "react-router";
|
|
11
|
+
import { redirectRequest } from "./redirect-url";
|
|
10
12
|
// @todo think about how to make __generated__ typeable
|
|
11
13
|
// @ts-ignore
|
|
12
14
|
import { CustomCode, projectId, lastPublished } from "./__generated__/_index";
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
import { redirects } from "./__generated__/$resources.redirects";
|
|
17
|
+
|
|
18
|
+
export const loader = ({ request }: LoaderFunctionArgs) => {
|
|
19
|
+
const redirectResponse = redirectRequest(request, redirects);
|
|
20
|
+
if (redirectResponse !== undefined) {
|
|
21
|
+
return redirectResponse;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
13
26
|
|
|
14
27
|
export const headers: HeadersFunction = ({ errorHeaders }) => {
|
|
15
28
|
if (errorHeaders) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComponentProps, memo, useMemo } from "react";
|
|
1
2
|
import {
|
|
2
3
|
type MetaFunction,
|
|
3
4
|
type LinksFunction,
|
|
@@ -327,20 +328,33 @@ export const action = async ({
|
|
|
327
328
|
}
|
|
328
329
|
};
|
|
329
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
|
+
|
|
330
342
|
const Outlet = () => {
|
|
331
343
|
const { system, resources, url, pageMeta, host } =
|
|
332
344
|
useLoaderData<typeof loader>();
|
|
345
|
+
const sdkContext = useMemo(
|
|
346
|
+
() => ({
|
|
347
|
+
...constants,
|
|
348
|
+
resources,
|
|
349
|
+
breakpoints,
|
|
350
|
+
onError: console.error,
|
|
351
|
+
}),
|
|
352
|
+
[resources]
|
|
353
|
+
);
|
|
354
|
+
|
|
333
355
|
return (
|
|
334
|
-
<ReactSdkContext.Provider
|
|
335
|
-
|
|
336
|
-
...constants,
|
|
337
|
-
resources,
|
|
338
|
-
breakpoints,
|
|
339
|
-
onError: console.error,
|
|
340
|
-
}}
|
|
341
|
-
>
|
|
342
|
-
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
|
343
|
-
<Page key={url} system={system} />
|
|
356
|
+
<ReactSdkContext.Provider value={sdkContext}>
|
|
357
|
+
<PageBoundary url={url} system={system} />
|
|
344
358
|
<PageSettingsMeta
|
|
345
359
|
url={url}
|
|
346
360
|
pageMeta={pageMeta}
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@react-router/dev": "^7.5.3",
|
|
12
12
|
"@react-router/fs-routes": "^7.5.3",
|
|
13
|
-
"@webstudio-is/image": "0.
|
|
14
|
-
"@webstudio-is/react-sdk": "0.
|
|
15
|
-
"@webstudio-is/sdk": "0.
|
|
16
|
-
"@webstudio-is/sdk-components-animation": "0.
|
|
17
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
18
|
-
"@webstudio-is/sdk-components-react-router": "0.
|
|
19
|
-
"@webstudio-is/sdk-components-react": "0.
|
|
20
|
-
"@webstudio-is/wsauth": "0.
|
|
13
|
+
"@webstudio-is/image": "0.270.0",
|
|
14
|
+
"@webstudio-is/react-sdk": "0.270.0",
|
|
15
|
+
"@webstudio-is/sdk": "0.270.0",
|
|
16
|
+
"@webstudio-is/sdk-components-animation": "0.270.0",
|
|
17
|
+
"@webstudio-is/sdk-components-react-radix": "0.270.0",
|
|
18
|
+
"@webstudio-is/sdk-components-react-router": "0.270.0",
|
|
19
|
+
"@webstudio-is/sdk-components-react": "0.270.0",
|
|
20
|
+
"@webstudio-is/wsauth": "0.270.0",
|
|
21
21
|
"isbot": "^5.1.25",
|
|
22
22
|
"react": "18.3.0-canary-14898b6a9-20240318",
|
|
23
23
|
"react-dom": "18.3.0-canary-14898b6a9-20240318",
|
|
@@ -1,26 +1,49 @@
|
|
|
1
|
+
import { type ComponentProps, memo, useMemo } from "react";
|
|
1
2
|
import type { PageContext } from "vike/types";
|
|
2
3
|
import {
|
|
3
4
|
PageSettingsMeta,
|
|
4
5
|
PageSettingsTitle,
|
|
5
6
|
ReactSdkContext,
|
|
6
7
|
} from "@webstudio-is/react-sdk/runtime";
|
|
8
|
+
import { LinkCurrentUrlContext } from "@webstudio-is/sdk-components-react";
|
|
7
9
|
import { assetBaseUrl, imageLoader } from "__CONSTANTS__";
|
|
8
10
|
import { Page, breakpoints, siteName } from "__CLIENT__";
|
|
9
11
|
|
|
12
|
+
const getPageKey = (url: string) => {
|
|
13
|
+
const { origin, pathname, search } = new URL(url);
|
|
14
|
+
return `${origin}${pathname}${search}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const PageBoundary = memo(
|
|
18
|
+
({ pageKey, system }: ComponentProps<typeof Page> & { pageKey: string }) => {
|
|
19
|
+
// Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages
|
|
20
|
+
return <Page key={pageKey} system={system} />;
|
|
21
|
+
},
|
|
22
|
+
// Vike can rerender the current page during client-side navigation and
|
|
23
|
+
// hash-only URL updates. Keep the generated page out of that render path,
|
|
24
|
+
// but let actual page URL changes remount it.
|
|
25
|
+
(prevProps, nextProps) => prevProps.pageKey === nextProps.pageKey
|
|
26
|
+
);
|
|
27
|
+
|
|
10
28
|
const PageComponent = ({ data }: { data: PageContext["data"] }) => {
|
|
11
29
|
const { system, resources, url, pageMeta } = data;
|
|
30
|
+
const pageKey = getPageKey(url);
|
|
31
|
+
const sdkContext = useMemo(
|
|
32
|
+
() => ({
|
|
33
|
+
imageLoader,
|
|
34
|
+
assetBaseUrl,
|
|
35
|
+
resources,
|
|
36
|
+
breakpoints,
|
|
37
|
+
onError: console.error,
|
|
38
|
+
}),
|
|
39
|
+
[resources]
|
|
40
|
+
);
|
|
41
|
+
|
|
12
42
|
return (
|
|
13
|
-
<ReactSdkContext.Provider
|
|
14
|
-
value={
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
resources,
|
|
18
|
-
breakpoints,
|
|
19
|
-
onError: console.error,
|
|
20
|
-
}}
|
|
21
|
-
>
|
|
22
|
-
{/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}
|
|
23
|
-
<Page key={url} system={system} />
|
|
43
|
+
<ReactSdkContext.Provider value={sdkContext}>
|
|
44
|
+
<LinkCurrentUrlContext.Provider value={url}>
|
|
45
|
+
<PageBoundary pageKey={pageKey} system={system} />
|
|
46
|
+
</LinkCurrentUrlContext.Provider>
|
|
24
47
|
<PageSettingsMeta
|
|
25
48
|
url={url}
|
|
26
49
|
pageMeta={pageMeta}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"typecheck": "tsgo --noEmit"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@webstudio-is/image": "0.
|
|
12
|
-
"@webstudio-is/react-sdk": "0.
|
|
13
|
-
"@webstudio-is/sdk": "0.
|
|
14
|
-
"@webstudio-is/sdk-components-react": "0.
|
|
15
|
-
"@webstudio-is/sdk-components-animation": "0.
|
|
16
|
-
"@webstudio-is/sdk-components-react-radix": "0.
|
|
11
|
+
"@webstudio-is/image": "0.270.0",
|
|
12
|
+
"@webstudio-is/react-sdk": "0.270.0",
|
|
13
|
+
"@webstudio-is/sdk": "0.270.0",
|
|
14
|
+
"@webstudio-is/sdk-components-react": "0.270.0",
|
|
15
|
+
"@webstudio-is/sdk-components-animation": "0.270.0",
|
|
16
|
+
"@webstudio-is/sdk-components-react-radix": "0.270.0",
|
|
17
17
|
"react": "18.3.0-canary-14898b6a9-20240318",
|
|
18
18
|
"react-dom": "18.3.0-canary-14898b6a9-20240318",
|
|
19
19
|
"vike": "^0.4.229"
|