@reckona/mreact-router 0.0.201 → 0.0.203

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.
Files changed (93) hide show
  1. package/README.md +6 -3
  2. package/dist/actions.d.ts +2 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +61 -41
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts +3 -1
  7. package/dist/adapters/cloudflare.d.ts.map +1 -1
  8. package/dist/adapters/cloudflare.js +74 -11
  9. package/dist/adapters/cloudflare.js.map +1 -1
  10. package/dist/adapters/node.d.ts +7 -0
  11. package/dist/adapters/node.d.ts.map +1 -1
  12. package/dist/adapters/node.js +7 -1
  13. package/dist/adapters/node.js.map +1 -1
  14. package/dist/adapters/static.d.ts.map +1 -1
  15. package/dist/adapters/static.js +14 -3
  16. package/dist/adapters/static.js.map +1 -1
  17. package/dist/build.d.ts +5 -0
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +137 -14
  20. package/dist/build.js.map +1 -1
  21. package/dist/built-runtime.d.ts +4 -1
  22. package/dist/built-runtime.d.ts.map +1 -1
  23. package/dist/built-runtime.js.map +1 -1
  24. package/dist/cache.d.ts +27 -1
  25. package/dist/cache.d.ts.map +1 -1
  26. package/dist/cache.js +83 -16
  27. package/dist/cache.js.map +1 -1
  28. package/dist/cli-options.d.ts +7 -0
  29. package/dist/cli-options.d.ts.map +1 -1
  30. package/dist/cli-options.js +27 -2
  31. package/dist/cli-options.js.map +1 -1
  32. package/dist/cli.js +3 -2
  33. package/dist/cli.js.map +1 -1
  34. package/dist/client.d.ts +5 -0
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +11 -2
  37. package/dist/client.js.map +1 -1
  38. package/dist/navigation-marker.d.ts +2 -0
  39. package/dist/navigation-marker.d.ts.map +1 -0
  40. package/dist/navigation-marker.js +294 -0
  41. package/dist/navigation-marker.js.map +1 -0
  42. package/dist/navigation-runtime.d.ts +1 -1
  43. package/dist/navigation-runtime.d.ts.map +1 -1
  44. package/dist/navigation-runtime.js +1 -1
  45. package/dist/navigation-runtime.js.map +1 -1
  46. package/dist/node-server.d.ts +6 -0
  47. package/dist/node-server.d.ts.map +1 -1
  48. package/dist/node-server.js +19 -1
  49. package/dist/node-server.js.map +1 -1
  50. package/dist/prerender-entry.d.ts +7 -0
  51. package/dist/prerender-entry.d.ts.map +1 -0
  52. package/dist/prerender-entry.js +99 -0
  53. package/dist/prerender-entry.js.map +1 -0
  54. package/dist/render.d.ts +20 -1
  55. package/dist/render.d.ts.map +1 -1
  56. package/dist/render.js +190 -110
  57. package/dist/render.js.map +1 -1
  58. package/dist/request-header-tracking.d.ts +44 -0
  59. package/dist/request-header-tracking.d.ts.map +1 -0
  60. package/dist/request-header-tracking.js +116 -0
  61. package/dist/request-header-tracking.js.map +1 -0
  62. package/dist/route-source.d.ts +16 -0
  63. package/dist/route-source.d.ts.map +1 -1
  64. package/dist/route-source.js +55 -1
  65. package/dist/route-source.js.map +1 -1
  66. package/dist/security-headers.d.ts +16 -0
  67. package/dist/security-headers.d.ts.map +1 -1
  68. package/dist/security-headers.js +37 -0
  69. package/dist/security-headers.js.map +1 -1
  70. package/dist/serve.d.ts +10 -0
  71. package/dist/serve.d.ts.map +1 -1
  72. package/dist/serve.js +132 -42
  73. package/dist/serve.js.map +1 -1
  74. package/package.json +11 -11
  75. package/src/actions.ts +72 -56
  76. package/src/adapters/cloudflare.ts +118 -16
  77. package/src/adapters/node.ts +14 -1
  78. package/src/adapters/static.ts +18 -5
  79. package/src/build.ts +176 -12
  80. package/src/built-runtime.ts +2 -2
  81. package/src/cache.ts +127 -15
  82. package/src/cli-options.ts +39 -2
  83. package/src/cli.ts +7 -1
  84. package/src/client.ts +15 -2
  85. package/src/navigation-marker.ts +353 -0
  86. package/src/navigation-runtime.ts +1 -0
  87. package/src/node-server.ts +27 -1
  88. package/src/prerender-entry.ts +129 -0
  89. package/src/render.ts +267 -121
  90. package/src/request-header-tracking.ts +161 -0
  91. package/src/route-source.ts +71 -0
  92. package/src/security-headers.ts +41 -0
  93. package/src/serve.ts +188 -48
