@reckona/mreact-router 0.0.66 → 0.0.68
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/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/devtools.js.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.js.map +1 -1
- package/dist/app-router-globals.js.map +1 -1
- package/dist/assets.js.map +1 -1
- package/dist/build.js.map +1 -1
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/cache-config.js.map +1 -1
- package/dist/cache-stats.js.map +1 -1
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/cookies.js.map +1 -1
- package/dist/csp.js.map +1 -1
- package/dist/csrf.js.map +1 -1
- package/dist/deferred.js.map +1 -1
- package/dist/dev-server.js.map +1 -1
- package/dist/file-conventions.js.map +1 -1
- package/dist/http.js.map +1 -1
- package/dist/i18n.js.map +1 -1
- package/dist/import-policy.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/module-runner.js.map +1 -1
- package/dist/multipart.js.map +1 -1
- package/dist/native-escape.js.map +1 -1
- package/dist/native-route-matcher.js.map +1 -1
- package/dist/navigation-state.js.map +1 -1
- package/dist/navigation.js.map +1 -1
- package/dist/prerender-store.js.map +1 -1
- package/dist/render.js.map +1 -1
- package/dist/route-path.js.map +1 -1
- package/dist/route-shells.js.map +1 -1
- package/dist/route-source.js.map +1 -1
- package/dist/route-styles.js.map +1 -1
- package/dist/routes.js.map +1 -1
- package/dist/runtime-cache.js.map +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/security-headers.js.map +1 -1
- package/dist/serve.js.map +1 -1
- package/dist/session.js.map +1 -1
- package/dist/source-jsx.js.map +1 -1
- package/dist/source-modules.js.map +1 -1
- package/dist/stream-list.js.map +1 -1
- package/dist/trace.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/vite-config.js.map +1 -1
- package/dist/vite-plugin-cache-key.js.map +1 -1
- package/dist/vite.js.map +1 -1
- package/dist/workspace-packages.js.map +1 -1
- package/package.json +13 -12
- package/src/actions.ts +1130 -0
- package/src/adapters/aws-lambda.ts +993 -0
- package/src/adapters/cloudflare.ts +1286 -0
- package/src/adapters/devtools.ts +5 -0
- package/src/adapters/edge.ts +70 -0
- package/src/adapters/node.ts +126 -0
- package/src/adapters/static.ts +61 -0
- package/src/app-router-globals.ts +19 -0
- package/src/assets.ts +113 -0
- package/src/build.ts +2948 -0
- package/src/bundle-pipeline.ts +496 -0
- package/src/cache-config.ts +35 -0
- package/src/cache-stats.ts +54 -0
- package/src/cache.ts +418 -0
- package/src/cli-options.ts +296 -0
- package/src/cli.ts +94 -0
- package/src/client.ts +3398 -0
- package/src/config.ts +146 -0
- package/src/cookies.ts +113 -0
- package/src/csp.ts +103 -0
- package/src/csrf.ts +132 -0
- package/src/deferred.ts +52 -0
- package/src/dev-server.ts +262 -0
- package/src/file-conventions.ts +88 -0
- package/src/http.ts +128 -0
- package/src/i18n.ts +98 -0
- package/src/import-policy.ts +261 -0
- package/src/index.ts +221 -0
- package/src/link.ts +47 -0
- package/src/logger.ts +149 -0
- package/src/module-runner.ts +554 -0
- package/src/multipart.ts +577 -0
- package/src/native-escape.ts +53 -0
- package/src/native-route-matcher.ts +173 -0
- package/src/navigation-state.ts +98 -0
- package/src/navigation.ts +215 -0
- package/src/prerender-store.ts +233 -0
- package/src/render.ts +5187 -0
- package/src/route-path.ts +5 -0
- package/src/route-shells.ts +47 -0
- package/src/route-source.ts +224 -0
- package/src/route-styles.ts +91 -0
- package/src/routes.ts +362 -0
- package/src/runtime-cache.ts +8 -0
- package/src/runtime-state.ts +37 -0
- package/src/security-headers.ts +134 -0
- package/src/serve.ts +1265 -0
- package/src/session.ts +238 -0
- package/src/source-jsx.ts +30 -0
- package/src/source-modules.ts +69 -0
- package/src/stream-list.ts +45 -0
- package/src/trace.ts +114 -0
- package/src/types.ts +155 -0
- package/src/upgrade.ts +8 -0
- package/src/vite-config.ts +63 -0
- package/src/vite-plugin-cache-key.ts +53 -0
- package/src/vite.ts +690 -0
- package/src/workspace-packages.ts +67 -0
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import type { BuiltPrerenderedRoute, BuiltServerManifest } from "../build.js";
|
|
2
|
+
import type { ClientRouteManifestEntry } from "../client.js";
|
|
3
|
+
import type { AppRouterResponseHook } from "../render.js";
|
|
4
|
+
import type {
|
|
5
|
+
GenerateMetadataContext,
|
|
6
|
+
ManifestDescriptor,
|
|
7
|
+
RobotsManifest,
|
|
8
|
+
RouteMetadata,
|
|
9
|
+
SitemapEntry,
|
|
10
|
+
} from "../types.js";
|
|
11
|
+
import {
|
|
12
|
+
emitRouterLog,
|
|
13
|
+
logDurationMs,
|
|
14
|
+
logError,
|
|
15
|
+
logNow,
|
|
16
|
+
requestLogFields,
|
|
17
|
+
type AppRouterLogger,
|
|
18
|
+
} from "../logger.js";
|
|
19
|
+
import { normalizeRoutePath } from "../route-path.js";
|
|
20
|
+
import type { AppRoute } from "../routes.js";
|
|
21
|
+
import { isNotFoundError } from "../navigation.js";
|
|
22
|
+
import { routeSecurityHeaders } from "../security-headers.js";
|
|
23
|
+
import type { AppRouterPrerenderStore } from "../serve.js";
|
|
24
|
+
import { emitRouterDevtoolsEvent } from "./devtools.js";
|
|
25
|
+
import { escapeHtmlAttribute, escapeHtmlText } from "@reckona/mreact-shared/html-escape";
|
|
26
|
+
|
|
27
|
+
export interface CloudflareExecutionContext {
|
|
28
|
+
passThroughOnException(): void;
|
|
29
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CloudflareAssetLoader<Env = unknown> {
|
|
33
|
+
fetch?:
|
|
34
|
+
| ((
|
|
35
|
+
pathname: string,
|
|
36
|
+
request: Request,
|
|
37
|
+
env: Env,
|
|
38
|
+
context: CloudflareExecutionContext,
|
|
39
|
+
) => Response | Promise<Response | undefined> | undefined)
|
|
40
|
+
| undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CloudflareRenderContext<Env = unknown> {
|
|
44
|
+
clientManifest: CloudflareClientManifest;
|
|
45
|
+
context: CloudflareExecutionContext;
|
|
46
|
+
env: Env;
|
|
47
|
+
serverManifest: BuiltServerManifest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CloudflareRequestHandlerOptions<Env = unknown> {
|
|
51
|
+
assets?: CloudflareAssetLoader<Env> | undefined;
|
|
52
|
+
clientManifest: CloudflareClientManifest;
|
|
53
|
+
logger?: AppRouterLogger | undefined;
|
|
54
|
+
onResponse?: AppRouterResponseHook | undefined;
|
|
55
|
+
onError?:
|
|
56
|
+
| ((
|
|
57
|
+
error: unknown,
|
|
58
|
+
request: Request,
|
|
59
|
+
env: Env,
|
|
60
|
+
context: CloudflareExecutionContext,
|
|
61
|
+
) => Response | Promise<Response>)
|
|
62
|
+
| undefined;
|
|
63
|
+
render?:
|
|
64
|
+
| ((request: Request, context: CloudflareRenderContext<Env>) => Response | Promise<Response>)
|
|
65
|
+
| undefined;
|
|
66
|
+
serverManifest: BuiltServerManifest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CloudflareRequestHandler<Env = unknown> {
|
|
70
|
+
fetch(request: Request, env: Env, context: CloudflareExecutionContext): Promise<Response>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CloudflareBuiltRouteRenderContext<
|
|
74
|
+
Env = unknown,
|
|
75
|
+
> extends CloudflareRenderContext<Env> {
|
|
76
|
+
params: Record<string, readonly string[] | string>;
|
|
77
|
+
route: AppRoute;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CloudflareRouteModuleLoaderContext<
|
|
81
|
+
Env = unknown,
|
|
82
|
+
> extends CloudflareBuiltRouteRenderContext<Env> {
|
|
83
|
+
request: Request;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CloudflareServerRouteContext<
|
|
87
|
+
Env = unknown,
|
|
88
|
+
> extends CloudflareBuiltRouteRenderContext<Env> {
|
|
89
|
+
request: Request;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CloudflareRouteModuleComponentProps<
|
|
93
|
+
Data = unknown,
|
|
94
|
+
Env = unknown,
|
|
95
|
+
> extends CloudflareBuiltRouteRenderContext<Env> {
|
|
96
|
+
data: Data;
|
|
97
|
+
request: Request;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type CloudflareRouteModuleComponent<Data = unknown, Env = unknown> = (
|
|
101
|
+
props: CloudflareRouteModuleComponentProps<Data, Env>,
|
|
102
|
+
) => Response | string | PromiseLike<Response | string>;
|
|
103
|
+
|
|
104
|
+
export interface CloudflareRouteModule<Data = unknown, Env = unknown> {
|
|
105
|
+
App?: CloudflareRouteModuleComponent<Data, Env> | undefined;
|
|
106
|
+
default?: CloudflareRouteModuleComponent<Data, Env> | undefined;
|
|
107
|
+
generateMetadata?:
|
|
108
|
+
| ((
|
|
109
|
+
context: GenerateMetadataContext<Data>,
|
|
110
|
+
) => RouteMetadata | PromiseLike<RouteMetadata | undefined> | undefined)
|
|
111
|
+
| undefined;
|
|
112
|
+
loader?:
|
|
113
|
+
| ((context: CloudflareRouteModuleLoaderContext<Env>) => Data | PromiseLike<Data>)
|
|
114
|
+
| undefined;
|
|
115
|
+
metadata?: RouteMetadata | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CloudflareServerRouteHandler<Env = unknown> = (
|
|
119
|
+
request: Request,
|
|
120
|
+
context: CloudflareServerRouteContext<Env>,
|
|
121
|
+
) => unknown | PromiseLike<unknown>;
|
|
122
|
+
|
|
123
|
+
export interface CloudflareServerRouteModule<Env = unknown> {
|
|
124
|
+
ALL?: CloudflareServerRouteHandler<Env> | undefined;
|
|
125
|
+
DELETE?: CloudflareServerRouteHandler<Env> | undefined;
|
|
126
|
+
default?: CloudflareServerRouteHandler<Env> | undefined;
|
|
127
|
+
GET?: CloudflareServerRouteHandler<Env> | undefined;
|
|
128
|
+
HEAD?: CloudflareServerRouteHandler<Env> | undefined;
|
|
129
|
+
OPTIONS?: CloudflareServerRouteHandler<Env> | undefined;
|
|
130
|
+
PATCH?: CloudflareServerRouteHandler<Env> | undefined;
|
|
131
|
+
POST?: CloudflareServerRouteHandler<Env> | undefined;
|
|
132
|
+
PUT?: CloudflareServerRouteHandler<Env> | undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CloudflareMetadataRouteContext {
|
|
136
|
+
baseUrl: string;
|
|
137
|
+
host: string;
|
|
138
|
+
params: Record<string, readonly string[] | string>;
|
|
139
|
+
request: Request;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CloudflareMetadataRouteModule {
|
|
143
|
+
default?: ((context: CloudflareMetadataRouteContext) => unknown | PromiseLike<unknown>) | undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type CloudflareRouteModuleRegistryEntry<Env = unknown> =
|
|
147
|
+
| CloudflareRouteModule<unknown, Env>
|
|
148
|
+
| CloudflareMetadataRouteModule
|
|
149
|
+
| CloudflareServerRouteModule<Env>;
|
|
150
|
+
|
|
151
|
+
export type CloudflareRouteModuleRegistry<Env = unknown> = Record<
|
|
152
|
+
string,
|
|
153
|
+
| CloudflareRouteModuleRegistryEntry<Env>
|
|
154
|
+
| (() => CloudflareRouteModuleRegistryEntry<Env> | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
|
|
155
|
+
>;
|
|
156
|
+
|
|
157
|
+
export interface CloudflareRouteModuleRendererOptions<Env = unknown> {
|
|
158
|
+
document?:
|
|
159
|
+
| ((
|
|
160
|
+
context: CloudflareRouteModuleComponentProps<unknown, Env> & {
|
|
161
|
+
body: string;
|
|
162
|
+
modulePreload: string;
|
|
163
|
+
},
|
|
164
|
+
) => Response | string | PromiseLike<Response | string>)
|
|
165
|
+
| undefined;
|
|
166
|
+
modules: CloudflareRouteModuleRegistry<Env>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type CloudflareRouteModuleGlob<Env = unknown> = Record<
|
|
170
|
+
string,
|
|
171
|
+
| CloudflareRouteModuleRegistryEntry<Env>
|
|
172
|
+
| (() => CloudflareRouteModuleRegistryEntry<Env> | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
|
|
173
|
+
>;
|
|
174
|
+
|
|
175
|
+
export interface CollectCloudflareRouteModulesOptions {
|
|
176
|
+
manifest: BuiltServerManifest;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface CloudflareBuiltRequestHandlerOptions<Env = unknown> extends Omit<
|
|
180
|
+
CloudflareRequestHandlerOptions<Env>,
|
|
181
|
+
"render"
|
|
182
|
+
> {
|
|
183
|
+
renderRoute?:
|
|
184
|
+
| ((
|
|
185
|
+
request: Request,
|
|
186
|
+
context: CloudflareBuiltRouteRenderContext<Env>,
|
|
187
|
+
) => Response | Promise<Response>)
|
|
188
|
+
| undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface CloudflareClientManifest {
|
|
192
|
+
publicAssets?: readonly string[] | undefined;
|
|
193
|
+
routes: ClientRouteManifestEntry[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface CloudflareAssetBinding {
|
|
197
|
+
fetch(request: Request): Response | Promise<Response>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface CloudflareStaticAssetLoaderOptions<Env = unknown> {
|
|
201
|
+
binding:
|
|
202
|
+
| CloudflareAssetBinding
|
|
203
|
+
| ((
|
|
204
|
+
env: Env,
|
|
205
|
+
) => CloudflareAssetBinding | Promise<CloudflareAssetBinding | undefined> | undefined);
|
|
206
|
+
clientManifest: CloudflareClientManifest;
|
|
207
|
+
extraPaths?: readonly string[] | undefined;
|
|
208
|
+
prefix?: string | undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface CloudflareCache {
|
|
212
|
+
delete(request: Request | string): boolean | Promise<boolean>;
|
|
213
|
+
match(request: Request | string): Response | Promise<Response | undefined> | undefined;
|
|
214
|
+
put(request: Request | string, response: Response): void | Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface CloudflarePrerenderStoreOptions {
|
|
218
|
+
cache: CloudflareCache;
|
|
219
|
+
keyOrigin?: string | undefined;
|
|
220
|
+
keyPrefix?: string | undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const clientPrefix = "/_mreact/client/";
|
|
224
|
+
const defaultPrerenderCacheOrigin = "https://mreact.local";
|
|
225
|
+
const defaultPrerenderCachePrefix = "/_mreact/prerender";
|
|
226
|
+
|
|
227
|
+
export function createCloudflareRequestHandler<Env = unknown>(
|
|
228
|
+
options: CloudflareRequestHandlerOptions<Env>,
|
|
229
|
+
): CloudflareRequestHandler<Env> {
|
|
230
|
+
return {
|
|
231
|
+
async fetch(request, env, context) {
|
|
232
|
+
const startedAt = logNow();
|
|
233
|
+
try {
|
|
234
|
+
return await handleCloudflareRequest(options, request, env, context);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const logFields = requestLogFields(request, "cloudflare");
|
|
237
|
+
emitRouterDevtoolsEvent({
|
|
238
|
+
method: request.method,
|
|
239
|
+
type: "router:request:error",
|
|
240
|
+
url: request.url,
|
|
241
|
+
});
|
|
242
|
+
emitRouterLog(options.logger, "error", {
|
|
243
|
+
...logFields,
|
|
244
|
+
durationMs: logDurationMs(startedAt),
|
|
245
|
+
error: logError(error),
|
|
246
|
+
type: "router:request:error",
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return await applyCloudflareResponseHook(
|
|
250
|
+
options.onError === undefined
|
|
251
|
+
? new Response("Internal Server Error", {
|
|
252
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
253
|
+
status: 500,
|
|
254
|
+
})
|
|
255
|
+
: await options.onError(error, request, env, context),
|
|
256
|
+
options,
|
|
257
|
+
request,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createCloudflareBuiltRequestHandler<Env = unknown>(
|
|
265
|
+
options: CloudflareBuiltRequestHandlerOptions<Env>,
|
|
266
|
+
): CloudflareRequestHandler<Env> {
|
|
267
|
+
const sortedRoutes = [...options.serverManifest.routes].sort(compareCloudflareRoutes);
|
|
268
|
+
|
|
269
|
+
return createCloudflareRequestHandler({
|
|
270
|
+
...options,
|
|
271
|
+
render(request, context) {
|
|
272
|
+
const matched = matchCloudflareRoute(
|
|
273
|
+
sortedRoutes,
|
|
274
|
+
new URL(request.url).pathname,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (matched === undefined || options.renderRoute === undefined) {
|
|
278
|
+
return new Response("Not Found", { status: 404 });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return options.renderRoute(request, {
|
|
282
|
+
...context,
|
|
283
|
+
params: matched.params,
|
|
284
|
+
route: matched.route,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createCloudflareRouteModuleRenderer<Env = unknown>(
|
|
291
|
+
options: CloudflareRouteModuleRendererOptions<Env>,
|
|
292
|
+
): NonNullable<CloudflareBuiltRequestHandlerOptions<Env>["renderRoute"]> {
|
|
293
|
+
return async (request, context) => {
|
|
294
|
+
if (context.route.kind !== "server" && isCloudflareNavigationRequest(request)) {
|
|
295
|
+
return cloudflareDocumentReloadNavigationResponse();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const module = await loadCloudflareRouteModule(options.modules, context.route.file);
|
|
299
|
+
|
|
300
|
+
if (module === undefined) {
|
|
301
|
+
return new Response(`No Cloudflare route module registered for ${context.route.file}.`, {
|
|
302
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
303
|
+
status: 500,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (context.route.kind === "server") {
|
|
308
|
+
return withDefaultSecurityHeaders(
|
|
309
|
+
await dispatchCloudflareServerRoute(module as CloudflareServerRouteModule<Env>, request, {
|
|
310
|
+
...context,
|
|
311
|
+
request,
|
|
312
|
+
}),
|
|
313
|
+
request,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (context.route.kind === "metadata") {
|
|
318
|
+
return withDefaultSecurityHeaders(
|
|
319
|
+
await dispatchCloudflareMetadataRoute(module as CloudflareMetadataRouteModule, request, {
|
|
320
|
+
...context,
|
|
321
|
+
request,
|
|
322
|
+
}),
|
|
323
|
+
request,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const pageModule = module as CloudflareRouteModule<unknown, Env>;
|
|
328
|
+
const component = pageModule.default ?? pageModule.App;
|
|
329
|
+
|
|
330
|
+
if (component === undefined) {
|
|
331
|
+
return new Response(`No Cloudflare page component registered for ${context.route.file}.`, {
|
|
332
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
333
|
+
status: 500,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const loaderContext = {
|
|
338
|
+
...context,
|
|
339
|
+
request,
|
|
340
|
+
};
|
|
341
|
+
let data: unknown;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
data = pageModule.loader === undefined ? undefined : await pageModule.loader(loaderContext);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof Response) {
|
|
347
|
+
return error;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isNotFoundError(error)) {
|
|
351
|
+
return cloudflareNotFoundResponse(request);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
const props = {
|
|
357
|
+
...context,
|
|
358
|
+
data,
|
|
359
|
+
request,
|
|
360
|
+
};
|
|
361
|
+
let rendered: Awaited<ReturnType<typeof component>>;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
rendered = await component(props);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (isNotFoundError(error)) {
|
|
367
|
+
return cloudflareNotFoundResponse(request);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (rendered instanceof Response) {
|
|
374
|
+
return withDefaultSecurityHeaders(rendered, request);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const modulePreload = cloudflareModulePreloadTag(context.clientManifest, context.route.path);
|
|
378
|
+
const metadata = await resolveCloudflareRouteMetadata([pageModule], props);
|
|
379
|
+
const documented =
|
|
380
|
+
options.document === undefined
|
|
381
|
+
? defaultCloudflareDocument(rendered, modulePreload, metadata)
|
|
382
|
+
: await options.document({
|
|
383
|
+
...props,
|
|
384
|
+
body: rendered,
|
|
385
|
+
modulePreload,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return withDefaultSecurityHeaders(
|
|
389
|
+
documented instanceof Response
|
|
390
|
+
? documented
|
|
391
|
+
: new Response(documented, {
|
|
392
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
393
|
+
}),
|
|
394
|
+
request,
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function dispatchCloudflareServerRoute<Env>(
|
|
400
|
+
module: CloudflareServerRouteModule<Env>,
|
|
401
|
+
request: Request,
|
|
402
|
+
context: CloudflareServerRouteContext<Env>,
|
|
403
|
+
): Promise<Response> {
|
|
404
|
+
const handler =
|
|
405
|
+
module[request.method as keyof CloudflareServerRouteModule<Env>] ?? module.ALL ?? module.default;
|
|
406
|
+
|
|
407
|
+
if (typeof handler !== "function") {
|
|
408
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let response: unknown;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
response = await handler(request, context);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error instanceof Response) {
|
|
417
|
+
return error;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return response instanceof Response
|
|
424
|
+
? response
|
|
425
|
+
: new Response("Invalid route response", { status: 500 });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function dispatchCloudflareMetadataRoute(
|
|
429
|
+
module: CloudflareMetadataRouteModule,
|
|
430
|
+
request: Request,
|
|
431
|
+
context: CloudflareBuiltRouteRenderContext & { request: Request },
|
|
432
|
+
): Promise<Response> {
|
|
433
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
434
|
+
return new Response("Method Not Allowed", {
|
|
435
|
+
headers: { allow: "GET, HEAD" },
|
|
436
|
+
status: 405,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (typeof module.default !== "function") {
|
|
441
|
+
return new Response("Invalid metadata route response", { status: 500 });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const url = new URL(request.url);
|
|
445
|
+
const value = await module.default({
|
|
446
|
+
baseUrl: url.origin,
|
|
447
|
+
host: url.host,
|
|
448
|
+
params: context.params,
|
|
449
|
+
request,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (value instanceof Response) {
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (context.route.kind !== "metadata") {
|
|
457
|
+
return new Response("Invalid metadata route convention", { status: 500 });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (context.route.convention === "robots") {
|
|
461
|
+
return new Response(serializeCloudflareRobots(value as RobotsManifest), {
|
|
462
|
+
headers: {
|
|
463
|
+
"cache-control": "public, max-age=3600",
|
|
464
|
+
"content-type": "text/plain; charset=utf-8",
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (context.route.convention === "sitemap") {
|
|
470
|
+
return new Response(serializeCloudflareSitemap(value as readonly SitemapEntry[]), {
|
|
471
|
+
headers: {
|
|
472
|
+
"cache-control": "no-cache",
|
|
473
|
+
"content-type": "application/xml; charset=utf-8",
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (context.route.convention === "manifest") {
|
|
479
|
+
return new Response(JSON.stringify(value as ManifestDescriptor), {
|
|
480
|
+
headers: {
|
|
481
|
+
"cache-control": "public, max-age=3600",
|
|
482
|
+
"content-type": "application/manifest+json; charset=utf-8",
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (context.route.convention === "opengraph-image") {
|
|
488
|
+
const body = value instanceof Uint8Array ? value.slice().buffer : String(value);
|
|
489
|
+
return new Response(body, {
|
|
490
|
+
headers: {
|
|
491
|
+
"cache-control": "public, max-age=3600",
|
|
492
|
+
"content-type": typeof value === "string" && value.trimStart().startsWith("<svg")
|
|
493
|
+
? "image/svg+xml"
|
|
494
|
+
: "application/octet-stream",
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return new Response("Invalid metadata route convention", { status: 500 });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isCloudflareNavigationRequest(request: Request): boolean {
|
|
503
|
+
return request.headers.get("x-mreact-navigation") === "1";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function cloudflareDocumentReloadNavigationResponse(): Response {
|
|
507
|
+
return new Response(null, {
|
|
508
|
+
headers: { "x-mreact-navigation": "reload" },
|
|
509
|
+
status: 204,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function withDefaultSecurityHeaders(response: Response, request: Request): Response {
|
|
514
|
+
const headers = new Headers(response.headers);
|
|
515
|
+
|
|
516
|
+
for (const [name, value] of Object.entries(routeSecurityHeaders({ request, security: undefined }))) {
|
|
517
|
+
if (!headers.has(name)) {
|
|
518
|
+
headers.set(name, value);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return new Response(response.body, {
|
|
523
|
+
headers,
|
|
524
|
+
status: response.status,
|
|
525
|
+
statusText: response.statusText,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function collectCloudflareRouteModules<Env = unknown>(
|
|
530
|
+
glob: CloudflareRouteModuleGlob<Env>,
|
|
531
|
+
options: CollectCloudflareRouteModulesOptions,
|
|
532
|
+
): CloudflareRouteModuleRegistry<Env> {
|
|
533
|
+
const requiredRoutes = options.manifest.routes.filter((route) =>
|
|
534
|
+
cloudflareRouteRequiresModule(route, options.manifest),
|
|
535
|
+
);
|
|
536
|
+
const matchedKeys = new Set<string>();
|
|
537
|
+
const modules: CloudflareRouteModuleRegistry<Env> = {};
|
|
538
|
+
|
|
539
|
+
for (const route of requiredRoutes) {
|
|
540
|
+
const match = Object.entries(glob).find(([key]) =>
|
|
541
|
+
cloudflareRouteGlobKeyMatchesRoute(key, route.file),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
if (match === undefined) {
|
|
545
|
+
throw new Error(`Missing Cloudflare route module for ${route.file}.`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const [key, module] = match;
|
|
549
|
+
matchedKeys.add(key);
|
|
550
|
+
modules[route.file] = module;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const extraKeys = Object.keys(glob).filter((key) => !matchedKeys.has(key));
|
|
554
|
+
|
|
555
|
+
if (extraKeys.length > 0) {
|
|
556
|
+
throw new Error(`Extra Cloudflare route module entries: ${extraKeys.join(", ")}.`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return modules;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function createCloudflareStaticAssetLoader<Env = unknown>(
|
|
563
|
+
options: CloudflareStaticAssetLoaderOptions<Env>,
|
|
564
|
+
): CloudflareAssetLoader<Env> {
|
|
565
|
+
const prefix = normalizeAssetPrefix(options.prefix ?? clientPrefix);
|
|
566
|
+
const allowedPaths = cloudflareClientAssetPaths(options.clientManifest, {
|
|
567
|
+
extraPaths: options.extraPaths,
|
|
568
|
+
prefix,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
async fetch(pathname, request, env) {
|
|
573
|
+
if (!allowedPaths.has(pathname)) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const binding =
|
|
578
|
+
typeof options.binding === "function" ? await options.binding(env) : options.binding;
|
|
579
|
+
|
|
580
|
+
if (binding === undefined) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const assetUrl = new URL(request.url);
|
|
585
|
+
assetUrl.pathname = pathname;
|
|
586
|
+
assetUrl.search = "";
|
|
587
|
+
|
|
588
|
+
return await binding.fetch(new Request(assetUrl, request));
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function cloudflareClientAssetPaths(
|
|
594
|
+
manifest: CloudflareClientManifest,
|
|
595
|
+
options: { extraPaths?: readonly string[] | undefined; prefix?: string | undefined } = {},
|
|
596
|
+
): Set<string> {
|
|
597
|
+
const prefix = normalizeAssetPrefix(options.prefix ?? clientPrefix);
|
|
598
|
+
const paths = new Set<string>([`${prefix}manifest.json`]);
|
|
599
|
+
|
|
600
|
+
for (const route of manifest.routes) {
|
|
601
|
+
for (const asset of [
|
|
602
|
+
route.script,
|
|
603
|
+
route.sourceMap,
|
|
604
|
+
route.navigationScript,
|
|
605
|
+
...(route.imports ?? []),
|
|
606
|
+
]) {
|
|
607
|
+
const path = safeClientAssetPath(prefix, asset);
|
|
608
|
+
|
|
609
|
+
if (path !== undefined) {
|
|
610
|
+
paths.add(path);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for (const extraPath of options.extraPaths ?? []) {
|
|
616
|
+
const path = safeClientAssetPath(prefix, extraPath);
|
|
617
|
+
|
|
618
|
+
if (path !== undefined) {
|
|
619
|
+
paths.add(path);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const publicAsset of manifest.publicAssets ?? []) {
|
|
624
|
+
const path = safePublicAssetPath(publicAsset);
|
|
625
|
+
|
|
626
|
+
if (path !== undefined) {
|
|
627
|
+
paths.add(path);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return paths;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function safePublicAssetPath(asset: string): string | undefined {
|
|
635
|
+
if (!asset.startsWith("/") || asset.startsWith("//") || asset.includes("..")) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return asset;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function createCloudflarePrerenderStore(
|
|
643
|
+
options: CloudflarePrerenderStoreOptions,
|
|
644
|
+
): AppRouterPrerenderStore {
|
|
645
|
+
return {
|
|
646
|
+
async delete(path) {
|
|
647
|
+
await options.cache.delete(prerenderCacheRequest(options, path));
|
|
648
|
+
},
|
|
649
|
+
async get(path) {
|
|
650
|
+
const response = await options.cache.match(prerenderCacheRequest(options, path));
|
|
651
|
+
|
|
652
|
+
if (response === undefined) {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return (await response.json()) as BuiltPrerenderedRoute;
|
|
657
|
+
},
|
|
658
|
+
async set(path, entry) {
|
|
659
|
+
await options.cache.put(
|
|
660
|
+
prerenderCacheRequest(options, path),
|
|
661
|
+
Response.json(entry, {
|
|
662
|
+
headers: { "cache-control": "no-store" },
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function handleCloudflareRequest<Env>(
|
|
670
|
+
options: CloudflareRequestHandlerOptions<Env>,
|
|
671
|
+
request: Request,
|
|
672
|
+
env: Env,
|
|
673
|
+
context: CloudflareExecutionContext,
|
|
674
|
+
): Promise<Response> {
|
|
675
|
+
const startedAt = logNow();
|
|
676
|
+
const logFields = requestLogFields(request, "cloudflare");
|
|
677
|
+
emitRouterLog(options.logger, "info", {
|
|
678
|
+
...logFields,
|
|
679
|
+
type: "router:request:start",
|
|
680
|
+
});
|
|
681
|
+
emitRouterDevtoolsEvent({
|
|
682
|
+
method: request.method,
|
|
683
|
+
type: "router:request:start",
|
|
684
|
+
url: request.url,
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const url = new URL(request.url);
|
|
688
|
+
|
|
689
|
+
if (
|
|
690
|
+
url.pathname.startsWith(clientPrefix) ||
|
|
691
|
+
isCloudflarePublicAssetPath(options.clientManifest, url.pathname)
|
|
692
|
+
) {
|
|
693
|
+
const response = await options.assets?.fetch?.(url.pathname, request, env, context);
|
|
694
|
+
const assetResponse = await applyCloudflareResponseHook(
|
|
695
|
+
response ?? new Response("Not Found", { status: 404 }),
|
|
696
|
+
options,
|
|
697
|
+
request,
|
|
698
|
+
);
|
|
699
|
+
emitRouterLog(options.logger, "info", {
|
|
700
|
+
...logFields,
|
|
701
|
+
durationMs: logDurationMs(startedAt),
|
|
702
|
+
status: assetResponse.status,
|
|
703
|
+
type: "router:request:end",
|
|
704
|
+
});
|
|
705
|
+
return assetResponse;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const staticResponse = prerenderedResponse(
|
|
709
|
+
options.serverManifest.prerenderedRoutes,
|
|
710
|
+
normalizeRoutePath(url.pathname),
|
|
711
|
+
request.method,
|
|
712
|
+
isCloudflareNavigationRequest(request),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
if (staticResponse !== undefined) {
|
|
716
|
+
emitRouterDevtoolsEvent({
|
|
717
|
+
method: request.method,
|
|
718
|
+
status: staticResponse.status,
|
|
719
|
+
type: "router:request:end",
|
|
720
|
+
url: request.url,
|
|
721
|
+
});
|
|
722
|
+
emitRouterLog(options.logger, "info", {
|
|
723
|
+
...logFields,
|
|
724
|
+
durationMs: logDurationMs(startedAt),
|
|
725
|
+
status: staticResponse.status,
|
|
726
|
+
type: "router:request:end",
|
|
727
|
+
});
|
|
728
|
+
return await applyCloudflareResponseHook(staticResponse, options, request);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (options.render === undefined) {
|
|
732
|
+
const notFoundResponse = await applyCloudflareResponseHook(
|
|
733
|
+
new Response("Not Found", { status: 404 }),
|
|
734
|
+
options,
|
|
735
|
+
request,
|
|
736
|
+
);
|
|
737
|
+
emitRouterLog(options.logger, "info", {
|
|
738
|
+
...logFields,
|
|
739
|
+
durationMs: logDurationMs(startedAt),
|
|
740
|
+
status: notFoundResponse.status,
|
|
741
|
+
type: "router:request:end",
|
|
742
|
+
});
|
|
743
|
+
return notFoundResponse;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const renderedResponse = await options.render(request, {
|
|
747
|
+
clientManifest: options.clientManifest,
|
|
748
|
+
context,
|
|
749
|
+
env,
|
|
750
|
+
serverManifest: options.serverManifest,
|
|
751
|
+
});
|
|
752
|
+
const response = await applyCloudflareResponseHook(
|
|
753
|
+
preserveCloudflareStreamedHtmlResponse(renderedResponse),
|
|
754
|
+
options,
|
|
755
|
+
request,
|
|
756
|
+
);
|
|
757
|
+
emitRouterDevtoolsEvent({
|
|
758
|
+
method: request.method,
|
|
759
|
+
status: response.status,
|
|
760
|
+
type: "router:request:end",
|
|
761
|
+
url: request.url,
|
|
762
|
+
});
|
|
763
|
+
emitRouterLog(options.logger, "info", {
|
|
764
|
+
...logFields,
|
|
765
|
+
durationMs: logDurationMs(startedAt),
|
|
766
|
+
status: response.status,
|
|
767
|
+
type: "router:request:end",
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
return response;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isCloudflarePublicAssetPath(manifest: CloudflareClientManifest, pathname: string): boolean {
|
|
774
|
+
return (manifest.publicAssets ?? []).includes(pathname);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function applyCloudflareResponseHook<Env>(
|
|
778
|
+
response: Response,
|
|
779
|
+
options: Pick<CloudflareRequestHandlerOptions<Env>, "onResponse">,
|
|
780
|
+
request: Request,
|
|
781
|
+
): Promise<Response> {
|
|
782
|
+
const hooked = await options.onResponse?.(response, { request });
|
|
783
|
+
|
|
784
|
+
return hooked instanceof Response ? hooked : response;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function preserveCloudflareStreamedHtmlResponse(response: Response): Response {
|
|
788
|
+
if (
|
|
789
|
+
response.headers.get("x-mreact-stream") !== "1" ||
|
|
790
|
+
!isHtmlContentType(response.headers.get("content-type"))
|
|
791
|
+
) {
|
|
792
|
+
return response;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const headers = new Headers(response.headers);
|
|
796
|
+
const cacheControl = headers.get("cache-control");
|
|
797
|
+
|
|
798
|
+
if (!cacheControlHasDirective(cacheControl, "no-transform")) {
|
|
799
|
+
headers.set(
|
|
800
|
+
"cache-control",
|
|
801
|
+
cacheControl === null || cacheControl.trim() === ""
|
|
802
|
+
? "no-transform"
|
|
803
|
+
: `${cacheControl}, no-transform`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (headers.get("content-encoding") === null) {
|
|
808
|
+
headers.set("content-encoding", "identity");
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return new Response(response.body, {
|
|
812
|
+
headers,
|
|
813
|
+
status: response.status,
|
|
814
|
+
statusText: response.statusText,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function isHtmlContentType(contentType: string | null): boolean {
|
|
819
|
+
return contentType?.toLowerCase().split(";", 1)[0]?.trim() === "text/html";
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function cacheControlHasDirective(cacheControl: string | null, directive: string): boolean {
|
|
823
|
+
if (cacheControl === null) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const normalizedDirective = directive.toLowerCase();
|
|
828
|
+
return cacheControl
|
|
829
|
+
.split(",")
|
|
830
|
+
.some((part) => part.trim().toLowerCase() === normalizedDirective);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function prerenderedResponse(
|
|
834
|
+
prerenderedRoutes: Record<string, BuiltPrerenderedRoute> | undefined,
|
|
835
|
+
path: string,
|
|
836
|
+
method: string,
|
|
837
|
+
isNavigation: boolean,
|
|
838
|
+
): Response | undefined {
|
|
839
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const prerendered = prerenderedRoutes?.[path];
|
|
844
|
+
|
|
845
|
+
if (prerendered === undefined) {
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (isNavigation && !prerendered.html.includes("data-mreact-route-id")) {
|
|
850
|
+
return cloudflareDocumentReloadNavigationResponse();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return new Response(method === "HEAD" ? null : prerendered.html, {
|
|
854
|
+
headers: prerendered.headers,
|
|
855
|
+
status: prerendered.status,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function cloudflareRouteRequiresModule(
|
|
860
|
+
route: AppRoute,
|
|
861
|
+
manifest: BuiltServerManifest,
|
|
862
|
+
): boolean {
|
|
863
|
+
return (
|
|
864
|
+
route.kind === "metadata" ||
|
|
865
|
+
route.kind === "server" ||
|
|
866
|
+
(route.kind === "page" &&
|
|
867
|
+
(route.segments.some((segment) => segment.kind !== "static") ||
|
|
868
|
+
manifest.prerenderedRoutes?.[route.path] === undefined))
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function cloudflareRouteGlobKeyMatchesRoute(key: string, routeFile: string): boolean {
|
|
873
|
+
const normalizedKey = normalizeCloudflareRouteModulePath(key);
|
|
874
|
+
const normalizedRoute = normalizeCloudflareRouteModulePath(routeFile);
|
|
875
|
+
|
|
876
|
+
return (
|
|
877
|
+
normalizedKey === normalizedRoute ||
|
|
878
|
+
normalizedKey.endsWith(`/${normalizedRoute}`)
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function normalizeCloudflareRouteModulePath(path: string): string {
|
|
883
|
+
const withoutPrefix = path
|
|
884
|
+
.replace(/\\/g, "/")
|
|
885
|
+
.replace(/^\.\//, "")
|
|
886
|
+
.replace(/^\/+/, "");
|
|
887
|
+
|
|
888
|
+
return withoutPrefix.replace(/\.(?:mjs|js|ts|tsx)$/, "");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function matchCloudflareRoute(
|
|
892
|
+
routes: readonly AppRoute[],
|
|
893
|
+
pathname: string,
|
|
894
|
+
): { params: Record<string, readonly string[] | string>; route: AppRoute } | undefined {
|
|
895
|
+
const normalizedPath = normalizeRoutePath(pathname);
|
|
896
|
+
const pathSegments = normalizedPath === "/" ? [] : normalizedPath.slice(1).split("/");
|
|
897
|
+
|
|
898
|
+
for (const route of routes) {
|
|
899
|
+
const params: Record<string, readonly string[] | string> = {};
|
|
900
|
+
const catchAllIndex = route.segments.findIndex((segment) => segment.kind === "catch-all");
|
|
901
|
+
|
|
902
|
+
if (catchAllIndex === -1 && route.segments.length !== pathSegments.length) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (
|
|
907
|
+
catchAllIndex !== -1 &&
|
|
908
|
+
pathSegments.length <
|
|
909
|
+
catchAllIndex + 1 + route.segments.length - catchAllIndex - 1
|
|
910
|
+
) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
let matched = true;
|
|
915
|
+
|
|
916
|
+
for (const [index, segment] of route.segments.entries()) {
|
|
917
|
+
const value = pathSegments[index];
|
|
918
|
+
|
|
919
|
+
if (value === undefined) {
|
|
920
|
+
matched = false;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (segment.kind === "static") {
|
|
925
|
+
if (segment.value !== value) {
|
|
926
|
+
matched = false;
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (segment.kind === "dynamic") {
|
|
933
|
+
const decoded = safeDecodePathSegment(value);
|
|
934
|
+
if (decoded === undefined) {
|
|
935
|
+
matched = false;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
params[segment.name] = decoded;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const suffixSegments = route.segments.slice(index + 1);
|
|
943
|
+
const catchAllEnd = pathSegments.length - suffixSegments.length;
|
|
944
|
+
|
|
945
|
+
if (catchAllEnd <= index) {
|
|
946
|
+
matched = false;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const decodedParts: string[] = [];
|
|
951
|
+
for (const part of pathSegments.slice(index, catchAllEnd)) {
|
|
952
|
+
const decoded = safeDecodePathSegment(part);
|
|
953
|
+
if (decoded === undefined) {
|
|
954
|
+
matched = false;
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
decodedParts.push(decoded);
|
|
958
|
+
}
|
|
959
|
+
params[segment.name] = decodedParts;
|
|
960
|
+
|
|
961
|
+
for (let suffixIndex = 0; suffixIndex < suffixSegments.length; suffixIndex += 1) {
|
|
962
|
+
const suffixSegment = suffixSegments[suffixIndex];
|
|
963
|
+
const suffixValue = pathSegments[catchAllEnd + suffixIndex];
|
|
964
|
+
|
|
965
|
+
if (suffixSegment === undefined || suffixValue === undefined) {
|
|
966
|
+
matched = false;
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (suffixSegment.kind === "static") {
|
|
971
|
+
if (suffixSegment.value !== suffixValue) {
|
|
972
|
+
matched = false;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (suffixSegment.kind === "dynamic") {
|
|
979
|
+
const decoded = safeDecodePathSegment(suffixValue);
|
|
980
|
+
if (decoded === undefined) {
|
|
981
|
+
matched = false;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
params[suffixSegment.name] = decoded;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
matched = false;
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (matched) {
|
|
996
|
+
return { params, route };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function compareCloudflareRoutes(a: AppRoute, b: AppRoute): number {
|
|
1004
|
+
const scoreDelta = routeSpecificityScore(b) - routeSpecificityScore(a);
|
|
1005
|
+
|
|
1006
|
+
return scoreDelta === 0 ? a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind) : scoreDelta;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function routeSpecificityScore(route: AppRoute): number {
|
|
1010
|
+
return route.segments.reduce((score, segment) => {
|
|
1011
|
+
if (segment.kind === "static") {
|
|
1012
|
+
return score + 100;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (segment.kind === "dynamic") {
|
|
1016
|
+
return score + 10;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return score;
|
|
1020
|
+
}, route.segments.length);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function safeDecodePathSegment(segment: string): string | undefined {
|
|
1024
|
+
try {
|
|
1025
|
+
return decodeURIComponent(segment);
|
|
1026
|
+
} catch {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function normalizeAssetPrefix(prefix: string): string {
|
|
1032
|
+
const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1033
|
+
|
|
1034
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function safeClientAssetPath(prefix: string, asset: string | undefined): string | undefined {
|
|
1038
|
+
if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const segments = asset.split("/");
|
|
1043
|
+
|
|
1044
|
+
if (segments.some((segment) => unsafeAssetSegment(segment))) {
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return `${prefix}${segments.join("/")}`;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function unsafeAssetSegment(segment: string): boolean {
|
|
1052
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
const decoded = decodeURIComponent(segment);
|
|
1058
|
+
return decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\");
|
|
1059
|
+
} catch {
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function loadCloudflareRouteModule<Env>(
|
|
1065
|
+
modules: CloudflareRouteModuleRegistry<Env>,
|
|
1066
|
+
file: string,
|
|
1067
|
+
): Promise<CloudflareRouteModuleRegistryEntry<Env> | undefined> {
|
|
1068
|
+
const entry = modules[file];
|
|
1069
|
+
|
|
1070
|
+
return typeof entry === "function" ? await entry() : entry;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function cloudflareModulePreloadTag(manifest: CloudflareClientManifest, routePath: string): string {
|
|
1074
|
+
const script = manifest.routes.find((route) => route.path === routePath)?.script;
|
|
1075
|
+
|
|
1076
|
+
return script === undefined
|
|
1077
|
+
? ""
|
|
1078
|
+
: `<link rel="modulepreload" href="/_mreact/client/${escapeHtmlAttribute(script)}">`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function resolveCloudflareRouteMetadata<Data, Env>(
|
|
1082
|
+
modules: readonly CloudflareRouteModule<Data, Env>[],
|
|
1083
|
+
context: GenerateMetadataContext<Data>,
|
|
1084
|
+
): Promise<RouteMetadata | undefined> {
|
|
1085
|
+
const metadata: RouteMetadata[] = [];
|
|
1086
|
+
|
|
1087
|
+
for (const module of modules) {
|
|
1088
|
+
let next = module.metadata;
|
|
1089
|
+
|
|
1090
|
+
if (module.generateMetadata !== undefined) {
|
|
1091
|
+
const generated = await module.generateMetadata(context);
|
|
1092
|
+
next = mergeCloudflareRouteMetadata([next, generated].filter(isCloudflareRouteMetadata));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (next !== undefined) {
|
|
1096
|
+
metadata.push(next);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return mergeCloudflareRouteMetadata(metadata);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function isCloudflareRouteMetadata(value: RouteMetadata | undefined): value is RouteMetadata {
|
|
1104
|
+
return value !== undefined;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function mergeCloudflareRouteMetadata(metadata: readonly RouteMetadata[]): RouteMetadata | undefined {
|
|
1108
|
+
if (metadata.length === 0) {
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return metadata.reduce<RouteMetadata>((merged, next) => {
|
|
1113
|
+
const openGraph = mergeCloudflareMetadataObject(merged.openGraph, next.openGraph);
|
|
1114
|
+
const openGraphImages = mergeCloudflareMetadataArrays(
|
|
1115
|
+
merged.openGraph?.images,
|
|
1116
|
+
next.openGraph?.images,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
const alternates = mergeCloudflareMetadataObject(merged.alternates, next.alternates);
|
|
1120
|
+
const csp = mergeCloudflareMetadataObject(merged.csp, next.csp);
|
|
1121
|
+
const head = mergeCloudflareMetadataArrays(merged.head, next.head);
|
|
1122
|
+
const icons = mergeCloudflareMetadataObject(merged.icons, next.icons);
|
|
1123
|
+
const mergedMetadata: RouteMetadata = {
|
|
1124
|
+
...merged,
|
|
1125
|
+
...next,
|
|
1126
|
+
...(alternates === undefined ? {} : { alternates }),
|
|
1127
|
+
...(csp === undefined ? {} : { csp }),
|
|
1128
|
+
...(head === undefined ? {} : { head }),
|
|
1129
|
+
...(icons === undefined ? {} : { icons }),
|
|
1130
|
+
...(openGraph === undefined && openGraphImages === undefined
|
|
1131
|
+
? {}
|
|
1132
|
+
: { openGraph: { ...openGraph, ...(openGraphImages === undefined ? {} : { images: openGraphImages }) } }),
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
return mergedMetadata;
|
|
1136
|
+
}, {});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function mergeCloudflareMetadataObject<T extends object>(
|
|
1140
|
+
left: T | undefined,
|
|
1141
|
+
right: T | undefined,
|
|
1142
|
+
): T | undefined {
|
|
1143
|
+
if (left === undefined) {
|
|
1144
|
+
return right;
|
|
1145
|
+
}
|
|
1146
|
+
if (right === undefined) {
|
|
1147
|
+
return left;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return { ...left, ...right };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function mergeCloudflareMetadataArrays<T>(
|
|
1154
|
+
left: readonly T[] | undefined,
|
|
1155
|
+
right: readonly T[] | undefined,
|
|
1156
|
+
): readonly T[] | undefined {
|
|
1157
|
+
if (left === undefined || left.length === 0) {
|
|
1158
|
+
return right;
|
|
1159
|
+
}
|
|
1160
|
+
if (right === undefined || right.length === 0) {
|
|
1161
|
+
return left;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return [...left, ...right];
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function defaultCloudflareDocument(
|
|
1168
|
+
body: string,
|
|
1169
|
+
modulePreload: string,
|
|
1170
|
+
metadata: RouteMetadata | undefined,
|
|
1171
|
+
): string {
|
|
1172
|
+
return `<!DOCTYPE html><html><head>${modulePreload}${cloudflareMetadataTitle(metadata)}</head><body>${body}</body></html>`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function cloudflareMetadataTitle(metadata: RouteMetadata | undefined): string {
|
|
1176
|
+
if (metadata?.title === undefined) {
|
|
1177
|
+
return "";
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return `<title>${escapeHtmlText(metadataString(metadata.title))}</title>`;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function metadataString(value: boolean | number | string): string {
|
|
1184
|
+
return String(value);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function serializeCloudflareRobots(manifest: RobotsManifest): string {
|
|
1188
|
+
const lines: string[] = [];
|
|
1189
|
+
const rules =
|
|
1190
|
+
manifest.rules === undefined
|
|
1191
|
+
? []
|
|
1192
|
+
: Array.isArray(manifest.rules)
|
|
1193
|
+
? manifest.rules
|
|
1194
|
+
: [manifest.rules];
|
|
1195
|
+
|
|
1196
|
+
for (const rule of rules) {
|
|
1197
|
+
for (const userAgent of arrayValue(rule.userAgent)) {
|
|
1198
|
+
lines.push(`User-agent: ${userAgent}`);
|
|
1199
|
+
}
|
|
1200
|
+
for (const allow of arrayValue(rule.allow)) {
|
|
1201
|
+
lines.push(`Allow: ${allow}`);
|
|
1202
|
+
}
|
|
1203
|
+
for (const disallow of arrayValue(rule.disallow)) {
|
|
1204
|
+
lines.push(`Disallow: ${disallow}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
for (const sitemap of arrayValue(manifest.sitemap)) {
|
|
1209
|
+
lines.push(`Sitemap: ${sitemap}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (manifest.host !== undefined) {
|
|
1212
|
+
lines.push(`Host: ${manifest.host}`);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return `${lines.join("\n")}\n`;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function serializeCloudflareSitemap(entries: readonly SitemapEntry[]): string {
|
|
1219
|
+
const urls = entries
|
|
1220
|
+
.map((entry) => {
|
|
1221
|
+
const fields = [
|
|
1222
|
+
`<loc>${escapeXml(entry.url)}</loc>`,
|
|
1223
|
+
entry.lastModified === undefined
|
|
1224
|
+
? undefined
|
|
1225
|
+
: `<lastmod>${escapeXml(sitemapDate(entry.lastModified))}</lastmod>`,
|
|
1226
|
+
entry.changeFrequency === undefined
|
|
1227
|
+
? undefined
|
|
1228
|
+
: `<changefreq>${escapeXml(entry.changeFrequency)}</changefreq>`,
|
|
1229
|
+
entry.priority === undefined ? undefined : `<priority>${entry.priority}</priority>`,
|
|
1230
|
+
].filter((field): field is string => field !== undefined);
|
|
1231
|
+
|
|
1232
|
+
return `<url>${fields.join("")}</url>`;
|
|
1233
|
+
})
|
|
1234
|
+
.join("");
|
|
1235
|
+
|
|
1236
|
+
return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function sitemapDate(value: Date | number | string): string {
|
|
1240
|
+
if (value instanceof Date) {
|
|
1241
|
+
return value.toISOString();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return typeof value === "number" ? new Date(value).toISOString() : value;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function arrayValue<T>(value: T | readonly T[] | undefined): readonly T[] {
|
|
1248
|
+
if (value === undefined) {
|
|
1249
|
+
return [];
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return Array.isArray(value) ? (value as readonly T[]) : [value as T];
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function escapeXml(value: string): string {
|
|
1256
|
+
return value
|
|
1257
|
+
.replaceAll("&", "&")
|
|
1258
|
+
.replaceAll("<", "<")
|
|
1259
|
+
.replaceAll(">", ">")
|
|
1260
|
+
.replaceAll('"', """);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function cloudflareNotFoundResponse(request: Request): Response {
|
|
1264
|
+
return withDefaultSecurityHeaders(
|
|
1265
|
+
new Response("<!DOCTYPE html><html><head></head><body>Not Found</body></html>", {
|
|
1266
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
1267
|
+
status: 404,
|
|
1268
|
+
}),
|
|
1269
|
+
request,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function prerenderCacheRequest(options: CloudflarePrerenderStoreOptions, path: string): Request {
|
|
1274
|
+
const origin = options.keyOrigin ?? defaultPrerenderCacheOrigin;
|
|
1275
|
+
const prefix = options.keyPrefix ?? defaultPrerenderCachePrefix;
|
|
1276
|
+
const normalizedPath = normalizeRoutePath(path.startsWith("/") ? path : `/${path}`);
|
|
1277
|
+
const url = new URL(`${normalizePrerenderPrefix(prefix)}${normalizedPath}`, origin);
|
|
1278
|
+
|
|
1279
|
+
return new Request(url, { method: "GET" });
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function normalizePrerenderPrefix(prefix: string): string {
|
|
1283
|
+
const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1284
|
+
|
|
1285
|
+
return withLeadingSlash.replace(/\/+$/, "");
|
|
1286
|
+
}
|