vinext 0.0.8 → 0.0.10

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