@@ -9,6 +9,8 @@ import { validateRouteMetadata } from "../metadata.js";
9
9
  import { routeSecurityHeaders } from "../security-headers.js";
10
10
  import { emitRouterDevtoolsEvent } from "./devtools.js";
11
11
  import { escapeHtmlAttribute, escapeHtmlText } from "@reckona/mreact-shared/html-escape";
12
+ import { isCurrentPrerenderedRoute, replayedPrerenderedRouteHeaders } from "../prerender-entry.js";
13
+ import { hasNavigationRouteMarker } from "../navigation-marker.js";
12
14
  const clientPrefix = "/_mreact/client/";
13
15
  const cloudflareMiddlewareRouteModuleKey = "__middleware__";
14
16
  const defaultPrerenderCacheOrigin = "https://mreact.local";
@@ -81,7 +83,25 @@ export function createCloudflareRouteModuleRenderer(options) {
81
83
  if (middlewareResult.type === "response") {
82
84
  return middlewareResult.response;
83
85
  }
84
- request = middlewareResult.request;
86
+ if (middlewareResult.request !== request) {
87
+ // The route was matched against the pre-middleware URL, so a rewrite has
88
+ // to re-match: otherwise the rewritten path would still render the module
89
+ // the visitor asked for, which is the module a rewrite is often used to
90
+ // keep them away from. The built runtime re-matches the same way.
91
+ request = middlewareResult.request;
92
+ const rematched = matchCloudflareRoute(sortedCloudflareRoutes(context.serverManifest), new URL(request.url).pathname);
93
+ if (rematched === undefined) {
94
+ return new Response("Not Found", { status: 404 });
95
+ }
96
+ context = { ...context, params: rematched.params, route: rematched.route };
97
+ }
98
+ // Middleware apps skip the prerendered fast path in `handleCloudflareRequest`
99
+ // so that access gates run first, so the stored HTML is resolved here
100
+ // instead, against the post-middleware path.
101
+ const prerendered = prerenderedResponse(context.serverManifest.prerenderedRoutes, normalizeRoutePath(new URL(request.url).pathname), request, isCloudflareNavigationRequest(request));
102
+ if (prerendered !== undefined) {
103
+ return prerendered;
104
+ }
85
105
  if (context.route.kind !== "server" && isCloudflareNavigationRequest(request)) {
86
106
  return cloudflareDocumentReloadNavigationResponse();
87
107
  }
@@ -152,6 +172,9 @@ export function createCloudflareRouteModuleRenderer(options) {
152
172
  }
153
173
  const metadata = await runWithCloudflareQueryClient(queryClient, () => resolveCloudflareRouteMetadata([pageModule], props));
154
174
  if (rendered instanceof Response) {
175
+ if (pageModule.__mreactSecurityHeadersApplied === true) {
176
+ return rendered;
177
+ }
155
178
  return withDefaultSecurityHeaders(rendered, request, metadata);
156
179
  }
157
180
  const modulePreload = cloudflareModulePreloadTag(context.clientManifest, context.route.path);
@@ -170,7 +193,7 @@ export function createCloudflareRouteModuleRenderer(options) {
170
193
  body,
171
194
  modulePreload,
172
195
  }));
