vinext 0.0.20 → 0.0.22
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/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +6 -3
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +233 -22
- package/dist/index.js.map +1 -1
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +1 -41
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +1 -27
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/utils.d.ts +25 -0
- package/dist/routing/utils.d.ts.map +1 -0
- package/dist/routing/utils.js +70 -0
- package/dist/routing/utils.js.map +1 -0
- package/dist/server/app-dev-server.d.ts.map +1 -1
- package/dist/server/app-dev-server.js +140 -6
- package/dist/server/app-dev-server.js.map +1 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +77 -4
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +13 -4
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +7 -0
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-log.d.ts +34 -0
- package/dist/server/request-log.d.ts.map +1 -0
- package/dist/server/request-log.js +65 -0
- package/dist/server/request-log.js.map +1 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +5 -1
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +6 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +22 -2
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/head.d.ts +11 -0
- package/dist/shims/head.d.ts.map +1 -1
- package/dist/shims/head.js +21 -0
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +8 -0
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +41 -0
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/script.d.ts.map +1 -1
- package/dist/shims/script.js +7 -1
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +2 -1
- package/dist/shims/server.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { handleApiRoute } from "./server/api-handler.js";
|
|
|
6
6
|
import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.js";
|
|
7
7
|
import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
|
|
8
8
|
import { findMiddlewareFile, isProxyFile, runMiddleware } from "./server/middleware.js";
|
|
9
|
+
import { logRequest, now } from "./server/request-log.js";
|
|
9
10
|
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
|
|
10
11
|
import { normalizePath } from "./server/normalize-path.js";
|
|
11
12
|
import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
|
|
@@ -1026,6 +1027,12 @@ function createReqRes(request, url, query, body) {
|
|
|
1026
1027
|
else { res.writeHead(statusOrUrl, { Location: url2 }); }
|
|
1027
1028
|
res.end();
|
|
1028
1029
|
},
|
|
1030
|
+
getHeaders: function() {
|
|
1031
|
+
var h = Object.assign({}, resHeaders);
|
|
1032
|
+
if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
|
|
1033
|
+
return h;
|
|
1034
|
+
},
|
|
1035
|
+
get headersSent() { return ended; },
|
|
1029
1036
|
};
|
|
1030
1037
|
|
|
1031
1038
|
return { req, res, responsePromise };
|
|
@@ -1140,8 +1147,9 @@ export async function renderPage(request, url, manifest) {
|
|
|
1140
1147
|
}
|
|
1141
1148
|
|
|
1142
1149
|
let pageProps = {};
|
|
1150
|
+
var gsspRes = null;
|
|
1143
1151
|
if (typeof pageModule.getServerSideProps === "function") {
|
|
1144
|
-
const { req, res } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1152
|
+
const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1145
1153
|
const ctx = {
|
|
1146
1154
|
params, req, res,
|
|
1147
1155
|
query: parseQuery(routeUrl),
|
|
@@ -1151,6 +1159,10 @@ export async function renderPage(request, url, manifest) {
|
|
|
1151
1159
|
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1152
1160
|
};
|
|
1153
1161
|
const result = await pageModule.getServerSideProps(ctx);
|
|
1162
|
+
// If gSSP called res.end() directly (short-circuit), return that response.
|
|
1163
|
+
if (res.headersSent) {
|
|
1164
|
+
return await responsePromise;
|
|
1165
|
+
}
|
|
1154
1166
|
if (result && result.props) pageProps = result.props;
|
|
1155
1167
|
if (result && result.redirect) {
|
|
1156
1168
|
var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
@@ -1159,6 +1171,9 @@ export async function renderPage(request, url, manifest) {
|
|
|
1159
1171
|
if (result && result.notFound) {
|
|
1160
1172
|
return new Response("404", { status: 404 });
|
|
1161
1173
|
}
|
|
1174
|
+
// Preserve the res object so headers/status/cookies set by gSSP
|
|
1175
|
+
// can be merged into the final HTML response.
|
|
1176
|
+
gsspRes = res;
|
|
1162
1177
|
}
|
|
1163
1178
|
// Build font Link header early so it's available for ISR cached responses too.
|
|
1164
1179
|
// Font preloads are module-level state populated at import time and persist across requests.
|
|
@@ -1335,16 +1350,33 @@ export async function renderPage(request, url, manifest) {
|
|
|
1335
1350
|
await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
|
|
1336
1351
|
}
|
|
1337
1352
|
|
|
1338
|
-
|
|
1353
|
+
// Merge headers/status/cookies set by getServerSideProps on the res object.
|
|
1354
|
+
// gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
|
|
1355
|
+
var finalStatus = 200;
|
|
1356
|
+
const responseHeaders = new Headers({ "Content-Type": "text/html" });
|
|
1357
|
+
if (gsspRes) {
|
|
1358
|
+
finalStatus = gsspRes.statusCode;
|
|
1359
|
+
var gsspHeaders = gsspRes.getHeaders();
|
|
1360
|
+
for (var hk of Object.keys(gsspHeaders)) {
|
|
1361
|
+
var hv = gsspHeaders[hk];
|
|
1362
|
+
if (hk === "set-cookie" && Array.isArray(hv)) {
|
|
1363
|
+
for (var sc of hv) responseHeaders.append("set-cookie", sc);
|
|
1364
|
+
} else if (hv != null) {
|
|
1365
|
+
responseHeaders.set(hk, String(hv));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
// Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
|
|
1369
|
+
responseHeaders.set("Content-Type", "text/html");
|
|
1370
|
+
}
|
|
1339
1371
|
if (isrRevalidateSeconds) {
|
|
1340
|
-
responseHeaders
|
|
1341
|
-
responseHeaders
|
|
1372
|
+
responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
|
|
1373
|
+
responseHeaders.set("X-Vinext-Cache", "MISS");
|
|
1342
1374
|
}
|
|
1343
1375
|
// Set HTTP Link header for font preloading
|
|
1344
1376
|
if (_fontLinkHeader) {
|
|
1345
|
-
responseHeaders
|
|
1377
|
+
responseHeaders.set("Link", _fontLinkHeader);
|
|
1346
1378
|
}
|
|
1347
|
-
return new Response(compositeStream, { status:
|
|
1379
|
+
return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
|
|
1348
1380
|
} catch (e) {
|
|
1349
1381
|
console.error("[vinext] SSR error:", e);
|
|
1350
1382
|
return new Response("Internal Server Error", { status: 500 });
|
|
@@ -1559,6 +1591,9 @@ hydrate();
|
|
|
1559
1591
|
});
|
|
1560
1592
|
}
|
|
1561
1593
|
const imageImportDimCache = new Map();
|
|
1594
|
+
// Shared state for the MDX proxy plugin. Populated during config() if MDX
|
|
1595
|
+
// files are detected and @mdx-js/rollup is installed.
|
|
1596
|
+
let mdxDelegate = null;
|
|
1562
1597
|
const plugins = [
|
|
1563
1598
|
// Resolve tsconfig paths/baseUrl aliases so real-world Next.js repos
|
|
1564
1599
|
// that use @/*, #/*, or baseUrl imports work out of the box.
|
|
@@ -1753,11 +1788,10 @@ hydrate();
|
|
|
1753
1788
|
// already configured. Applies remark/rehype plugins from next.config.
|
|
1754
1789
|
const hasMdxPlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" &&
|
|
1755
1790
|
(p.name === "@mdx-js/rollup" || p.name === "mdx"));
|
|
1756
|
-
const mdxPlugins = [];
|
|
1757
1791
|
if (!hasMdxPlugin && hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null)) {
|
|
1758
1792
|
try {
|
|
1759
1793
|
const mdxRollup = await import("@mdx-js/rollup");
|
|
1760
|
-
const
|
|
1794
|
+
const mdxFactory = mdxRollup.default ?? mdxRollup;
|
|
1761
1795
|
const mdxOpts = {};
|
|
1762
1796
|
if (nextConfig.mdx) {
|
|
1763
1797
|
if (nextConfig.mdx.remarkPlugins)
|
|
@@ -1767,7 +1801,7 @@ hydrate();
|
|
|
1767
1801
|
if (nextConfig.mdx.recmaPlugins)
|
|
1768
1802
|
mdxOpts.recmaPlugins = nextConfig.mdx.recmaPlugins;
|
|
1769
1803
|
}
|
|
1770
|
-
|
|
1804
|
+
mdxDelegate = mdxFactory(mdxOpts);
|
|
1771
1805
|
if (nextConfig.mdx) {
|
|
1772
1806
|
console.log("[vinext] Auto-injected @mdx-js/rollup with remark/rehype plugins from next.config");
|
|
1773
1807
|
}
|
|
@@ -1991,10 +2025,6 @@ hydrate();
|
|
|
1991
2025
|
},
|
|
1992
2026
|
};
|
|
1993
2027
|
}
|
|
1994
|
-
// Add auto-injected MDX plugin if needed
|
|
1995
|
-
if (mdxPlugins.length > 0) {
|
|
1996
|
-
viteConfig.plugins = mdxPlugins;
|
|
1997
|
-
}
|
|
1998
2028
|
return viteConfig;
|
|
1999
2029
|
},
|
|
2000
2030
|
configResolved(config) {
|
|
@@ -2100,6 +2130,31 @@ hydrate();
|
|
|
2100
2130
|
}
|
|
2101
2131
|
},
|
|
2102
2132
|
},
|
|
2133
|
+
// Proxy plugin for @mdx-js/rollup. The real MDX plugin is created lazily
|
|
2134
|
+
// during vinext:config's config() (when MDX files are detected), but
|
|
2135
|
+
// plugins returned from config() hooks run too late in the pipeline —
|
|
2136
|
+
// after vite:import-analysis. This top-level proxy with enforce:"pre"
|
|
2137
|
+
// ensures MDX transforms run at the correct stage. Both vinext:config
|
|
2138
|
+
// and this proxy are enforce:"pre", and vinext:config comes first in
|
|
2139
|
+
// the array, so mdxDelegate is already set when this proxy's hooks fire.
|
|
2140
|
+
{
|
|
2141
|
+
name: "vinext:mdx",
|
|
2142
|
+
enforce: "pre",
|
|
2143
|
+
config(config, env) {
|
|
2144
|
+
if (!mdxDelegate?.config)
|
|
2145
|
+
return;
|
|
2146
|
+
const hook = mdxDelegate.config;
|
|
2147
|
+
const fn = typeof hook === "function" ? hook : hook.handler;
|
|
2148
|
+
return fn.call(this, config, env);
|
|
2149
|
+
},
|
|
2150
|
+
transform(code, id, options) {
|
|
2151
|
+
if (!mdxDelegate?.transform)
|
|
2152
|
+
return;
|
|
2153
|
+
const hook = mdxDelegate.transform;
|
|
2154
|
+
const fn = typeof hook === "function" ? hook : hook.handler;
|
|
2155
|
+
return fn.call(this, code, id, options);
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2103
2158
|
// Shim React canary/experimental APIs (ViewTransition, addTransitionType)
|
|
2104
2159
|
// that exist in Next.js's bundled React canary but not in stable React 19.
|
|
2105
2160
|
// Provides graceful no-op fallbacks so projects using these APIs degrade
|
|
@@ -2213,8 +2268,135 @@ hydrate();
|
|
|
2213
2268
|
console.error("[vinext] Instrumentation error:", err);
|
|
2214
2269
|
});
|
|
2215
2270
|
}
|
|
2271
|
+
// ── Dev request origin check ─────────────────────────────────────
|
|
2272
|
+
// Registered directly (not in the returned function) so it runs
|
|
2273
|
+
// BEFORE Vite's built-in middleware. This ensures all requests
|
|
2274
|
+
// (including /@*, /__vite*, /node_modules* paths) are validated
|
|
2275
|
+
// before Vite serves any content.
|
|
2276
|
+
server.middlewares.use((req, res, next) => {
|
|
2277
|
+
const blockReason = validateDevRequest({
|
|
2278
|
+
origin: req.headers.origin,
|
|
2279
|
+
host: req.headers.host,
|
|
2280
|
+
"x-forwarded-host": req.headers["x-forwarded-host"],
|
|
2281
|
+
"sec-fetch-site": req.headers["sec-fetch-site"],
|
|
2282
|
+
"sec-fetch-mode": req.headers["sec-fetch-mode"],
|
|
2283
|
+
}, nextConfig?.serverActionsAllowedOrigins);
|
|
2284
|
+
if (blockReason) {
|
|
2285
|
+
console.warn(`[vinext] Blocked dev request: ${blockReason} (${req.url})`);
|
|
2286
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2287
|
+
res.end("Forbidden");
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
next();
|
|
2291
|
+
});
|
|
2216
2292
|
// Return a function to register middleware AFTER Vite's built-in middleware
|
|
2217
2293
|
return () => {
|
|
2294
|
+
// App Router request logging in dev server
|
|
2295
|
+
//
|
|
2296
|
+
// For App Router, the RSC plugin handles requests internally.
|
|
2297
|
+
// We install a timing middleware here that:
|
|
2298
|
+
// 1. Intercepts writeHead() to pluck the X-Vinext-Timing header
|
|
2299
|
+
// (compileMs,renderMs) that the RSC entry attaches before
|
|
2300
|
+
// it is flushed to the client.
|
|
2301
|
+
// 2. Logs the full request after res finishes, using those timings.
|
|
2302
|
+
if (hasAppDir) {
|
|
2303
|
+
server.middlewares.use((req, res, next) => {
|
|
2304
|
+
const url = req.url ?? "/";
|
|
2305
|
+
// Skip Vite internals, HMR, and static assets.
|
|
2306
|
+
// Do NOT skip .rsc-suffixed URLs or RSC wire requests (Accept: text/x-component)
|
|
2307
|
+
// — those are soft navigations and should be logged like any other page request.
|
|
2308
|
+
const [pathname] = url.split("?");
|
|
2309
|
+
if (url.startsWith("/@") ||
|
|
2310
|
+
url.startsWith("/__vite") ||
|
|
2311
|
+
url.startsWith("/node_modules") ||
|
|
2312
|
+
(url.includes(".") && !pathname.endsWith(".html") && !pathname.endsWith(".rsc"))) {
|
|
2313
|
+
return next();
|
|
2314
|
+
}
|
|
2315
|
+
const _reqStart = now();
|
|
2316
|
+
let _compileMs;
|
|
2317
|
+
let _renderMs;
|
|
2318
|
+
// Intercept setHeader and writeHead so we can strip X-Vinext-Timing
|
|
2319
|
+
// before it reaches the client and capture the compile/render split.
|
|
2320
|
+
// The RSC plugin may set headers either way depending on its version.
|
|
2321
|
+
// Parse the three-part X-Vinext-Timing header:
|
|
2322
|
+
// "handlerStart,inHandlerCompileMs,renderMs"
|
|
2323
|
+
//
|
|
2324
|
+
// True compile time = time the RSC plugin spent loading/transforming
|
|
2325
|
+
// modules before our handler code ran, plus any in-handler work before
|
|
2326
|
+
// renderToReadableStream. Concretely:
|
|
2327
|
+
// compileMs = (handlerStart - _reqStart) + inHandlerCompileMs
|
|
2328
|
+
// renderMs = renderMs from header, or -1 for RSC-only (soft-nav)
|
|
2329
|
+
// responses where rendering is not measured in the handler.
|
|
2330
|
+
// In that case the middleware computes render time as
|
|
2331
|
+
// totalMs - compileMs.
|
|
2332
|
+
//
|
|
2333
|
+
// handlerStart is performance.now() recorded at the very top of
|
|
2334
|
+
// _handleRequest in the generated RSC entry. _reqStart is recorded
|
|
2335
|
+
// here in the Node middleware, one stack frame before the RSC plugin
|
|
2336
|
+
// loads the module. The gap between them is exactly the Vite
|
|
2337
|
+
// compile/transform cost.
|
|
2338
|
+
function _parseTiming(raw) {
|
|
2339
|
+
const [handlerStart, inHandlerCompileMs, renderMs] = String(raw).split(",").map((v) => Number(v));
|
|
2340
|
+
if (!Number.isNaN(handlerStart) && !Number.isNaN(inHandlerCompileMs) && inHandlerCompileMs !== -1) {
|
|
2341
|
+
_compileMs = Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs;
|
|
2342
|
+
}
|
|
2343
|
+
if (!Number.isNaN(renderMs) && renderMs !== -1) {
|
|
2344
|
+
_renderMs = renderMs;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
const _origSetHeader = res.setHeader.bind(res);
|
|
2348
|
+
res.setHeader = function (name, value) {
|
|
2349
|
+
if (name.toLowerCase() === "x-vinext-timing") {
|
|
2350
|
+
_parseTiming(value);
|
|
2351
|
+
return res; // drop the header — don't forward to client
|
|
2352
|
+
}
|
|
2353
|
+
return _origSetHeader(name, value);
|
|
2354
|
+
};
|
|
2355
|
+
const _origWriteHead = res.writeHead.bind(res);
|
|
2356
|
+
res.writeHead = function (statusCode, ...args) {
|
|
2357
|
+
// Normalise the optional headers argument (may be reason, headers object, or both).
|
|
2358
|
+
let headers;
|
|
2359
|
+
const [reasonOrHeaders, maybeHeaders] = args;
|
|
2360
|
+
if (typeof reasonOrHeaders === "string") {
|
|
2361
|
+
headers = maybeHeaders;
|
|
2362
|
+
}
|
|
2363
|
+
else {
|
|
2364
|
+
headers = reasonOrHeaders;
|
|
2365
|
+
}
|
|
2366
|
+
// Pull timing out of the headers object when present.
|
|
2367
|
+
if (headers && typeof headers === "object" && !Array.isArray(headers)) {
|
|
2368
|
+
const timingKey = Object.keys(headers).find((k) => k.toLowerCase() === "x-vinext-timing");
|
|
2369
|
+
if (timingKey) {
|
|
2370
|
+
_parseTiming(headers[timingKey]);
|
|
2371
|
+
delete headers[timingKey];
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return _origWriteHead(statusCode, ...args);
|
|
2375
|
+
};
|
|
2376
|
+
res.on("finish", () => {
|
|
2377
|
+
// Strip .rsc suffix — it's an internal RSC protocol detail,
|
|
2378
|
+
// not part of the actual page path the user navigated to.
|
|
2379
|
+
const logUrl = url.replace(/\.rsc(\?|$)/, "$1");
|
|
2380
|
+
const totalMs = now() - _reqStart;
|
|
2381
|
+
// For RSC-only responses (soft nav), renderMs is -1 (sentinel meaning
|
|
2382
|
+
// "not measured in the handler"). Compute it as totalMs - compileMs,
|
|
2383
|
+
// which is how long the RSC stream took to fully flush to the client —
|
|
2384
|
+
// matching what Next.js shows for soft navigations.
|
|
2385
|
+
const resolvedRenderMs = _renderMs !== undefined
|
|
2386
|
+
? _renderMs
|
|
2387
|
+
: (_compileMs !== undefined ? Math.max(0, Math.round(totalMs - _compileMs)) : undefined);
|
|
2388
|
+
logRequest({
|
|
2389
|
+
method: req.method ?? "GET",
|
|
2390
|
+
url: logUrl,
|
|
2391
|
+
status: res.statusCode,
|
|
2392
|
+
totalMs,
|
|
2393
|
+
compileMs: _compileMs,
|
|
2394
|
+
renderMs: resolvedRenderMs,
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
2397
|
+
next();
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2218
2400
|
server.middlewares.use(async (req, res, next) => {
|
|
2219
2401
|
try {
|
|
2220
2402
|
let url = req.url ?? "/";
|
|
@@ -2232,9 +2414,12 @@ hydrate();
|
|
|
2232
2414
|
if (url.split("?")[0].endsWith(".rsc")) {
|
|
2233
2415
|
return next();
|
|
2234
2416
|
}
|
|
2235
|
-
// ── Cross-origin request protection
|
|
2236
|
-
//
|
|
2237
|
-
//
|
|
2417
|
+
// ── Cross-origin request protection (defense-in-depth) ──────
|
|
2418
|
+
// The pre-Vite middleware above already blocks cross-origin
|
|
2419
|
+
// requests before Vite serves any content. This second check
|
|
2420
|
+
// guards the Pages Router handler specifically, in case the
|
|
2421
|
+
// middleware ordering changes or new middleware is added between
|
|
2422
|
+
// the two. Both calls use the same validateDevRequest() function.
|
|
2238
2423
|
const blockReason = validateDevRequest({
|
|
2239
2424
|
origin: req.headers.origin,
|
|
2240
2425
|
host: req.headers.host,
|
|
@@ -2258,7 +2443,14 @@ hydrate();
|
|
|
2258
2443
|
const imgUrl = rawImgUrl?.replaceAll("\\", "/") ?? null;
|
|
2259
2444
|
// Allowlist: must start with "/" but not "//" — blocks absolute
|
|
2260
2445
|
// URLs, protocol-relative, backslash variants, and exotic schemes.
|
|
2261
|
-
|
|
2446
|
+
// Also block internal Vite paths (/@*, /__vite*, /node_modules*)
|
|
2447
|
+
// to prevent redirecting to dev server endpoints.
|
|
2448
|
+
if (!imgUrl ||
|
|
2449
|
+
!imgUrl.startsWith("/") ||
|
|
2450
|
+
imgUrl.startsWith("//") ||
|
|
2451
|
+
imgUrl.startsWith("/@") ||
|
|
2452
|
+
imgUrl.startsWith("/__vite") ||
|
|
2453
|
+
imgUrl.startsWith("/node_modules")) {
|
|
2262
2454
|
res.writeHead(400);
|
|
2263
2455
|
res.end(!rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed");
|
|
2264
2456
|
return;
|
|
@@ -2366,9 +2558,22 @@ hydrate();
|
|
|
2366
2558
|
const result = await runMiddleware(server, middlewarePath, middlewareRequest);
|
|
2367
2559
|
if (!result.continue) {
|
|
2368
2560
|
if (result.redirectUrl) {
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2561
|
+
const redirectHeaders = { Location: result.redirectUrl };
|
|
2562
|
+
if (result.responseHeaders) {
|
|
2563
|
+
for (const [key, value] of result.responseHeaders) {
|
|
2564
|
+
const existing = redirectHeaders[key];
|
|
2565
|
+
if (existing === undefined) {
|
|
2566
|
+
redirectHeaders[key] = value;
|
|
2567
|
+
}
|
|
2568
|
+
else if (Array.isArray(existing)) {
|
|
2569
|
+
existing.push(value);
|
|
2570
|
+
}
|
|
2571
|
+
else {
|
|
2572
|
+
redirectHeaders[key] = [existing, value];
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
res.writeHead(result.redirectStatus ?? 307, redirectHeaders);
|
|
2372
2577
|
res.end();
|
|
2373
2578
|
return;
|
|
2374
2579
|
}
|
|
@@ -2438,7 +2643,10 @@ hydrate();
|
|
|
2438
2643
|
const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
|
|
2439
2644
|
if (handled)
|
|
2440
2645
|
return;
|
|
2441
|
-
// No API route matched —
|
|
2646
|
+
// No API route matched — if app dir exists, let the RSC plugin handle it
|
|
2647
|
+
// (app/api/* route handlers live there). Otherwise hard-404.
|
|
2648
|
+
if (hasAppDir)
|
|
2649
|
+
return next();
|
|
2442
2650
|
res.statusCode = 404;
|
|
2443
2651
|
res.end("404 - API route not found");
|
|
2444
2652
|
return;
|
|
@@ -2479,7 +2687,10 @@ hydrate();
|
|
|
2479
2687
|
return;
|
|
2480
2688
|
}
|
|
2481
2689
|
}
|
|
2482
|
-
// No fallback matched
|
|
2690
|
+
// No fallback matched - if app dir exists, let the RSC plugin handle it,
|
|
2691
|
+
// otherwise render via the pages SSR handler (will 404 for unknown routes).
|
|
2692
|
+
if (hasAppDir)
|
|
2693
|
+
return next();
|
|
2483
2694
|
await handler(req, res, resolvedUrl, mwStatus);
|
|
2484
2695
|
}
|
|
2485
2696
|
catch (e) {
|