@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.
- package/README.md +6 -3
- package/dist/actions.d.ts +2 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +61 -41
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +3 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +74 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/node.d.ts +7 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +7 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +14 -3
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +137 -14
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +4 -1
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +27 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +83 -16
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +7 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +27 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +294 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/node-server.d.ts +6 -0
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +19 -1
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +7 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +99 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +20 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +190 -110
- package/dist/render.js.map +1 -1
- package/dist/request-header-tracking.d.ts +44 -0
- package/dist/request-header-tracking.d.ts.map +1 -0
- package/dist/request-header-tracking.js +116 -0
- package/dist/request-header-tracking.js.map +1 -0
- package/dist/route-source.d.ts +16 -0
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +55 -1
- package/dist/route-source.js.map +1 -1
- package/dist/security-headers.d.ts +16 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +37 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/serve.d.ts +10 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +132 -42
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +72 -56
- package/src/adapters/cloudflare.ts +118 -16
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +176 -12
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +127 -15
- package/src/cli-options.ts +39 -2
- package/src/cli.ts +7 -1
- package/src/client.ts +15 -2
- package/src/navigation-marker.ts +353 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +129 -0
- package/src/render.ts +267 -121
- package/src/request-header-tracking.ts +161 -0
- package/src/route-source.ts +71 -0
- package/src/security-headers.ts +41 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
705
|
-
headers: prerendered
|
|
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]
|
|
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
|