@reckona/mreact-router 0.0.179 → 0.0.181

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.
@@ -28,10 +28,11 @@ import {
28
28
  requestLogFields,
29
29
  type AppRouterLogger,
30
30
  } from "../logger.js";
31
+ import { middlewareMatches, type MiddlewareModule } from "../middleware.js";
31
32
  import { normalizeRoutePath } from "../route-path.js";
32
33
  import type { AppRoute } from "../routes.js";
33
34
  import { contentSecurityPolicy } from "../csp.js";
34
- import { isNotFoundError } from "../navigation.js";
35
+ import { isNotFoundError, rewriteLocation } from "../navigation.js";
35
36
  import { validateRouteMetadata } from "../metadata.js";
36
37
  import { routeSecurityHeaders } from "../security-headers.js";
37
38
  import type { AppRouterPrerenderStore } from "../serve.js";
@@ -202,7 +203,9 @@ export interface CloudflareMetadataRouteContext {
202
203
  * Defines the default export accepted from a Cloudflare metadata route module.
203
204
  */
204
205
  export interface CloudflareMetadataRouteModule {
205
- default?: ((context: CloudflareMetadataRouteContext) => unknown | PromiseLike<unknown>) | undefined;
206
+ default?:
207
+ | ((context: CloudflareMetadataRouteContext) => unknown | PromiseLike<unknown>)
208
+ | undefined;
206
209
  }
207
210
 
208
211
  /**
@@ -219,7 +222,9 @@ export type CloudflareRouteModuleRegistryEntry<Env = unknown> =
219
222
  export type CloudflareRouteModuleRegistry<Env = unknown> = Record<
220
223
  string,
221
224
  | CloudflareRouteModuleRegistryEntry<Env>
222
- | (() => CloudflareRouteModuleRegistryEntry<Env> | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
225
+ | (() =>
226
+ | CloudflareRouteModuleRegistryEntry<Env>
227
+ | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
223
228
  >;
224
229
 
225
230
  /**
@@ -243,7 +248,9 @@ export interface CloudflareRouteModuleRendererOptions<Env = unknown> {
243
248
  export type CloudflareRouteModuleGlob<Env = unknown> = Record<
244
249
  string,
245
250
  | CloudflareRouteModuleRegistryEntry<Env>
246
- | (() => CloudflareRouteModuleRegistryEntry<Env> | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
251
+ | (() =>
252
+ | CloudflareRouteModuleRegistryEntry<Env>
253
+ | PromiseLike<CloudflareRouteModuleRegistryEntry<Env>>)
247
254
  >;
248
255
 
249
256
  /**
@@ -317,6 +324,7 @@ export interface CloudflarePrerenderStoreOptions {
317
324
  }
318
325
 
319
326
  const clientPrefix = "/_mreact/client/";
327
+ const cloudflareMiddlewareRouteModuleKey = "__middleware__";
320
328
  const defaultPrerenderCacheOrigin = "https://mreact.local";
321
329
  const defaultPrerenderCachePrefix = "/_mreact/prerender";
322
330
 
@@ -375,10 +383,7 @@ export function createCloudflareBuiltRequestHandler<Env = unknown>(
375
383
  return createCloudflareRequestHandler({
376
384
  ...options,
377
385
  render(request, context) {
378
- const matched = matchCloudflareRoute(
379
- sortedRoutes,
380
- new URL(request.url).pathname,
381
- );
386
+ const matched = matchCloudflareRoute(sortedRoutes, new URL(request.url).pathname);
382
387
 
383
388
  if (matched === undefined || options.renderRoute === undefined) {
384
389
  return new Response("Not Found", { status: 404 });
@@ -402,6 +407,12 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
402
407
  options: CloudflareRouteModuleRendererOptions<Env>,
403
408
  ): NonNullable<CloudflareBuiltRequestHandlerOptions<Env>["renderRoute"]> {
404
409
  return async (request, context) => {
410
+ const middlewareResult = await resolveCloudflareRouteModuleMiddleware(options.modules, request);
411
+ if (middlewareResult.type === "response") {
412
+ return middlewareResult.response;
413
+ }
414
+ request = middlewareResult.request;
415
+
405
416
  if (context.route.kind !== "server" && isCloudflareNavigationRequest(request)) {
406
417
  return cloudflareDocumentReloadNavigationResponse();
407
418
  }
@@ -460,7 +471,9 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
460
471
  data =
461
472
  pageModule.loader === undefined
462
473
  ? undefined
463
- : await runWithCloudflareQueryClient(queryClient, () => pageModule.loader!(loaderContext));
474
+ : await runWithCloudflareQueryClient(queryClient, () =>
475
+ pageModule.loader!(loaderContext),
476
+ );
464
477
  } catch (error) {
465
478
  if (error instanceof Response) {
466
479
  return error;
@@ -534,6 +547,39 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
534
547
  };
535
548
  }
536
549
 
550
+ async function resolveCloudflareRouteModuleMiddleware<Env>(
551
+ modules: CloudflareRouteModuleRegistry<Env>,
552
+ request: Request,
553
+ ): Promise<{ request: Request; type: "continue" } | { response: Response; type: "response" }> {
554
+ const module = (await loadCloudflareRouteModule(modules, cloudflareMiddlewareRouteModuleKey)) as
555
+ | MiddlewareModule
556
+ | undefined;
557
+
558
+ if (module === undefined || !middlewareMatches(module.config, new URL(request.url).pathname)) {
559
+ return { request, type: "continue" };
560
+ }
561
+
562
+ const middleware = module.middleware ?? module.default;
563
+ if (typeof middleware !== "function") {
564
+ return { request, type: "continue" };
565
+ }
566
+
567
+ const response = await middleware(request);
568
+ if (!(response instanceof Response)) {
569
+ return { request, type: "continue" };
570
+ }
571
+
572
+ const location = rewriteLocation(response);
573
+ if (location !== undefined) {
574
+ return {
575
+ request: new Request(new URL(location, request.url), request),
576
+ type: "continue",
577
+ };
578
+ }
579
+
580
+ return { response, type: "response" };
581
+ }
582
+
537
583
  async function injectCloudflareQueryState<T extends Response | string>(
538
584
  document: T,
539
585
  state: DehydratedQueryClient,
@@ -574,11 +620,17 @@ async function runWithCloudflareQueryClient<T>(
574
620
  queryClient: QueryClient,
575
621
  fn: () => T,
576
622
  ): Promise<Awaited<T>> {
623
+ if (cloudflareQueryClientFallbackInstalled) {
624
+ installQueryAsyncStorage(cloudflareQueryClientStorage);
625
+ return await runWithSerializedCloudflareQueryClient(queryClient, fn);
626
+ }
627
+
577
628
  try {
578
629
  return await runWithQueryClient(queryClient, fn);
579
630
  } catch (error) {
580
631
  if (isQueryClientScopeUnavailableError(error)) {
581
632
  installQueryAsyncStorage(cloudflareQueryClientStorage);
633
+ cloudflareQueryClientFallbackInstalled = true;
582
634
  return await runWithSerializedCloudflareQueryClient(queryClient, fn);
583
635
  }
584
636
 
@@ -588,6 +640,7 @@ async function runWithCloudflareQueryClient<T>(
588
640
 
589
641
  const cloudflareQueryClientStorage = createCloudflareQueryClientStorage();
590
642
  let cloudflareQueryClientFallbackQueue: Promise<void> = Promise.resolve();
643
+ let cloudflareQueryClientFallbackInstalled = false;
591
644
 
592
645
  async function runWithSerializedCloudflareQueryClient<T>(
593
646
  queryClient: QueryClient,
@@ -598,7 +651,10 @@ async function runWithSerializedCloudflareQueryClient<T>(
598
651
  const current = new Promise<void>((resolve) => {
599
652
  releaseCurrent = resolve;
600
653
  });
601
- cloudflareQueryClientFallbackQueue = previous.then(() => current, () => current);
654
+ cloudflareQueryClientFallbackQueue = previous.then(
655
+ () => current,
656
+ () => current,
657
+ );
602
658
 
603
659
  await previous;
604
660
 
@@ -662,7 +718,9 @@ async function dispatchCloudflareServerRoute<Env>(
662
718
  context: CloudflareServerRouteContext<Env>,
663
719
  ): Promise<Response> {
664
720
  const handler =
665
- module[request.method as keyof CloudflareServerRouteModule<Env>] ?? module.ALL ?? module.default;
721
+ module[request.method as keyof CloudflareServerRouteModule<Env>] ??
722
+ module.ALL ??
723
+ module.default;
666
724
 
667
725
  if (typeof handler !== "function") {
668
726
  return new Response("Method Not Allowed", { status: 405 });
@@ -721,17 +779,17 @@ function selectCloudflarePageComponent<Data, Env>(
721
779
  return appExport as CloudflareRouteModuleComponent<Data, Env>;
722
780
  }
723
781
 
724
- const cloudflareRouteComponentExport = readCloudflareModuleExport(module, "CloudflareRouteComponent");
782
+ const cloudflareRouteComponentExport = readCloudflareModuleExport(
783
+ module,
784
+ "CloudflareRouteComponent",
785
+ );
725
786
  if (typeof cloudflareRouteComponentExport === "function") {
726
787
  return cloudflareRouteComponentExport as CloudflareRouteModuleComponent<Data, Env>;
727
788
  }
728
789
 
729
790
  for (const name of Object.keys(module)) {
730
791
  const value = readCloudflareModuleExport(module, name);
731
- if (
732
- !cloudflareReservedPageModuleExportNames.has(name) &&
733
- typeof value === "function"
734
- ) {
792
+ if (!cloudflareReservedPageModuleExportNames.has(name) && typeof value === "function") {
735
793
  return value as CloudflareRouteModuleComponent<Data, Env>;
736
794
  }
737
795
  }
@@ -752,7 +810,12 @@ function readCloudflareModuleExport(module: object, name: string): unknown {
752
810
  return descriptor.get?.call(module);
753
811
  }
754
812
 
755
- const cloudflareDiagnosticPageModuleExportNames = ["default", "App", "CloudflareRouteComponent", "slots"] as const;
813
+ const cloudflareDiagnosticPageModuleExportNames = [
814
+ "default",
815
+ "App",
816
+ "CloudflareRouteComponent",
817
+ "slots",
818
+ ] as const;
756
819
 
757
820
  // Fixed names + typeof only (never values) so the 500 response is useful without
758
821
  // listing app-specific export names. Accessors are not invoked.
@@ -841,9 +904,10 @@ async function dispatchCloudflareMetadataRoute(
841
904
  return new Response(body, {
842
905
  headers: {
843
906
  "cache-control": "public, max-age=3600",
844
- "content-type": typeof value === "string" && value.trimStart().startsWith("<svg")
845
- ? "image/svg+xml"
846
- : "application/octet-stream",
907
+ "content-type":
908
+ typeof value === "string" && value.trimStart().startsWith("<svg")
909
+ ? "image/svg+xml"
910
+ : "application/octet-stream",
847
911
  },
848
912
  });
849
913
  }
@@ -1135,7 +1199,10 @@ async function handleCloudflareRequest<Env>(
1135
1199
  return response;
1136
1200
  }
1137
1201
 
1138
- function isCloudflarePublicAssetPath(manifest: CloudflareClientManifest, pathname: string): boolean {
1202
+ function isCloudflarePublicAssetPath(
1203
+ manifest: CloudflareClientManifest,
1204
+ pathname: string,
1205
+ ): boolean {
1139
1206
  return (manifest.publicAssets ?? []).includes(pathname);
1140
1207
  }
1141
1208
 
@@ -1190,9 +1257,7 @@ function cacheControlHasDirective(cacheControl: string | null, directive: string
1190
1257
  }
1191
1258
 
1192
1259
  const normalizedDirective = directive.toLowerCase();
1193
- return cacheControl
1194
- .split(",")
1195
- .some((part) => part.trim().toLowerCase() === normalizedDirective);
1260
+ return cacheControl.split(",").some((part) => part.trim().toLowerCase() === normalizedDirective);
1196
1261
  }
1197
1262
 
1198
1263
  function prerenderedResponse(
@@ -1221,10 +1286,7 @@ function prerenderedResponse(
1221
1286
  });
1222
1287
  }
1223
1288
 
1224
- function cloudflareRouteRequiresModule(
1225
- route: AppRoute,
1226
- manifest: BuiltServerManifest,
1227
- ): boolean {
1289
+ function cloudflareRouteRequiresModule(route: AppRoute, manifest: BuiltServerManifest): boolean {
1228
1290
  return (
1229
1291
  route.kind === "metadata" ||
1230
1292
  route.kind === "server" ||
@@ -1238,17 +1300,11 @@ function cloudflareRouteGlobKeyMatchesRoute(key: string, routeFile: string): boo
1238
1300
  const normalizedKey = normalizeCloudflareRouteModulePath(key);
1239
1301
  const normalizedRoute = normalizeCloudflareRouteModulePath(routeFile);
1240
1302
 
1241
- return (
1242
- normalizedKey === normalizedRoute ||
1243
- normalizedKey.endsWith(`/${normalizedRoute}`)
1244
- );
1303
+ return normalizedKey === normalizedRoute || normalizedKey.endsWith(`/${normalizedRoute}`);
1245
1304
  }
1246
1305
 
1247
1306
  function normalizeCloudflareRouteModulePath(path: string): string {
1248
- const withoutPrefix = path
1249
- .replace(/\\/g, "/")
1250
- .replace(/^\.\//, "")
1251
- .replace(/^\/+/, "");
1307
+ const withoutPrefix = path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
1252
1308
 
1253
1309
  return withoutPrefix.replace(/\.(?:mjs|js|ts|tsx)$/, "");
1254
1310
  }
@@ -1270,8 +1326,7 @@ function matchCloudflareRoute(
1270
1326
 
1271
1327
  if (
1272
1328
  catchAllIndex !== -1 &&
1273
- pathSegments.length <
1274
- catchAllIndex + 1 + route.segments.length - catchAllIndex - 1
1329
+ pathSegments.length < catchAllIndex + 1 + route.segments.length - catchAllIndex - 1
1275
1330
  ) {
1276
1331
  continue;
1277
1332
  }
@@ -1368,7 +1423,9 @@ function matchCloudflareRoute(
1368
1423
  function compareCloudflareRoutes(a: AppRoute, b: AppRoute): number {
1369
1424
  const scoreDelta = routeSpecificityScore(b) - routeSpecificityScore(a);
1370
1425
 
1371
- return scoreDelta === 0 ? a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind) : scoreDelta;
1426
+ return scoreDelta === 0
1427
+ ? a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind)
1428
+ : scoreDelta;
1372
1429
  }
1373
1430
 
1374
1431
  function routeSpecificityScore(route: AppRoute): number {
@@ -1530,7 +1587,9 @@ function isCloudflareRouteMetadata(value: RouteMetadata | undefined): value is R
1530
1587
  return value !== undefined;
1531
1588
  }
1532
1589
 
1533
- function mergeCloudflareRouteMetadata(metadata: readonly RouteMetadata[]): RouteMetadata | undefined {
1590
+ function mergeCloudflareRouteMetadata(
1591
+ metadata: readonly RouteMetadata[],
1592
+ ): RouteMetadata | undefined {
1534
1593
  if (metadata.length === 0) {
1535
1594
  return undefined;
1536
1595
  }
@@ -1555,7 +1614,12 @@ function mergeCloudflareRouteMetadata(metadata: readonly RouteMetadata[]): Route
1555
1614
  ...(icons === undefined ? {} : { icons }),
1556
1615
  ...(openGraph === undefined && openGraphImages === undefined
1557
1616
  ? {}
1558
- : { openGraph: { ...openGraph, ...(openGraphImages === undefined ? {} : { images: openGraphImages }) } }),
1617
+ : {
1618
+ openGraph: {
1619
+ ...openGraph,
1620
+ ...(openGraphImages === undefined ? {} : { images: openGraphImages }),
1621
+ },
1622
+ }),
1559
1623
  };
1560
1624
 
1561
1625
  return mergedMetadata;
package/src/build.ts CHANGED
@@ -514,8 +514,8 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
514
514
  }),
515
515
  ),
516
516
  ]);
517
- const { artifacts: serverModules, sharedChunks: serverModuleSharedChunks } = await (
518
- shouldTrackBuildPhases === false
517
+ const { artifacts: serverModules, sharedChunks: serverModuleSharedChunks } =
518
+ await (shouldTrackBuildPhases === false
519
519
  ? buildServerModuleArtifacts({
520
520
  bundleRequestRuntimePackages: shouldBuildAwsLambda,
521
521
  bundleCache: new Map(),
@@ -547,8 +547,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
547
547
  serverTransformCache,
548
548
  vitePlugins,
549
549
  }),
550
- )
551
- );
550
+ ));
552
551
  const serverRoutes = routes.map((route) => ({
553
552
  ...route,
554
553
  file: relative(project.projectRoot, route.file),
@@ -626,10 +625,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
626
625
  ...(navigationRuntimeScript === undefined ? [] : [navigationRuntimeScript]),
627
626
  ]),
628
627
  ).sort();
629
- const serverModuleClosureFiles = buildServerModuleClosureManifest(
630
- serverModules,
631
- sourceAnalysis,
632
- );
628
+ const serverModuleClosureFiles = buildServerModuleClosureManifest(serverModules, sourceAnalysis);
633
629
  const prerenderedRoutes =
634
630
  shouldTrackBuildPhases === false
635
631
  ? await prerenderStaticRoutes({
@@ -713,9 +709,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
713
709
  ...(serverActionManifest.allowedActions.length === 0
714
710
  ? {}
715
711
  : { serverActionManifest: serverActionManifest.allowedActions }),
716
- ...(Object.keys(serverModuleClosureFiles).length === 0
717
- ? {}
718
- : { serverModuleClosureFiles }),
712
+ ...(Object.keys(serverModuleClosureFiles).length === 0 ? {} : { serverModuleClosureFiles }),
719
713
  ...(Object.keys(serverModuleArtifacts.files).length === 0
720
714
  ? {}
721
715
  : { serverModuleFiles: serverModuleArtifacts.files }),
@@ -1051,13 +1045,7 @@ function collectManifestServerModuleClosureFiles(
1051
1045
  for (const specifier of localManifestServerModuleSpecifiers(source)) {
1052
1046
  const resolved = resolveManifestLocalServerSourceImport(file, specifier, sourceFiles);
1053
1047
  if (resolved !== undefined) {
1054
- collectManifestServerModuleClosureFiles(
1055
- resolved,
1056
- sourceAnalysis,
1057
- sourceFiles,
1058
- seen,
1059
- closure,
1060
- );
1048
+ collectManifestServerModuleClosureFiles(resolved, sourceAnalysis, sourceFiles, seen, closure);
1061
1049
  }
1062
1050
  }
1063
1051
  }
@@ -2238,7 +2226,10 @@ async function buildServerModuleArtifacts(options: {
2238
2226
  project: ResolvedAppRouterProject;
2239
2227
  projectRoot: string;
2240
2228
  routes: readonly AppRoute[];
2241
- serverActionReferencesByFile: ReadonlyMap<string, readonly BuiltServerActionExpressionReference[]>;
2229
+ serverActionReferencesByFile: ReadonlyMap<
2230
+ string,
2231
+ readonly BuiltServerActionExpressionReference[]
2232
+ >;
2242
2233
  sourceAnalysis: BuildSourceAnalysisScope;
2243
2234
  serverTransformCache: ServerTransformCache;
2244
2235
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -2323,11 +2314,7 @@ async function buildServerModuleArtifacts(options: {
2323
2314
  continue;
2324
2315
  }
2325
2316
 
2326
- if (
2327
- requestArtifactFiles.has(file) &&
2328
- route?.kind !== "server" &&
2329
- route?.kind !== "metadata"
2330
- ) {
2317
+ if (requestArtifactFiles.has(file) && route?.kind !== "server" && route?.kind !== "metadata") {
2331
2318
  requestBatchEntries.push({
2332
2319
  code: stripRouteRequestOnlyExports(source, absoluteFile),
2333
2320
  filename: absoluteFile,
@@ -3138,6 +3125,8 @@ interface RouteRequestModuleBatchOutput {
3138
3125
  sharedChunks: readonly SharedServerModuleChunk[];
3139
3126
  }
3140
3127
 
3128
+ const cloudflareMiddlewareRouteModuleKey = "__middleware__";
3129
+
3141
3130
  async function writeCloudflareRouteModules(options: {
3142
3131
  cacheDir?: string | undefined;
3143
3132
  cloudflareDir: string;
@@ -3195,6 +3184,26 @@ async function writeCloudflareRouteModules(options: {
3195
3184
  root: options.projectRoot,
3196
3185
  vitePlugins: options.vitePlugins,
3197
3186
  });
3187
+ const cloudflareMiddlewareFile = findCloudflareMiddlewareFile({
3188
+ files: options.files,
3189
+ projectRoot: options.projectRoot,
3190
+ routesDir: options.routesDir,
3191
+ });
3192
+ const middlewareRouteModules = await buildCloudflareMiddlewareModuleBatch({
3193
+ cacheDir: options.cacheDir,
3194
+ define: options.define,
3195
+ root: options.projectRoot,
3196
+ routes:
3197
+ cloudflareMiddlewareFile === undefined
3198
+ ? []
3199
+ : [
3200
+ {
3201
+ filename: cloudflareMiddlewareFile,
3202
+ routeId: cloudflareMiddlewareRouteModuleKey,
3203
+ },
3204
+ ],
3205
+ vitePlugins: options.vitePlugins,
3206
+ });
3198
3207
  const directComponentRoutes = await collectCloudflareDirectComponentRoutes({
3199
3208
  requiredRoutes,
3200
3209
  routesDir: options.routesDir,
@@ -3235,6 +3244,7 @@ async function writeCloudflareRouteModules(options: {
3235
3244
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, loaderRouteModules),
3236
3245
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, directComponentModules),
3237
3246
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, stringShellComponentModules),
3247
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, middlewareRouteModules),
3238
3248
  ]);
3239
3249
 
3240
3250
  const registryEntries = await mapWithBuildConcurrency(
@@ -3354,6 +3364,7 @@ async function writeCloudflareRouteModules(options: {
3354
3364
 
3355
3365
  const registrySource = [
3356
3366
  `export const routeModules = {`,
3367
+ ...cloudflareMiddlewareRegistryEntries(middlewareRouteModules),
3357
3368
  ...registryEntries.map((entry) => ` ${entry},`),
3358
3369
  `};`,
3359
3370
  `export default routeModules;`,
@@ -3365,6 +3376,31 @@ async function writeCloudflareRouteModules(options: {
3365
3376
  return { registryFile: "route-modules.mjs" };
3366
3377
  }
3367
3378
 
3379
+ function findCloudflareMiddlewareFile(options: {
3380
+ files: Record<string, string>;
3381
+ projectRoot: string;
3382
+ routesDir: string;
3383
+ }): string | undefined {
3384
+ const relativeRoutesDir = relative(options.projectRoot, options.routesDir).replaceAll(sep, "/");
3385
+
3386
+ const relativeFile = ["middleware.ts", "middleware.mreact.ts"]
3387
+ .map((file) => (relativeRoutesDir === "" ? file : `${relativeRoutesDir}/${file}`))
3388
+ .find((file) => options.files[file] !== undefined);
3389
+
3390
+ return relativeFile === undefined ? undefined : join(options.projectRoot, relativeFile);
3391
+ }
3392
+
3393
+ function cloudflareMiddlewareRegistryEntries(modules: CloudflareBatchedRouteModules): string[] {
3394
+ const middleware = modules.entries.get(cloudflareMiddlewareRouteModuleKey);
3395
+ if (middleware === undefined) {
3396
+ return [];
3397
+ }
3398
+
3399
+ return [
3400
+ ` ${JSON.stringify(cloudflareMiddlewareRouteModuleKey)}: () => import(${JSON.stringify(`./${middleware.fileName}`)}),`,
3401
+ ];
3402
+ }
3403
+
3368
3404
  function cloudflarePageRouteFacadeModuleSource(componentImport: string): string {
3369
3405
  return `import * as componentModule from ${JSON.stringify(componentImport)};
3370
3406
 
@@ -4904,6 +4940,27 @@ async function buildCloudflareRouteLoaderModuleBatch(options: {
4904
4940
  });
4905
4941
  }
4906
4942
 
4943
+ async function buildCloudflareMiddlewareModuleBatch(options: {
4944
+ cacheDir?: string | undefined;
4945
+ define?: UserConfig["define"] | undefined;
4946
+ routes: readonly { filename: string; routeId: string }[];
4947
+ root: string;
4948
+ vitePlugins?: readonly PluginOption[] | undefined;
4949
+ }): Promise<CloudflareBatchedRouteModules> {
4950
+ return await bundleCloudflareModuleBatch({
4951
+ cacheDir: options.cacheDir,
4952
+ define: options.define,
4953
+ entries: options.routes.map((route) => ({
4954
+ code: cloudflareMiddlewareModuleEntry(route.filename),
4955
+ filename: `${route.filename}.mreact-cloudflare-middleware.js`,
4956
+ name: `${route.routeId}.middleware`,
4957
+ routeId: route.routeId,
4958
+ })),
4959
+ root: options.root,
4960
+ vitePlugins: options.vitePlugins,
4961
+ });
4962
+ }
4963
+
4907
4964
  export async function __buildCloudflareRouteLoaderModuleBatchForTests(options: {
4908
4965
  cacheDir?: string | undefined;
4909
4966
  define?: UserConfig["define"] | undefined;
@@ -5001,6 +5058,15 @@ function cloudflareRouteLoaderModuleEntry(filename: string): string {
5001
5058
  return `export { loader } from ${JSON.stringify(filename)};`;
5002
5059
  }
5003
5060
 
5061
+ function cloudflareMiddlewareModuleEntry(filename: string): string {
5062
+ return `import * as middlewareModule from ${JSON.stringify(filename)};
5063
+
5064
+ export const config = middlewareModule.config;
5065
+ export const middleware = middlewareModule.middleware;
5066
+ const defaultMiddleware = middlewareModule.default;
5067
+ export default defaultMiddleware;`;
5068
+ }
5069
+
5004
5070
  async function buildCloudflareRouteMetadataExportModule(options: {
5005
5071
  cacheDir?: string | undefined;
5006
5072
  define?: UserConfig["define"] | undefined;
@@ -5678,10 +5744,7 @@ async function writeClientRouteBundles(options: {
5678
5744
  await writeFile(join(options.clientDir, asset.fileName), source);
5679
5745
  }
5680
5746
 
5681
- const generatedAssets = new Set<string>([
5682
- ...routeCssAssets.assets,
5683
- ...specialCssAssets.assets,
5684
- ]);
5747
+ const generatedAssets = new Set<string>([...routeCssAssets.assets, ...specialCssAssets.assets]);
5685
5748
 
5686
5749
  for (const chunk of output.chunks) {
5687
5750
  generatedAssets.add(chunk.fileName);
@@ -5777,13 +5840,13 @@ async function writeSpecialRouteCssAssetBatches(options: {
5777
5840
 
5778
5841
  return batch.css.length === 0
5779
5842
  ? undefined
5780
- : ({
5843
+ : {
5781
5844
  assets: batch.assets,
5782
5845
  style: {
5783
5846
  css: batch.css,
5784
5847
  file: relative(options.appDir, file).split(sep).join("/"),
5785
5848
  } satisfies ClientStyleManifestEntry,
5786
- });
5849
+ };
5787
5850
  });
5788
5851
 
5789
5852
  return {
package/src/cache.ts CHANGED
@@ -348,9 +348,70 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
348
348
  return false;
349
349
  }
350
350
 
351
- return request.headers.has("authorization") || request.headers.has("cookie");
351
+ if (request.headers.has("authorization") || request.headers.has("cookie")) {
352
+ return true;
353
+ }
354
+
355
+ for (const name of request.headers.keys()) {
356
+ const lower = name.toLowerCase();
357
+ if (PUBLIC_ROUTE_CACHE_REQUEST_HEADERS.has(lower)) {
358
+ continue;
359
+ }
360
+
361
+ if (
362
+ lower === "x-api-key" ||
363
+ lower === "cf-access-jwt-assertion" ||
364
+ lower === "proxy-authorization" ||
365
+ lower === "x-auth-token" ||
366
+ lower === "x-authenticated-user" ||
367
+ lower === "x-forwarded-user" ||
368
+ lower === "x-session-id" ||
369
+ lower === "x-user-email" ||
370
+ lower === "x-user-id" ||
371
+ lower.endsWith("-api-key") ||
372
+ lower.endsWith("-auth-token") ||
373
+ lower.endsWith("-session-id") ||
374
+ lower.endsWith("-user") ||
375
+ lower.endsWith("-user-email") ||
376
+ lower.endsWith("-user-id") ||
377
+ lower.includes("-jwt-") ||
378
+ lower.endsWith("-jwt")
379
+ ) {
380
+ return true;
381
+ }
382
+
383
+ return true;
384
+ }
385
+
386
+ return false;
352
387
  }
353
388
 
389
+ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
390
+ "accept",
391
+ "accept-encoding",
392
+ "accept-language",
393
+ "cache-control",
394
+ "connection",
395
+ "dnt",
396
+ "host",
397
+ "pragma",
398
+ "purpose",
399
+ "referer",
400
+ "sec-ch-prefers-color-scheme",
401
+ "sec-ch-prefers-reduced-motion",
402
+ "sec-ch-ua",
403
+ "sec-ch-ua-mobile",
404
+ "sec-ch-ua-platform",
405
+ "sec-fetch-dest",
406
+ "sec-fetch-mode",
407
+ "sec-fetch-site",
408
+ "sec-fetch-user",
409
+ "upgrade-insecure-requests",
410
+ "user-agent",
411
+ "x-mreact-navigation",
412
+ "x-mreact-navigation-cache",
413
+ ]);
414
+
354
415
  /**
355
416
  * Invalidates cached route responses for a normalized app-router path.
356
417
  *
package/src/cookies.ts CHANGED
@@ -73,6 +73,20 @@ export function serializeCookie(
73
73
  if (options.sameSite === "None" && options.secure !== true) {
74
74
  throw new TypeError("SameSite=None requires Secure");
75
75
  }
76
+ if (name.startsWith("__Secure-") && options.secure !== true) {
77
+ throw new TypeError("__Secure- cookies require Secure");
78
+ }
79
+ if (name.startsWith("__Host-")) {
80
+ if (options.secure !== true) {
81
+ throw new TypeError("__Host- cookies require Secure");
82
+ }
83
+ if (options.path !== "/") {
84
+ throw new TypeError("__Host- cookies require Path=/");
85
+ }
86
+ if (options.domain !== undefined) {
87
+ throw new TypeError("__Host- cookies must not set Domain");
88
+ }
89
+ }
76
90
 
77
91
  const parts = [`${name}=${encodeURIComponent(value)}`];
78
92