Skip to content

Commit b6941bc

Browse files
committed
Merge branch 'main' into feat/create-vinext-app
2 parents 5249f5a + d558923 commit b6941bc

2 files changed

Lines changed: 241 additions & 7 deletions

File tree

.npmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 241 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,250 @@
11
/**
22
* Cloudflare Worker entry point for vinext Pages Router.
33
*
4-
* Delegates to vinext's built-in pages-router-entry handler which
5-
* handles the full request lifecycle: open-redirect guard, basePath
6-
* stripping, trailing slash normalization, config redirects/headers,
7-
* middleware, rewrites, API routes, SSR rendering, and static assets.
4+
* The built server entry (virtual:vinext-server-entry) exports:
5+
* - renderPage(request, url, manifest) -> Response
6+
* - handleApiRoute(request, url) -> Response
7+
* - runMiddleware(request) -> middleware result
8+
* - vinextConfig -> embedded next.config.js settings
9+
* - matchPageRoute(url, request) -> route match metadata
10+
*
11+
* Both use Web-standard Request/Response APIs, making them
12+
* directly usable in a Worker fetch handler.
813
*/
9-
import handler from "vinext/server/pages-router-entry";
14+
import {
15+
matchRedirect,
16+
matchRewrite,
17+
matchHeaders,
18+
requestContextFromRequest,
19+
isExternalUrl,
20+
proxyExternalRequest,
21+
sanitizeDestination,
22+
} from "vinext/config/config-matchers";
23+
import { mergeHeaders } from "vinext/server/worker-utils";
24+
25+
// @ts-expect-error -- virtual module resolved by vinext at build time
26+
import { renderPage, handleApiRoute, runMiddleware, vinextConfig, matchPageRoute } from "virtual:vinext-server-entry";
27+
28+
// Extract config values (embedded at build time in the server entry)
29+
const basePath: string = vinextConfig?.basePath ?? "";
30+
const trailingSlash: boolean = vinextConfig?.trailingSlash ?? false;
31+
const configRedirects = vinextConfig?.redirects ?? [];
32+
const configRewrites = vinextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] };
33+
const configHeaders = vinextConfig?.headers ?? [];
1034

