@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
package/src/logger.ts CHANGED
@@ -27,7 +27,9 @@ export type AppRouterLogEvent =
27
27
  | AppRouterRequestErrorLogEvent
28
28
  | AppRouterRequestTimingLogEvent
29
29
  | AppRouterRenderTimingLogEvent
30
- | AppRouterCspInlineNonceWarningLogEvent;
30
+ | AppRouterCspInlineNonceWarningLogEvent
31
+ | AppRouterUpgradeErrorLogEvent
32
+ | AppRouterUpgradeRejectedLogEvent;
31
33
 
32
34
  /**
33
35
  * Logs the start of an app-router request.
@@ -63,6 +65,29 @@ export interface AppRouterRequestErrorLogEvent {
63
65
  type: "router:request:error";
64
66
  }
65
67
 
68
+ /**
69
+ * Logs a failed or timed-out HTTP upgrade handler.
70
+ */
71
+ export interface AppRouterUpgradeErrorLogEvent {
72
+ durationMs: number;
73
+ error: AppRouterLogError;
74
+ method: string;
75
+ path: string;
76
+ runtime: "node";
77
+ type: "router:upgrade:error";
78
+ }
79
+
80
+ /**
81
+ * Logs an HTTP upgrade rejected before application ownership.
82
+ */
83
+ export interface AppRouterUpgradeRejectedLogEvent {
84
+ method: string;
85
+ path: string;
86
+ reason: "disallowed-origin" | "malformed-origin" | "missing-origin" | "opaque-origin";
87
+ runtime: "node";
88
+ type: "router:upgrade:rejected";
89
+ }
90
+
66
91
  /**
67
92
  * Logs per-phase timings for a complete app-router request.
68
93
  */
