vinext 0.0.9 → 0.0.11

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 (97) hide show
  1. package/dist/cli.js +4 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/entry.js +1 -15
  4. package/dist/client/entry.js.map +1 -1
  5. package/dist/client/validate-module-path.d.ts +15 -0
  6. package/dist/client/validate-module-path.d.ts.map +1 -0
  7. package/dist/client/validate-module-path.js +31 -0
  8. package/dist/client/validate-module-path.js.map +1 -0
  9. package/dist/config/config-matchers.d.ts +20 -0
  10. package/dist/config/config-matchers.d.ts.map +1 -1
  11. package/dist/config/config-matchers.js +185 -36
  12. package/dist/config/config-matchers.js.map +1 -1
  13. package/dist/config/next-config.d.ts +4 -0
  14. package/dist/config/next-config.d.ts.map +1 -1
  15. package/dist/config/next-config.js.map +1 -1
  16. package/dist/deploy.d.ts.map +1 -1
  17. package/dist/deploy.js +20 -12
  18. package/dist/deploy.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +173 -155
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/api-handler.d.ts.map +1 -1
  23. package/dist/server/api-handler.js +2 -1
  24. package/dist/server/api-handler.js.map +1 -1
  25. package/dist/server/app-dev-server.d.ts +2 -0
  26. package/dist/server/app-dev-server.d.ts.map +1 -1
  27. package/dist/server/app-dev-server.js +305 -159
  28. package/dist/server/app-dev-server.js.map +1 -1
  29. package/dist/server/app-router-entry.d.ts.map +1 -1
  30. package/dist/server/app-router-entry.js +16 -3
  31. package/dist/server/app-router-entry.js.map +1 -1
  32. package/dist/server/dev-origin-check.d.ts +61 -0
  33. package/dist/server/dev-origin-check.d.ts.map +1 -0
  34. package/dist/server/dev-origin-check.js +164 -0
  35. package/dist/server/dev-origin-check.js.map +1 -0
  36. package/dist/server/dev-server.d.ts +0 -2
  37. package/dist/server/dev-server.d.ts.map +1 -1
  38. package/dist/server/dev-server.js +390 -372
  39. package/dist/server/dev-server.js.map +1 -1
  40. package/dist/server/image-optimization.d.ts +32 -2
  41. package/dist/server/image-optimization.d.ts.map +1 -1
  42. package/dist/server/image-optimization.js +110 -9
  43. package/dist/server/image-optimization.js.map +1 -1
  44. package/dist/server/middleware-codegen.d.ts +41 -0
  45. package/dist/server/middleware-codegen.d.ts.map +1 -0
  46. package/dist/server/middleware-codegen.js +187 -0
  47. package/dist/server/middleware-codegen.js.map +1 -0
  48. package/dist/server/middleware.d.ts.map +1 -1
  49. package/dist/server/middleware.js +37 -19
  50. package/dist/server/middleware.js.map +1 -1
  51. package/dist/server/normalize-path.d.ts +22 -0
  52. package/dist/server/normalize-path.d.ts.map +1 -0
  53. package/dist/server/normalize-path.js +50 -0
  54. package/dist/server/normalize-path.js.map +1 -0
  55. package/dist/server/prod-server.d.ts.map +1 -1
  56. package/dist/server/prod-server.js +95 -26
  57. package/dist/server/prod-server.js.map +1 -1
  58. package/dist/shims/cache-runtime.d.ts +7 -0
  59. package/dist/shims/cache-runtime.d.ts.map +1 -1
  60. package/dist/shims/cache-runtime.js +19 -15
  61. package/dist/shims/cache-runtime.js.map +1 -1
  62. package/dist/shims/cache.d.ts +8 -0
  63. package/dist/shims/cache.d.ts.map +1 -1
  64. package/dist/shims/cache.js +20 -15
  65. package/dist/shims/cache.js.map +1 -1
  66. package/dist/shims/fetch-cache.d.ts +2 -3
  67. package/dist/shims/fetch-cache.d.ts.map +1 -1
  68. package/dist/shims/fetch-cache.js +80 -9
  69. package/dist/shims/fetch-cache.js.map +1 -1
  70. package/dist/shims/head-state.d.ts +6 -1
  71. package/dist/shims/head-state.d.ts.map +1 -1
  72. package/dist/shims/head-state.js +18 -15
  73. package/dist/shims/head-state.js.map +1 -1
  74. package/dist/shims/head.d.ts.map +1 -1
  75. package/dist/shims/head.js +4 -1
  76. package/dist/shims/head.js.map +1 -1
  77. package/dist/shims/headers.d.ts +9 -13
  78. package/dist/shims/headers.d.ts.map +1 -1
  79. package/dist/shims/headers.js +30 -49
  80. package/dist/shims/headers.js.map +1 -1
  81. package/dist/shims/image.d.ts.map +1 -1
  82. package/dist/shims/image.js +11 -2
  83. package/dist/shims/image.js.map +1 -1
  84. package/dist/shims/navigation-state.d.ts +6 -1
  85. package/dist/shims/navigation-state.d.ts.map +1 -1
  86. package/dist/shims/navigation-state.js +20 -29
  87. package/dist/shims/navigation-state.js.map +1 -1
  88. package/dist/shims/navigation.js +2 -2
  89. package/dist/shims/navigation.js.map +1 -1
  90. package/dist/shims/router-state.d.ts +6 -1
  91. package/dist/shims/router-state.d.ts.map +1 -1
  92. package/dist/shims/router-state.js +16 -21
  93. package/dist/shims/router-state.js.map +1 -1
  94. package/dist/shims/router.d.ts.map +1 -1
  95. package/dist/shims/router.js +19 -6
  96. package/dist/shims/router.js.map +1 -1
  97. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
