@reckona/mreact-router 0.0.203 → 0.0.205

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 (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  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 +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  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 +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  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 +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. package/src/vite.ts +157 -5
package/src/upgrade.ts CHANGED
@@ -1,11 +1,410 @@
1
- import type { IncomingMessage } from "node:http";
1
+ import type { IncomingMessage, Server } from "node:http";
2
2
  import type { Duplex } from "node:stream";
3
+ import {
4
+ emitRouterLog,
5
+ logDurationMs,
6
+ logError,
7
+ logNow,
8
+ nodeRequestPath,
9
+ type AppRouterLogger,
10
+ } from "./logger.js";
11
+
12
+ export type HttpUpgradeDisposition = "declined" | "handled";
13
+
14
+ /**
15
+ * Explicitly claims or declines one HTTP upgrade before asynchronous verification begins.
16
+ */
17
+ export interface HttpUpgradeContext {
18
+ accept(): HttpUpgradeDisposition;
19
+ decline(): HttpUpgradeDisposition;
20
+ }
21
+
22
+ /** Handles an HTTP upgrade using the legacy three-argument callback contract. */
23
+ export type HttpUpgradeHandler = (request: IncomingMessage, socket: Duplex, head: Buffer) => void;
3
24
 
4
25
  /**
5
- * Handles an HTTP upgrade request on the app-router Node server.
26
+ * Handles a managed HTTP upgrade on the app-router Node server.
27
+ *
28
+ * Existing `HttpUpgradeHandler` callbacks remain assignable. Handlers that defer verification must
29
+ * call `context.accept()` synchronously; returning without writing a handshake is treated as a
30
+ * decline.
6
31
  */
7
- export type HttpUpgradeHandler = (
32
+ export type ManagedHttpUpgradeHandler = (
8
33
  request: IncomingMessage,
9
34
  socket: Duplex,
10
35
  head: Buffer,
36
+ context: HttpUpgradeContext,
11
37
  ) => void;
38
+
39
+ export type HttpUpgradeOriginFailureReason =
40
+ | "disallowed-origin"
41
+ | "malformed-origin"
42
+ | "missing-origin"
43
+ | "opaque-origin";
44
+
45
+ export type HttpUpgradeOriginValidation =
46
+ | { ok: true; origin: string | undefined }
47
+ | { ok: false; reason: HttpUpgradeOriginFailureReason };
48
+
49
+ export interface ValidateHttpUpgradeOriginOptions {
50
+ allowedOrigins: readonly string[];
51
+ allowMissingOrigin?: boolean | undefined;
52
+ }
53
+
54
+ export type HttpUpgradeOriginPolicy =
55
+ | "same-origin"
56
+ | "unchecked"
57
+ | {
58
+ allowedOrigins: readonly string[];
59
+ allowMissingOrigin?: boolean | undefined;
60
+ };
61
+
62
+ export function assertValidHttpUpgradeOriginPolicy(
63
+ policy: HttpUpgradeOriginPolicy | undefined,
64
+ ): void {
65
+ if (typeof policy === "object") {
66
+ for (const origin of policy.allowedOrigins) {
67
+ normalizeConfiguredOrigin(origin);
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validates a WebSocket/HTTP upgrade Origin against exact serialized origins.
74
+ *
75
+ * Missing origins are rejected by default. Configured origins must contain only an HTTP(S)
76
+ * scheme, host, and optional port; paths, credentials, queries, and fragments are invalid.
77
+ */
78
+ export function validateHttpUpgradeOrigin(
79
+ request: IncomingMessage,
80
+ options: ValidateHttpUpgradeOriginOptions,
81
+ ): HttpUpgradeOriginValidation {
82
+ const allowedOrigins = options.allowedOrigins.map(normalizeConfiguredOrigin);
83
+ const value = request.headers.origin;
84
+
85
+ if (value === undefined) {
86
+ return options.allowMissingOrigin === true
87
+ ? { ok: true, origin: undefined }
88
+ : { ok: false, reason: "missing-origin" };
89
+ }
90
+ if (Array.isArray(value) || value.includes(",")) {
91
+ return { ok: false, reason: "malformed-origin" };
92
+ }
93
+ if (value === "null") {
94
+ return { ok: false, reason: "opaque-origin" };
95
+ }
96
+
97
+ const origin = normalizeRequestOrigin(value);
98
+ if (origin === undefined) {
99
+ return { ok: false, reason: "malformed-origin" };
100
+ }
101
+
102
+ return allowedOrigins.includes(origin)
103
+ ? { ok: true, origin }
104
+ : { ok: false, reason: "disallowed-origin" };
105
+ }
106
+
107
+ function normalizeConfiguredOrigin(value: string): string {
108
+ const origin = normalizeOrigin(value);
109
+ if (origin === undefined) {
110
+ throw new TypeError(`Invalid HTTP upgrade allowed origin: ${value}`);
111
+ }
112
+ return origin;
113
+ }
114
+
115
+ function normalizeRequestOrigin(value: string): string | undefined {
116
+ return normalizeOrigin(value);
117
+ }
118
+
119
+ function normalizeOrigin(value: string): string | undefined {
120
+ let url: URL;
121
+ try {
122
+ url = new URL(value);
123
+ } catch {
124
+ return undefined;
125
+ }
126
+
127
+ if (
128
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
129
+ url.username !== "" ||
130
+ url.password !== "" ||
131
+ url.pathname !== "/" ||
132
+ url.search !== "" ||
133
+ url.hash !== "" ||
134
+ url.origin === "null"
135
+ ) {
136
+ return undefined;
137
+ }
138
+
139
+ return url.origin;
140
+ }
141
+
142
+ export interface ManagedHttpUpgradeOptions {
143
+ decisionTimeoutMs: number;
144
+ handler: ManagedHttpUpgradeHandler;
145
+ isOriginAllowed(request: IncomingMessage): HttpUpgradeOriginValidation;
146
+ logger?: AppRouterLogger | undefined;
147
+ shouldBypass?: ((request: IncomingMessage) => boolean) | undefined;
148
+ }
149
+
150
+ export interface ManagedHttpUpgradeLifecycle {
151
+ beginClose(): void;
152
+ handle(request: IncomingMessage, socket: Duplex, head: Buffer): void;
153
+ waitForSockets(timeoutMs: number): Promise<void>;
154
+ }
155
+
156
+ export function createManagedHttpUpgradeLifecycle(
157
+ options: ManagedHttpUpgradeOptions,
158
+ ): ManagedHttpUpgradeLifecycle {
159
+ const sockets = new Set<Duplex>();
160
+ const emptyWaiters = new Set<() => void>();
161
+ const pendingDecisionCancels = new Set<() => void>();
162
+ let closing = false;
163
+
164
+ function track(socket: Duplex): void {
165
+ sockets.add(socket);
166
+ socket.once("close", () => {
167
+ sockets.delete(socket);
168
+ if (sockets.size === 0) {
169
+ for (const resolve of emptyWaiters) {
170
+ resolve();
171
+ }
172
+ emptyWaiters.clear();
173
+ }
174
+ });
175
+ }
176
+
177
+ function destroy(socket: Duplex): void {
178
+ if (!socket.destroyed) {
179
+ socket.destroy();
180
+ }
181
+ }
182
+
183
+ function emitError(request: IncomingMessage, startedAt: number, error: unknown): void {
184
+ emitRouterLog(options.logger, "error", {
185
+ durationMs: logDurationMs(startedAt),
186
+ error: logError(error),
187
+ method: request.method ?? "GET",
188
+ path: nodeRequestPath(request.url),
189
+ runtime: "node",
190
+ type: "router:upgrade:error",
191
+ });
192
+ if (options.logger === undefined) {
193
+ try {
194
+ console.error("[mreact] HTTP upgrade handler failed:", error);
195
+ } catch {
196
+ // Failure reporting must never escape the HTTP upgrade listener.
197
+ }
198
+ }
199
+ }
200
+
201
+ function handle(request: IncomingMessage, socket: Duplex, head: Buffer): void {
202
+ const startedAt = logNow();
203
+ if (closing) {
204
+ destroy(socket);
205
+ return;
206
+ }
207
+ let bypass = false;
208
+ try {
209
+ bypass = options.shouldBypass?.(request) === true;
210
+ } catch (error) {
211
+ emitError(request, startedAt, error);
212
+ destroy(socket);
213
+ return;
214
+ }
215
+ if (bypass) {
216
+ return;
217
+ }
218
+
219
+ track(socket);
220
+
221
+ let origin: HttpUpgradeOriginValidation;
222
+ try {
223
+ origin = options.isOriginAllowed(request);
224
+ } catch (error) {
225
+ emitError(request, startedAt, error);
226
+ destroy(socket);
227
+ return;
228
+ }
229
+ if (!origin.ok) {
230
+ emitRouterLog(options.logger, "warn", {
231
+ method: request.method ?? "GET",
232
+ path: nodeRequestPath(request.url),
233
+ reason: origin.reason,
234
+ runtime: "node",
235
+ type: "router:upgrade:rejected",
236
+ });
237
+ destroy(socket);
238
+ return;
239
+ }
240
+
241
+ let disposition: HttpUpgradeDisposition | undefined;
242
+ let decisionTimer: ReturnType<typeof setTimeout> | undefined;
243
+ let failureReported = false;
244
+ let socketClosed = false;
245
+ const clearDecisionTimer = (): void => {
246
+ if (decisionTimer !== undefined) {
247
+ clearTimeout(decisionTimer);
248
+ decisionTimer = undefined;
249
+ }
250
+ pendingDecisionCancels.delete(clearDecisionTimer);
251
+ };
252
+ socket.once("close", () => {
253
+ socketClosed = true;
254
+ clearDecisionTimer();
255
+ });
256
+ const before = {
257
+ bytesWritten: socketBytesWritten(socket),
258
+ destroyed: socket.destroyed,
259
+ writableEnded: socket.writableEnded,
260
+ };
261
+
262
+ const decide = (next: HttpUpgradeDisposition): HttpUpgradeDisposition => {
263
+ if (disposition !== undefined) {
264
+ return disposition;
265
+ }
266
+ disposition = next;
267
+ clearDecisionTimer();
268
+ if (next === "declined") {
269
+ destroy(socket);
270
+ }
271
+ return next;
272
+ };
273
+ const context: HttpUpgradeContext = {
274
+ accept: () => decide("handled"),
275
+ decline: () => decide("declined"),
276
+ };
277
+
278
+ const fail = (error: unknown): void => {
279
+ if (failureReported) {
280
+ return;
281
+ }
282
+ failureReported = true;
283
+ clearDecisionTimer();
284
+ emitError(request, startedAt, error);
285
+ disposition ??= "declined";
286
+ destroy(socket);
287
+ };
288
+
289
+ let result: unknown;
290
+ try {
291
+ result = (
292
+ options.handler as (
293
+ request: IncomingMessage,
294
+ socket: Duplex,
295
+ head: Buffer,
296
+ context: HttpUpgradeContext,
297
+ ) => unknown
298
+ )(request, socket, head, context);
299
+ } catch (error) {
300
+ fail(error);
301
+ return;
302
+ }
303
+
304
+ if ((typeof result === "object" || typeof result === "function") && result !== null) {
305
+ if (disposition === undefined && !closing && !socketClosed) {
306
+ decisionTimer = setTimeout(() => {
307
+ if (disposition === undefined) {
308
+ fail(
309
+ new Error(`HTTP upgrade decision timed out after ${options.decisionTimeoutMs}ms.`),
310
+ );
311
+ }
312
+ }, options.decisionTimeoutMs);
313
+ decisionTimer.unref?.();
314
+ pendingDecisionCancels.add(clearDecisionTimer);
315
+ }
316
+
317
+ void Promise.resolve(result as PromiseLike<HttpUpgradeDisposition | void>).then(
318
+ (value) => {
319
+ if (socketClosed) {
320
+ return;
321
+ }
322
+ if (value === "declined" || value === "handled") {
323
+ decide(value);
324
+ } else if (disposition === undefined) {
325
+ decide(legacySocketAction(socket, before) ? "handled" : "declined");
326
+ }
327
+ },
328
+ (error) => {
329
+ fail(error);
330
+ },
331
+ );
332
+ return;
333
+ }
334
+
335
+ if (result === "declined" || result === "handled") {
336
+ decide(result);
337
+ } else if (disposition === undefined) {
338
+ decide(legacySocketAction(socket, before) ? "handled" : "declined");
339
+ }
340
+ }
341
+
342
+ return {
343
+ beginClose() {
344
+ if (closing) {
345
+ return;
346
+ }
347
+ closing = true;
348
+ for (const cancel of pendingDecisionCancels) {
349
+ cancel();
350
+ }
351
+ },
352
+ handle,
353
+ async waitForSockets(timeoutMs) {
354
+ if (sockets.size === 0) {
355
+ return;
356
+ }
357
+
358
+ await new Promise<void>((resolve) => {
359
+ let settled = false;
360
+ const finish = () => {
361
+ if (settled) {
362
+ return;
363
+ }
364
+ settled = true;
365
+ clearTimeout(timer);
366
+ emptyWaiters.delete(finish);
367
+ resolve();
368
+ };
369
+ const timer = setTimeout(() => {
370
+ for (const socket of sockets) {
371
+ destroy(socket);
372
+ }
373
+ finish();
374
+ }, timeoutMs);
375
+ timer.unref?.();
376
+ emptyWaiters.add(finish);
377
+ });
378
+ },
379
+ };
380
+ }
381
+
382
+ export function closeServerWithUpgrades(options: {
383
+ lifecycle: ManagedHttpUpgradeLifecycle;
384
+ server: Server;
385
+ timeoutMs: number;
386
+ }): Promise<void> {
387
+ options.lifecycle.beginClose();
388
+ const serverClosed = new Promise<void>((resolve, reject) => {
389
+ options.server.close((error) => (error ? reject(error) : resolve()));
390
+ });
391
+
392
+ return Promise.all([serverClosed, options.lifecycle.waitForSockets(options.timeoutMs)]).then(
393
+ () => undefined,
394
+ );
395
+ }
396
+
397
+ function legacySocketAction(
398
+ socket: Duplex,
399
+ before: { bytesWritten: number | undefined; destroyed: boolean; writableEnded: boolean },
400
+ ): boolean {
401
+ return (
402
+ socketBytesWritten(socket) !== before.bytesWritten ||
403
+ socket.destroyed !== before.destroyed ||
404
+ socket.writableEnded !== before.writableEnded
405
+ );
406
+ }
407
+
408
+ function socketBytesWritten(socket: Duplex): number | undefined {
409
+ return (socket as Duplex & { bytesWritten?: number }).bytesWritten;
410
+ }
@@ -1,9 +1,11 @@
1
1
  import { loadConfigFromFile, type ConfigEnv, type PluginOption, type UserConfig } from "vite";
2
+ import type { DehydrateOptions } from "@reckona/mreact-query";
2
3
  import type { ResolvedAppRouterProject } from "./config.js";
3
4
  import type { AppRouterImportPolicy } from "./import-policy.js";
4
5
  import { mreactRouterConfigFromPlugins } from "./vite.js";
5
6
 
6
7
  export interface LoadedMreactRouterViteConfig {
8
+ dehydrateOptions?: DehydrateOptions | undefined;
7
9
  importPolicy?: AppRouterImportPolicy | undefined;
8
10
  project: ResolvedAppRouterProject;
9
11
  serverPort?: number | undefined;
@@ -46,8 +48,12 @@ export async function loadMreactRouterViteConfigDetails(options: {
46
48
  const importPolicy = (config as ResolvedAppRouterProject & {
47
49
  importPolicy?: AppRouterImportPolicy | undefined;
48
50
  }).importPolicy;
51
+ const dehydrateOptions = (config as ResolvedAppRouterProject & {
52
+ dehydrateOptions?: DehydrateOptions | undefined;
53
+ }).dehydrateOptions;
49
54
 
50
55
  return {
56
+ ...(dehydrateOptions === undefined ? {} : { dehydrateOptions }),
51
57
  ...(importPolicy === undefined ? {} : { importPolicy }),
52
58
  project: config,
53
59
  ...(typeof serverPort === "number" ? { serverPort } : {}),