@reckona/mreact-router 0.0.202 → 0.0.204

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 (152) hide show
  1. package/README.md +58 -11
  2. package/dist/actions.d.ts +8 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +125 -45
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +5 -2
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +35 -11
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +4 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +185 -13
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +4 -2
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +216 -17
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli-options.d.ts +3 -0
  37. package/dist/cli-options.d.ts.map +1 -1
  38. package/dist/cli-options.js +10 -2
  39. package/dist/cli-options.js.map +1 -1
  40. package/dist/cli.js +14 -3
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +5 -0
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +43 -3
  45. package/dist/client.js.map +1 -1
  46. package/dist/config.d.ts +3 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +21 -2
  49. package/dist/config.js.map +1 -1
  50. package/dist/csp.d.ts +1 -0
  51. package/dist/csp.d.ts.map +1 -1
  52. package/dist/csp.js +9 -0
  53. package/dist/csp.js.map +1 -1
  54. package/dist/dehydrate-policy.d.ts +3 -0
  55. package/dist/dehydrate-policy.d.ts.map +1 -0
  56. package/dist/dehydrate-policy.js +16 -0
  57. package/dist/dehydrate-policy.js.map +1 -0
  58. package/dist/dev-server.d.ts +7 -2
  59. package/dist/dev-server.d.ts.map +1 -1
  60. package/dist/dev-server.js +120 -11
  61. package/dist/dev-server.js.map +1 -1
  62. package/dist/http.d.ts +1 -1
  63. package/dist/http.d.ts.map +1 -1
  64. package/dist/http.js +147 -7
  65. package/dist/http.js.map +1 -1
  66. package/dist/index.d.ts +4 -3
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/logger.d.ts +22 -1
  71. package/dist/logger.d.ts.map +1 -1
  72. package/dist/logger.js +22 -8
  73. package/dist/logger.js.map +1 -1
  74. package/dist/metadata.d.ts +8 -0
  75. package/dist/metadata.d.ts.map +1 -1
  76. package/dist/metadata.js +60 -22
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/navigation-marker.d.ts +2 -0
  79. package/dist/navigation-marker.d.ts.map +1 -0
  80. package/dist/navigation-marker.js +357 -0
  81. package/dist/navigation-marker.js.map +1 -0
  82. package/dist/navigation-runtime.d.ts +1 -1
  83. package/dist/navigation-runtime.d.ts.map +1 -1
  84. package/dist/navigation-runtime.js +1 -1
  85. package/dist/navigation-runtime.js.map +1 -1
  86. package/dist/navigation.d.ts +1 -0
  87. package/dist/navigation.d.ts.map +1 -1
  88. package/dist/navigation.js +1 -1
  89. package/dist/navigation.js.map +1 -1
  90. package/dist/node-server.d.ts +5 -2
  91. package/dist/node-server.d.ts.map +1 -1
  92. package/dist/node-server.js +75 -6
  93. package/dist/node-server.js.map +1 -1
  94. package/dist/prerender-entry.d.ts +17 -1
  95. package/dist/prerender-entry.d.ts.map +1 -1
  96. package/dist/prerender-entry.js +98 -3
  97. package/dist/prerender-entry.js.map +1 -1
  98. package/dist/render.d.ts +16 -2
  99. package/dist/render.d.ts.map +1 -1
  100. package/dist/render.js +142 -135
  101. package/dist/render.js.map +1 -1
  102. package/dist/serve.d.ts +11 -2
  103. package/dist/serve.d.ts.map +1 -1
  104. package/dist/serve.js +67 -41
  105. package/dist/serve.js.map +1 -1
  106. package/dist/session.d.ts.map +1 -1
  107. package/dist/session.js +52 -19
  108. package/dist/session.js.map +1 -1
  109. package/dist/upgrade.d.ts +59 -2
  110. package/dist/upgrade.d.ts.map +1 -1
  111. package/dist/upgrade.js +282 -1
  112. package/dist/upgrade.js.map +1 -1
  113. package/dist/vite-config.d.ts +2 -0
  114. package/dist/vite-config.d.ts.map +1 -1
  115. package/dist/vite-config.js +2 -0
  116. package/dist/vite-config.js.map +1 -1
  117. package/dist/vite.d.ts +4 -1
  118. package/dist/vite.d.ts.map +1 -1
  119. package/dist/vite.js +117 -6
  120. package/dist/vite.js.map +1 -1
  121. package/package.json +11 -11
  122. package/src/actions.ts +146 -60
  123. package/src/adapters/aws-lambda.ts +426 -115
  124. package/src/adapters/cloudflare.ts +58 -12
  125. package/src/adapters/edge.ts +2 -0
  126. package/src/adapters/node.ts +8 -10
  127. package/src/adapters/static.ts +31 -2
  128. package/src/build.ts +217 -13
  129. package/src/built-runtime.ts +28 -0
  130. package/src/cache.ts +272 -19
  131. package/src/cli-options.ts +15 -2
  132. package/src/cli.ts +16 -2
  133. package/src/client.ts +47 -3
  134. package/src/config.ts +43 -6
  135. package/src/csp.ts +11 -0
  136. package/src/dehydrate-policy.ts +23 -0
  137. package/src/dev-server.ts +188 -15
  138. package/src/http.ts +172 -6
  139. package/src/index.ts +14 -1
  140. package/src/logger.ts +47 -10
  141. package/src/metadata.ts +125 -60
  142. package/src/navigation-marker.ts +423 -0
  143. package/src/navigation-runtime.ts +1 -0
  144. package/src/navigation.ts +1 -1
  145. package/src/node-server.ts +128 -22
  146. package/src/prerender-entry.ts +127 -5
  147. package/src/render.ts +859 -749
  148. package/src/serve.ts +112 -82
  149. package/src/session.ts +56 -25
  150. package/src/upgrade.ts +402 -3
  151. package/src/vite-config.ts +6 -0
  152. package/src/vite.ts +157 -5