1
  import { matchRoute, patternToNextFormat } from "../routing/pages-router.js";
2
2
  import { isrGet, isrSet, isrCacheKey, buildPagesCacheValue, triggerBackgroundRegeneration, setRevalidateDuration, getRevalidateDuration, } from "./isr-cache.js";
3
- import { withFetchCache } from "../shims/fetch-cache.js";
4
- import { _initRequestScopedCacheState } from "../shims/cache.js";
5
- import { clearPrivateCache } from "../shims/cache-runtime.js";
3
+ import { runWithFetchCache } from "../shims/fetch-cache.js";
4
+ import { _runWithCacheState } from "../shims/cache.js";
5
+ import { runWithPrivateCache } from "../shims/cache-runtime.js";
6
6
  // Import server-only state modules to register ALS-backed accessors.
7
7
  // These modules must be imported before any rendering occurs.
8
- import "../shims/router-state.js";
9
- import "../shims/head-state.js";
8
+ import { runWithRouterState } from "../shims/router-state.js";
9
+ import { runWithHeadState } from "../shims/head-state.js";
10
10
  import { reportRequestError } from "./instrumentation.js";
11
11
  import { safeJsonStringify } from "./html.js";
12
12
  import { parseQueryString as parseQuery } from "../utils/query.js";
@@ -237,302 +237,312 @@ export function createSSRHandler(server, routes, pagesDir, i18nConfig) {
237
237
  return;
238
238
  }
239
239
  const { route, params } = match;
