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.
Files changed (70) hide show
  1. package/dist/build/static-export.d.ts.map +1 -1
  2. package/dist/build/static-export.js +9 -7
  3. package/dist/build/static-export.js.map +1 -1
  4. package/dist/config/next-config.d.ts +4 -1
  5. package/dist/config/next-config.d.ts.map +1 -1
  6. package/dist/config/next-config.js +10 -5
  7. package/dist/config/next-config.js.map +1 -1
  8. package/dist/deploy.d.ts.map +1 -1
  9. package/dist/deploy.js +17 -4
  10. package/dist/deploy.js.map +1 -1
  11. package/dist/index.d.ts +25 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +381 -38
  14. package/dist/index.js.map +1 -1
  15. package/dist/routing/app-router.d.ts +2 -1
  16. package/dist/routing/app-router.d.ts.map +1 -1
  17. package/dist/routing/app-router.js +74 -107
  18. package/dist/routing/app-router.js.map +1 -1
  19. package/dist/routing/file-matcher.d.ts +24 -0
  20. package/dist/routing/file-matcher.d.ts.map +1 -0
  21. package/dist/routing/file-matcher.js +75 -0
  22. package/dist/routing/file-matcher.js.map +1 -0
  23. package/dist/routing/pages-router.d.ts +3 -2
  24. package/dist/routing/pages-router.d.ts.map +1 -1
  25. package/dist/routing/pages-router.js +25 -44
  26. package/dist/routing/pages-router.js.map +1 -1
  27. package/dist/routing/utils.d.ts +25 -0
  28. package/dist/routing/utils.d.ts.map +1 -0
  29. package/dist/routing/utils.js +70 -0
  30. package/dist/routing/utils.js.map +1 -0
  31. package/dist/server/app-dev-server.d.ts.map +1 -1
  32. package/dist/server/app-dev-server.js +123 -47
  33. package/dist/server/app-dev-server.js.map +1 -1
  34. package/dist/server/dev-server.d.ts +2 -1
  35. package/dist/server/dev-server.d.ts.map +1 -1
  36. package/dist/server/dev-server.js +93 -18
  37. package/dist/server/dev-server.js.map +1 -1
  38. package/dist/server/prod-server.d.ts.map +1 -1
  39. package/dist/server/prod-server.js +33 -2
  40. package/dist/server/prod-server.js.map +1 -1
  41. package/dist/server/request-log.d.ts +34 -0
  42. package/dist/server/request-log.d.ts.map +1 -0
  43. package/dist/server/request-log.js +65 -0
  44. package/dist/server/request-log.js.map +1 -0
  45. package/dist/shims/cache-runtime.d.ts.map +1 -1
  46. package/dist/shims/cache-runtime.js +5 -1
  47. package/dist/shims/cache-runtime.js.map +1 -1
  48. package/dist/shims/cache.d.ts +7 -1
  49. package/dist/shims/cache.d.ts.map +1 -1
  50. package/dist/shims/cache.js +30 -5
  51. package/dist/shims/cache.js.map +1 -1
  52. package/dist/shims/head.d.ts +11 -0
  53. package/dist/shims/head.d.ts.map +1 -1
  54. package/dist/shims/head.js +21 -0
  55. package/dist/shims/head.js.map +1 -1
  56. package/dist/shims/headers.d.ts +8 -0
  57. package/dist/shims/headers.d.ts.map +1 -1
  58. package/dist/shims/headers.js +41 -0
  59. package/dist/shims/headers.js.map +1 -1
  60. package/dist/shims/metadata.d.ts +1 -0
  61. package/dist/shims/metadata.d.ts.map +1 -1
  62. package/dist/shims/metadata.js +5 -1
  63. package/dist/shims/metadata.js.map +1 -1
  64. package/dist/shims/script.d.ts.map +1 -1
  65. package/dist/shims/script.js +7 -1
  66. package/dist/shims/script.js.map +1 -1
  67. package/dist/shims/server.d.ts.map +1 -1
  68. package/dist/shims/server.js +2 -1
  69. package/dist/shims/server.js.map +1 -1
  70. 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 hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
530
- const hasDoc = fs.existsSync(path.join(pagesDir, "_document.tsx")) || fs.existsSync(path.join(pagesDir, "_document.jsx")) || fs.existsSync(path.join(pagesDir, "_document.ts")) || fs.existsSync(path.join(pagesDir, "_document.js"));
531
- // Use absolute paths for _app and _document too
532
- const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
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(appFileBase)};`
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(docFileBase)};`
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
- const responseHeaders = { "Content-Type": "text/html" };
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["Cache-Control"] = "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate";
1341
- responseHeaders["X-Vinext-Cache"] = "MISS";
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["Link"] = _fontLinkHeader;
1379
+ responseHeaders.set("Link", _fontLinkHeader);
1346
1380
  }
1347
- return new Response(compositeStream, { status: 200, headers: responseHeaders });
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 hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
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 = path.join(pagesDir, "_app").replace(/\\/g, "/");
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 rawConfig = await loadNextConfig(root);
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
- const ext = /\.(tsx?|jsx?|mdx)$/;
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 = /\.(tsx?|jsx?|mdx)$/;
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
- // Block requests from non-localhost origins to prevent
2260
- // cross-origin data exfiltration from the dev server.
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
- if (!imgUrl || !imgUrl.startsWith("/") || imgUrl.startsWith("//")) {
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
- res.appendHeader(key, value);
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
- res.setHeader(header.key, header.value);
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 .tsx, .ts, .jsx, .js extensions.
3792
+ * Checks the configured page extensions.
3450
3793
  */
3451
- function findFileWithExts(dir, name) {
3452
- const extensions = [".tsx", ".ts", ".jsx", ".js"];
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