1135
export default {
1236
async fetch(request: Request): Promise<Response> {
13-
return handler.fetch(request);
37+
try {
38+
const url = new URL(request.url);
39+
let pathname = url.pathname;
40+
let urlWithQuery = pathname + url.search;
41+
42+
// Block protocol-relative URL open redirects (//evil.com/ or /\evil.com/).
43+
// Normalize backslashes: browsers treat /\ as // in URL context.
44+
if (pathname.replaceAll("\\", "/").startsWith("//")) {
45+
return new Response("404 Not Found", { status: 404 });
46+
}
47+
48+
// Strip basePath
49+
if (basePath && pathname.startsWith(basePath)) {
50+
const stripped = pathname.slice(basePath.length) || "/";
51+
urlWithQuery = stripped + url.search;
52+
pathname = stripped;
53+
}
54+
55+
// Trailing slash normalization
56+
if (pathname !== "/" && !pathname.startsWith("/api")) {
57+
const hasTrailing = pathname.endsWith("/");
58+
if (trailingSlash && !hasTrailing) {
59+
return new Response(null, {
60+
status: 308,
61+
headers: { Location: basePath + pathname + "/" + url.search },
62+
});
63+
} else if (!trailingSlash && hasTrailing) {
64+
return new Response(null, {
65+
status: 308,
66+
headers: { Location: basePath + pathname.replace(/\/+$/, "") + url.search },
67+
});
68+
}
69+
}
70+
71+
// Build request with basePath-stripped URL for middleware
72+
if (basePath) {
73+
const strippedUrl = new URL(request.url);
74+
strippedUrl.pathname = pathname;
75+
request = new Request(strippedUrl, request);
76+
}
77+
78+
// Build request context for config matching that runs before middleware.
79+
const reqCtx = requestContextFromRequest(request);
80+
81+
// Apply redirects from next.config.js before middleware.
82+
if (configRedirects.length) {
83+
const redirect = matchRedirect(pathname, configRedirects, reqCtx);
84+
if (redirect) {
85+
const dest = sanitizeDestination(
86+
basePath && !isExternalUrl(redirect.destination) && !redirect.destination.startsWith(basePath)
87+
? basePath + redirect.destination
88+
: redirect.destination,
89+
);
90+
return new Response(null, {
91+
status: redirect.permanent ? 308 : 307,
92+
headers: { Location: dest },
93+
});
94+
}
95+
}
96+
97+
// Run middleware
98+
let resolvedUrl = urlWithQuery;
99+
const middlewareHeaders: Record<string, string | string[]> = {};
100+
let middlewareRewriteStatus: number | undefined;
101+
if (typeof runMiddleware === "function") {
102+
const result = await runMiddleware(request);
103+
104+
if (!result.continue) {
105+
if (result.redirectUrl) {
106+
return new Response(null, {
107+
status: result.redirectStatus ?? 307,
108+
headers: { Location: result.redirectUrl },
109+
});
110+
}
111+
if (result.response) {
112+
return result.response;
113+
}
114+
}
115+
116+
// Collect middleware response headers to merge into final response.
117+
// Use an array for Set-Cookie to preserve multiple values.
118+
if (result.responseHeaders) {
119+
for (const [key, value] of result.responseHeaders) {
120+
if (key === "set-cookie") {
121+
const existing = middlewareHeaders[key];
122+
if (Array.isArray(existing)) {
123+
existing.push(value);
124+
} else if (existing) {
125+
middlewareHeaders[key] = [existing as string, value];
126+
} else {
127+
middlewareHeaders[key] = [value];
128+
}
129+
} else {
130+
middlewareHeaders[key] = value;
131+
}
132+
}
133+
}
134+
if (result.rewriteUrl) {
135+
resolvedUrl = result.rewriteUrl;
136+
}
137+
middlewareRewriteStatus = result.rewriteStatus;
138+
}
139+
140+
// Unpack x-middleware-request-* headers
141+
const mwReqPrefix = "x-middleware-request-";
142+
const mwReqHeaders: Record<string, string> = {};
143+
for (const key of Object.keys(middlewareHeaders)) {
144+
if (key.startsWith(mwReqPrefix)) {
145+
mwReqHeaders[key.slice(mwReqPrefix.length)] = middlewareHeaders[key] as string;
146+
delete middlewareHeaders[key];
147+
}
148+
}
149+
if (Object.keys(mwReqHeaders).length > 0) {
150+
const newHeaders = new Headers(request.headers);
151+
for (const [k, v] of Object.entries(mwReqHeaders)) {
152+
newHeaders.set(k, v);
153+
}
154+
request = new Request(request.url, {
155+
method: request.method,
156+
headers: newHeaders,
157+
body: request.body,
158+
// @ts-expect-error -- duplex needed for streaming request bodies
159+
duplex: request.body ? "half" : undefined,
160+
});
161+
}
162+
163+
const postMwReqCtx = requestContextFromRequest(request);
164+
let resolvedPathname = resolvedUrl.split("?")[0];
165+
166+
// Apply custom headers from next.config.js
167+
if (configHeaders.length) {
168+
const matched = matchHeaders(pathname, configHeaders, reqCtx);
169+
for (const h of matched) {
170+
const lk = h.key.toLowerCase();
171+
if (lk === "set-cookie") {
172+
const existing = middlewareHeaders[lk];
173+
if (Array.isArray(existing)) {
174+
existing.push(h.value);
175+
} else if (existing) {
176+
middlewareHeaders[lk] = [existing as string, h.value];
177+
} else {
178+
middlewareHeaders[lk] = [h.value];
179+
}
180+
} else if (lk === "vary" && middlewareHeaders[lk]) {
181+
middlewareHeaders[lk] += ", " + h.value;
182+
} else if (!(lk in middlewareHeaders)) {
183+
middlewareHeaders[lk] = h.value;
184+
}
185+
}
186+
}
187+
188+
// Apply beforeFiles rewrites from next.config.js
189+
if (configRewrites.beforeFiles?.length) {
190+
const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx);
191+
if (rewritten) {
192+
if (isExternalUrl(rewritten)) {
193+
return proxyExternalRequest(request, rewritten);
194+
}
195+
resolvedUrl = rewritten;
196+
resolvedPathname = rewritten.split("?")[0];
197+
}
198+
}
199+
200+
// API routes
201+
if (resolvedPathname.startsWith("/api/") || resolvedPathname === "/api") {
202+
const response = typeof handleApiRoute === "function"
203+
? await handleApiRoute(request, resolvedUrl)
204+
: new Response("404 - API route not found", { status: 404 });
205+
return mergeHeaders(response, middlewareHeaders, middlewareRewriteStatus);
206+
}
207+
208+
const pageMatch =
209+
typeof matchPageRoute === "function" ? matchPageRoute(resolvedPathname, request) : null;
210+
211+
// Apply afterFiles rewrites
212+
if ((!pageMatch || pageMatch.route.isDynamic) && configRewrites.afterFiles?.length) {
213+
const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, postMwReqCtx);
214+
if (rewritten) {
215+
if (isExternalUrl(rewritten)) {
216+
return proxyExternalRequest(request, rewritten);
217+
}
218+
resolvedUrl = rewritten;
219+
resolvedPathname = rewritten.split("?")[0];
220+
}
221+
}
222+
223+
// Page routes
224+
let response: Response | undefined;
225+
if (typeof renderPage === "function") {
226+
response = await renderPage(request, resolvedUrl, null);
227+
228+
// Fallback rewrites (if SSR returned 404)
229+
if (response && response.status === 404 && configRewrites.fallback?.length) {
230+
const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, postMwReqCtx);
231+
if (fallbackRewrite) {
232+
if (isExternalUrl(fallbackRewrite)) {
233+
return proxyExternalRequest(request, fallbackRewrite);
234+
}
235+
response = await renderPage(request, fallbackRewrite, null);
236+
}
237+
}
238+
}
239+
240+
if (!response) {
241+
return new Response("404 - Not found", { status: 404 });
242+
}
243+
244+
return mergeHeaders(response, middlewareHeaders, middlewareRewriteStatus);
245+
} catch (error) {
246+
console.error("[vinext] Worker error:", error);
247+
return new Response("Internal Server Error", { status: 500 });
248+
}
14249
},
15250
};

0 commit comments

Comments
 (0)