240
- // Initialize per-request state for cache isolation
241
- _initRequestScopedCacheState();
242
- clearPrivateCache();
243
- // Install patched fetch with Next.js caching semantics for this request
244
- const cleanupFetchCache = withFetchCache();
245
- try {
246
- // Set SSR context for the router shim so useRouter() returns
247
- // the correct URL and params during server-side rendering.
248
- const routerShim = await server.ssrLoadModule("next/router");
249
- if (typeof routerShim.setSSRContext === "function") {
250
- routerShim.setSSRContext({
251
- pathname: localeStrippedUrl.split("?")[0],
252
- query: { ...params, ...parseQuery(url) },
253
- asPath: url,
254
- locale: locale ?? i18nConfig?.defaultLocale,
255
- locales: i18nConfig?.locales,
256
- defaultLocale: i18nConfig?.defaultLocale,
257
- });
258
- }
259
- // Set globalThis locale info for Link component locale prop support during SSR
260
- if (i18nConfig) {
261
- globalThis.__VINEXT_LOCALE__ = locale ?? i18nConfig.defaultLocale;
262
- globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
263
- globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
264
- }
265
- // Load the page module through Vite's SSR pipeline
266
- // This gives us HMR and transform support for free
267
- const pageModule = await server.ssrLoadModule(route.filePath);
268
- // Get the page component (default export)
269
- const PageComponent = pageModule.default;
270
- if (!PageComponent) {
271
- res.statusCode = 500;
272
- res.end(`Page ${route.filePath} has no default export`);
273
- return;
274
- }
275
- // Collect page props via data fetching methods
276
- let pageProps = {};
277
- let isrRevalidateSeconds = null;
278
- // Handle getStaticPaths for dynamic routes: validate the path
279
- // and respect fallback: false (return 404 for unlisted paths).
280
- if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
281
- const pathsResult = await pageModule.getStaticPaths({
282
- locales: i18nConfig?.locales ?? [],
283
- defaultLocale: i18nConfig?.defaultLocale ?? "",
284
- });
285
- const fallback = pathsResult?.fallback ?? false;
286
- if (fallback === false) {
287
- // Only allow paths explicitly listed in getStaticPaths
288
- const paths = pathsResult?.paths ?? [];
289
- const isValidPath = paths.some((p) => {
290
- return Object.entries(p.params).every(([key, val]) => {
291
- const actual = params[key];
292
- if (Array.isArray(val)) {
293
- return Array.isArray(actual) && val.join("/") === actual.join("/");
294
- }
295
- return String(val) === String(actual);
296
- });
240
+ // Wrap the entire request in nested AsyncLocalStorage.run() scopes to
241
+ // ensure per-request isolation for all state modules.
242
+ return runWithRouterState(() => runWithHeadState(() => _runWithCacheState(() => runWithPrivateCache(() => runWithFetchCache(async () => {
243
+ try {
244
+ // Set SSR context for the router shim so useRouter() returns
245
+ // the correct URL and params during server-side rendering.
246
+ const routerShim = await server.ssrLoadModule("next/router");
247
+ if (typeof routerShim.setSSRContext === "function") {
248
+ routerShim.setSSRContext({
249
+ pathname: localeStrippedUrl.split("?")[0],
250
+ query: { ...params, ...parseQuery(url) },
251
+ asPath: url,
252
+ locale: locale ?? i18nConfig?.defaultLocale,
253
+ locales: i18nConfig?.locales,
254
+ defaultLocale: i18nConfig?.defaultLocale,
297
255
  });
298
- if (!isValidPath) {
299
- await renderErrorPage(server, req, res, url, pagesDir, 404);
300
- return;
301
- }
302
256
  }
303
- // fallback: true or "blocking" always SSR on-demand.
304
- // In dev mode, Next.js does the same (no fallback shell).
305
- // In production, both modes SSR on-demand with caching.
306
- // The difference is that fallback:true could serve a shell first,
307
- // but since we always have data available via SSR, we render fully.
308
- }
309
- if (typeof pageModule.getServerSideProps === "function") {
310
- const context = {
311
- params,
312
- req,
313
- res,
314
- query: parseQuery(url),
315
- resolvedUrl: localeStrippedUrl,
316
- locale: locale ?? i18nConfig?.defaultLocale,
317
- locales: i18nConfig?.locales,
318
- defaultLocale: i18nConfig?.defaultLocale,
319
- };
320
- const result = await pageModule.getServerSideProps(context);
321
- if (result && "props" in result) {
322
- pageProps = result.props;
323
- }
324
- if (result && "redirect" in result) {
325
- const { redirect } = result;
326
- const status = redirect.statusCode ?? (redirect.permanent ? 308 : 307);
327
- res.writeHead(status, {
328
- Location: redirect.destination,
329
- });
330
- res.end();
331
- return;
257
+ // Set globalThis locale info for Link component locale prop support during SSR
258
+ if (i18nConfig) {
259
+ globalThis.__VINEXT_LOCALE__ = locale ?? i18nConfig.defaultLocale;
260
+ globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
261
+ globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
332
262
  }
333
- if (result && "notFound" in result && result.notFound) {
334
- await renderErrorPage(server, req, res, url, pagesDir, 404);
263
+ // Load the page module through Vite's SSR pipeline
264
+ // This gives us HMR and transform support for free
265
+ const pageModule = await server.ssrLoadModule(route.filePath);
266
+ // Get the page component (default export)
267
+ const PageComponent = pageModule.default;
268
+ if (!PageComponent) {
269
+ console.error(`[vinext] Page ${route.filePath} has no default export`);
270
+ res.statusCode = 500;
271
+ res.end("Page has no default export");
335
272
  return;
336
273
  }
337
- }
338
- // Collect font preloads early so ISR cached responses can include
339
- // the Link header (font preloads are module-level state that persists
340
- // across requests after the font modules are first loaded).
341
- let earlyFontLinkHeader = "";
342
- try {
343
- const earlyPreloads = [];
344
- const fontGoogleEarly = await server.ssrLoadModule("next/font/google");
345
- if (typeof fontGoogleEarly.getSSRFontPreloads === "function") {
346
- earlyPreloads.push(...fontGoogleEarly.getSSRFontPreloads());
274
+ // Collect page props via data fetching methods
275
+ let pageProps = {};
276
+ let isrRevalidateSeconds = null;
277
+ // Handle getStaticPaths for dynamic routes: validate the path
278
+ // and respect fallback: false (return 404 for unlisted paths).
279
+ if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
280
+ const pathsResult = await pageModule.getStaticPaths({
281
+ locales: i18nConfig?.locales ?? [],
282
+ defaultLocale: i18nConfig?.defaultLocale ?? "",
283
+ });
284
+ const fallback = pathsResult?.fallback ?? false;
285
+ if (fallback === false) {
286
+ // Only allow paths explicitly listed in getStaticPaths
287
+ const paths = pathsResult?.paths ?? [];
288
+ const isValidPath = paths.some((p) => {
289
+ return Object.entries(p.params).every(([key, val]) => {
290
+ const actual = params[key];
291
+ if (Array.isArray(val)) {
292
+ return Array.isArray(actual) && val.join("/") === actual.join("/");
293
+ }
294
+ return String(val) === String(actual);
295
+ });
296
+ });
297
+ if (!isValidPath) {
298
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
299
+ return;
300
+ }
301
+ }
302
+ // fallback: true or "blocking" — always SSR on-demand.
303
+ // In dev mode, Next.js does the same (no fallback shell).
304
+ // In production, both modes SSR on-demand with caching.
305
+ // The difference is that fallback:true could serve a shell first,
306
+ // but since we always have data available via SSR, we render fully.
347
307
  }
348
- const fontLocalEarly = await server.ssrLoadModule("next/font/local");
349
- if (typeof fontLocalEarly.getSSRFontPreloads === "function") {
350
- earlyPreloads.push(...fontLocalEarly.getSSRFontPreloads());
308
+ if (typeof pageModule.getServerSideProps === "function") {
309
+ const context = {
310
+ params,
311
+ req,
312
+ res,
313
+ query: parseQuery(url),
314
+ resolvedUrl: localeStrippedUrl,
315
+ locale: locale ?? i18nConfig?.defaultLocale,
316
+ locales: i18nConfig?.locales,
317
+ defaultLocale: i18nConfig?.defaultLocale,
318
+ };
319
+ const result = await pageModule.getServerSideProps(context);
320
+ if (result && "props" in result) {
321
+ pageProps = result.props;
322
+ }
323
+ if (result && "redirect" in result) {
324
+ const { redirect } = result;
325
+ const status = redirect.statusCode ?? (redirect.permanent ? 308 : 307);
326
+ // Sanitize destination to prevent open redirect via protocol-relative URLs.
327
+ // Also normalize backslashes — browsers treat \ as / in URL contexts.
328
+ let dest = redirect.destination;
329
+ if (!dest.startsWith("http://") && !dest.startsWith("https://")) {
330
+ dest = dest.replace(/^[\\/]+/, "/");
331
+ }
332
+ res.writeHead(status, {
333
+ Location: dest,
334
+ });
335
+ res.end();
336
+ return;
337
+ }
338
+ if (result && "notFound" in result && result.notFound) {
339
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
340
+ return;
341
+ }
351
342
  }
352
- if (earlyPreloads.length > 0) {
353
- earlyFontLinkHeader = earlyPreloads
354
- .map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`)
355
- .join(", ");
343
+ // Collect font preloads early so ISR cached responses can include
344
+ // the Link header (font preloads are module-level state that persists
345
+ // across requests after the font modules are first loaded).
346
+ let earlyFontLinkHeader = "";
347
+ try {
348
+ const earlyPreloads = [];
349
+ const fontGoogleEarly = await server.ssrLoadModule("next/font/google");
350
+ if (typeof fontGoogleEarly.getSSRFontPreloads === "function") {
351
+ earlyPreloads.push(...fontGoogleEarly.getSSRFontPreloads());
352
+ }
353
+ const fontLocalEarly = await server.ssrLoadModule("next/font/local");
354
+ if (typeof fontLocalEarly.getSSRFontPreloads === "function") {
355
+ earlyPreloads.push(...fontLocalEarly.getSSRFontPreloads());
356
+ }
357
+ if (earlyPreloads.length > 0) {
358
+ earlyFontLinkHeader = earlyPreloads
359
+ .map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`)
360
+ .join(", ");
361
+ }
356
362
  }
357
- }
358
- catch {
359
- // Font modules not loaded yet — skip
360
- }
361
- if (typeof pageModule.getStaticProps === "function") {
362
- // Check ISR cache before calling getStaticProps
363
- const cacheKey = isrCacheKey("pages", url.split("?")[0]);
364
- const cached = await isrGet(cacheKey);
365
- if (cached && !cached.isStale && cached.value.value?.kind === "PAGES") {
366
- // Fresh cache hit — serve directly
367
- const cachedPage = cached.value.value;
368
- const cachedHtml = cachedPage.html;
369
- const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
370
- const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
371
- const hitHeaders = {
372
- "Content-Type": "text/html",
373
- "X-Vinext-Cache": "HIT",
374
- "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`,
375
- };
376
- if (earlyFontLinkHeader)
377
- hitHeaders["Link"] = earlyFontLinkHeader;
378
- res.writeHead(200, hitHeaders);
379
- res.end(transformedHtml);
380
- return;
363
+ catch {
364
+ // Font modules not loaded yet — skip
381
365
  }
382
- if (cached && cached.isStale && cached.value.value?.kind === "PAGES") {
383
- // Stale hit serve stale immediately, trigger background regen
384
- const cachedPage = cached.value.value;
385
- const cachedHtml = cachedPage.html;
386
- const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
387
- // Trigger background regeneration: re-run getStaticProps and
388
- // update the cache so the next request is a HIT with fresh data.
389
- triggerBackgroundRegeneration(cacheKey, async () => {
390
- const freshResult = await pageModule.getStaticProps({ params });
391
- if (freshResult && "props" in freshResult) {
392
- const revalidate = typeof freshResult.revalidate === "number" ? freshResult.revalidate : 0;
393
- if (revalidate > 0) {
394
- await isrSet(cacheKey, buildPagesCacheValue(cachedHtml, freshResult.props), revalidate);
366
+ if (typeof pageModule.getStaticProps === "function") {
367
+ // Check ISR cache before calling getStaticProps
368
+ const cacheKey = isrCacheKey("pages", url.split("?")[0]);
369
+ const cached = await isrGet(cacheKey);
370
+ if (cached && !cached.isStale && cached.value.value?.kind === "PAGES") {
371
+ // Fresh cache hit serve directly
372
+ const cachedPage = cached.value.value;
373
+ const cachedHtml = cachedPage.html;
374
+ const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
375
+ const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
376
+ const hitHeaders = {
377
+ "Content-Type": "text/html",
378
+ "X-Vinext-Cache": "HIT",
379
+ "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`,
380
+ };
381
+ if (earlyFontLinkHeader)
382
+ hitHeaders["Link"] = earlyFontLinkHeader;
383
+ res.writeHead(200, hitHeaders);
384
+ res.end(transformedHtml);
385
+ return;
386
+ }
387
+ if (cached && cached.isStale && cached.value.value?.kind === "PAGES") {
388
+ // Stale hit — serve stale immediately, trigger background regen
389
+ const cachedPage = cached.value.value;
390
+ const cachedHtml = cachedPage.html;
391
+ const transformedHtml = await server.transformIndexHtml(url, cachedHtml);
392
+ // Trigger background regeneration: re-run getStaticProps and
393
+ // update the cache so the next request is a HIT with fresh data.
394
+ triggerBackgroundRegeneration(cacheKey, async () => {
395
+ const freshResult = await pageModule.getStaticProps({ params });
396
+ if (freshResult && "props" in freshResult) {
397
+ const revalidate = typeof freshResult.revalidate === "number" ? freshResult.revalidate : 0;
398
+ if (revalidate > 0) {
399
+ await isrSet(cacheKey, buildPagesCacheValue(cachedHtml, freshResult.props), revalidate);
400
+ }
395
401
  }
396
- }
397
- });
398
- const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
399
- const staleHeaders = {
400
- "Content-Type": "text/html",
401
- "X-Vinext-Cache": "STALE",
402
- "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`,
402
+ });
403
+ const revalidateSecs = getRevalidateDuration(cacheKey) ?? 60;
404
+ const staleHeaders = {
405
+ "Content-Type": "text/html",
406
+ "X-Vinext-Cache": "STALE",
407
+ "Cache-Control": `s-maxage=${revalidateSecs}, stale-while-revalidate`,
408
+ };
409
+ if (earlyFontLinkHeader)
410
+ staleHeaders["Link"] = earlyFontLinkHeader;
411
+ res.writeHead(200, staleHeaders);
412
+ res.end(transformedHtml);
413
+ return;
414
+ }
415
+ // Cache miss — call getStaticProps normally
416
+ const context = {
417
+ params,
418
+ locale: locale ?? i18nConfig?.defaultLocale,
419
+ locales: i18nConfig?.locales,
420
+ defaultLocale: i18nConfig?.defaultLocale,
403
421
  };
404
- if (earlyFontLinkHeader)
405
- staleHeaders["Link"] = earlyFontLinkHeader;
406
- res.writeHead(200, staleHeaders);
407
- res.end(transformedHtml);
408
- return;
422
+ const result = await pageModule.getStaticProps(context);
423
+ if (result && "props" in result) {
424
+ pageProps = result.props;
425
+ }
426
+ if (result && "redirect" in result) {
427
+ const { redirect } = result;
428
+ const status = redirect.statusCode ?? (redirect.permanent ? 308 : 307);
429
+ // Sanitize destination to prevent open redirect via protocol-relative URLs
430
+ let dest = redirect.destination;
431
+ if (!dest.startsWith("http://") && !dest.startsWith("https://") && dest.startsWith("//")) {
432
+ dest = dest.replace(/^\/\/+/, "/");
433
+ }
434
+ res.writeHead(status, {
435
+ Location: dest,
436
+ });
437
+ res.end();
438
+ return;
439
+ }
440
+ if (result && "notFound" in result && result.notFound) {
441
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
442
+ return;
443
+ }
444
+ // Extract revalidate period for ISR caching after render
445
+ if (typeof result?.revalidate === "number" && result.revalidate > 0) {
446
+ isrRevalidateSeconds = result.revalidate;
447
+ }
409
448
  }
410
- // Cache miss call getStaticProps normally
411
- const context = {
412
- params,
413
- locale: locale ?? i18nConfig?.defaultLocale,
414
- locales: i18nConfig?.locales,
415
- defaultLocale: i18nConfig?.defaultLocale,
416
- };
417
- const result = await pageModule.getStaticProps(context);
418
- if (result && "props" in result) {
419
- pageProps = result.props;
420
- }
421
- if (result && "redirect" in result) {
422
- const { redirect } = result;
423
- const status = redirect.statusCode ?? (redirect.permanent ? 308 : 307);
424
- res.writeHead(status, {
425
- Location: redirect.destination,
449
+ // Try to load _app.tsx if it exists
450
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
451
+ let AppComponent = null;
452
+ const appPath = path.join(pagesDir, "_app");
453
+ if (findFileWithExtensions(appPath)) {
454
+ try {
455
+ const appModule = await server.ssrLoadModule(appPath);
456
+ AppComponent = appModule.default ?? null;
457
+ }
458
+ catch {
459
+ // _app exists but failed to load
460
+ }
461
+ }
462
+ // React and ReactDOMServer are imported at the top level as native Node
463
+ // modules. They must NOT go through Vite's SSR module runner because
464
+ // React is CJS and the ESModulesEvaluator doesn't define `module`.
465
+ const createElement = React.createElement;
466
+ let element;
467
+ if (AppComponent) {
468
+ element = createElement(AppComponent, {
469
+ Component: PageComponent,
470
+ pageProps,
426
471
  });
427
- res.end();
428
- return;
429
472
  }
430
- if (result && "notFound" in result && result.notFound) {
431
- await renderErrorPage(server, req, res, url, pagesDir, 404);
432
- return;
473
+ else {
474
+ element = createElement(PageComponent, pageProps);
433
475
  }
434
- // Extract revalidate period for ISR caching after render
435
- if (typeof result?.revalidate === "number" && result.revalidate > 0) {
436
- isrRevalidateSeconds = result.revalidate;
476
+ // Reset SSR head collector before rendering so <Head> tags are captured
477
+ const headShim = await server.ssrLoadModule("next/head");
478
+ if (typeof headShim.resetSSRHead === "function") {
479
+ headShim.resetSSRHead();
437
480
  }
438
- }
439
- // Try to load _app.tsx if it exists
440
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
441
- let AppComponent = null;
442
- const appPath = path.join(pagesDir, "_app");
443
- if (findFileWithExtensions(appPath)) {
481
+ // Flush any pending dynamic() preloads so components are ready
482
+ const dynamicShim = await server.ssrLoadModule("next/dynamic");
483
+ if (typeof dynamicShim.flushPreloads === "function") {
484
+ await dynamicShim.flushPreloads();
485
+ }
486
+ // Collect any <Head> tags that were rendered during data fetching
487
+ // (shell head tags — Suspense children's head tags arrive late,
488
+ // matching Next.js behavior)
489
+ // Collect SSR font links (Google Fonts <link> tags) and font class styles
490
+ let fontHeadHTML = "";
491
+ const allFontStyles = [];
492
+ const allFontPreloads = [];
444
493
  try {
445
- const appModule = await server.ssrLoadModule(appPath);
446
- AppComponent = appModule.default ?? null;
494
+ const fontGoogle = await server.ssrLoadModule("next/font/google");
495
+ if (typeof fontGoogle.getSSRFontLinks === "function") {
496
+ const fontUrls = fontGoogle.getSSRFontLinks();
497
+ for (const fontUrl of fontUrls) {
498
+ const safeFontUrl = fontUrl.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
499
+ fontHeadHTML += `<link rel="stylesheet" href="${safeFontUrl}" />\n `;
500
+ }
501
+ }
502
+ if (typeof fontGoogle.getSSRFontStyles === "function") {
503
+ allFontStyles.push(...fontGoogle.getSSRFontStyles());
504
+ }
505
+ // Collect preloads from self-hosted Google fonts
506
+ if (typeof fontGoogle.getSSRFontPreloads === "function") {
507
+ allFontPreloads.push(...fontGoogle.getSSRFontPreloads());
508
+ }
447
509
  }
448
510
  catch {
449
- // _app exists but failed to load
511
+ // next/font/google not used skip
450
512
  }
451
- }
452
- // React and ReactDOMServer are imported at the top level as native Node
453
- // modules. They must NOT go through Vite's SSR module runner because
454
- // React is CJS and the ESModulesEvaluator doesn't define `module`.
455
- const createElement = React.createElement;
456
- let element;
457
- if (AppComponent) {
458
- element = createElement(AppComponent, {
459
- Component: PageComponent,
460
- pageProps,
461
- });
462
- }
463
- else {
464
- element = createElement(PageComponent, pageProps);
465
- }
466
- // Reset SSR head collector before rendering so <Head> tags are captured
467
- const headShim = await server.ssrLoadModule("next/head");
468
- if (typeof headShim.resetSSRHead === "function") {
469
- headShim.resetSSRHead();
470
- }
471
- // Flush any pending dynamic() preloads so components are ready
472
- const dynamicShim = await server.ssrLoadModule("next/dynamic");
473
- if (typeof dynamicShim.flushPreloads === "function") {
474
- await dynamicShim.flushPreloads();
475
- }
476
- // Collect any <Head> tags that were rendered during data fetching
477
- // (shell head tags — Suspense children's head tags arrive late,
478
- // matching Next.js behavior)
479
- // Collect SSR font links (Google Fonts <link> tags) and font class styles
480
- let fontHeadHTML = "";
481
- const allFontStyles = [];
482
- const allFontPreloads = [];
483
- try {
484
- const fontGoogle = await server.ssrLoadModule("next/font/google");
485
- if (typeof fontGoogle.getSSRFontLinks === "function") {
486
- const fontUrls = fontGoogle.getSSRFontLinks();
487
- for (const fontUrl of fontUrls) {
488
- const safeFontUrl = fontUrl.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
489
- fontHeadHTML += `<link rel="stylesheet" href="${safeFontUrl}" />\n `;
513
+ try {
514
+ const fontLocal = await server.ssrLoadModule("next/font/local");
515
+ if (typeof fontLocal.getSSRFontStyles === "function") {
516
+ allFontStyles.push(...fontLocal.getSSRFontStyles());
517
+ }
518
+ // Collect preloads from local font files
519
+ if (typeof fontLocal.getSSRFontPreloads === "function") {
520
+ allFontPreloads.push(...fontLocal.getSSRFontPreloads());
490
521
  }
491
522
  }
492
- if (typeof fontGoogle.getSSRFontStyles === "function") {
493
- allFontStyles.push(...fontGoogle.getSSRFontStyles());
523
+ catch {
524
+ // next/font/local not used — skip
494
525
  }
495
- // Collect preloads from self-hosted Google fonts
496
- if (typeof fontGoogle.getSSRFontPreloads === "function") {
497
- allFontPreloads.push(...fontGoogle.getSSRFontPreloads());
526
+ // Emit <link rel="preload"> for all collected font files (Google + local)
527
+ for (const { href, type } of allFontPreloads) {
528
+ // Escape href/type to prevent HTML attribute injection (defense-in-depth;
529
+ // Vite-resolved asset paths should never contain special chars).
530
+ const safeHref = href.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
531
+ const safeType = type.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
532
+ fontHeadHTML += `<link rel="preload" href="${safeHref}" as="font" type="${safeType}" crossorigin />\n `;
498
533
  }
499
- }
500
- catch {
501
- // next/font/google not used — skip
502
- }
503
- try {
504
- const fontLocal = await server.ssrLoadModule("next/font/local");
505
- if (typeof fontLocal.getSSRFontStyles === "function") {
506
- allFontStyles.push(...fontLocal.getSSRFontStyles());
507
- }
508
- // Collect preloads from local font files
509
- if (typeof fontLocal.getSSRFontPreloads === "function") {
510
- allFontPreloads.push(...fontLocal.getSSRFontPreloads());
534
+ if (allFontStyles.length > 0) {
535
+ fontHeadHTML += `<style data-vinext-fonts>${allFontStyles.join("\n")}</style>\n `;
511
536
  }
512
- }
513
- catch {
514
- // next/font/local not used — skip
515
- }
516
- // Emit <link rel="preload"> for all collected font files (Google + local)
517
- for (const { href, type } of allFontPreloads) {
518
- // Escape href/type to prevent HTML attribute injection (defense-in-depth;
519
- // Vite-resolved asset paths should never contain special chars).
520
- const safeHref = href.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
521
- const safeType = type.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
522
- fontHeadHTML += `<link rel="preload" href="${safeHref}" as="font" type="${safeType}" crossorigin />\n `;
523
- }
524
- if (allFontStyles.length > 0) {
525
- fontHeadHTML += `<style data-vinext-fonts>${allFontStyles.join("\n")}</style>\n `;
526
- }
527
- // Convert absolute file paths to Vite-servable URLs (relative to root)
528
- const viteRoot = server.config.root;
529
- const pageModuleUrl = "/" + path.relative(viteRoot, route.filePath);
530
- const appModuleUrl = AppComponent
531
- ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app"))
532
- : null;
533
- // Hydration entry: inline script that imports the page and hydrates.
534
- // Stores the React root and page loader for client-side navigation.
535
- const hydrationScript = `
537
+ // Convert absolute file paths to Vite-servable URLs (relative to root)
538
+ const viteRoot = server.config.root;
539
+ const pageModuleUrl = "/" + path.relative(viteRoot, route.filePath);
540
+ const appModuleUrl = AppComponent
541
+ ? "/" + path.relative(viteRoot, path.join(pagesDir, "_app"))
542
+ : null;
543
+ // Hydration entry: inline script that imports the page and hydrates.
544
+ // Stores the React root and page loader for client-side navigation.
545
+ const hydrationScript = `
536
546
  <script type="module">
537
547
  import React from "react";
538
548
  import { hydrateRoot } from "react-dom/client";
@@ -545,13 +555,13 @@ async function hydrate() {
545
555
  const PageComponent = pageModule.default;
546
556
  let element;
547
557
  ${appModuleUrl
548
- ? `
558
+ ? `
549
559
  const appModule = await import("${appModuleUrl}");
550
560
  const AppComponent = appModule.default;
551
561
  window.__VINEXT_APP__ = AppComponent;
552
562
  element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
553
563
  `
554
- : `
564
+ : `
555
565
  element = React.createElement(PageComponent, pageProps);
556
566
  `}
557
567
  const root = hydrateRoot(document.getElementById("__next"), element);
@@ -559,105 +569,113 @@ async function hydrate() {
559
569
  }
560
570
  hydrate();
561
571
  </script>`;
562
- const nextDataScript = `<script>window.__NEXT_DATA__ = ${safeJsonStringify({
563
- props: { pageProps },
564
- page: patternToNextFormat(route.pattern),
565
- query: params,
566
- isFallback: false,
567
- locale: locale ?? i18nConfig?.defaultLocale,
568
- locales: i18nConfig?.locales,
569
- defaultLocale: i18nConfig?.defaultLocale,
570
- // Include module URLs so client navigation can import pages directly
571
- __vinext: {
572
- pageModuleUrl,
573
- appModuleUrl,
574
- },
575
- })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? i18nConfig.defaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(i18nConfig.defaultLocale)}` : ""}</script>`;
576
- // Try to load custom _document.tsx
577
- const docPath = path.join(pagesDir, "_document");
578
- let DocumentComponent = null;
579
- if (findFileWithExtensions(docPath)) {
580
- try {
581
- const docModule = await server.ssrLoadModule(docPath);
582
- DocumentComponent = docModule.default ?? null;
572
+ const nextDataScript = `<script>window.__NEXT_DATA__ = ${safeJsonStringify({
573
+ props: { pageProps },
574
+ page: patternToNextFormat(route.pattern),
575
+ query: params,
576
+ isFallback: false,
577
+ locale: locale ?? i18nConfig?.defaultLocale,
578
+ locales: i18nConfig?.locales,
579
+ defaultLocale: i18nConfig?.defaultLocale,
580
+ // Include module URLs so client navigation can import pages directly
581
+ __vinext: {
582
+ pageModuleUrl,
583
+ appModuleUrl,
584
+ },
585
+ })}${i18nConfig ? `;window.__VINEXT_LOCALE__=${safeJsonStringify(locale ?? i18nConfig.defaultLocale)};window.__VINEXT_LOCALES__=${safeJsonStringify(i18nConfig.locales)};window.__VINEXT_DEFAULT_LOCALE__=${safeJsonStringify(i18nConfig.defaultLocale)}` : ""}</script>`;
586
+ // Try to load custom _document.tsx
587
+ const docPath = path.join(pagesDir, "_document");
588
+ let DocumentComponent = null;
589
+ if (findFileWithExtensions(docPath)) {
590
+ try {
591
+ const docModule = await server.ssrLoadModule(docPath);
592
+ DocumentComponent = docModule.default ?? null;
593
+ }
594
+ catch {
595
+ // _document exists but failed to load
596
+ }
583
597
  }
584
- catch {
585
- // _document exists but failed to load
598
+ const allScripts = `${nextDataScript}\n ${hydrationScript}`;
599
+ // Build cache headers for ISR responses
600
+ const extraHeaders = {};
601
+ if (isrRevalidateSeconds) {
602
+ extraHeaders["Cache-Control"] = `s-maxage=${isrRevalidateSeconds}, stale-while-revalidate`;
603
+ extraHeaders["X-Vinext-Cache"] = "MISS";
604
+ }
605
+ // Set HTTP Link header for font preloading.
606
+ // This lets the browser (and CDN) start fetching font files before parsing HTML.
607
+ if (allFontPreloads.length > 0) {
608
+ extraHeaders["Link"] = allFontPreloads
609
+ .map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`)
610
+ .join(", ");
611
+ }
612
+ // Stream the page using progressive SSR.
613
+ // The shell (layouts, non-suspended content) arrives immediately.
614
+ // Suspense content streams in as it resolves.
615
+ await streamPageToResponse(res, element, {
616
+ url,
617
+ server,
618
+ fontHeadHTML,
619
+ scripts: allScripts,
620
+ DocumentComponent,
621
+ statusCode,
622
+ extraHeaders,
623
+ // Collect head HTML AFTER the shell renders (inside streamPageToResponse,
624
+ // after renderToReadableStream resolves). Head tags from Suspense
625
+ // children arrive late — this matches Next.js behavior.
626
+ getHeadHTML: () => typeof headShim.getSSRHeadHTML === "function"
627
+ ? headShim.getSSRHeadHTML()
628
+ : "",
629
+ });
630
+ // Clear SSR context after rendering
631
+ if (typeof routerShim.setSSRContext === "function") {
632
+ routerShim.setSSRContext(null);
633
+ }
634
+ // If ISR is enabled, we need the full HTML for caching.
635
+ // For ISR, re-render synchronously to get the complete HTML string.
636
+ // This runs after the stream is already sent, so it doesn't affect TTFB.
637
+ if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
638
+ const isrElement = AppComponent
639
+ ? createElement(AppComponent, { Component: pageModule.default, pageProps })
640
+ : createElement(pageModule.default, pageProps);
641
+ const isrBodyHtml = await renderToStringAsync(isrElement);
642
+ const isrHtml = `<!DOCTYPE html><html><head></head><body><div id="__next">${isrBodyHtml}</div>${allScripts}</body></html>`;
643
+ const cacheKey = isrCacheKey("pages", url.split("?")[0]);
644
+ await isrSet(cacheKey, buildPagesCacheValue(isrHtml, pageProps), isrRevalidateSeconds);
645
+ setRevalidateDuration(cacheKey, isrRevalidateSeconds);
586
646
  }
587
647
  }
588
- const allScripts = `${nextDataScript}\n ${hydrationScript}`;
589
- // Build cache headers for ISR responses
590
- const extraHeaders = {};
591
- if (isrRevalidateSeconds) {
592
- extraHeaders["Cache-Control"] = `s-maxage=${isrRevalidateSeconds}, stale-while-revalidate`;
593
- extraHeaders["X-Vinext-Cache"] = "MISS";
594
- }
595
- // Set HTTP Link header for font preloading.
596
- // This lets the browser (and CDN) start fetching font files before parsing HTML.
597
- if (allFontPreloads.length > 0) {
598
- extraHeaders["Link"] = allFontPreloads
599
- .map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`)
600
- .join(", ");
601
- }
602
- // Stream the page using progressive SSR.
603
- // The shell (layouts, non-suspended content) arrives immediately.
604
- // Suspense content streams in as it resolves.
605
- await streamPageToResponse(res, element, {
606
- url,
607
- server,
608
- fontHeadHTML,
609
- scripts: allScripts,
610
- DocumentComponent,
611
- statusCode,
612
- extraHeaders,
613
- // Collect head HTML AFTER the shell renders (inside streamPageToResponse,
614
- // after renderToReadableStream resolves). Head tags from Suspense
615
- // children arrive late — this matches Next.js behavior.
616
- getHeadHTML: () => typeof headShim.getSSRHeadHTML === "function"
617
- ? headShim.getSSRHeadHTML()
618
- : "",
619
- });
620
- // Clear SSR context after rendering
621
- if (typeof routerShim.setSSRContext === "function") {
622
- routerShim.setSSRContext(null);
623
- }
624
- // If ISR is enabled, we need the full HTML for caching.
625
- // For ISR, re-render synchronously to get the complete HTML string.
626
- // This runs after the stream is already sent, so it doesn't affect TTFB.
627
- if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
628
- const isrElement = AppComponent
629
- ? createElement(AppComponent, { Component: pageModule.default, pageProps })
630
- : createElement(pageModule.default, pageProps);
631
- const isrBodyHtml = await renderToStringAsync(isrElement);
632
- const isrHtml = `<!DOCTYPE html><html><head></head><body><div id="__next">${isrBodyHtml}</div>${allScripts}</body></html>`;
633
- const cacheKey = isrCacheKey("pages", url.split("?")[0]);
634
- await isrSet(cacheKey, buildPagesCacheValue(isrHtml, pageProps), isrRevalidateSeconds);
635
- setRevalidateDuration(cacheKey, isrRevalidateSeconds);
636
- }
637
- }
638
- catch (e) {
639
- // Let Vite fix the stack trace for better dev experience
640
- server.ssrFixStacktrace(e);
641
- console.error(e);
642
- // Report error via instrumentation hook if registered
643
- reportRequestError(e instanceof Error ? e : new Error(String(e)), {
644
- path: url,
645
- method: req.method ?? "GET",
646
- headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v ?? "")])),
647
- }, { routerKind: "Pages Router", routePath: route.pattern, routeType: "render" }).catch(() => { });
648
- // Try to render custom 500 error page
649
- try {
650
- await renderErrorPage(server, req, res, url, pagesDir, 500);
648
+ catch (e) {
649
+ // Let Vite fix the stack trace for better dev experience
650
+ server.ssrFixStacktrace(e);
651
+ console.error(e);
652
+ // Report error via instrumentation hook if registered
653
+ reportRequestError(e instanceof Error ? e : new Error(String(e)), {
654
+ path: url,
655
+ method: req.method ?? "GET",
656
+ headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v ?? "")])),
657
+ }, { routerKind: "Pages Router", routePath: route.pattern, routeType: "render" }).catch(() => { });
658
+ // Try to render custom 500 error page
659
+ try {
660
+ await renderErrorPage(server, req, res, url, pagesDir, 500);
661
+ }
662
+ catch (fallbackErr) {
663
+ // If error page itself fails, fall back to plain text.
664
+ // This is a dev-only code path (prod uses prod-server.ts), so
665
+ // include the error message for debugging.
666
+ res.statusCode = 500;
667
+ res.end(`Internal Server Error: ${fallbackErr.message}`);
668
+ }
651
669
  }
652
- catch {
653
- // If error page itself fails, fall back to plain text
654
- res.statusCode = 500;
655
- res.end(`Internal Server Error: ${e.message}`);
670
+ finally {
671
+ // Cleanup is handled by ALS scope unwinding
672
+ // each runWith*() scope is automatically cleaned up when it exits.
656
673
  }
657
- }
658
- finally {
659
- cleanupFetchCache();
660
- }
674
+ }) // end runWithFetchCache
675
+ ) // end runWithPrivateCache
676
+ ) // end _runWithCacheState
677
+ ) // end runWithHeadState
678
+ ); // end runWithRouterState
661
679
  };
662
680
  }
663
681
  /**