@@ -116,7 +141,12 @@ export function emitRouterLog(
116
141
  level: AppRouterLogLevel,
117
142
  event: AppRouterLogEvent,
118
143
  ): void {
119
- const sink = logger?.[level];
144
+ let sink: AppRouterLogger[AppRouterLogLevel];
145
+ try {
146
+ sink = logger?.[level];
147
+ } catch {
148
+ return;
149
+ }
120
150
 
121
151
  if (sink === undefined) {
122
152
  return;
@@ -176,15 +206,22 @@ export function logDurationMs(startedAt: number): number {
176
206
  }
177
207
 
178
208
  export function logError(error: unknown): AppRouterLogError {
179
- if (error instanceof Error) {
209
+ try {
210
+ if (error instanceof Error) {
211
+ return {
212
+ message: String(error.message),
213
+ name: String(error.name),
214
+ };
215
+ }
216
+
180
217
  return {
181
- message: error.message,
182
- name: error.name,
218
+ message: String(error),
219
+ name: "Error",
220
+ };
221
+ } catch {
222
+ return {
223
+ message: "Unknown non-coercible error",
224
+ name: "Error",
183
225
  };
184
226
  }
185
-
186
- return {
187
- message: String(error),
188
- name: "Error",
189
- };
190
227
  }
package/src/metadata.ts CHANGED
@@ -4,10 +4,10 @@ import {
4
4
  } from "@reckona/mreact-shared/html-escape";
5
5
  import {
6
6
  isDangerousHtmlAttribute,
7
+ isUnsafeMetaRefreshContent,
7
8
  isUnsafeUrlAttribute,
8
9
  } from "@reckona/mreact-shared/url-safety";
9
10
  import { contentSecurityPolicy } from "./csp.js";
10
- import type { AppFileConvention } from "./file-conventions.js";
11
11
  import type { AppRoute } from "./routes.js";
12
12
  import { routeSecurityHeaders } from "./security-headers.js";
13
13
  import type {
@@ -28,6 +28,13 @@ const DEFAULT_HTML_RESPONSE_HEADERS = Object.freeze({
28
28
  "content-type": "text/html; charset=utf-8",
29
29
  });
30
30
 
31
+ export interface FileConventionMetadataTemplate {
32
+ appleIconPath?: string | undefined;
33
+ iconPath?: string | undefined;
34
+ openGraphImageFallbackPath?: string | undefined;
35
+ openGraphImageRoutePath?: string | undefined;
36
+ }
37
+
31
38
  export function mergeRouteMetadata(metadata: readonly RouteMetadata[]): RouteMetadata | undefined {
32
39
  if (metadata.length === 0) {
33
40
  return undefined;
@@ -67,23 +74,64 @@ export function applyFileConventionMetadata(
67
74
  filename: string,
68
75
  params: RouteParams,
69
76
  ): RouteMetadata | undefined {
70
- const next: RouteMetadata = metadata === undefined ? {} : { ...metadata };
77
+ return applyFileConventionMetadataTemplate(
78
+ metadata,
79
+ fileConventionMetadataTemplate(routes, filename),
80
+ params,
81
+ );
82
+ }
83
+
84
+ export function fileConventionMetadataTemplate(
85
+ routes: readonly AppRoute[],
86
+ filename: string,
87
+ ): FileConventionMetadataTemplate {
88
+ const pageRoute = routes.find((route) => route.kind === "page" && route.file === filename);
71
89
  const iconRoute = routes.find((route) => route.kind === "asset" && route.convention === "icon");
72
90
  const appleIconRoute = routes.find(
73
91
  (route) => route.kind === "asset" && route.convention === "apple-icon",
74
92
  );
75
- const openGraphImagePath = fileConventionMetadataRoutePath(
76
- routes,
77
- filename,
78
- params,
79
- "opengraph-image",
93
+ const openGraphImageRoutes = routes.filter(
94
+ (route) =>
95
+ (route.kind === "asset" || route.kind === "metadata") &&
96
+ route.convention === "opengraph-image",
80
97
  );
98
+ const expectedOpenGraphImagePath =
99
+ pageRoute === undefined
100
+ ? undefined
101
+ : pageRoute.path === "/"
102
+ ? "/opengraph-image"
103
+ : `${pageRoute.path}/opengraph-image`;
81
104
 
82
- if (iconRoute !== undefined && next.icons?.icon === undefined) {
83
- next.icons = { ...next.icons, icon: iconRoute.path };
105
+ return {
106
+ ...(appleIconRoute === undefined ? {} : { appleIconPath: appleIconRoute.path }),
107
+ ...(iconRoute === undefined ? {} : { iconPath: iconRoute.path }),
108
+ ...(openGraphImageRoutes.find((route) => route.path === "/opengraph-image") === undefined
109
+ ? {}
110
+ : { openGraphImageFallbackPath: "/opengraph-image" }),
111
+ ...(expectedOpenGraphImagePath === undefined ||
112
+ openGraphImageRoutes.find((route) => route.path === expectedOpenGraphImagePath) === undefined
113
+ ? {}
114
+ : { openGraphImageRoutePath: expectedOpenGraphImagePath }),
115
+ };
116
+ }
117
+
118
+ export function applyFileConventionMetadataTemplate(
119
+ metadata: RouteMetadata | undefined,
120
+ template: FileConventionMetadataTemplate,
121
+ params: RouteParams,
122
+ ): RouteMetadata | undefined {
123
+ const next: RouteMetadata = metadata === undefined ? {} : { ...metadata };
124
+ const openGraphImagePath =
125
+ (template.openGraphImageRoutePath === undefined
126
+ ? undefined
127
+ : concreteRoutePath(template.openGraphImageRoutePath, params)) ??
128
+ template.openGraphImageFallbackPath;
129
+
130
+ if (template.iconPath !== undefined && next.icons?.icon === undefined) {
131
+ next.icons = { ...next.icons, icon: template.iconPath };
84
132
  }
85
- if (appleIconRoute !== undefined && next.icons?.apple === undefined) {
86
- next.icons = { ...next.icons, apple: appleIconRoute.path };
133
+ if (template.appleIconPath !== undefined && next.icons?.apple === undefined) {
134
+ next.icons = { ...next.icons, apple: template.appleIconPath };
87
135
  }
88
136
  if (
89
137
  openGraphImagePath !== undefined &&
@@ -268,33 +316,6 @@ export function serializeSitemap(entries: readonly SitemapEntry[]): string {
268
316
  return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`;
269
317
  }
270
318
 
271
- function fileConventionMetadataRoutePath(
272
- routes: readonly AppRoute[],
273
- filename: string,
274
- params: RouteParams,
275
- convention: AppFileConvention,
276
- ): string | undefined {
277
- const pageRoute = routes.find((route) => route.kind === "page" && route.file === filename);
278
- const candidateRoutes = routes.filter(
279
- (route) =>
280
- (route.kind === "asset" || route.kind === "metadata") &&
281
- route.convention === convention,
282
- );
283
-
284
- if (pageRoute !== undefined) {
285
- const expectedPath = pageRoute.path === "/" ? `/${convention}` : `${pageRoute.path}/${convention}`;
286
- const routeLocal = candidateRoutes.find((route) => route.path === expectedPath);
287
- const routeLocalPath =
288
- routeLocal === undefined ? undefined : concreteRoutePath(routeLocal.path, params);
289
-
290
- if (routeLocalPath !== undefined) {
291
- return routeLocalPath;
292
- }
293
- }
294
-
295
- return candidateRoutes.find((route) => route.path === `/${convention}`)?.path;
296
- }
297
-
298
319
  function concreteRoutePath(path: string, params: RouteParams): string | undefined {
299
320
  const segments = path === "/" ? [] : path.slice(1).split("/");
300
321
  const concrete: string[] = [];
@@ -695,7 +716,9 @@ function validateOptionalHeadMetadata(value: unknown, path: string): void {
695
716
  typeof head.nonce !== "boolean" &&
696
717
  typeof head.nonce !== "string"
697
718
  ) {
698
- throw new Error(`Invalid metadata field ${descriptorPath}.nonce: expected string or boolean.`);
719
+ throw new Error(
720
+ `Invalid metadata field ${descriptorPath}.nonce: expected string or boolean.`,
721
+ );
699
722
  }
700
723
  if (head.attrs !== undefined) {
701
724
  assertPlainMetadataObject(head.attrs, `${descriptorPath}.attrs`);
@@ -713,6 +736,7 @@ function validateOptionalHeadMetadata(value: unknown, path: string): void {
713
736
  }
714
737
  }
715
738
  }
739
+ validateMetaRefreshHeadDescriptor(head, descriptorPath);
716
740
  validateUnknownJsonMetadataFields(
717
741
  descriptor as Record<string, unknown>,
718
742
  descriptorPath,
@@ -721,6 +745,36 @@ function validateOptionalHeadMetadata(value: unknown, path: string): void {
721
745
  });
722
746
  }
723
747
 
748
+ function validateMetaRefreshHeadDescriptor(head: RouteHeadDescriptor, path: string): void {
749
+ if (head.tag !== "meta" || head.attrs === undefined) {
750
+ return;
751
+ }
752
+
753
+ const attrs = Object.entries(head.attrs);
754
+ const hasRefresh = attrs.some(([name, value]) => {
755
+ const canonicalName = name.toLowerCase();
756
+ return (
757
+ (canonicalName === "http-equiv" || canonicalName === "httpequiv") &&
758
+ typeof value === "string" &&
759
+ value.toLowerCase() === "refresh"
760
+ );
761
+ });
762
+
763
+ if (!hasRefresh) {
764
+ return;
765
+ }
766
+
767
+ for (const [name, value] of attrs) {
768
+ if (
769
+ name.toLowerCase() === "content" &&
770
+ typeof value === "string" &&
771
+ isUnsafeMetaRefreshContent("refresh", value)
772
+ ) {
773
+ throw new Error(`Invalid metadata field ${path}.attrs.${name}: unsafe refresh URL value.`);
774
+ }
775
+ }
776
+ }
777
+
724
778
  function validateHeadAttribute(name: string, value: unknown, path: string): void {
725
779
  if (!isSafeHeadAttributeName(name)) {
726
780
  throw new Error(`Invalid metadata field ${path}: expected safe HTML attribute name.`);
@@ -728,7 +782,9 @@ function validateHeadAttribute(name: string, value: unknown, path: string): void
728
782
 
729
783
  const canonicalName = name.toLowerCase();
730
784
  if (canonicalName.startsWith("on") || isDangerousHtmlAttribute(canonicalName)) {
731
- throw new Error(`Invalid metadata field ${path}: event and dangerous attributes are not allowed.`);
785
+ throw new Error(
786
+ `Invalid metadata field ${path}: event and dangerous attributes are not allowed.`,
787
+ );
732
788
  }
733
789
 
734
790
  if (typeof value === "string" && isUnsafeUrlAttribute(canonicalName, value)) {
@@ -866,20 +922,24 @@ function validateOptionalViewportMetadata(value: unknown, path: string): void {
866
922
  }
867
923
 
868
924
  function validateUnknownMetadataFields(metadata: RouteMetadata, path: string): void {
869
- validateUnknownJsonMetadataFields(metadata as Record<string, unknown>, path, new Set([
870
- "alternates",
871
- "csp",
872
- "description",
873
- "head",
874
- "icons",
875
- "lang",
876
- "openGraph",
877
- "robots",
878
- "security",
879
- "themeColor",
880
- "title",
881
- "viewport",
882
- ]));
925
+ validateUnknownJsonMetadataFields(
926
+ metadata as Record<string, unknown>,
927
+ path,
928
+ new Set([
929
+ "alternates",
930
+ "csp",
931
+ "description",
932
+ "head",
933
+ "icons",
934
+ "lang",
935
+ "openGraph",
936
+ "robots",
937
+ "security",
938
+ "themeColor",
939
+ "title",
940
+ "viewport",
941
+ ]),
942
+ );
883
943
  }
884
944
 
885
945
  function validateUnknownJsonMetadataFields(
@@ -911,12 +971,13 @@ function validateJsonSerializableMetadata(value: unknown, path: string): void {
911
971
  return;
912
972
  }
913
973
 
914
- throw new Error(
915
- `Invalid metadata field ${path}: expected a JSON-serializable value.`,
916
- );
974
+ throw new Error(`Invalid metadata field ${path}: expected a JSON-serializable value.`);
917
975
  }
918
976
 
919
- function assertPlainMetadataObject(value: unknown, path: string): asserts value is Record<string, unknown> {
977
+ function assertPlainMetadataObject(
978
+ value: unknown,
979
+ path: string,
980
+ ): asserts value is Record<string, unknown> {
920
981
  if (!isPlainMetadataObject(value)) {
921
982
  throw new Error(`Invalid metadata field ${path}: expected object.`);
922
983
  }
@@ -938,7 +999,9 @@ function openGraphImages(openGraph: RouteMetadata["openGraph"]): readonly string
938
999
  );
939
1000
  }
940
1001
 
941
- return openGraph?.image === undefined ? [] : [metadataImageUrl(openGraph.image, "openGraph.image")];
1002
+ return openGraph?.image === undefined
1003
+ ? []
1004
+ : [metadataImageUrl(openGraph.image, "openGraph.image")];
942
1005
  }
943
1006
 
944
1007
  function metadataImageUrl(value: MetadataImage | MetadataScalar, path: string): string {
@@ -950,7 +1013,9 @@ function metadataImageUrl(value: MetadataImage | MetadataScalar, path: string):
950
1013
  return metadataString(value.url, `${path}.url`);
951
1014
  }
952
1015
 
953
- throw new Error(`Invalid metadata field ${path}: expected string, number, boolean, or object with url.`);
1016
+ throw new Error(
1017
+ `Invalid metadata field ${path}: expected string, number, boolean, or object with url.`,
1018
+ );
954
1019
  }
955
1020
 
956
1021
  function robotsContent(robots: NonNullable<RouteMetadata["robots"]>): string {