vinext 0.0.21 → 0.0.23
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/build/static-export.d.ts.map +1 -1
- package/dist/build/static-export.js +9 -7
- package/dist/build/static-export.js.map +1 -1
- package/dist/config/next-config.d.ts +4 -1
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +10 -5
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +17 -4
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +381 -38
- package/dist/index.js.map +1 -1
- package/dist/routing/app-router.d.ts +2 -1
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +74 -107
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts +24 -0
- package/dist/routing/file-matcher.d.ts.map +1 -0
- package/dist/routing/file-matcher.js +75 -0
- package/dist/routing/file-matcher.js.map +1 -0
- package/dist/routing/pages-router.d.ts +3 -2
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +25 -44
- 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 +123 -47
- package/dist/server/app-dev-server.js.map +1 -1
- package/dist/server/dev-server.d.ts +2 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +93 -18
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +33 -2
- 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 +7 -1
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +30 -5
- 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/metadata.d.ts +1 -0
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +5 -1
- package/dist/shims/metadata.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
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { loadEnv, parseAst } from "vite";
|
|
2
2
|
import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, patternToNextFormat as pagesPatternToNextFormat } from "./routing/pages-router.js";
|
|
3
3
|
import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
|
|
4
|
+
import { createValidFileMatcher } from "./routing/file-matcher.js";
|
|
4
5
|
import { createSSRHandler } from "./server/dev-server.js";
|
|
5
6
|
import { handleApiRoute } from "./server/api-handler.js";
|
|
6
7
|
import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.js";
|
|
7
8
|
import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
|
|
8
9
|
import { findMiddlewareFile, isProxyFile, runMiddleware } from "./server/middleware.js";
|
|
10
|
+
import { logRequest, now } from "./server/request-log.js";
|
|
9
11
|
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
|
|
10
12
|
import { normalizePath } from "./server/normalize-path.js";
|
|
11
13
|
import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
|
|
14
|
+
import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/constants.js";
|
|
12
15
|
import { validateDevRequest } from "./server/dev-origin-check.js";
|
|
13
16
|
import { safeRegExp, isExternalUrl, proxyExternalRequest, parseCookies, matchHeaders, matchRedirect, matchRewrite, } from "./config/config-matchers.js";
|
|
14
17
|
import { scanMetadataFiles } from "./server/metadata-routes.js";
|
|
@@ -492,6 +495,7 @@ export default function vinext(options = {}) {
|
|
|
492
495
|
let hasAppDir = false;
|
|
493
496
|
let hasPagesDir = false;
|
|
494
497
|
let nextConfig;
|
|
498
|
+
let fileMatcher;
|
|
495
499
|
let middlewarePath = null;
|
|
496
500
|
let instrumentationPath = null;
|
|
497
501
|
let hasCloudflarePlugin = false;
|
|
@@ -505,8 +509,8 @@ export default function vinext(options = {}) {
|
|
|
505
509
|
* This is the entry point for `vite build --ssr`.
|
|
506
510
|
*/
|
|
507
511
|
async function generateServerEntry() {
|
|
508
|
-
const pageRoutes = await pagesRouter(pagesDir);
|
|
509
|
-
const apiRoutes = await apiRouter(pagesDir);
|
|
512
|
+
const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
513
|
+
const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
510
514
|
// Generate import statements using absolute paths since virtual
|
|
511
515
|
// modules don't have a real file location for relative resolution.
|
|
512
516
|
const pageImports = pageRoutes.map((r, i) => {
|
|
@@ -526,16 +530,15 @@ export default function vinext(options = {}) {
|
|
|
526
530
|
return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
|
|
527
531
|
});
|
|
528
532
|
// Check for _app and _document
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
const docFileBase = path.join(pagesDir, "_document").replace(/\\/g, "/");
|
|
533
|
+
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
534
|
+
const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
|
|
535
|
+
const hasApp = appFilePath !== null;
|
|
536
|
+
const hasDoc = docFilePath !== null;
|
|
534
537
|
const appImportCode = hasApp
|
|
535
|
-
? `import { default as AppComponent } from ${JSON.stringify(
|
|
538
|
+
? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
|
|
536
539
|
: `const AppComponent = null;`;
|
|
537
540
|
const docImportCode = hasDoc
|
|
538
|
-
? `import { default as DocumentComponent } from ${JSON.stringify(
|
|
541
|
+
? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
|
|
539
542
|
: `const DocumentComponent = null;`;
|
|
540
543
|
// Serialize i18n config for embedding in the server entry
|
|
541
544
|
const i18nConfigJson = nextConfig?.i18n
|
|
@@ -1026,6 +1029,12 @@ function createReqRes(request, url, query, body) {
|
|
|
1026
1029
|
else { res.writeHead(statusOrUrl, { Location: url2 }); }
|
|
1027
1030
|
res.end();
|
|
1028
1031
|
},
|
|
1032
|
+
getHeaders: function() {
|
|
1033
|
+
var h = Object.assign({}, resHeaders);
|
|
1034
|
+
if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
|
|
1035
|
+
return h;
|
|
1036
|
+
},
|
|
1037
|
+
get headersSent() { return ended; },
|
|
1029
1038
|
};
|
|
1030
1039
|
|
|
1031
1040
|
return { req, res, responsePromise };
|
|
@@ -1140,8 +1149,9 @@ export async function renderPage(request, url, manifest) {
|
|
|
1140
1149
|
}
|
|
1141
1150
|
|
|
1142
1151
|
let pageProps = {};
|
|
1152
|
+
var gsspRes = null;
|
|
1143
1153
|
if (typeof pageModule.getServerSideProps === "function") {
|
|
1144
|
-
const { req, res } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1154
|
+
const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
|
|
1145
1155
|
const ctx = {
|
|
1146
1156
|
params, req, res,
|
|
1147
1157
|
query: parseQuery(routeUrl),
|
|
@@ -1151,6 +1161,10 @@ export async function renderPage(request, url, manifest) {
|
|
|
1151
1161
|
defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
|
|
1152
1162
|
};
|
|
1153
1163
|
const result = await pageModule.getServerSideProps(ctx);
|
|
1164
|
+
// If gSSP called res.end() directly (short-circuit), return that response.
|
|
1165
|
+
if (res.headersSent) {
|
|
1166
|
+
return await responsePromise;
|
|
1167
|
+
}
|
|
1154
1168
|
if (result && result.props) pageProps = result.props;
|
|
1155
1169
|
if (result && result.redirect) {
|
|
1156
1170
|
var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
@@ -1159,6 +1173,9 @@ export async function renderPage(request, url, manifest) {
|
|
|
1159
1173
|
if (result && result.notFound) {
|
|
1160
1174
|
return new Response("404", { status: 404 });
|
|
1161
1175
|
}
|
|
1176
|
+
// Preserve the res object so headers/status/cookies set by gSSP
|
|
1177
|
+
// can be merged into the final HTML response.
|
|
1178
|
+
gsspRes = res;
|
|
1162
1179
|
}
|
|
1163
1180
|
// Build font Link header early so it's available for ISR cached responses too.
|
|
1164
1181
|
// Font preloads are module-level state populated at import time and persist across requests.
|
|
@@ -1335,16 +1352,33 @@ export async function renderPage(request, url, manifest) {
|
|
|
1335
1352
|
await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
|
|
1336
1353
|
}
|
|
1337
1354
|
|
|
1338
|
-
|
|
1355
|
+
// Merge headers/status/cookies set by getServerSideProps on the res object.
|
|
1356
|
+
// gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
|
|
1357
|
+
var finalStatus = 200;
|
|
1358
|
+
const responseHeaders = new Headers({ "Content-Type": "text/html" });
|
|
1359
|
+
if (gsspRes) {
|
|
1360
|
+
finalStatus = gsspRes.statusCode;
|
|
1361
|
+
var gsspHeaders = gsspRes.getHeaders();
|
|
1362
|
+
for (var hk of Object.keys(gsspHeaders)) {
|
|
1363
|
+
var hv = gsspHeaders[hk];
|
|
1364
|
+
if (hk === "set-cookie" && Array.isArray(hv)) {
|
|
1365
|
+
for (var sc of hv) responseHeaders.append("set-cookie", sc);
|
|
1366
|
+
} else if (hv != null) {
|
|
1367
|
+
responseHeaders.set(hk, String(hv));
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
// Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
|
|
1371
|
+
responseHeaders.set("Content-Type", "text/html");
|
|
1372
|
+
}
|
|
1339
1373
|
if (isrRevalidateSeconds) {
|
|
1340
|
-
responseHeaders
|
|
1341
|
-
responseHeaders
|
|
1374
|
+
responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
|
|
1375
|
+
responseHeaders.set("X-Vinext-Cache", "MISS");
|
|
1342
1376
|
}
|
|
1343
1377
|
// Set HTTP Link header for font preloading
|
|
1344
1378
|
if (_fontLinkHeader) {
|
|
1345
|
-
responseHeaders
|
|
1379
|
+
responseHeaders.set("Link", _fontLinkHeader);
|
|
1346
1380
|
}
|
|
1347
|
-
return new Response(compositeStream, { status:
|
|
1381
|
+
return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
|
|
1348
1382
|
} catch (e) {
|
|
1349
1383
|
console.error("[vinext] SSR error:", e);
|
|
1350
1384
|
return new Response("Internal Server Error", { status: 500 });
|
|
@@ -1428,8 +1462,9 @@ ${middlewareExportCode}
|
|
|
1428
1462
|
* __NEXT_DATA__ to determine which page to hydrate.
|
|
1429
1463
|
*/
|
|
1430
1464
|
async function generateClientEntry() {
|
|
1431
|
-
const pageRoutes = await pagesRouter(pagesDir);
|
|
1432
|
-
const
|
|
1465
|
+
const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
1466
|
+
const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
|
|
1467
|
+
const hasApp = appFilePath !== null;
|
|
1433
1468
|
// Build a map of route pattern -> dynamic import.
|
|
1434
1469
|
// Keys must use Next.js bracket format (e.g. "/user/[id]") to match
|
|
1435
1470
|
// __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
|
|
@@ -1441,7 +1476,7 @@ ${middlewareExportCode}
|
|
|
1441
1476
|
// lgtm[js/bad-code-sanitization]
|
|
1442
1477
|
return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
|
|
1443
1478
|
});
|
|
1444
|
-
const appFileBase =
|
|
1479
|
+
const appFileBase = appFilePath?.replace(/\\/g, "/");
|
|
1445
1480
|
return `
|
|
1446
1481
|
import React from "react";
|
|
1447
1482
|
import { hydrateRoot } from "react-dom/client";
|
|
@@ -1634,8 +1669,10 @@ hydrate();
|
|
|
1634
1669
|
middlewarePath = findMiddlewareFile(root);
|
|
1635
1670
|
instrumentationPath = findInstrumentationFile(root);
|
|
1636
1671
|
// Load next.config.js if present (always from project root, not src/)
|
|
1637
|
-
const
|
|
1672
|
+
const phase = env?.command === "build" ? PHASE_PRODUCTION_BUILD : PHASE_DEVELOPMENT_SERVER;
|
|
1673
|
+
const rawConfig = await loadNextConfig(root, phase);
|
|
1638
1674
|
nextConfig = await resolveNextConfig(rawConfig);
|
|
1675
|
+
fileMatcher = createValidFileMatcher(nextConfig.pageExtensions);
|
|
1639
1676
|
// Merge env from next.config.js with NEXT_PUBLIC_* env vars
|
|
1640
1677
|
const defines = getNextPublicEnvDefines();
|
|
1641
1678
|
if (!config.define ||
|
|
@@ -2078,10 +2115,10 @@ hydrate();
|
|
|
2078
2115
|
}
|
|
2079
2116
|
// App Router virtual modules
|
|
2080
2117
|
if (id === RESOLVED_RSC_ENTRY && hasAppDir) {
|
|
2081
|
-
const routes = await appRouter(appDir);
|
|
2118
|
+
const routes = await appRouter(appDir, nextConfig?.pageExtensions, fileMatcher);
|
|
2082
2119
|
const metaRoutes = scanMetadataFiles(appDir);
|
|
2083
2120
|
// Check for global-error.tsx at app root
|
|
2084
|
-
const globalErrorPath = findFileWithExts(appDir, "global-error");
|
|
2121
|
+
const globalErrorPath = findFileWithExts(appDir, "global-error", fileMatcher);
|
|
2085
2122
|
return generateRscEntry(appDir, routes, middlewarePath, metaRoutes, globalErrorPath, nextConfig?.basePath, nextConfig?.trailingSlash, {
|
|
2086
2123
|
redirects: nextConfig?.redirects,
|
|
2087
2124
|
rewrites: nextConfig?.rewrites,
|
|
@@ -2184,15 +2221,14 @@ hydrate();
|
|
|
2184
2221
|
hotUpdate(options) {
|
|
2185
2222
|
if (!hasPagesDir || hasAppDir)
|
|
2186
2223
|
return;
|
|
2187
|
-
|
|
2188
|
-
if (options.file.startsWith(pagesDir) && ext.test(options.file)) {
|
|
2224
|
+
if (options.file.startsWith(pagesDir) && fileMatcher.extensionRegex.test(options.file)) {
|
|
2189
2225
|
options.server.environments.client.hot.send({ type: "full-reload" });
|
|
2190
2226
|
return [];
|
|
2191
2227
|
}
|
|
2192
2228
|
},
|
|
2193
2229
|
configureServer(server) {
|
|
2194
2230
|
// Watch pages directory for file additions/removals to invalidate route cache.
|
|
2195
|
-
const pageExtensions =
|
|
2231
|
+
const pageExtensions = fileMatcher.extensionRegex;
|
|
2196
2232
|
/**
|
|
2197
2233
|
* Invalidate the virtual RSC entry module in Vite's module graph.
|
|
2198
2234
|
*
|
|
@@ -2236,8 +2272,135 @@ hydrate();
|
|
|
2236
2272
|
console.error("[vinext] Instrumentation error:", err);
|
|
2237
2273
|
});
|
|
2238
2274
|
}
|
|
2275
|
+
// ── Dev request origin check ─────────────────────────────────────
|
|
2276
|
+
// Registered directly (not in the returned function) so it runs
|
|
2277
|
+
// BEFORE Vite's built-in middleware. This ensures all requests
|
|
2278
|
+
// (including /@*, /__vite*, /node_modules* paths) are validated
|
|
2279
|
+
// before Vite serves any content.
|
|
2280
|
+
server.middlewares.use((req, res, next) => {
|
|
2281
|
+
const blockReason = validateDevRequest({
|
|
2282
|
+
origin: req.headers.origin,
|
|
2283
|
+
host: req.headers.host,
|
|
2284
|
+
"x-forwarded-host": req.headers["x-forwarded-host"],
|
|
2285
|
+
"sec-fetch-site": req.headers["sec-fetch-site"],
|
|
2286
|
+
"sec-fetch-mode": req.headers["sec-fetch-mode"],
|
|
2287
|
+
}, nextConfig?.serverActionsAllowedOrigins);
|
|
2288
|
+
if (blockReason) {
|
|
2289
|
+
console.warn(`[vinext] Blocked dev request: ${blockReason} (${req.url})`);
|
|
2290
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2291
|
+
res.end("Forbidden");
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
next();
|
|
2295
|
+
});
|
|
2239
2296
|
// Return a function to register middleware AFTER Vite's built-in middleware
|
|
2240
2297
|
return () => {
|
|
2298
|
+
// App Router request logging in dev server
|
|
2299
|
+
//
|
|
2300
|
+
// For App Router, the RSC plugin handles requests internally.
|
|
2301
|
+
// We install a timing middleware here that:
|
|
2302
|
+
// 1. Intercepts writeHead() to pluck the X-Vinext-Timing header
|
|
2303
|
+
// (compileMs,renderMs) that the RSC entry attaches before
|
|
2304
|
+
// it is flushed to the client.
|
|
2305
|
+
// 2. Logs the full request after res finishes, using those timings.
|
|
2306
|
+
if (hasAppDir) {
|
|
2307
|
+
server.middlewares.use((req, res, next) => {
|
|
2308
|
+
const url = req.url ?? "/";
|
|
2309
|
+
// Skip Vite internals, HMR, and static assets.
|
|
2310
|
+
// Do NOT skip .rsc-suffixed URLs or RSC wire requests (Accept: text/x-component)
|
|
2311
|
+
// — those are soft navigations and should be logged like any other page request.
|
|
2312
|
+
const [pathname] = url.split("?");
|
|
2313
|
+
if (url.startsWith("/@") ||
|
|
2314
|
+
url.startsWith("/__vite") ||
|
|
2315
|
+
url.startsWith("/node_modules") ||
|
|
2316
|
+
(url.includes(".") && !pathname.endsWith(".html") && !pathname.endsWith(".rsc"))) {
|
|
2317
|
+
return next();
|
|
2318
|
+
}
|
|
2319
|
+
const _reqStart = now();
|
|
2320
|
+
let _compileMs;
|
|
2321
|
+
let _renderMs;
|
|
2322
|
+
// Intercept setHeader and writeHead so we can strip X-Vinext-Timing
|
|
2323
|
+
// before it reaches the client and capture the compile/render split.
|
|
2324
|
+
// The RSC plugin may set headers either way depending on its version.
|
|
2325
|
+
// Parse the three-part X-Vinext-Timing header:
|
|
2326
|
+
// "handlerStart,inHandlerCompileMs,renderMs"
|
|
2327
|
+
//
|
|
2328
|
+
// True compile time = time the RSC plugin spent loading/transforming
|
|
2329
|
+
// modules before our handler code ran, plus any in-handler work before
|
|
2330
|
+
// renderToReadableStream. Concretely:
|
|
2331
|
+
// compileMs = (handlerStart - _reqStart) + inHandlerCompileMs
|
|
2332
|
+
// renderMs = renderMs from header, or -1 for RSC-only (soft-nav)
|
|
2333
|
+
// responses where rendering is not measured in the handler.
|
|
2334
|
+
// In that case the middleware computes render time as
|
|
2335
|
+
// totalMs - compileMs.
|
|
2336
|
+
//
|
|
2337
|
+
// handlerStart is performance.now() recorded at the very top of
|
|
2338
|
+
// _handleRequest in the generated RSC entry. _reqStart is recorded
|
|
2339
|
+
// here in the Node middleware, one stack frame before the RSC plugin
|
|
2340
|
+
// loads the module. The gap between them is exactly the Vite
|
|
2341
|
+
// compile/transform cost.
|
|
2342
|
+
function _parseTiming(raw) {
|
|
2343
|
+
const [handlerStart, inHandlerCompileMs, renderMs] = String(raw).split(",").map((v) => Number(v));
|
|
2344
|
+
if (!Number.isNaN(handlerStart) && !Number.isNaN(inHandlerCompileMs) && inHandlerCompileMs !== -1) {
|
|
2345
|
+
_compileMs = Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs;
|
|
2346
|
+
}
|
|
2347
|
+
if (!Number.isNaN(renderMs) && renderMs !== -1) {
|
|
2348
|
+
_renderMs = renderMs;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const _origSetHeader = res.setHeader.bind(res);
|
|
2352
|
+
res.setHeader = function (name, value) {
|
|
2353
|
+
if (name.toLowerCase() === "x-vinext-timing") {
|
|
2354
|
+
_parseTiming(value);
|
|
2355
|
+
return res; // drop the header — don't forward to client
|
|
2356
|
+
}
|
|
2357
|
+
return _origSetHeader(name, value);
|
|
2358
|
+
};
|
|
2359
|
+
const _origWriteHead = res.writeHead.bind(res);
|
|
2360
|
+
res.writeHead = function (statusCode, ...args) {
|
|
2361
|
+
// Normalise the optional headers argument (may be reason, headers object, or both).
|
|
2362
|
+
let headers;
|
|
2363
|
+
const [reasonOrHeaders, maybeHeaders] = args;
|
|
2364
|
+
if (typeof reasonOrHeaders === "string") {
|
|
2365
|
+
headers = maybeHeaders;
|
|
2366
|
+
}
|
|
2367
|
+
else {
|
|
2368
|
+
headers = reasonOrHeaders;
|
|
2369
|
+
}
|
|
2370
|
+
// Pull timing out of the headers object when present.
|
|
2371
|
+
if (headers && typeof headers === "object" && !Array.isArray(headers)) {
|
|
2372
|
+
const timingKey = Object.keys(headers).find((k) => k.toLowerCase() === "x-vinext-timing");
|
|
2373
|
+
if (timingKey) {
|
|
2374
|
+
_parseTiming(headers[timingKey]);
|
|
2375
|
+
delete headers[timingKey];
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return _origWriteHead(statusCode, ...args);
|
|
2379
|
+
};
|
|
2380
|
+
res.on("finish", () => {
|
|
2381
|
+
// Strip .rsc suffix — it's an internal RSC protocol detail,
|
|
2382
|
+
// not part of the actual page path the user navigated to.
|
|
2383
|
+
const logUrl = url.replace(/\.rsc(\?|$)/, "$1");
|
|
2384
|
+
const totalMs = now() - _reqStart;
|
|
2385
|
+
// For RSC-only responses (soft nav), renderMs is -1 (sentinel meaning
|
|
2386
|
+
// "not measured in the handler"). Compute it as totalMs - compileMs,
|
|
2387
|
+
// which is how long the RSC stream took to fully flush to the client —
|
|
2388
|
+
// matching what Next.js shows for soft navigations.
|
|
2389
|
+
const resolvedRenderMs = _renderMs !== undefined
|
|
2390
|
+
? _renderMs
|
|
2391
|
+
: (_compileMs !== undefined ? Math.max(0, Math.round(totalMs - _compileMs)) : undefined);
|
|
2392
|
+
logRequest({
|
|
2393
|
+
method: req.method ?? "GET",
|
|
2394
|
+
url: logUrl,
|
|
2395
|
+
status: res.statusCode,
|
|
2396
|
+
totalMs,
|
|
2397
|
+
compileMs: _compileMs,
|
|
2398
|
+
renderMs: resolvedRenderMs,
|
|
2399
|
+
});
|
|
2400
|
+
});
|
|
2401
|
+
next();
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2241
2404
|
server.middlewares.use(async (req, res, next) => {
|
|
2242
2405
|
try {
|
|
2243
2406
|
let url = req.url ?? "/";
|
|
@@ -2255,9 +2418,12 @@ hydrate();
|
|
|
2255
2418
|
if (url.split("?")[0].endsWith(".rsc")) {
|
|
2256
2419
|
return next();
|
|
2257
2420
|
}
|
|
2258
|
-
// ── Cross-origin request protection
|
|
2259
|
-
//
|
|
2260
|
-
//
|
|
2421
|
+
// ── Cross-origin request protection (defense-in-depth) ──────
|
|
2422
|
+
// The pre-Vite middleware above already blocks cross-origin
|
|
2423
|
+
// requests before Vite serves any content. This second check
|
|
2424
|
+
// guards the Pages Router handler specifically, in case the
|
|
2425
|
+
// middleware ordering changes or new middleware is added between
|
|
2426
|
+
// the two. Both calls use the same validateDevRequest() function.
|
|
2261
2427
|
const blockReason = validateDevRequest({
|
|
2262
2428
|
origin: req.headers.origin,
|
|
2263
2429
|
host: req.headers.host,
|
|
@@ -2281,7 +2447,14 @@ hydrate();
|
|
|
2281
2447
|
const imgUrl = rawImgUrl?.replaceAll("\\", "/") ?? null;
|
|
2282
2448
|
// Allowlist: must start with "/" but not "//" — blocks absolute
|
|
2283
2449
|
// URLs, protocol-relative, backslash variants, and exotic schemes.
|
|
2284
|
-
|
|
2450
|
+
// Also block internal Vite paths (/@*, /__vite*, /node_modules*)
|
|
2451
|
+
// to prevent redirecting to dev server endpoints.
|
|
2452
|
+
if (!imgUrl ||
|
|
2453
|
+
!imgUrl.startsWith("/") ||
|
|
2454
|
+
imgUrl.startsWith("//") ||
|
|
2455
|
+
imgUrl.startsWith("/@") ||
|
|
2456
|
+
imgUrl.startsWith("/__vite") ||
|
|
2457
|
+
imgUrl.startsWith("/node_modules")) {
|
|
2285
2458
|
res.writeHead(400);
|
|
2286
2459
|
res.end(!rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed");
|
|
2287
2460
|
return;
|
|
@@ -2418,10 +2591,20 @@ hydrate();
|
|
|
2418
2591
|
return;
|
|
2419
2592
|
}
|
|
2420
2593
|
}
|
|
2421
|
-
// Apply middleware response headers
|
|
2594
|
+
// Apply middleware response headers. Unpack
|
|
2595
|
+
// x-middleware-request-* headers into req.headers so
|
|
2596
|
+
// config has/missing conditions and downstream handlers
|
|
2597
|
+
// see middleware-modified cookies and headers.
|
|
2422
2598
|
if (result.responseHeaders) {
|
|
2599
|
+
const mwReqPrefix = "x-middleware-request-";
|
|
2423
2600
|
for (const [key, value] of result.responseHeaders) {
|
|
2424
|
-
|
|
2601
|
+
if (key.startsWith(mwReqPrefix)) {
|
|
2602
|
+
const realName = key.slice(mwReqPrefix.length);
|
|
2603
|
+
req.headers[realName] = value;
|
|
2604
|
+
}
|
|
2605
|
+
else if (!key.startsWith("x-middleware-")) {
|
|
2606
|
+
res.appendHeader(key, value);
|
|
2607
|
+
}
|
|
2425
2608
|
}
|
|
2426
2609
|
}
|
|
2427
2610
|
// Apply middleware rewrite (URL and optional status code)
|
|
@@ -2470,7 +2653,7 @@ hydrate();
|
|
|
2470
2653
|
const resolvedPathname = resolvedUrl.split("?")[0];
|
|
2471
2654
|
if (resolvedPathname.startsWith("/api/") ||
|
|
2472
2655
|
resolvedPathname === "/api") {
|
|
2473
|
-
const apiRoutes = await apiRouter(pagesDir);
|
|
2656
|
+
const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
2474
2657
|
const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
|
|
2475
2658
|
if (handled)
|
|
2476
2659
|
return;
|
|
@@ -2482,7 +2665,7 @@ hydrate();
|
|
|
2482
2665
|
res.end("404 - API route not found");
|
|
2483
2666
|
return;
|
|
2484
2667
|
}
|
|
2485
|
-
const routes = await pagesRouter(pagesDir);
|
|
2668
|
+
const routes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
|
|
2486
2669
|
// Apply afterFiles rewrites — these run after initial route matching
|
|
2487
2670
|
// If beforeFiles already rewrote the URL, afterFiles still run on the
|
|
2488
2671
|
// *resolved* pathname. Next.js applies these when route matching succeeds
|
|
@@ -2497,7 +2680,7 @@ hydrate();
|
|
|
2497
2680
|
await proxyExternalRewriteNode(req, res, resolvedUrl);
|
|
2498
2681
|
return;
|
|
2499
2682
|
}
|
|
2500
|
-
const handler = createSSRHandler(server, routes, pagesDir, nextConfig?.i18n);
|
|
2683
|
+
const handler = createSSRHandler(server, routes, pagesDir, nextConfig?.i18n, fileMatcher);
|
|
2501
2684
|
const mwStatus = req.__vinextRewriteStatus;
|
|
2502
2685
|
// Try rendering the resolved URL
|
|
2503
2686
|
const match = matchRoute(resolvedUrl.split("?")[0], routes);
|
|
@@ -2531,6 +2714,41 @@ hydrate();
|
|
|
2531
2714
|
};
|
|
2532
2715
|
},
|
|
2533
2716
|
},
|
|
2717
|
+
// Strip server-only data-fetching exports (getServerSideProps, getStaticProps,
|
|
2718
|
+
// getStaticPaths) from page modules in the client bundle. These functions
|
|
2719
|
+
// often import server-only modules (database drivers, fs, etc.) that would
|
|
2720
|
+
// break or bloat the client bundle. Next.js does this via an SWC transform
|
|
2721
|
+
// (next-ssg-transform); we use Vite's parseAst + MagicString.
|
|
2722
|
+
//
|
|
2723
|
+
// Only applies to client builds (not SSR) and only to files under the
|
|
2724
|
+
// pages/ directory.
|
|
2725
|
+
{
|
|
2726
|
+
name: "vinext:strip-server-exports",
|
|
2727
|
+
transform: {
|
|
2728
|
+
// Only match page source files, not node_modules
|
|
2729
|
+
filter: { id: /\.(tsx?|jsx?|mjs)$/ },
|
|
2730
|
+
handler(code, id) {
|
|
2731
|
+
const ssr = this.environment?.name !== "client";
|
|
2732
|
+
if (ssr)
|
|
2733
|
+
return null;
|
|
2734
|
+
if (!hasPagesDir)
|
|
2735
|
+
return null;
|
|
2736
|
+
// Only transform files under the pages/ directory
|
|
2737
|
+
if (!id.startsWith(pagesDir))
|
|
2738
|
+
return null;
|
|
2739
|
+
// Skip API routes, _app, _document, _error
|
|
2740
|
+
const relativePath = id.slice(pagesDir.length);
|
|
2741
|
+
if (relativePath.startsWith("/api/") || relativePath === "/api")
|
|
2742
|
+
return null;
|
|
2743
|
+
if (/\/_(?:app|document|error)\b/.test(relativePath))
|
|
2744
|
+
return null;
|
|
2745
|
+
const result = stripServerExports(code);
|
|
2746
|
+
if (!result)
|
|
2747
|
+
return null;
|
|
2748
|
+
return { code: result, map: null };
|
|
2749
|
+
},
|
|
2750
|
+
},
|
|
2751
|
+
},
|
|
2534
2752
|
// Local image import transform:
|
|
2535
2753
|
// When a source file imports a local image (e.g., `import hero from './hero.jpg'`),
|
|
2536
2754
|
// this plugin transforms the default import to a StaticImageData object with
|
|
@@ -3173,6 +3391,7 @@ hydrate();
|
|
|
3173
3391
|
" Cache-Control: public, max-age=31536000, immutable",
|
|
3174
3392
|
"",
|
|
3175
3393
|
].join("\n");
|
|
3394
|
+
fs.mkdirSync(clientDir, { recursive: true });
|
|
3176
3395
|
fs.writeFileSync(headersPath, headersContent);
|
|
3177
3396
|
}
|
|
3178
3397
|
},
|
|
@@ -3231,6 +3450,101 @@ function extractConstraint(str, re) {
|
|
|
3231
3450
|
* :param+ — matches one or more segments
|
|
3232
3451
|
* (regex) — inline regex patterns in the source
|
|
3233
3452
|
*/
|
|
3453
|
+
/**
|
|
3454
|
+
* Strip server-only data-fetching exports from a page module's source code.
|
|
3455
|
+
* Returns the transformed code, or null if no changes were made.
|
|
3456
|
+
*
|
|
3457
|
+
* Handles:
|
|
3458
|
+
* - export (async) function getServerSideProps(...) { ... }
|
|
3459
|
+
* - export const getStaticProps = async (...) => { ... }
|
|
3460
|
+
* - export const getServerSideProps = someHelper;
|
|
3461
|
+
*/
|
|
3462
|
+
/**
|
|
3463
|
+
* Skip past balanced brackets/parens/braces starting at `pos` (which should
|
|
3464
|
+
* point to the opening bracket). Returns the position AFTER the closing bracket.
|
|
3465
|
+
* Handles nested brackets, string literals, and comments.
|
|
3466
|
+
*/
|
|
3467
|
+
/**
|
|
3468
|
+
* Strip server-only data-fetching exports (getServerSideProps,
|
|
3469
|
+
* getStaticProps, getStaticPaths) from page modules for the client
|
|
3470
|
+
* bundle. Uses Vite's parseAst (Rollup/acorn) for correct handling
|
|
3471
|
+
* of all export patterns including function expressions, arrow
|
|
3472
|
+
* functions with TS return types, and re-exports.
|
|
3473
|
+
*
|
|
3474
|
+
* Modeled after Next.js's SWC `next-ssg-transform`.
|
|
3475
|
+
*/
|
|
3476
|
+
function stripServerExports(code) {
|
|
3477
|
+
const SERVER_EXPORTS = new Set(["getServerSideProps", "getStaticProps", "getStaticPaths"]);
|
|
3478
|
+
if (![...SERVER_EXPORTS].some(name => code.includes(name)))
|
|
3479
|
+
return null;
|
|
3480
|
+
let ast;
|
|
3481
|
+
try {
|
|
3482
|
+
ast = parseAst(code);
|
|
3483
|
+
}
|
|
3484
|
+
catch {
|
|
3485
|
+
// If parsing fails (shouldn't happen post-JSX/TS transform), bail out
|
|
3486
|
+
return null;
|
|
3487
|
+
}
|
|
3488
|
+
const s = new MagicString(code);
|
|
3489
|
+
let changed = false;
|
|
3490
|
+
for (const node of ast.body) {
|
|
3491
|
+
if (node.type !== "ExportNamedDeclaration")
|
|
3492
|
+
continue;
|
|
3493
|
+
// Case 1: export function name() {} / export async function name() {}
|
|
3494
|
+
// Case 2: export const/let/var name = ...
|
|
3495
|
+
if (node.declaration) {
|
|
3496
|
+
const decl = node.declaration;
|
|
3497
|
+
if (decl.type === "FunctionDeclaration" && SERVER_EXPORTS.has(decl.id?.name)) {
|
|
3498
|
+
s.overwrite(node.start, node.end, `export function ${decl.id.name}() { return { props: {} }; }`);
|
|
3499
|
+
changed = true;
|
|
3500
|
+
}
|
|
3501
|
+
else if (decl.type === "VariableDeclaration") {
|
|
3502
|
+
for (const declarator of decl.declarations) {
|
|
3503
|
+
if (declarator.id?.type === "Identifier" && SERVER_EXPORTS.has(declarator.id.name)) {
|
|
3504
|
+
s.overwrite(node.start, node.end, `export const ${declarator.id.name} = undefined;`);
|
|
3505
|
+
changed = true;
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
// Case 3: export { getServerSideProps } or export { getServerSideProps as gSSP }
|
|
3512
|
+
if (node.specifiers && node.specifiers.length > 0 && !node.source) {
|
|
3513
|
+
const kept = [];
|
|
3514
|
+
const stripped = [];
|
|
3515
|
+
for (const spec of node.specifiers) {
|
|
3516
|
+
// spec.local.name is the binding name, spec.exported.name is the export name
|
|
3517
|
+
const exportedName = spec.exported?.name ?? spec.exported?.value;
|
|
3518
|
+
if (SERVER_EXPORTS.has(exportedName)) {
|
|
3519
|
+
stripped.push(exportedName);
|
|
3520
|
+
}
|
|
3521
|
+
else {
|
|
3522
|
+
kept.push(spec);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
if (stripped.length > 0) {
|
|
3526
|
+
// Build replacement: keep non-server specifiers, add stubs for stripped ones
|
|
3527
|
+
const parts = [];
|
|
3528
|
+
if (kept.length > 0) {
|
|
3529
|
+
const keptStr = kept.map((sp) => {
|
|
3530
|
+
const local = sp.local.name;
|
|
3531
|
+
const exported = sp.exported?.name ?? sp.exported?.value;
|
|
3532
|
+
return local === exported ? local : `${local} as ${exported}`;
|
|
3533
|
+
}).join(", ");
|
|
3534
|
+
parts.push(`export { ${keptStr} };`);
|
|
3535
|
+
}
|
|
3536
|
+
for (const name of stripped) {
|
|
3537
|
+
parts.push(`export const ${name} = undefined;`);
|
|
3538
|
+
}
|
|
3539
|
+
s.overwrite(node.start, node.end, parts.join("\n"));
|
|
3540
|
+
changed = true;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (!changed)
|
|
3545
|
+
return null;
|
|
3546
|
+
return s.toString();
|
|
3547
|
+
}
|
|
3234
3548
|
export function matchConfigPattern(pathname, pattern) {
|
|
3235
3549
|
// If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
|
|
3236
3550
|
// Also enter this branch when a catch-all parameter (:param* or :param+) is
|
|
@@ -3441,16 +3755,44 @@ function applyRewrites(pathname, rewrites, ctx) {
|
|
|
3441
3755
|
function applyHeaders(pathname, res, headers, ctx) {
|
|
3442
3756
|
const matched = matchHeaders(pathname, headers, ctx);
|
|
3443
3757
|
for (const header of matched) {
|
|
3444
|
-
|
|
3758
|
+
// Use append semantics for headers where multiple values must coexist
|
|
3759
|
+
// (Vary, Set-Cookie). Using setHeader() on these would destroy
|
|
3760
|
+
// existing values like "Vary: RSC, Accept".
|
|
3761
|
+
const lk = header.key.toLowerCase();
|
|
3762
|
+
if (lk === "set-cookie") {
|
|
3763
|
+
// Node.js res.getHeader("set-cookie") returns string[] when
|
|
3764
|
+
// multiple Set-Cookie headers have been set. Preserve the array.
|
|
3765
|
+
const existing = res.getHeader(lk);
|
|
3766
|
+
if (Array.isArray(existing)) {
|
|
3767
|
+
res.setHeader(header.key, [...existing, header.value]);
|
|
3768
|
+
}
|
|
3769
|
+
else if (existing) {
|
|
3770
|
+
res.setHeader(header.key, [String(existing), header.value]);
|
|
3771
|
+
}
|
|
3772
|
+
else {
|
|
3773
|
+
res.setHeader(header.key, header.value);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
else if (lk === "vary") {
|
|
3777
|
+
const existing = res.getHeader(lk);
|
|
3778
|
+
if (existing) {
|
|
3779
|
+
res.setHeader(header.key, existing + ", " + header.value);
|
|
3780
|
+
}
|
|
3781
|
+
else {
|
|
3782
|
+
res.setHeader(header.key, header.value);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
else {
|
|
3786
|
+
res.setHeader(header.key, header.value);
|
|
3787
|
+
}
|
|
3445
3788
|
}
|
|
3446
3789
|
}
|
|
3447
3790
|
/**
|
|
3448
3791
|
* Find a file by name (without extension) in a directory.
|
|
3449
|
-
* Checks
|
|
3792
|
+
* Checks the configured page extensions.
|
|
3450
3793
|
*/
|
|
3451
|
-
function findFileWithExts(dir, name) {
|
|
3452
|
-
const
|
|
3453
|
-
for (const ext of extensions) {
|
|
3794
|
+
function findFileWithExts(dir, name, matcher) {
|
|
3795
|
+
for (const ext of matcher.dottedExtensions) {
|
|
3454
3796
|
const filePath = path.join(dir, name + ext);
|
|
3455
3797
|
if (fs.existsSync(filePath))
|
|
3456
3798
|
return filePath;
|
|
@@ -3495,4 +3837,5 @@ export { staticExportPages, staticExportApp } from "./build/static-export.js";
|
|
|
3495
3837
|
export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks };
|
|
3496
3838
|
export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins };
|
|
3497
3839
|
export { parseStaticObjectLiteral as _parseStaticObjectLiteral };
|
|
3840
|
+
export { stripServerExports as _stripServerExports };
|
|
3498
3841
|
//# sourceMappingURL=index.js.map
|