173
- const documentedWithQueryState = await injectCloudflareQueryState(documented, dehydrate(queryClient));
196
+ const documentedWithQueryState = await injectCloudflareQueryState(documented, dehydrate(queryClient, options.dehydrateOptions));
174
197
  return withDefaultSecurityHeaders(documentedWithQueryState instanceof Response
175
198
  ? documentedWithQueryState
176
199
  : new Response(documentedWithQueryState, {
@@ -237,6 +260,7 @@ async function runWithCloudflareQueryClient(queryClient, fn) {
237
260
  if (isQueryClientScopeUnavailableError(error)) {
238
261
  installQueryAsyncStorage(cloudflareQueryClientStorage);
239
262
  cloudflareQueryClientFallbackInstalled = true;
263
+ console.warn('[mreact] Cloudflare AsyncLocalStorage is unavailable. Enable the "nodejs_compat" compatibility flag; rendering is serialized until native request-local storage is available.');
240
264
  return await runWithSerializedCloudflareQueryClient(queryClient, fn);
241
265
  }
242
266
  throw error;
@@ -245,6 +269,10 @@ async function runWithCloudflareQueryClient(queryClient, fn) {
245
269
  const cloudflareQueryClientStorage = createCloudflareQueryClientStorage();
246
270
  let cloudflareQueryClientFallbackQueue = Promise.resolve();
247
271
  let cloudflareQueryClientFallbackInstalled = false;
272
+ export function __resetCloudflareQueryClientFallbackForTesting() {
273
+ cloudflareQueryClientFallbackQueue = Promise.resolve();
274
+ cloudflareQueryClientFallbackInstalled = false;
275
+ }
248
276
  async function runWithSerializedCloudflareQueryClient(queryClient, fn) {
249
277
  const previous = cloudflareQueryClientFallbackQueue;
250
278
  let releaseCurrent;
@@ -605,7 +633,13 @@ async function handleCloudflareRequest(options, request, env, context) {
605
633
  });
606
634
  return assetResponse;
607
635
  }
608
- const staticResponse = prerenderedResponse(options.serverManifest.prerenderedRoutes, normalizeRoutePath(url.pathname), request.method, isCloudflareNavigationRequest(request));
636
+ // Prerendered HTML may only short-circuit `render` when the app has no
637
+ // middleware. Middleware runs inside `render`, so serving stored HTML here
638
+ // would bypass auth, geo blocking and maintenance gates for exactly the
639
+ // routes they are most often used to protect.
640
+ const staticResponse = builtServerManifestHasMiddleware(options.serverManifest)
641
+ ? undefined
642
+ : prerenderedResponse(options.serverManifest.prerenderedRoutes, normalizeRoutePath(url.pathname), request, isCloudflareNavigationRequest(request));
609
643
  if (staticResponse !== undefined) {
610
644
  emitRouterDevtoolsEvent({
611
645
  method: request.method,
@@ -690,19 +724,36 @@ function cacheControlHasDirective(cacheControl, directive) {
690
724
  const normalizedDirective = directive.toLowerCase();
691
725
  return cacheControl.split(",").some((part) => part.trim().toLowerCase() === normalizedDirective);
692
726
  }
693
- function prerenderedResponse(prerenderedRoutes, path, method, isNavigation) {
694
- if (method !== "GET" && method !== "HEAD") {
727
+ // `serverManifest.files` is keyed on paths relative to the project root, so a
728
+ // default `src/app` project stores the middleware as "src/app/middleware.ts".
729
+ // This mirrors `hasMiddleware` in built-runtime.ts, which resolves the same two
730
+ // names against the routes directory.
731
+ function builtServerManifestHasMiddleware(manifest) {
732
+ const routesDir = (manifest.routesDir ?? "").replaceAll("\\", "/").replace(/\/+$/, "");
733
+ const prefix = routesDir === "" ? "" : `${routesDir}/`;
734
+ return (manifest.files[`${prefix}middleware.ts`] !== undefined ||
735
+ manifest.files[`${prefix}middleware.mreact.ts`] !== undefined);
736
+ }
737
+ function prerenderedResponse(prerenderedRoutes, path, request, isNavigation) {
738
+ if (request.method !== "GET" && request.method !== "HEAD") {
695
739
  return undefined;
696
740
  }
697
741
  const prerendered = prerenderedRoutes?.[path];
698
- if (prerendered === undefined) {
742
+ const candidateNavigationHtml = prerendered?.navigationHtml;
743
+ if (!isCurrentPrerenderedRoute(prerendered)) {
744
+ if (isNavigation &&
745
+ typeof candidateNavigationHtml === "string" &&
746
+ !hasNavigationRouteMarker(candidateNavigationHtml)) {
747
+ return cloudflareDocumentReloadNavigationResponse();
748
+ }
699
749
  return undefined;
700
750
  }
701
- if (isNavigation && !prerendered.html.includes("data-mreact-route-id")) {
751
+ const html = isNavigation ? prerendered.navigationHtml : prerendered.html;
752
+ if (html === undefined) {
702
753
  return cloudflareDocumentReloadNavigationResponse();
703
754
  }
704
- return new Response(method === "HEAD" ? null : prerendered.html, {
705
- headers: prerendered.headers,
755
+ return new Response(request.method === "HEAD" ? null : html, {
756
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
706
757
  status: prerendered.status,
707
758
  });
708
759
  }
@@ -711,7 +762,7 @@ function cloudflareRouteRequiresModule(route, manifest) {
711
762
  route.kind === "server" ||
712
763
  (route.kind === "page" &&
713
764
  (route.segments.some((segment) => segment.kind !== "static") ||
714
- manifest.prerenderedRoutes?.[route.path] === undefined)));
765
+ !isCurrentPrerenderedRoute(manifest.prerenderedRoutes?.[route.path]))));
715
766
  }
716
767
  function cloudflareRouteGlobKeyMatchesRoute(key, routeFile) {
717
768
  const normalizedKey = normalizeCloudflareRouteModulePath(key);
@@ -722,6 +773,18 @@ function normalizeCloudflareRouteModulePath(path) {
722
773
  const withoutPrefix = path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
723
774
  return withoutPrefix.replace(/\.(?:mjs|js|ts|tsx)$/, "");
724
775
  }
776
+ // The manifest is the same object across requests, so a rewrite-heavy app pays
777
+ // for the specificity sort once rather than on every rewritten request.
778
+ const sortedRoutesByManifest = new WeakMap();
779
+ function sortedCloudflareRoutes(manifest) {
780
+ const cached = sortedRoutesByManifest.get(manifest);
781
+ if (cached !== undefined) {
782
+ return cached;
783
+ }
784
+ const sorted = [...manifest.routes].sort(compareCloudflareRoutes);
785
+ sortedRoutesByManifest.set(manifest, sorted);
786
+ return sorted;
787
+ }
725
788
  function matchCloudflareRoute(routes, pathname) {
726
789
  const normalizedPath = normalizeRoutePath(pathname);
727
790
  const pathSegments = normalizedPath === "/" ? [] : normalizedPath.slice(1).split("/");
@@ -852,7 +915,7 @@ function cloudflareHydrationMarkerParts(options) {
852
915
  const escapedRouteId = escapeHtmlAttribute(routeId);
853
916
  const propsJson = escapeScriptJson(JSON.stringify({
854
917
  params: options.params,
855
- request: { url: options.request.url },
918
+ request: { url: new URL(options.request.url).pathname },
856
919
  data: options.data,
857
920
  }));
858
921
  const clientReferencesJson = route.clientReferenceManifest === undefined || route.clientReferenceManifest.length === 0