@@ -10,6 +10,7 @@ import {
10
10
  isQueryClientScopeUnavailableError,
11
11
  runWithQueryClient,
12
12
  type DehydratedQueryClient,
13
+ type DehydrateOptions,
13
14
  type QueryAsyncStorage,
14
15
  type QueryClient,
15
16
  } from "@reckona/mreact-query";
@@ -32,13 +33,17 @@ import { middlewareMatches, type MiddlewareModule } from "../middleware.js";
32
33
  import { normalizeRoutePath } from "../route-path.js";
33
34
  import type { AppRoute } from "../routes.js";
34
35
  import { contentSecurityPolicy } from "../csp.js";
35
- import { isNotFoundError, rewriteLocation } from "../navigation.js";
36
+ import { isNotFoundError, isRedirectError, rewriteLocation } from "../navigation.js";
36
37
  import { validateRouteMetadata } from "../metadata.js";
37
38
  import { routeSecurityHeaders } from "../security-headers.js";
38
39
  import type { AppRouterPrerenderStore } from "../serve.js";
39
40
  import { emitRouterDevtoolsEvent } from "./devtools.js";
40
41
  import { escapeHtmlAttribute, escapeHtmlText } from "@reckona/mreact-shared/html-escape";
41
- import { isCurrentPrerenderedRoute } from "../prerender-entry.js";
42
+ import {
43
+ isCurrentPrerenderedRoute,
44
+ replayedPrerenderedRouteHeaders,
45
+ validatedPrerenderedNavigationHtml,
46
+ } from "../prerender-entry.js";
42
47
 
43
48
  /** Re-exports build manifest contracts used by Cloudflare handlers. */
