@textjs/app 0.0.0-stage → 1.0.2
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 +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `middleware.ts` on Node HTTP servers (`text dev` via Vite, `text start`).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
import { join, relative, sep } from "node:path";
|
|
8
|
+
import { LIVE_PATH } from "../live-protocol";
|
|
9
|
+
import { isLoopbackUrl, proxyFetch, requestOrigin, requestUrl, sendWebResponse, setMiddlewareState, toWebRequest } from "./http";
|
|
10
|
+
import type { MiddlewareModule } from "./middleware";
|
|
11
|
+
import { matchesMiddleware, MIDDLEWARE_FILES, middlewareMatchers, runMiddleware } from "./middleware-runtime";
|
|
12
|
+
|
|
13
|
+
/** `toWebRequest()` returns this request (once) instead of re-reading the Node stream. */
|
|
14
|
+
export const WEB_REQUEST = Symbol.for("textjs.webRequest");
|
|
15
|
+
|
|
16
|
+
export function findMiddlewareFile(root: string): string | null {
|
|
17
|
+
for (const rel of MIDDLEWARE_FILES) {
|
|
18
|
+
const p = join(root, rel);
|
|
19
|
+
if (existsSync(p)) return p;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripBase(pathname: string, basePath: string): string | null {
|
|
25
|
+
if (!basePath) return pathname;
|
|
26
|
+
if (pathname === basePath) return "/";
|
|
27
|
+
return pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NodeMiddlewareOptions {
|
|
31
|
+
basePath: string;
|
|
32
|
+
origin: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Request headers of a Node request, for matching before the body is touched. */
|
|
36
|
+
export function nodeHeaders(req: IncomingMessage): Headers {
|
|
37
|
+
const headers = new Headers();
|
|
38
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
39
|
+
if (v === undefined) continue;
|
|
40
|
+
if (Array.isArray(v)) for (const item of v) headers.append(k, item);
|
|
41
|
+
else headers.set(k, v);
|
|
42
|
+
}
|
|
43
|
+
return headers;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run middleware for a Node request (`req.url` must still include basePath).
|
|
48
|
+
* Resolves to the app URL to continue with (no basePath, may be rewritten),
|
|
49
|
+
* or `null` once middleware has sent the response itself.
|
|
50
|
+
*/
|
|
51
|
+
export async function runNodeMiddleware(mod: MiddlewareModule, req: IncomingMessage, res: ServerResponse, options: NodeMiddlewareOptions): Promise<string | null> {
|
|
52
|
+
const url = requestUrl(req.url, options.origin);
|
|
53
|
+
const pathname = stripBase(url.pathname, options.basePath);
|
|
54
|
+
// Match on the URL and headers first: unmatched requests keep their Node body stream untouched.
|
|
55
|
+
const probe = new Request(url, { method: "GET", headers: nodeHeaders(req) });
|
|
56
|
+
if (pathname === null || !matchesMiddleware(middlewareMatchers(mod), pathname, probe)) {
|
|
57
|
+
return `${pathname ?? url.pathname}${url.search}`;
|
|
58
|
+
}
|
|
59
|
+
const request = toWebRequest(req, options.origin, res);
|
|
60
|
+
const outcome = await runMiddleware(mod, request, { basePath: options.basePath, matchers: null });
|
|
61
|
+
if (outcome.type === "response") {
|
|
62
|
+
await sendWebResponse(res, outcome.response, request.method);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
outcome.headers.forEach((value, key) => {
|
|
67
|
+
if (key !== "set-cookie") res.setHeader(key, value);
|
|
68
|
+
});
|
|
69
|
+
const cookies = outcome.headers.getSetCookie();
|
|
70
|
+
if (cookies.length > 0) res.setHeader("set-cookie", cookies);
|
|
71
|
+
if (outcome.private || outcome.status !== undefined) setMiddlewareState(res, { private: outcome.private, status: outcome.status });
|
|
72
|
+
|
|
73
|
+
// Mirror forwarded headers onto the Node request (Vite and proxies read `req.headers`).
|
|
74
|
+
const forwarded = outcome.request.headers;
|
|
75
|
+
for (const key of Object.keys(req.headers)) if (!forwarded.has(key)) delete req.headers[key];
|
|
76
|
+
forwarded.forEach((value, key) => {
|
|
77
|
+
req.headers[key] = value;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (outcome.rewrite && outcome.rewrite.origin !== url.origin) {
|
|
81
|
+
const response = await proxyFetch(outcome.request, outcome.rewrite, { stripCredentials: !isLoopbackUrl(outcome.rewrite.href) });
|
|
82
|
+
await sendWebResponse(res, response, request.method);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
(req as unknown as Record<symbol, Request>)[WEB_REQUEST] = outcome.request;
|
|
86
|
+
const target = outcome.rewrite ?? url;
|
|
87
|
+
return `${stripBase(target.pathname, options.basePath) ?? target.pathname}${target.search}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Server-only sources `text dev` must never hand to the browser as modules:
|
|
94
|
+
* `middleware.ts`, `text.config.*`, the cache handler, route handlers,
|
|
95
|
+
* `*.server.*` modules and `.env` files.
|
|
96
|
+
*/
|
|
97
|
+
export function isServerOnlySource(pathname: string, root: string, appDir = "app", extra: readonly string[] = []): boolean {
|
|
98
|
+
let decoded: string;
|
|
99
|
+
try {
|
|
100
|
+
decoded = decodeURIComponent(pathname);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const rel = decoded.replace(/^[/\\]+/, "").replace(/\\/g, "/");
|
|
105
|
+
const name = rel.split("/").pop() ?? "";
|
|
106
|
+
if (/^\.env(?:\..*)?$/.test(name)) return true;
|
|
107
|
+
if ((MIDDLEWARE_FILES as readonly string[]).includes(rel) || /^text\.config\.[cm]?[jt]s$/.test(rel)) return true;
|
|
108
|
+
if (/\.server\.[cm]?[jt]sx?$/.test(name)) return true;
|
|
109
|
+
if (rel.startsWith(`${appDir.replace(/^\.?\//, "")}/`) && /^route\.[cm]?[jt]sx?$/.test(name)) return true;
|
|
110
|
+
return extra.some((file) => {
|
|
111
|
+
const r = relative(root, file).split(sep).join("/");
|
|
112
|
+
return r === rel && SOURCE_EXT.test(r);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Vite-served module and tooling requests never run middleware in `text dev`
|
|
118
|
+
* (they don't exist in production). `pathname` excludes basePath. The live
|
|
119
|
+
* session endpoint is not internal: it serves app routes.
|
|
120
|
+
*/
|
|
121
|
+
export function isDevInternalRequest(rawUrl: string, root: string): boolean {
|
|
122
|
+
const pathname = rawUrl.split("?")[0] ?? "/";
|
|
123
|
+
if (pathname === LIVE_PATH) return false;
|
|
124
|
+
if (pathname.startsWith("/@") || pathname.startsWith("/node_modules/") || pathname.startsWith("/__")) return true;
|
|
125
|
+
// Source files Vite serves as modules (`/app/page.tsx?t=…`); `public/` files still run middleware, as in production.
|
|
126
|
+
try {
|
|
127
|
+
const file = join(root, decodeURIComponent(pathname));
|
|
128
|
+
return file.startsWith(root + sep) && statSync(file).isFile();
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface DevServerLike {
|
|
135
|
+
ssrLoadModule(id: string): Promise<Record<string, unknown>>;
|
|
136
|
+
ssrFixStacktrace(error: Error): void;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type Next = (error?: unknown) => void;
|
|
140
|
+
|
|
141
|
+
/** Connect middleware for the Vite dev server. */
|
|
142
|
+
export function createDevMiddleware(server: DevServerLike, options: { root: string; basePath: string; log?: { info(m: string): void; error(m: string): void } }) {
|
|
143
|
+
return async (req: IncomingMessage, res: ServerResponse, next: Next): Promise<void> => {
|
|
144
|
+
const file = findMiddlewareFile(options.root);
|
|
145
|
+
const raw = req.url ?? "/";
|
|
146
|
+
if (!file) return next();
|
|
147
|
+
const stripped = stripBase(raw.split("?")[0] ?? "/", options.basePath);
|
|
148
|
+
if (stripped === null || isDevInternalRequest(stripped, options.root)) return next();
|
|
149
|
+
const started = performance.now();
|
|
150
|
+
try {
|
|
151
|
+
const mod = (await server.ssrLoadModule(file)) as MiddlewareModule;
|
|
152
|
+
const url = await runNodeMiddleware(mod, req, res, { basePath: options.basePath, origin: requestOrigin(req) });
|
|
153
|
+
if (url === null) {
|
|
154
|
+
options.log?.info(` ${req.method} ${raw} ${res.statusCode} (middleware) in ${Math.round(performance.now() - started)}ms`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
req.url = options.basePath + url;
|
|
158
|
+
next();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof Error) server.ssrFixStacktrace(error);
|
|
161
|
+
options.log?.error(` ⨯ middleware ${req.method} ${raw}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
162
|
+
if (res.headersSent) return void res.end();
|
|
163
|
+
res.statusCode = 500;
|
|
164
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
165
|
+
res.end(`▼ Text.js: middleware error: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic `middleware.ts` execution: matcher compilation, the
|
|
3
|
+
* `x-middleware-*` response protocol and request forwarding. Used by
|
|
4
|
+
* `text dev`, `text start` and every deploy adapter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { hasGeneratedCacheControl, PRIVATE_CACHE_CONTROL, requestOrigin, type OriginOptions } from "./http";
|
|
8
|
+
import {
|
|
9
|
+
isMiddlewareControl,
|
|
10
|
+
MIDDLEWARE_NEXT,
|
|
11
|
+
MIDDLEWARE_OVERRIDE_HEADERS,
|
|
12
|
+
MIDDLEWARE_REQUEST_PREFIX,
|
|
13
|
+
MIDDLEWARE_REWRITE,
|
|
14
|
+
NextRequest,
|
|
15
|
+
parseCookieHeader,
|
|
16
|
+
parseSetCookie,
|
|
17
|
+
type Matcher,
|
|
18
|
+
type MatcherCondition,
|
|
19
|
+
type MatcherObject,
|
|
20
|
+
type MiddlewareModule,
|
|
21
|
+
type NextMiddleware,
|
|
22
|
+
} from "./middleware";
|
|
23
|
+
|
|
24
|
+
/** Candidate file names, relative to the project root (first match wins). */
|
|
25
|
+
export { proxyFetch } from "./http";
|
|
26
|
+
|
|
27
|
+
export const MIDDLEWARE_FILES = ["middleware.ts", "middleware.tsx", "middleware.js", "middleware.mjs", "src/middleware.ts", "src/middleware.js"] as const;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Matchers (`path-to-regexp` syntax, as in Next.js `config.matcher`)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface CompiledMatcher {
|
|
34
|
+
source: string;
|
|
35
|
+
/** Anchored regular expression source (also valid PCRE, for Vercel routes). */
|
|
36
|
+
regex: string;
|
|
37
|
+
has?: MatcherCondition[];
|
|
38
|
+
missing?: MatcherCondition[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DEFAULT_SEGMENT = "[^/#?]+?";
|
|
42
|
+
|
|
43
|
+
function escapeRe(s: string): string {
|
|
44
|
+
return s.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read a balanced `( … )` group starting at `i` (which must be `(`). Returns the inner source and the index after `)`. */
|
|
48
|
+
function readGroup(src: string, i: number): [string, number] {
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let j = i;
|
|
51
|
+
for (; j < src.length; j++) {
|
|
52
|
+
const ch = src[j];
|
|
53
|
+
if (ch === "\\") {
|
|
54
|
+
j++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === "(") depth++;
|
|
58
|
+
else if (ch === ")") {
|
|
59
|
+
depth--;
|
|
60
|
+
if (depth === 0) return [src.slice(i + 1, j), j + 1];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw new SyntaxError(`Unbalanced parentheses in middleware matcher ${JSON.stringify(src)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Capturing groups in a regular-expression source (escapes and character classes skipped). */
|
|
67
|
+
function capturingGroups(src: string): number {
|
|
68
|
+
let n = 0;
|
|
69
|
+
let inClass = false;
|
|
70
|
+
for (let i = 0; i < src.length; i++) {
|
|
71
|
+
const ch = src[i];
|
|
72
|
+
if (ch === "\\") {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (inClass) {
|
|
77
|
+
if (ch === "]") inClass = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === "[") inClass = true;
|
|
81
|
+
else if (ch === "(" && (src[i + 1] !== "?" || (src[i + 2] === "<" && src[i + 3] !== "=" && src[i + 3] !== "!"))) n++;
|
|
82
|
+
}
|
|
83
|
+
return n;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Parameter names in capture-group order (`null` for unnamed groups) of a matcher source. */
|
|
87
|
+
export function matcherKeys(source: string): Array<string | null> {
|
|
88
|
+
const keys: Array<string | null> = [];
|
|
89
|
+
compileMatcherSource(source, keys);
|
|
90
|
+
return keys;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compile a matcher source: `/about`, `/blog/:slug`, `/docs/:path*`,
|
|
95
|
+
* `/a/:rest+`, `/:locale?/x`, `/user/:id(\\d+)` and raw groups such as
|
|
96
|
+
* `/((?!api|assets|favicon.ico).*)`. A trailing slash is optional.
|
|
97
|
+
*/
|
|
98
|
+
export function compileMatcherSource(source: string, keys: Array<string | null> = []): string {
|
|
99
|
+
if (!source.startsWith("/")) throw new SyntaxError(`Middleware matcher must start with "/": ${JSON.stringify(source)}`);
|
|
100
|
+
let re = "";
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < source.length) {
|
|
103
|
+
const ch = source[i] as string;
|
|
104
|
+
const prefix = ch === "/" && (source[i + 1] === ":" || source[i + 1] === "(") ? "/" : "";
|
|
105
|
+
let j = prefix ? i + 1 : i;
|
|
106
|
+
const c = source[j] as string;
|
|
107
|
+
if (c === ":" || c === "(") {
|
|
108
|
+
let pattern = DEFAULT_SEGMENT;
|
|
109
|
+
let key: string | null = null;
|
|
110
|
+
if (c === ":") {
|
|
111
|
+
const m = /^:([A-Za-z_]\w*)/.exec(source.slice(j));
|
|
112
|
+
if (!m) throw new SyntaxError(`Missing parameter name at ${j} in ${JSON.stringify(source)}`);
|
|
113
|
+
key = m[1] as string;
|
|
114
|
+
j += m[0].length;
|
|
115
|
+
if (source[j] === "(") {
|
|
116
|
+
const [inner, end] = readGroup(source, j);
|
|
117
|
+
pattern = inner;
|
|
118
|
+
j = end;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const [inner, end] = readGroup(source, j);
|
|
122
|
+
pattern = inner;
|
|
123
|
+
j = end;
|
|
124
|
+
}
|
|
125
|
+
const mod = source[j];
|
|
126
|
+
const p = escapeRe(prefix);
|
|
127
|
+
keys.push(key);
|
|
128
|
+
// `*` / `+` repeat the pattern once more in the emitted regex.
|
|
129
|
+
for (let n = capturingGroups(pattern) * (mod === "*" || mod === "+" ? 2 : 1); n > 0; n--) keys.push(null);
|
|
130
|
+
if (!prefix && pattern === DEFAULT_SEGMENT && (mod === "*" || mod === "+")) {
|
|
131
|
+
// No separator between repetitions: `(?:x+?)*` would backtrack exponentially.
|
|
132
|
+
re += mod === "*" ? "([^/#?]*)" : "([^/#?]+)";
|
|
133
|
+
i = j + 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (mod === "*") {
|
|
137
|
+
re += `(?:${p}((?:${pattern})(?:${p}(?:${pattern}))*))?`;
|
|
138
|
+
j++;
|
|
139
|
+
} else if (mod === "+") {
|
|
140
|
+
re += `${p}((?:${pattern})(?:${p}(?:${pattern}))*)`;
|
|
141
|
+
j++;
|
|
142
|
+
} else if (mod === "?") {
|
|
143
|
+
re += `(?:${p}(${pattern}))?`;
|
|
144
|
+
j++;
|
|
145
|
+
} else re += `${p}(${pattern})`;
|
|
146
|
+
i = j;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
re += escapeRe(ch);
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
if (re.endsWith("\\/")) re = re.slice(0, -2);
|
|
153
|
+
return `^${re}(?:\\/)?$`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function compileMatcher(matcher: Matcher | undefined): CompiledMatcher[] | null {
|
|
157
|
+
if (matcher === undefined) return null;
|
|
158
|
+
const list = Array.isArray(matcher) ? matcher : [matcher];
|
|
159
|
+
return list.map((m) => {
|
|
160
|
+
const o: MatcherObject = typeof m === "string" ? { source: m } : m;
|
|
161
|
+
return { source: o.source, regex: compileMatcherSource(o.source), ...(o.has ? { has: o.has } : {}), ...(o.missing ? { missing: o.missing } : {}) };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function conditionHolds(cond: MatcherCondition, request: Request, url: URL, cookies: Map<string, string>): boolean {
|
|
166
|
+
let actual: string | null | undefined;
|
|
167
|
+
if (cond.type === "header") actual = request.headers.get(cond.key);
|
|
168
|
+
else if (cond.type === "cookie") actual = cookies.get(cond.key);
|
|
169
|
+
else if (cond.type === "query") actual = url.searchParams.get(cond.key);
|
|
170
|
+
else actual = url.hostname;
|
|
171
|
+
if (actual === null || actual === undefined) return false;
|
|
172
|
+
if (cond.value === undefined) return true;
|
|
173
|
+
return new RegExp(`^(?:${cond.value})$`).test(actual);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const regexCache = new Map<string, RegExp>();
|
|
177
|
+
|
|
178
|
+
/** `true` when some matcher's path pattern covers `pathname`, ignoring `has` / `missing` (build-time routing decisions). */
|
|
179
|
+
export function matcherCouldMatch(matchers: readonly CompiledMatcher[] | null, pathname: string): boolean {
|
|
180
|
+
if (matchers === null) return true;
|
|
181
|
+
return matchers.some((m) => new RegExp(m.regex).test(pathname));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** `true` when `pathname` (without basePath) is covered by the compiled matchers (`null` = every path). */
|
|
185
|
+
export function matchesMiddleware(matchers: readonly CompiledMatcher[] | null, pathname: string, request?: Request): boolean {
|
|
186
|
+
if (matchers === null) return true;
|
|
187
|
+
const url = request ? new URL(request.url) : null;
|
|
188
|
+
const cookies = request ? parseCookieHeader(request.headers.get("cookie")) : new Map<string, string>();
|
|
189
|
+
for (const m of matchers) {
|
|
190
|
+
let re = regexCache.get(m.regex);
|
|
191
|
+
if (!re) {
|
|
192
|
+
re = new RegExp(m.regex);
|
|
193
|
+
regexCache.set(m.regex, re);
|
|
194
|
+
}
|
|
195
|
+
if (!re.test(pathname)) continue;
|
|
196
|
+
if (m.has || m.missing) {
|
|
197
|
+
if (!request || !url) return false;
|
|
198
|
+
if (m.has && !m.has.every((c) => conditionHolds(c, request, url, cookies))) continue;
|
|
199
|
+
if (m.missing && m.missing.some((c) => conditionHolds(c, request, url, cookies))) continue;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Execution
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export type MiddlewareOutcome =
|
|
211
|
+
/** Middleware answered the request itself (redirect, 401, JSON, …). */
|
|
212
|
+
| { type: "response"; response: Response }
|
|
213
|
+
/**
|
|
214
|
+
* Continue. `request` carries overridden headers / cookies; `headers` must be
|
|
215
|
+
* merged into the final response; `rewrite` is set for `NextResponse.rewrite()`.
|
|
216
|
+
*/
|
|
217
|
+
| {
|
|
218
|
+
type: "next";
|
|
219
|
+
request: Request;
|
|
220
|
+
headers: Headers;
|
|
221
|
+
rewrite?: URL;
|
|
222
|
+
matched: boolean;
|
|
223
|
+
/** `NextResponse.next({ status })` / `rewrite(url, { status })`. */
|
|
224
|
+
status?: number;
|
|
225
|
+
/** The response depends on this request's cookies or identity (set cookies, rewrote, overrode headers). */
|
|
226
|
+
private?: boolean;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export interface RunMiddlewareOptions {
|
|
230
|
+
basePath?: string;
|
|
231
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
232
|
+
/** Precompiled matchers (defaults to compiling `mod.config.matcher`). */
|
|
233
|
+
matchers?: CompiledMatcher[] | null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const compiledByModule = new WeakMap<object, CompiledMatcher[] | null>();
|
|
237
|
+
|
|
238
|
+
export function middlewareFunction(mod: MiddlewareModule): NextMiddleware {
|
|
239
|
+
const fn = mod.middleware ?? mod.default;
|
|
240
|
+
if (typeof fn !== "function") throw new TypeError("The middleware file must export a `middleware` function (or a default export).");
|
|
241
|
+
return fn;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function middlewareMatchers(mod: MiddlewareModule): CompiledMatcher[] | null {
|
|
245
|
+
let m = compiledByModule.get(mod);
|
|
246
|
+
if (m === undefined) {
|
|
247
|
+
m = compileMatcher(mod.config?.matcher);
|
|
248
|
+
compiledByModule.set(mod, m);
|
|
249
|
+
}
|
|
250
|
+
return m;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stripBase(pathname: string, basePath: string): string {
|
|
254
|
+
if (!basePath) return pathname;
|
|
255
|
+
if (pathname === basePath) return "/";
|
|
256
|
+
return pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const INTERNAL: string[] = [MIDDLEWARE_NEXT, MIDDLEWARE_REWRITE, MIDDLEWARE_OVERRIDE_HEADERS];
|
|
260
|
+
|
|
261
|
+
function cancelUnread(request: Request): void {
|
|
262
|
+
if (request.body && !request.bodyUsed && !request.body.locked) request.body.cancel().catch(() => undefined);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Drop `x-middleware-*` headers from a response that is returned to the client as-is. */
|
|
266
|
+
function stripInternal(response: Response): Response {
|
|
267
|
+
let found = false;
|
|
268
|
+
response.headers.forEach((_, key) => {
|
|
269
|
+
if (key.startsWith("x-middleware-")) found = true;
|
|
270
|
+
});
|
|
271
|
+
if (!found) return response;
|
|
272
|
+
const headers = new Headers(response.headers);
|
|
273
|
+
for (const key of [...headers.keys()]) if (key.startsWith("x-middleware-")) headers.delete(key);
|
|
274
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run middleware for `request`. `request` is never consumed: middleware gets a
|
|
279
|
+
* tee'd clone, and the outcome's `request` carries the original body.
|
|
280
|
+
*/
|
|
281
|
+
export async function runMiddleware(mod: MiddlewareModule, request: Request, options: RunMiddlewareOptions = {}): Promise<MiddlewareOutcome> {
|
|
282
|
+
const basePath = options.basePath ?? "";
|
|
283
|
+
const url = new URL(request.url);
|
|
284
|
+
const matchers = options.matchers !== undefined ? options.matchers : middlewareMatchers(mod);
|
|
285
|
+
if (!matchesMiddleware(matchers, stripBase(url.pathname, basePath), request)) {
|
|
286
|
+
return { type: "next", request, headers: new Headers(), matched: false };
|
|
287
|
+
}
|
|
288
|
+
const fn = middlewareFunction(mod);
|
|
289
|
+
const forwarded = request.body ? request.clone() : request;
|
|
290
|
+
const nextRequest = new NextRequest(request, { basePath });
|
|
291
|
+
const pending: Promise<unknown>[] = [];
|
|
292
|
+
const event = {
|
|
293
|
+
waitUntil(p: Promise<unknown>) {
|
|
294
|
+
if (options.waitUntil) options.waitUntil(p);
|
|
295
|
+
else pending.push(p.catch(() => undefined));
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
let result: Awaited<ReturnType<NextMiddleware>>;
|
|
299
|
+
try {
|
|
300
|
+
result = await fn(nextRequest, event);
|
|
301
|
+
} finally {
|
|
302
|
+
// The tee buffers whatever one branch doesn't read: release middleware's copy
|
|
303
|
+
// (Bun keeps the wrapped request's own stream separate, so release both).
|
|
304
|
+
cancelUnread(nextRequest);
|
|
305
|
+
if (forwarded !== request) cancelUnread(request);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Cookies the middleware set on `request.cookies` are forwarded, like Next.js.
|
|
309
|
+
const requestHeaders = new Headers(forwarded.headers);
|
|
310
|
+
const mutatedCookie = nextRequest.headers.get("cookie");
|
|
311
|
+
if (mutatedCookie !== request.headers.get("cookie")) {
|
|
312
|
+
if (mutatedCookie === null) requestHeaders.delete("cookie");
|
|
313
|
+
else requestHeaders.set("cookie", mutatedCookie);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!result) return { type: "next", request: rebuild(forwarded, requestHeaders), headers: new Headers(), matched: true };
|
|
317
|
+
if (!(result instanceof Response)) {
|
|
318
|
+
cancelUnread(forwarded);
|
|
319
|
+
throw new TypeError("Middleware must return a `Response`, `NextResponse` or nothing.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const control = isMiddlewareControl(result);
|
|
323
|
+
const rewriteTo = control ? result.headers.get(MIDDLEWARE_REWRITE) : null;
|
|
324
|
+
const isNext = control && result.headers.get(MIDDLEWARE_NEXT) === "1";
|
|
325
|
+
if (!rewriteTo && !isNext) {
|
|
326
|
+
cancelUnread(forwarded);
|
|
327
|
+
return { type: "response", response: stripInternal(result) };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const override = result.headers.get(MIDDLEWARE_OVERRIDE_HEADERS);
|
|
331
|
+
let finalHeaders = requestHeaders;
|
|
332
|
+
if (override !== null) {
|
|
333
|
+
finalHeaders = new Headers();
|
|
334
|
+
for (const key of override.split(",").map((k) => k.trim()).filter(Boolean)) {
|
|
335
|
+
const v = result.headers.get(`${MIDDLEWARE_REQUEST_PREFIX}${key}`);
|
|
336
|
+
if (v !== null) finalHeaders.set(key, v);
|
|
337
|
+
}
|
|
338
|
+
const host = request.headers.get("host");
|
|
339
|
+
if (host && !finalHeaders.has("host")) finalHeaders.set("host", host);
|
|
340
|
+
}
|
|
341
|
+
// Cookies set on the `next()` / `rewrite()` response are visible downstream too.
|
|
342
|
+
const setCookies = result.headers.getSetCookie();
|
|
343
|
+
if (setCookies.length > 0) {
|
|
344
|
+
const jar = parseCookieHeader(finalHeaders.get("cookie"));
|
|
345
|
+
for (const line of setCookies) {
|
|
346
|
+
const c = parseSetCookie(line);
|
|
347
|
+
if (!c) continue;
|
|
348
|
+
if (c.maxAge === 0 || (c.expires !== undefined && new Date(c.expires).getTime() <= Date.now())) jar.delete(c.name);
|
|
349
|
+
else jar.set(c.name, c.value);
|
|
350
|
+
}
|
|
351
|
+
if (jar.size === 0) finalHeaders.delete("cookie");
|
|
352
|
+
else finalHeaders.set("cookie", [...jar].map(([n, v]) => `${n}=${encodeURIComponent(v)}`).join("; "));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const responseHeaders = new Headers();
|
|
356
|
+
result.headers.forEach((value, key) => {
|
|
357
|
+
if (key === "set-cookie" || INTERNAL.includes(key) || key.startsWith(MIDDLEWARE_REQUEST_PREFIX)) return;
|
|
358
|
+
responseHeaders.set(key, value);
|
|
359
|
+
});
|
|
360
|
+
for (const c of setCookies) responseHeaders.append("set-cookie", c);
|
|
361
|
+
|
|
362
|
+
const target = rewriteTo ? new URL(rewriteTo, url) : undefined;
|
|
363
|
+
const nextUrl = target && target.origin === url.origin ? target : undefined;
|
|
364
|
+
if (pending.length > 0) void Promise.all(pending);
|
|
365
|
+
const isPrivate = setCookies.length > 0 || target !== undefined || override !== null;
|
|
366
|
+
return {
|
|
367
|
+
type: "next",
|
|
368
|
+
request: rebuild(forwarded, finalHeaders, nextUrl),
|
|
369
|
+
headers: responseHeaders,
|
|
370
|
+
rewrite: target,
|
|
371
|
+
matched: true,
|
|
372
|
+
...(result.status !== 200 ? { status: result.status } : {}),
|
|
373
|
+
...(isPrivate ? { private: true } : {}),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Copy `request` with new headers (and optionally a new URL), moving its body. */
|
|
378
|
+
export function rebuild(request: Request, headers: Headers, url?: URL | string): Request {
|
|
379
|
+
const init: RequestInit & { duplex?: "half" } = { method: request.method, headers, signal: request.signal, redirect: request.redirect };
|
|
380
|
+
if (request.body) {
|
|
381
|
+
init.body = request.body;
|
|
382
|
+
init.duplex = "half";
|
|
383
|
+
}
|
|
384
|
+
return new Request(url ?? request.url, init);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function sameHeaders(a: Headers, b: Headers): boolean {
|
|
388
|
+
const x = [...a].map(([k, v]) => `${k}:${v}`).sort();
|
|
389
|
+
const y = [...b].map(([k, v]) => `${k}:${v}`).sort();
|
|
390
|
+
return x.length === y.length && x.every((v, i) => v === y[i]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Re-encode an outcome as the `x-middleware-*` response protocol, for
|
|
395
|
+
* platforms that run middleware at the edge and route afterwards (Vercel).
|
|
396
|
+
*/
|
|
397
|
+
export function toMiddlewareResponse(outcome: MiddlewareOutcome, original: Request): Response {
|
|
398
|
+
if (outcome.type === "response") return stripInternal(outcome.response);
|
|
399
|
+
const headers = new Headers(outcome.headers);
|
|
400
|
+
if (outcome.rewrite) headers.set(MIDDLEWARE_REWRITE, outcome.rewrite.href);
|
|
401
|
+
else headers.set(MIDDLEWARE_NEXT, "1");
|
|
402
|
+
if (!sameHeaders(original.headers, outcome.request.headers)) {
|
|
403
|
+
const keys: string[] = [];
|
|
404
|
+
outcome.request.headers.forEach((value, key) => {
|
|
405
|
+
keys.push(key);
|
|
406
|
+
headers.set(`${MIDDLEWARE_REQUEST_PREFIX}${key}`, value);
|
|
407
|
+
});
|
|
408
|
+
headers.set(MIDDLEWARE_OVERRIDE_HEADERS, keys.join(","));
|
|
409
|
+
}
|
|
410
|
+
return new Response(null, { headers, status: outcome.status ?? 200 });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface MiddlewareMerge {
|
|
414
|
+
status?: number;
|
|
415
|
+
private?: boolean;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Return a copy of `response` with middleware headers applied: middleware
|
|
420
|
+
* cookies first, then the response's own; other headers only when absent.
|
|
421
|
+
*/
|
|
422
|
+
export function withMiddlewareHeaders(response: Response, headers: Headers, merge: MiddlewareMerge = {}): Response {
|
|
423
|
+
let empty = true;
|
|
424
|
+
headers.forEach(() => {
|
|
425
|
+
empty = false;
|
|
426
|
+
});
|
|
427
|
+
const downgrade = merge.private === true && hasGeneratedCacheControl(response);
|
|
428
|
+
if (empty && merge.status === undefined && !downgrade) return response;
|
|
429
|
+
const out = new Headers();
|
|
430
|
+
headers.forEach((value, key) => {
|
|
431
|
+
if (key !== "set-cookie") out.set(key, value);
|
|
432
|
+
});
|
|
433
|
+
response.headers.forEach((value, key) => {
|
|
434
|
+
if (key !== "set-cookie") out.set(key, value);
|
|
435
|
+
});
|
|
436
|
+
for (const c of headers.getSetCookie()) out.append("set-cookie", c);
|
|
437
|
+
for (const c of response.headers.getSetCookie()) out.append("set-cookie", c);
|
|
438
|
+
if (downgrade) out.set("cache-control", PRIVATE_CACHE_CONTROL);
|
|
439
|
+
const status = merge.status ?? response.status;
|
|
440
|
+
const body = status === 204 || status === 205 || status === 304 ? null : response.body;
|
|
441
|
+
if (body === null && response.body) response.body.cancel().catch(() => undefined);
|
|
442
|
+
return new Response(body, { status, statusText: merge.status === undefined ? response.statusText : "", headers: out });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Would `middleware.ts` let a document request for `href` (an app path,
|
|
447
|
+
* without basePath) through unchanged? Used to authorize what a live session
|
|
448
|
+
* renders: anything else (redirect, 401, rewrite elsewhere) is handed back to
|
|
449
|
+
* the browser as a document load, where middleware answers over HTTP.
|
|
450
|
+
*/
|
|
451
|
+
export async function authorizeWithMiddleware(mod: MiddlewareModule, href: string, init: { origin: string; basePath?: string; headers: Headers }): Promise<boolean> {
|
|
452
|
+
const basePath = init.basePath ?? "";
|
|
453
|
+
const url = new URL(basePath + href, init.origin);
|
|
454
|
+
if (url.origin !== new URL(init.origin).origin) return false;
|
|
455
|
+
const headers = new Headers(init.headers);
|
|
456
|
+
for (const h of ["content-type", "content-length", "last-event-id", "x-textjs-live", "origin"]) headers.delete(h);
|
|
457
|
+
headers.set("accept", "text/html,application/xhtml+xml,*/*;q=0.8");
|
|
458
|
+
headers.set("sec-fetch-dest", "document");
|
|
459
|
+
headers.set("sec-fetch-mode", "navigate");
|
|
460
|
+
const outcome = await runMiddleware(mod, new Request(url, { method: "GET", headers }), { basePath });
|
|
461
|
+
if (outcome.type === "response") return false;
|
|
462
|
+
if (outcome.rewrite) return outcome.rewrite.origin === url.origin && outcome.rewrite.pathname === url.pathname;
|
|
463
|
+
return outcome.status === undefined || outcome.status < 400;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface LiveAuthorizerOptions extends OriginOptions {
|
|
467
|
+
basePath?: string;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** `LiveServer` `authorize` backed by a middleware module (`null` = no middleware: allow). */
|
|
471
|
+
export function createLiveAuthorizer(load: () => MiddlewareModule | null | undefined | Promise<MiddlewareModule | null | undefined>, options: LiveAuthorizerOptions = {}): (href: string, headers: Headers) => Promise<boolean> {
|
|
472
|
+
return async (href, headers) => {
|
|
473
|
+
const mod = await load();
|
|
474
|
+
if (!mod) return true;
|
|
475
|
+
const origin = requestOrigin({ headers: Object.fromEntries(headers) }, options);
|
|
476
|
+
return authorizeWithMiddleware(mod, href, { origin, basePath: options.basePath, headers });
|
|
477
|
+
};
|
|
478
|
+
}
|