@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.
- package/README.md +53 -9
- package/dist/actions.d.ts +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +70 -10
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +15 -4
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +301 -63
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +2 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +18 -10
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +1 -1
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -3
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +2 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +24 -2
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +91 -5
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +2 -0
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js +16 -0
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +3 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +211 -13
- package/dist/cache.js.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -2
- package/dist/config.js.map +1 -1
- package/dist/csp.d.ts +1 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -6
- package/dist/csp.js.map +1 -1
- package/dist/dehydrate-policy.d.ts +3 -0
- package/dist/dehydrate-policy.d.ts.map +1 -0
- package/dist/dehydrate-policy.js +16 -0
- package/dist/dehydrate-policy.js.map +1 -0
- package/dist/dev-server.d.ts +7 -2
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +120 -11
- package/dist/dev-server.js.map +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +147 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +22 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +22 -8
- package/dist/logger.js.map +1 -1
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +60 -22
- package/dist/metadata.js.map +1 -1
- package/dist/navigation-marker.d.ts.map +1 -1
- package/dist/navigation-marker.js +72 -9
- package/dist/navigation-marker.js.map +1 -1
- package/dist/navigation.d.ts +1 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +1 -1
- package/dist/navigation.js.map +1 -1
- package/dist/node-server.d.ts +5 -2
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +75 -6
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +14 -0
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +40 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +102 -123
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +8 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +22 -20
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +52 -19
- package/dist/session.js.map +1 -1
- package/dist/upgrade.d.ts +59 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +282 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/vite-config.d.ts +2 -0
- package/dist/vite-config.d.ts.map +1 -1
- package/dist/vite-config.js +2 -0
- package/dist/vite-config.js.map +1 -1
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +117 -6
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +81 -11
- package/src/adapters/aws-lambda.ts +426 -115
- package/src/adapters/cloudflare.ts +29 -13
- package/src/adapters/edge.ts +2 -0
- package/src/adapters/node.ts +8 -10
- package/src/adapters/static.ts +31 -2
- package/src/build.ts +117 -10
- package/src/built-runtime.ts +28 -0
- package/src/cache.ts +262 -15
- package/src/cli.ts +14 -1
- package/src/client.ts +32 -1
- package/src/config.ts +43 -6
- package/src/csp.ts +14 -11
- package/src/dehydrate-policy.ts +23 -0
- package/src/dev-server.ts +188 -15
- package/src/http.ts +172 -6
- package/src/index.ts +14 -1
- package/src/logger.ts +47 -10
- package/src/metadata.ts +125 -60
- package/src/navigation-marker.ts +79 -9
- package/src/navigation.ts +1 -1
- package/src/node-server.ts +128 -22
- package/src/prerender-entry.ts +52 -3
- package/src/render.ts +807 -753
- package/src/serve.ts +49 -56
- package/src/session.ts +56 -25
- package/src/upgrade.ts +402 -3
- package/src/vite-config.ts +6 -0
- package/src/vite.ts +157 -5
package/src/dev-server.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createServer, type Server } from "node:http";
|
|
2
2
|
import type { ServerResponse } from "node:http";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
import { posix, resolve } from "node:path";
|
|
6
|
+
import type { DehydrateOptions } from "@reckona/mreact-query";
|
|
5
7
|
import { createServer as createViteServer, type UserConfig, type ViteDevServer } from "vite";
|
|
6
8
|
import type { AppRouterServerActionOptions } from "./actions.js";
|
|
7
9
|
import { createMemoryRouteCache, type AppRouterCache } from "./cache.js";
|
|
@@ -19,12 +21,21 @@ import {
|
|
|
19
21
|
nodeRequestPath,
|
|
20
22
|
type AppRouterLogger,
|
|
21
23
|
} from "./logger.js";
|
|
22
|
-
import
|
|
24
|
+
import {
|
|
25
|
+
assertValidHttpUpgradeOriginPolicy,
|
|
26
|
+
closeServerWithUpgrades,
|
|
27
|
+
createManagedHttpUpgradeLifecycle,
|
|
28
|
+
validateHttpUpgradeOrigin,
|
|
29
|
+
type HttpUpgradeOriginPolicy,
|
|
30
|
+
type HttpUpgradeOriginValidation,
|
|
31
|
+
type ManagedHttpUpgradeHandler,
|
|
32
|
+
} from "./upgrade.js";
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Configures the Vite-powered app-router development server.
|
|
26
36
|
*/
|
|
27
37
|
export interface StartDevServerOptions extends AppRouterProjectOptions {
|
|
38
|
+
dehydrateOptions?: DehydrateOptions | undefined;
|
|
28
39
|
port?: number | undefined;
|
|
29
40
|
hostname?: string;
|
|
30
41
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
@@ -33,7 +44,10 @@ export interface StartDevServerOptions extends AppRouterProjectOptions {
|
|
|
33
44
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
34
45
|
verboseErrors?: boolean | undefined;
|
|
35
46
|
viteConfig?: UserConfig | undefined;
|
|
36
|
-
onUpgrade?:
|
|
47
|
+
onUpgrade?: ManagedHttpUpgradeHandler | undefined;
|
|
48
|
+
upgradeCloseTimeoutMs?: number | undefined;
|
|
49
|
+
upgradeDecisionTimeoutMs?: number | undefined;
|
|
50
|
+
upgradeOriginPolicy?: HttpUpgradeOriginPolicy | undefined;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/**
|
|
@@ -53,6 +67,17 @@ export async function startDevServer(
|
|
|
53
67
|
const userHmrConfig =
|
|
54
68
|
typeof userViteServerConfig.hmr === "object" ? userViteServerConfig.hmr : {};
|
|
55
69
|
const port = options.port ?? resolved.serverPort ?? 3001;
|
|
70
|
+
const upgradeCloseTimeoutMs = finiteNonNegativeTimeout(
|
|
71
|
+
options.upgradeCloseTimeoutMs,
|
|
72
|
+
1_000,
|
|
73
|
+
"upgradeCloseTimeoutMs",
|
|
74
|
+
);
|
|
75
|
+
const upgradeDecisionTimeoutMs = finiteNonNegativeTimeout(
|
|
76
|
+
options.upgradeDecisionTimeoutMs,
|
|
77
|
+
1_000,
|
|
78
|
+
"upgradeDecisionTimeoutMs",
|
|
79
|
+
);
|
|
80
|
+
assertValidHttpUpgradeOriginPolicy(options.upgradeOriginPolicy);
|
|
56
81
|
const routeCache = options.routeCache ?? createMemoryRouteCache();
|
|
57
82
|
const declaredPackages = await readDeclaredProjectPackages(project.projectRoot);
|
|
58
83
|
const configImportPolicy = resolved.importPolicy;
|
|
@@ -68,6 +93,7 @@ export async function startDevServer(
|
|
|
68
93
|
],
|
|
69
94
|
};
|
|
70
95
|
let vite: ViteDevServer | undefined;
|
|
96
|
+
let actualPort = port;
|
|
71
97
|
const server = createServer((incoming, outgoing) => {
|
|
72
98
|
const logger = options.logger;
|
|
73
99
|
const startedAt = logger === undefined ? undefined : logNow();
|
|
@@ -115,8 +141,25 @@ export async function startDevServer(
|
|
|
115
141
|
});
|
|
116
142
|
});
|
|
117
143
|
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
const upgradeLifecycle =
|
|
145
|
+
options.onUpgrade === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: createManagedHttpUpgradeLifecycle({
|
|
148
|
+
decisionTimeoutMs: upgradeDecisionTimeoutMs,
|
|
149
|
+
handler: options.onUpgrade,
|
|
150
|
+
isOriginAllowed: (request) =>
|
|
151
|
+
validateDevUpgradeOrigin(
|
|
152
|
+
request,
|
|
153
|
+
options.upgradeOriginPolicy,
|
|
154
|
+
hostname,
|
|
155
|
+
actualPort,
|
|
156
|
+
vite,
|
|
157
|
+
),
|
|
158
|
+
logger: options.logger,
|
|
159
|
+
shouldBypass: (request) => isViteHmrUpgradeRequest(request, vite),
|
|
160
|
+
});
|
|
161
|
+
if (upgradeLifecycle !== undefined) {
|
|
162
|
+
server.on("upgrade", upgradeLifecycle.handle);
|
|
120
163
|
}
|
|
121
164
|
|
|
122
165
|
vite = await createViteServer({
|
|
@@ -133,6 +176,8 @@ export async function startDevServer(
|
|
|
133
176
|
...(userViteConfig.plugins ?? []),
|
|
134
177
|
createAppRouterVitePlugin({
|
|
135
178
|
allowedSourceDirs: project.allowedSourceDirs,
|
|
179
|
+
dehydrateOptions: options.dehydrateOptions ?? resolved.dehydrateOptions,
|
|
180
|
+
dehydratePolicyModule: project.dehydratePolicyModule,
|
|
136
181
|
projectRoot: project.projectRoot,
|
|
137
182
|
publicDir: project.publicDir,
|
|
138
183
|
routesDir: project.routesDir,
|
|
@@ -151,20 +196,140 @@ export async function startDevServer(
|
|
|
151
196
|
throw error;
|
|
152
197
|
}
|
|
153
198
|
const address = server.address();
|
|
154
|
-
|
|
199
|
+
actualPort = typeof address === "object" && address !== null ? address.port : port;
|
|
200
|
+
let closePromise: Promise<void> | undefined;
|
|
155
201
|
|
|
156
202
|
return {
|
|
157
203
|
server,
|
|
158
|
-
url: `http://${hostname
|
|
159
|
-
close:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
url: `http://${formatNodeAuthority(hostname, actualPort)}`,
|
|
205
|
+
close: () => {
|
|
206
|
+
closePromise ??= (async () => {
|
|
207
|
+
upgradeLifecycle?.beginClose();
|
|
208
|
+
const httpClose =
|
|
209
|
+
upgradeLifecycle === undefined
|
|
210
|
+
? new Promise<void>((resolve, reject) => {
|
|
211
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
212
|
+
})
|
|
213
|
+
: closeServerWithUpgrades({
|
|
214
|
+
lifecycle: upgradeLifecycle,
|
|
215
|
+
server,
|
|
216
|
+
timeoutMs: upgradeCloseTimeoutMs,
|
|
217
|
+
});
|
|
218
|
+
const results = await Promise.allSettled([vite?.close(), httpClose]);
|
|
219
|
+
const errors = results.flatMap((result) =>
|
|
220
|
+
result.status === "rejected" ? [result.reason] : [],
|
|
221
|
+
);
|
|
222
|
+
if (errors.length > 0) {
|
|
223
|
+
throw new AggregateError(errors, "Failed to close the mreact development server.");
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
return closePromise;
|
|
164
227
|
},
|
|
165
228
|
};
|
|
166
229
|
}
|
|
167
230
|
|
|
231
|
+
function validateDevUpgradeOrigin(
|
|
232
|
+
request: import("node:http").IncomingMessage,
|
|
233
|
+
policy: HttpUpgradeOriginPolicy | undefined,
|
|
234
|
+
hostname: string,
|
|
235
|
+
port: number,
|
|
236
|
+
vite: ViteDevServer | undefined,
|
|
237
|
+
): HttpUpgradeOriginValidation {
|
|
238
|
+
if (!isDevUpgradeHostAllowed(request.headers.host, vite?.config.server.allowedHosts)) {
|
|
239
|
+
return { ok: false, reason: "disallowed-origin" };
|
|
240
|
+
}
|
|
241
|
+
if (policy === "unchecked") {
|
|
242
|
+
return { ok: true, origin: request.headers.origin };
|
|
243
|
+
}
|
|
244
|
+
if (typeof policy === "object") {
|
|
245
|
+
return validateHttpUpgradeOrigin(request, policy);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return validateHttpUpgradeOrigin(request, {
|
|
249
|
+
allowedOrigins: [`http://${request.headers.host ?? formatNodeAuthority(hostname, port)}`],
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
return { ok: false, reason: "malformed-origin" };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isDevUpgradeHostAllowed(
|
|
257
|
+
hostHeader: string | undefined,
|
|
258
|
+
allowedHosts: true | string[] | undefined,
|
|
259
|
+
): boolean {
|
|
260
|
+
if (allowedHosts === true || hostHeader === undefined) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const host = hostHeader.trim();
|
|
265
|
+
if (host.startsWith("[")) {
|
|
266
|
+
const closingBracket = host.indexOf("]");
|
|
267
|
+
return closingBracket > 0 && isIP(host.slice(1, closingBracket)) === 6;
|
|
268
|
+
}
|
|
269
|
+
const colon = host.indexOf(":");
|
|
270
|
+
const hostname = colon === -1 ? host : host.slice(0, colon);
|
|
271
|
+
if (isIP(hostname) === 4 || hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return (allowedHosts ?? []).some(
|
|
276
|
+
(allowedHost) =>
|
|
277
|
+
allowedHost === hostname ||
|
|
278
|
+
(allowedHost.startsWith(".") &&
|
|
279
|
+
(allowedHost.slice(1) === hostname || hostname.endsWith(allowedHost))),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatNodeAuthority(hostname: string, port: number): string {
|
|
284
|
+
const host = hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
285
|
+
return `${host}:${port}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isViteHmrUpgradeRequest(
|
|
289
|
+
request: import("node:http").IncomingMessage,
|
|
290
|
+
vite: ViteDevServer | undefined,
|
|
291
|
+
): boolean {
|
|
292
|
+
if (vite === undefined) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const protocol = request.headers["sec-websocket-protocol"];
|
|
296
|
+
if (protocol !== "vite-hmr" && protocol !== "vite-ping") {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const hmr = vite.config.server.hmr;
|
|
300
|
+
if (hmr === false) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const hmrPath = typeof hmr === "object" ? hmr.path : undefined;
|
|
304
|
+
const expectedPath =
|
|
305
|
+
hmrPath === undefined ? vite.config.base : posix.join(vite.config.base, hmrPath);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
return new URL(request.url ?? "/", "http://mreact.local").pathname === expectedPath;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function finiteNonNegativeTimeout(
|
|
315
|
+
value: number | undefined,
|
|
316
|
+
fallback: number,
|
|
317
|
+
name: string,
|
|
318
|
+
): number {
|
|
319
|
+
const timeout = value ?? fallback;
|
|
320
|
+
if (
|
|
321
|
+
!Number.isFinite(timeout) ||
|
|
322
|
+
timeout < 0 ||
|
|
323
|
+
!Number.isSafeInteger(timeout) ||
|
|
324
|
+
timeout > 2_147_483_647
|
|
325
|
+
) {
|
|
326
|
+
throw new TypeError(
|
|
327
|
+
`${name} must be a finite non-negative safe integer no greater than 2147483647.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return timeout;
|
|
331
|
+
}
|
|
332
|
+
|
|
168
333
|
function listenDevHttpServer(server: Server, port: number, hostname: string): Promise<void> {
|
|
169
334
|
return new Promise((resolve, reject) => {
|
|
170
335
|
const onError = (error: Error) => {
|
|
@@ -202,6 +367,7 @@ function isNodeErrorCode(error: unknown, code: string): boolean {
|
|
|
202
367
|
}
|
|
203
368
|
|
|
204
369
|
async function resolveStartDevServerProject(options: StartDevServerOptions): Promise<{
|
|
370
|
+
dehydrateOptions?: LoadedMreactRouterViteConfig["dehydrateOptions"];
|
|
205
371
|
project: ReturnType<typeof resolveAppRouterProjectOptions>;
|
|
206
372
|
serverPort?: number | undefined;
|
|
207
373
|
importPolicy?: LoadedMreactRouterViteConfig["importPolicy"];
|
|
@@ -216,6 +382,9 @@ async function resolveStartDevServerProject(options: StartDevServerOptions): Pro
|
|
|
216
382
|
);
|
|
217
383
|
|
|
218
384
|
return {
|
|
385
|
+
...(config?.dehydrateOptions === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: { dehydrateOptions: config.dehydrateOptions }),
|
|
219
388
|
...(config?.importPolicy === undefined ? {} : { importPolicy: config.importPolicy }),
|
|
220
389
|
project: resolveAppRouterProjectOptions({
|
|
221
390
|
...config?.project,
|
|
@@ -280,7 +449,11 @@ function sendDevServerError(
|
|
|
280
449
|
): void {
|
|
281
450
|
outgoing.statusCode = 500;
|
|
282
451
|
outgoing.setHeader("content-type", "text/plain; charset=utf-8");
|
|
283
|
-
outgoing.end(
|
|
284
|
-
|
|
285
|
-
|
|
452
|
+
outgoing.end(
|
|
453
|
+
options.verboseErrors === true
|
|
454
|
+
? error instanceof Error
|
|
455
|
+
? error.stack
|
|
456
|
+
: String(error)
|
|
457
|
+
: "Internal Server Error",
|
|
458
|
+
);
|
|
286
459
|
}
|
package/src/http.ts
CHANGED
|
@@ -7,8 +7,10 @@ const rawUrlByRequest = new WeakMap<Request, string>();
|
|
|
7
7
|
export function nodeRequestToWebRequest(
|
|
8
8
|
incoming: IncomingMessage,
|
|
9
9
|
origin: string,
|
|
10
|
+
outgoing?: ServerResponse,
|
|
10
11
|
): Request {
|
|
11
12
|
const method = incoming.method ?? "GET";
|
|
13
|
+
const rawUrl = incoming.url ?? "/";
|
|
12
14
|
const headers = new Headers();
|
|
13
15
|
|
|
14
16
|
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
@@ -29,16 +31,81 @@ export function nodeRequestToWebRequest(
|
|
|
29
31
|
method,
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
if (outgoing !== undefined) {
|
|
35
|
+
init.signal = nodeRequestSignal(incoming, outgoing);
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
if (method !== "GET" && method !== "HEAD") {
|
|
33
39
|
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>;
|
|
34
40
|
init.duplex = "half";
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
const request = new Request(
|
|
38
|
-
rawUrlByRequest.set(request,
|
|
43
|
+
const request = new Request(nodeRequestUrl(rawUrl, origin), init);
|
|
44
|
+
rawUrlByRequest.set(request, rawUrl);
|
|
39
45
|
return request;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
function nodeRequestSignal(incoming: IncomingMessage, outgoing: ServerResponse): AbortSignal {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
let disposed = false;
|
|
51
|
+
|
|
52
|
+
const dispose = (): void => {
|
|
53
|
+
if (disposed) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
disposed = true;
|
|
57
|
+
incoming.off("aborted", onIncomingAborted);
|
|
58
|
+
outgoing.off("close", onOutgoingClose);
|
|
59
|
+
outgoing.off("error", onOutgoingError);
|
|
60
|
+
outgoing.off("finish", onOutgoingFinish);
|
|
61
|
+
};
|
|
62
|
+
const abort = (reason: Error): void => {
|
|
63
|
+
controller.abort(reason);
|
|
64
|
+
dispose();
|
|
65
|
+
};
|
|
66
|
+
const onIncomingAborted = (): void => {
|
|
67
|
+
abort(new Error("The incoming HTTP request was aborted."));
|
|
68
|
+
};
|
|
69
|
+
const onOutgoingClose = (): void => {
|
|
70
|
+
if (!outgoing.writableFinished) {
|
|
71
|
+
abort(new Error("The outgoing HTTP response closed before completion."));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
dispose();
|
|
75
|
+
};
|
|
76
|
+
const onOutgoingError = (error: Error): void => {
|
|
77
|
+
abort(error);
|
|
78
|
+
};
|
|
79
|
+
const onOutgoingFinish = (): void => {
|
|
80
|
+
dispose();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
incoming.once("aborted", onIncomingAborted);
|
|
84
|
+
outgoing.once("close", onOutgoingClose);
|
|
85
|
+
outgoing.once("error", onOutgoingError);
|
|
86
|
+
outgoing.once("finish", onOutgoingFinish);
|
|
87
|
+
|
|
88
|
+
if (incoming.aborted) {
|
|
89
|
+
onIncomingAborted();
|
|
90
|
+
} else if (outgoing.destroyed && !outgoing.writableFinished) {
|
|
91
|
+
onOutgoingClose();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return controller.signal;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nodeRequestUrl(rawUrl: string, origin: string): URL {
|
|
98
|
+
const validatedOrigin = new URL(origin).origin;
|
|
99
|
+
const rootedPath = rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
|
|
100
|
+
const requestUrl = new URL(`${validatedOrigin}${rootedPath}`);
|
|
101
|
+
|
|
102
|
+
if (requestUrl.origin !== validatedOrigin) {
|
|
103
|
+
throw new TypeError("Node request target changed the validated request origin.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return requestUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
42
109
|
export function rawNodeRequestUrl(request: Request): string | undefined {
|
|
43
110
|
return rawUrlByRequest.get(request);
|
|
44
111
|
}
|
|
@@ -121,12 +188,13 @@ export async function sendResponse(
|
|
|
121
188
|
return;
|
|
122
189
|
}
|
|
123
190
|
|
|
124
|
-
outgoing.flushHeaders();
|
|
125
191
|
const reader = response.body.getReader();
|
|
192
|
+
const lifecycle = observeOutgoingFailure(outgoing);
|
|
193
|
+
outgoing.flushHeaders();
|
|
126
194
|
|
|
127
195
|
try {
|
|
128
196
|
while (true) {
|
|
129
|
-
const result = await reader.
|
|
197
|
+
const result = await readWithAbort(reader, lifecycle.signal);
|
|
130
198
|
|
|
131
199
|
if (result.done) {
|
|
132
200
|
outgoing.end();
|
|
@@ -134,16 +202,114 @@ export async function sendResponse(
|
|
|
134
202
|
}
|
|
135
203
|
|
|
136
204
|
if (!outgoing.write(result.value)) {
|
|
137
|
-
await
|
|
205
|
+
await waitForDrain(outgoing, lifecycle.signal);
|
|
138
206
|
}
|
|
139
207
|
}
|
|
140
208
|
} catch (error) {
|
|
141
|
-
|
|
209
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
210
|
+
try {
|
|
211
|
+
await reader.cancel(reason);
|
|
212
|
+
} catch {
|
|
213
|
+
// The stream may already be errored or cancelled.
|
|
214
|
+
}
|
|
215
|
+
if (!outgoing.destroyed) {
|
|
216
|
+
outgoing.destroy(reason);
|
|
217
|
+
}
|
|
142
218
|
} finally {
|
|
219
|
+
lifecycle.dispose();
|
|
143
220
|
reader.releaseLock();
|
|
144
221
|
}
|
|
145
222
|
}
|
|
146
223
|
|
|
224
|
+
function observeOutgoingFailure(outgoing: ServerResponse): {
|
|
225
|
+
dispose(): void;
|
|
226
|
+
signal: AbortSignal;
|
|
227
|
+
} {
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
let disposed = false;
|
|
230
|
+
|
|
231
|
+
const dispose = (): void => {
|
|
232
|
+
if (disposed) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
disposed = true;
|
|
236
|
+
outgoing.off("close", onClose);
|
|
237
|
+
outgoing.off("error", onError);
|
|
238
|
+
};
|
|
239
|
+
const abort = (reason: Error): void => {
|
|
240
|
+
controller.abort(reason);
|
|
241
|
+
dispose();
|
|
242
|
+
};
|
|
243
|
+
const onClose = (): void => {
|
|
244
|
+
abort(new Error("The outgoing HTTP response closed during streaming."));
|
|
245
|
+
};
|
|
246
|
+
const onError = (error: Error): void => {
|
|
247
|
+
abort(error);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
outgoing.once("close", onClose);
|
|
251
|
+
outgoing.once("error", onError);
|
|
252
|
+
if (outgoing.destroyed) {
|
|
253
|
+
onClose();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { dispose, signal: controller.signal };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readWithAbort(
|
|
260
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
261
|
+
signal: AbortSignal,
|
|
262
|
+
): Promise<ReadableStreamReadResult<Uint8Array>> {
|
|
263
|
+
if (signal.aborted) {
|
|
264
|
+
return Promise.reject(abortReason(signal));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const onAbort = (): void => {
|
|
269
|
+
signal.removeEventListener("abort", onAbort);
|
|
270
|
+
reject(abortReason(signal));
|
|
271
|
+
};
|
|
272
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
273
|
+
void reader.read().then(
|
|
274
|
+
(result) => {
|
|
275
|
+
signal.removeEventListener("abort", onAbort);
|
|
276
|
+
resolve(result);
|
|
277
|
+
},
|
|
278
|
+
(error: unknown) => {
|
|
279
|
+
signal.removeEventListener("abort", onAbort);
|
|
280
|
+
reject(error);
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function waitForDrain(outgoing: ServerResponse, signal: AbortSignal): Promise<void> {
|
|
287
|
+
if (signal.aborted) {
|
|
288
|
+
return Promise.reject(abortReason(signal));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const dispose = (): void => {
|
|
293
|
+
outgoing.off("drain", onDrain);
|
|
294
|
+
signal.removeEventListener("abort", onAbort);
|
|
295
|
+
};
|
|
296
|
+
const onDrain = (): void => {
|
|
297
|
+
dispose();
|
|
298
|
+
resolve();
|
|
299
|
+
};
|
|
300
|
+
const onAbort = (): void => {
|
|
301
|
+
dispose();
|
|
302
|
+
reject(abortReason(signal));
|
|
303
|
+
};
|
|
304
|
+
outgoing.once("drain", onDrain);
|
|
305
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function abortReason(signal: AbortSignal): Error {
|
|
310
|
+
return signal.reason instanceof Error ? signal.reason : new Error("HTTP streaming was aborted.");
|
|
311
|
+
}
|
|
312
|
+
|
|
147
313
|
function responseSetCookieHeaders(headers: Headers): string[] {
|
|
148
314
|
const getSetCookie = (headers as Headers & { getSetCookie?: () => string[] }).getSetCookie;
|
|
149
315
|
|
package/src/index.ts
CHANGED
|
@@ -33,7 +33,17 @@ export type {
|
|
|
33
33
|
} from "./multipart.js";
|
|
34
34
|
export { getNavigationState, subscribeNavigationState } from "./navigation-state.js";
|
|
35
35
|
export { getRouterRuntimeCacheStats } from "./runtime-cache.js";
|
|
36
|
-
export
|
|
36
|
+
export { validateHttpUpgradeOrigin } from "./upgrade.js";
|
|
37
|
+
export type {
|
|
38
|
+
HttpUpgradeContext,
|
|
39
|
+
HttpUpgradeDisposition,
|
|
40
|
+
HttpUpgradeHandler,
|
|
41
|
+
HttpUpgradeOriginFailureReason,
|
|
42
|
+
HttpUpgradeOriginPolicy,
|
|
43
|
+
HttpUpgradeOriginValidation,
|
|
44
|
+
ManagedHttpUpgradeHandler,
|
|
45
|
+
ValidateHttpUpgradeOriginOptions,
|
|
46
|
+
} from "./upgrade.js";
|
|
37
47
|
export {
|
|
38
48
|
cookies,
|
|
39
49
|
headers,
|
|
@@ -250,6 +260,8 @@ export type {
|
|
|
250
260
|
AppRouterRenderTimingLogEvent,
|
|
251
261
|
AppRouterRequestStartLogEvent,
|
|
252
262
|
AppRouterRequestTimingLogEvent,
|
|
263
|
+
AppRouterUpgradeErrorLogEvent,
|
|
264
|
+
AppRouterUpgradeRejectedLogEvent,
|
|
253
265
|
} from "./logger.js";
|
|
254
266
|
export type { DetectedLocale, LocaleRoutingOptions, MessageTree } from "./i18n.js";
|
|
255
267
|
export {
|
|
@@ -288,6 +300,7 @@ export type {
|
|
|
288
300
|
AppRouterRenderPreload,
|
|
289
301
|
AppRouterResponseHook,
|
|
290
302
|
AppRouterResponseHookContext,
|
|
303
|
+
AppRouterServerRenderArtifactLoader,
|
|
291
304
|
RenderAppRequestOptions,
|
|
292
305
|
} from "./render.js";
|
|
293
306
|
export { parseTraceContext, traceContextFromRequest } from "./trace.js";
|
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
|
-
|
|
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
|
-
|
|
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
|
|
182
|
-
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
|
}
|