44
49
  export type {
@@ -66,6 +71,8 @@ export type {
66
71
  AppRouterRequestStartLogEvent,
67
72
  AppRouterRequestTimingLogEvent,
68
73
  AppRouterRuntime,
74
+ AppRouterUpgradeErrorLogEvent,
75
+ AppRouterUpgradeRejectedLogEvent,
69
76
  } from "../logger.js";
70
77
  /** Re-exports response hook contracts used by Cloudflare handlers. */
71
78
  export type { AppRouterResponseHook, AppRouterResponseHookContext } from "../render.js";
@@ -212,6 +219,7 @@ export type CloudflareRouteModuleComponent<Data = unknown, Env = unknown> = (
212
219
  * Defines the exports accepted from a Cloudflare page route module.
213
220
  */
214
221
  export interface CloudflareRouteModule<Data = unknown, Env = unknown> {
222
+ [exportName: string]: unknown;
215
223
  App?: CloudflareRouteModuleComponent<Data, Env> | undefined;
216
224
  CloudflareRouteComponent?: CloudflareRouteModuleComponent<Data, Env> | undefined;
217
225
  default?: CloudflareRouteModuleComponent<Data, Env> | undefined;
@@ -291,6 +299,7 @@ export type CloudflareRouteModuleRegistry<Env = unknown> = Record<
291
299
  * Configures the Cloudflare route module renderer.
292
300
  */
293
301
  export interface CloudflareRouteModuleRendererOptions<Env = unknown> {
302
+ dehydrateOptions?: DehydrateOptions | undefined;
294
303
  document?:
295
304
  | ((
296
305
  context: CloudflareRouteModuleComponentProps<unknown, Env> & {
@@ -496,7 +505,7 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
496
505
  const prerendered = prerenderedResponse(
497
506
  context.serverManifest.prerenderedRoutes,
498
507
  normalizeRoutePath(new URL(request.url).pathname),
499
- request.method,
508
+ request,
500
509
  isCloudflareNavigationRequest(request),
501
510
  );
502
511
 
@@ -570,12 +579,24 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
570
579
  return error;
571
580
  }
572
581
 
582
+ if (isRedirectError(error)) {
583
+ return new Response(null, {
584
+ headers: { location: error.location },
585
+ status: error.status,
586
+ });
587
+ }
588
+
573
589
  if (isNotFoundError(error)) {
574
590
  return cloudflareNotFoundResponse(request);
575
591
  }
576
592
 
577
593
  throw error;
578
594
  }
595
+
596
+ if (data instanceof Response) {
597
+ return data;
598
+ }
599
+
579
600
  const props = {
580
601
  ...context,
581
602
  data,
@@ -587,6 +608,13 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
587
608
  try {
588
609
  rendered = await runWithCloudflareQueryClient(queryClient, () => component(props));
589
610
  } catch (error) {
611
+ if (isRedirectError(error)) {
612
+ return new Response(null, {
613
+ headers: { location: error.location },
614
+ status: error.status,
615
+ });
616
+ }
617
+
590
618
  if (isNotFoundError(error)) {
591
619
  return cloudflareNotFoundResponse(request);
592
620
  }
@@ -599,6 +627,16 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
599
627
  );
600
628
 
601
629
  if (rendered instanceof Response) {
630
+ if (
631
+ (
632
+ pageModule as CloudflareRouteModule<unknown, Env> & {
633
+ __mreactSecurityHeadersApplied?: boolean | undefined;
634
+ }
635
+ ).__mreactSecurityHeadersApplied === true
636
+ ) {
637
+ return rendered;
638
+ }
639
+
602
640
  return withDefaultSecurityHeaders(rendered, request, metadata);
603
641
  }
604
642
 
@@ -623,7 +661,7 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
623
661
  );
624
662
  const documentedWithQueryState = await injectCloudflareQueryState(
625
663
  documented,
626
- dehydrate(queryClient),
664
+ dehydrate(queryClient, options.dehydrateOptions),
627
665
  );
628
666
 
629
667
  return withDefaultSecurityHeaders(
@@ -722,6 +760,9 @@ async function runWithCloudflareQueryClient<T>(
722
760
  if (isQueryClientScopeUnavailableError(error)) {
723
761
  installQueryAsyncStorage(cloudflareQueryClientStorage);
724
762
  cloudflareQueryClientFallbackInstalled = true;
763
+ console.warn(
764
+ '[mreact] Cloudflare AsyncLocalStorage is unavailable. Enable the "nodejs_compat" compatibility flag; rendering is serialized until native request-local storage is available.',
765
+ );
725
766
  return await runWithSerializedCloudflareQueryClient(queryClient, fn);
726
767
  }
727
768
 
@@ -733,6 +774,11 @@ const cloudflareQueryClientStorage = createCloudflareQueryClientStorage();
733
774
  let cloudflareQueryClientFallbackQueue: Promise<void> = Promise.resolve();
734
775
  let cloudflareQueryClientFallbackInstalled = false;
735
776
 
777
+ export function __resetCloudflareQueryClientFallbackForTesting(): void {
778
+ cloudflareQueryClientFallbackQueue = Promise.resolve();
779
+ cloudflareQueryClientFallbackInstalled = false;
780
+ }
781
+
736
782
  async function runWithSerializedCloudflareQueryClient<T>(
737
783
  queryClient: QueryClient,
738
784
  fn: () => T,
@@ -1234,7 +1280,7 @@ async function handleCloudflareRequest<Env>(
1234
1280
  : prerenderedResponse(
1235
1281
  options.serverManifest.prerenderedRoutes,
1236
1282
  normalizeRoutePath(url.pathname),
1237
- request.method,
1283
+ request,
1238
1284
  isCloudflareNavigationRequest(request),
1239
1285
  );
1240
1286
 
@@ -1377,25 +1423,25 @@ function builtServerManifestHasMiddleware(manifest: {
1377
1423
  function prerenderedResponse(
1378
1424
  prerenderedRoutes: Record<string, BuiltPrerenderedRoute> | undefined,
1379
1425
  path: string,
1380
- method: string,
1426
+ request: Request,
1381
1427
  isNavigation: boolean,
1382
1428
  ): Response | undefined {
1383
- if (method !== "GET" && method !== "HEAD") {
1429
+ if (request.method !== "GET" && request.method !== "HEAD") {
1384
1430
  return undefined;
1385
1431
  }
1386
1432
 
1387
1433
  const prerendered = prerenderedRoutes?.[path];
1388
-
1389
1434
  if (!isCurrentPrerenderedRoute(prerendered)) {
1390
1435
  return undefined;
1391
1436
  }
1392
1437
 
1393
- if (isNavigation && !prerendered.html.includes("data-mreact-route-id")) {
1438
+ const html = isNavigation ? validatedPrerenderedNavigationHtml(prerendered) : prerendered.html;
1439
+ if (html === undefined) {
1394
1440
  return cloudflareDocumentReloadNavigationResponse();
1395
1441
  }
1396
1442
 
1397
- return new Response(method === "HEAD" ? null : prerendered.html, {
1398
- headers: prerendered.headers,
1443
+ return new Response(request.method === "HEAD" ? null : html, {
1444
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
1399
1445
  status: prerendered.status,
1400
1446
  });
1401
1447
  }
@@ -1622,7 +1668,7 @@ function cloudflareHydrationMarkerParts(options: {
1622
1668
  const propsJson = escapeScriptJson(
1623
1669
  JSON.stringify({
1624
1670
  params: options.params,
1625
- request: { url: options.request.url },
1671
+ request: { url: new URL(options.request.url).pathname },
1626
1672
  data: options.data,
1627
1673
  }),
1628
1674
  );
@@ -20,6 +20,8 @@ export type {
20
20
  AppRouterRequestStartLogEvent,
21
21
  AppRouterRequestTimingLogEvent,
22
22
  AppRouterRuntime,
23
+ AppRouterUpgradeErrorLogEvent,
24
+ AppRouterUpgradeRejectedLogEvent,
23
25
  } from "../logger.js";
24
26
 
25
27
  /**
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DehydrateOptions } from "@reckona/mreact-query";
2
3
  import type { AppRouterCache } from "../cache.js";
3
4
  import { nodeRequestToWebRequest, sendResponse } from "../http.js";
4
5
  import type { AppRouterServerActionOptions } from "../actions.js";
@@ -19,10 +20,7 @@ import { resolveNodeRequestProtocol } from "../node-server.js";
19
20
  /** Re-exports cache contracts used by Node handlers. */
20
21
  export type { AppRouterCache, AppRouterCacheEntry } from "../cache.js";
21
22
  /** Re-exports server action contracts used by Node handlers. */
22
- export type {
23
- AppRouterAllowedServerAction,
24
- AppRouterServerActionOptions,
25
- } from "../actions.js";
23
+ export type { AppRouterAllowedServerAction, AppRouterServerActionOptions } from "../actions.js";
26
24
  /** Re-exports import policy contracts used by Node handlers. */
27
25
  export type { AppRouterImportPolicy } from "../import-policy.js";
28
26
  /** Re-exports logger contracts used by Node handlers. */
@@ -37,6 +35,8 @@ export type {
37
35
  AppRouterRequestStartLogEvent,
38
36
  AppRouterRequestTimingLogEvent,
39
37
  AppRouterRuntime,
38
+ AppRouterUpgradeErrorLogEvent,
39
+ AppRouterUpgradeRejectedLogEvent,
40
40
  } from "../logger.js";
41
41
  /** Re-exports response hook contracts used by Node handlers. */
42
42
  export type { AppRouterResponseHook, AppRouterResponseHookContext } from "../render.js";
@@ -63,17 +63,14 @@ import {
63
63
  } from "../serve.js";
64
64
 
65
65
  /** Re-exports request and rendering contracts used by Node handlers. */
66
- export type {
67
- AppRouterPrerenderStore,
68
- RequestHostPolicy,
69
- ResponseSinkStrategy,
70
- } from "../serve.js";
66
+ export type { AppRouterPrerenderStore, RequestHostPolicy, ResponseSinkStrategy } from "../serve.js";
71
67
 
72
68
  /**
73
69
  * Configures a Node request handler for built app-router output.
74
70
  */
75
71
  export interface NodeRequestHandlerOptions {
76
72
  allowedHosts?: readonly string[] | undefined;
73
+ dehydrateOptions?: DehydrateOptions | undefined;
77
74
  errorHandler?:
78
75
  | ((error: unknown) => {
79
76
  body: string;
@@ -139,13 +136,14 @@ export function createNodeRequestHandler(options: NodeRequestHandlerOptions): No
139
136
  forwardedProto: incoming.headers["x-forwarded-proto"],
140
137
  trustForwardedProto: options.trustForwardedProto,
141
138
  });
142
- const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`);
139
+ const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`, outgoing);
143
140
  const logFields = requestLogFields(request, "node");
144
141
  emitRouterLog(options.logger, "info", {
145
142
  ...logFields,
146
143
  type: "router:request:start",
147
144
  });
148
145
  const response = await renderBuiltAppRequest({
146
+ dehydrateOptions: options.dehydrateOptions,
149
147
  outDir: options.outDir,
150
148
  importPolicy: options.importPolicy,
151
149
  instrumentation: options.instrumentation,
@@ -1,7 +1,13 @@
1
1
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve, sep } from "node:path";
3
3
  import type { BuiltPrerenderedRoute, BuiltServerManifest } from "../build.js";
4
- import { isCurrentPrerenderedRoute } from "../prerender-entry.js";
4
+ import {
5
+ isCurrentPrerenderedRoute,
6
+ validatedPrerenderedNavigationHtml,
7
+ } from "../prerender-entry.js";
8
+
9
+ const staticNavigationBase = "/_mreact/navigation";
10
+ const staticNavigationMetadata = `<meta name="mreact-static-navigation" content="${staticNavigationBase}">`;
5
11
 
6
12
  /**
7
13
  * Configures static export from a built app-router output directory.
@@ -104,8 +110,31 @@ async function writePrerenderedRoute(
104
110
  }
105
111
 
106
112
  const file = routeToHtmlFile(exportDir, route);
113
+ const navigationHtml = validatedPrerenderedNavigationHtml(entry);
114
+ if (navigationHtml === undefined) {
115
+ throw new Error(`Cannot export route ${route} without a valid navigation variant.`);
116
+ }
117
+ const navigationFile = routeToHtmlFile(join(exportDir, staticNavigationBase), route);
107
118
  await mkdir(dirname(file), { recursive: true });
108
- await writeFile(file, entry.html);
119
+ await mkdir(dirname(navigationFile), { recursive: true });
120
+ await writeFile(file, withStaticNavigationMetadata(entry.html));
121
+ await writeFile(navigationFile, navigationHtml);
122
+ }
123
+
124
+ function withStaticNavigationMetadata(html: string): string {
125
+ const head = /<head(?:\s[^>]*)?>/i.exec(html);
126
+ if (head?.index !== undefined) {
127
+ const insertion = head.index + head[0].length;
128
+ return `${html.slice(0, insertion)}${staticNavigationMetadata}${html.slice(insertion)}`;
129
+ }
130
+
131
+ const doctype = /^\s*<!doctype\s+html\s*>/i.exec(html);
132
+ if (doctype !== null) {
133
+ const insertion = doctype[0].length;
134
+ return `${html.slice(0, insertion)}${staticNavigationMetadata}${html.slice(insertion)}`;
135
+ }
136
+
137
+ return `${staticNavigationMetadata}${html}`;
109
138
  }
110
139
 
111
140
  function routeToHtmlFile(exportDir: string, route: string): string {