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.
Files changed (54) hide show
  1. package/dist/deploy.d.ts.map +1 -1
  2. package/dist/deploy.js +6 -3
  3. package/dist/deploy.js.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +233 -22
  6. package/dist/index.js.map +1 -1
  7. package/dist/routing/app-router.d.ts.map +1 -1
  8. package/dist/routing/app-router.js +1 -41
  9. package/dist/routing/app-router.js.map +1 -1
  10. package/dist/routing/pages-router.d.ts.map +1 -1
  11. package/dist/routing/pages-router.js +1 -27
  12. package/dist/routing/pages-router.js.map +1 -1
  13. package/dist/routing/utils.d.ts +25 -0
  14. package/dist/routing/utils.d.ts.map +1 -0
  15. package/dist/routing/utils.js +70 -0
  16. package/dist/routing/utils.js.map +1 -0
  17. package/dist/server/app-dev-server.d.ts.map +1 -1
  18. package/dist/server/app-dev-server.js +140 -6
  19. package/dist/server/app-dev-server.js.map +1 -1
  20. package/dist/server/dev-server.d.ts.map +1 -1
  21. package/dist/server/dev-server.js +77 -4
  22. package/dist/server/dev-server.js.map +1 -1
  23. package/dist/server/middleware.d.ts.map +1 -1
  24. package/dist/server/middleware.js +13 -4
  25. package/dist/server/middleware.js.map +1 -1
  26. package/dist/server/prod-server.d.ts.map +1 -1
  27. package/dist/server/prod-server.js +7 -0
  28. package/dist/server/prod-server.js.map +1 -1
  29. package/dist/server/request-log.d.ts +34 -0
  30. package/dist/server/request-log.d.ts.map +1 -0
  31. package/dist/server/request-log.js +65 -0
  32. package/dist/server/request-log.js.map +1 -0
  33. package/dist/shims/cache-runtime.d.ts.map +1 -1
  34. package/dist/shims/cache-runtime.js +5 -1
  35. package/dist/shims/cache-runtime.js.map +1 -1
  36. package/dist/shims/cache.d.ts +6 -0
  37. package/dist/shims/cache.d.ts.map +1 -1
  38. package/dist/shims/cache.js +22 -2
  39. package/dist/shims/cache.js.map +1 -1
  40. package/dist/shims/head.d.ts +11 -0
  41. package/dist/shims/head.d.ts.map +1 -1
  42. package/dist/shims/head.js +21 -0
  43. package/dist/shims/head.js.map +1 -1
  44. package/dist/shims/headers.d.ts +8 -0
  45. package/dist/shims/headers.d.ts.map +1 -1
  46. package/dist/shims/headers.js +41 -0
  47. package/dist/shims/headers.js.map +1 -1
  48. package/dist/shims/script.d.ts.map +1 -1
  49. package/dist/shims/script.js +7 -1
  50. package/dist/shims/script.js.map +1 -1
  51. package/dist/shims/server.d.ts.map +1 -1
  52. package/dist/shims/server.js +2 -1
  53. package/dist/shims/server.js.map +1 -1
  54. 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
- const responseHeaders = { "Content-Type": "text/html" };
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["Cache-Control"] = "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate";
1341
- responseHeaders["X-Vinext-Cache"] = "MISS";
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["Link"] = _fontLinkHeader;
1377
+ responseHeaders.set("Link", _fontLinkHeader);
1346
1378
  }
1347
- return new Response(compositeStream, { status: 200, headers: responseHeaders });
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 mdxPlugin = mdxRollup.default ?? mdxRollup;
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
- mdxPlugins.push(mdxPlugin(mdxOpts));
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
- // Block requests from non-localhost origins to prevent
2237
- // cross-origin data exfiltration from the dev server.
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
- if (!imgUrl || !imgUrl.startsWith("/") || imgUrl.startsWith("//")) {
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
- res.writeHead(result.redirectStatus ?? 307, {
2370
- Location: result.redirectUrl,
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 — fall through to 404
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 render as-is (will hit 404 handler)
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) {