vinext 0.0.0 → 0.0.1

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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/build/static-export.d.ts +78 -0
  4. package/dist/build/static-export.d.ts.map +1 -0
  5. package/dist/build/static-export.js +553 -0
  6. package/dist/build/static-export.js.map +1 -0
  7. package/dist/check.d.ts +52 -0
  8. package/dist/check.d.ts.map +1 -0
  9. package/dist/check.js +483 -0
  10. package/dist/check.js.map +1 -0
  11. package/dist/cli.d.ts +15 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +565 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/client/entry.d.ts +2 -0
  16. package/dist/client/entry.d.ts.map +1 -0
  17. package/dist/client/entry.js +85 -0
  18. package/dist/client/entry.js.map +1 -0
  19. package/dist/cloudflare/index.d.ts +8 -0
  20. package/dist/cloudflare/index.d.ts.map +1 -0
  21. package/dist/cloudflare/index.js +8 -0
  22. package/dist/cloudflare/index.js.map +1 -0
  23. package/dist/cloudflare/kv-cache-handler.d.ts +68 -0
  24. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -0
  25. package/dist/cloudflare/kv-cache-handler.js +304 -0
  26. package/dist/cloudflare/kv-cache-handler.js.map +1 -0
  27. package/dist/cloudflare/tpr.d.ts +78 -0
  28. package/dist/cloudflare/tpr.d.ts.map +1 -0
  29. package/dist/cloudflare/tpr.js +672 -0
  30. package/dist/cloudflare/tpr.js.map +1 -0
  31. package/dist/config/config-matchers.d.ts +106 -0
  32. package/dist/config/config-matchers.d.ts.map +1 -0
  33. package/dist/config/config-matchers.js +499 -0
  34. package/dist/config/config-matchers.js.map +1 -0
  35. package/dist/config/next-config.d.ts +153 -0
  36. package/dist/config/next-config.d.ts.map +1 -0
  37. package/dist/config/next-config.js +274 -0
  38. package/dist/config/next-config.js.map +1 -0
  39. package/dist/deploy.d.ts +87 -0
  40. package/dist/deploy.d.ts.map +1 -0
  41. package/dist/deploy.js +644 -0
  42. package/dist/deploy.js.map +1 -0
  43. package/dist/index.d.ts +156 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3287 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/init.d.ts +55 -0
  48. package/dist/init.d.ts.map +1 -0
  49. package/dist/init.js +201 -0
  50. package/dist/init.js.map +1 -0
  51. package/dist/routing/app-router.d.ts +96 -0
  52. package/dist/routing/app-router.d.ts.map +1 -0
  53. package/dist/routing/app-router.js +815 -0
  54. package/dist/routing/app-router.js.map +1 -0
  55. package/dist/routing/pages-router.d.ts +52 -0
  56. package/dist/routing/pages-router.d.ts.map +1 -0
  57. package/dist/routing/pages-router.js +239 -0
  58. package/dist/routing/pages-router.js.map +1 -0
  59. package/dist/server/api-handler.d.ts +18 -0
  60. package/dist/server/api-handler.d.ts.map +1 -0
  61. package/dist/server/api-handler.js +169 -0
  62. package/dist/server/api-handler.js.map +1 -0
  63. package/dist/server/app-dev-server.d.ts +42 -0
  64. package/dist/server/app-dev-server.d.ts.map +1 -0
  65. package/dist/server/app-dev-server.js +2718 -0
  66. package/dist/server/app-dev-server.js.map +1 -0
  67. package/dist/server/app-router-entry.d.ts +18 -0
  68. package/dist/server/app-router-entry.d.ts.map +1 -0
  69. package/dist/server/app-router-entry.js +34 -0
  70. package/dist/server/app-router-entry.js.map +1 -0
  71. package/dist/server/dev-server.d.ts +40 -0
  72. package/dist/server/dev-server.d.ts.map +1 -0
  73. package/dist/server/dev-server.js +758 -0
  74. package/dist/server/dev-server.js.map +1 -0
  75. package/dist/server/html.d.ts +22 -0
  76. package/dist/server/html.d.ts.map +1 -0
  77. package/dist/server/html.js +29 -0
  78. package/dist/server/html.js.map +1 -0
  79. package/dist/server/image-optimization.d.ts +56 -0
  80. package/dist/server/image-optimization.d.ts.map +1 -0
  81. package/dist/server/image-optimization.js +103 -0
  82. package/dist/server/image-optimization.js.map +1 -0
  83. package/dist/server/instrumentation.d.ts +68 -0
  84. package/dist/server/instrumentation.d.ts.map +1 -0
  85. package/dist/server/instrumentation.js +90 -0
  86. package/dist/server/instrumentation.js.map +1 -0
  87. package/dist/server/isr-cache.d.ts +61 -0
  88. package/dist/server/isr-cache.d.ts.map +1 -0
  89. package/dist/server/isr-cache.js +134 -0
  90. package/dist/server/isr-cache.js.map +1 -0
  91. package/dist/server/metadata-routes.d.ts +103 -0
  92. package/dist/server/metadata-routes.d.ts.map +1 -0
  93. package/dist/server/metadata-routes.js +270 -0
  94. package/dist/server/metadata-routes.js.map +1 -0
  95. package/dist/server/middleware.d.ts +77 -0
  96. package/dist/server/middleware.d.ts.map +1 -0
  97. package/dist/server/middleware.js +228 -0
  98. package/dist/server/middleware.js.map +1 -0
  99. package/dist/server/prod-server.d.ts +78 -0
  100. package/dist/server/prod-server.d.ts.map +1 -0
  101. package/dist/server/prod-server.js +712 -0
  102. package/dist/server/prod-server.js.map +1 -0
  103. package/dist/shims/amp.d.ts +17 -0
  104. package/dist/shims/amp.d.ts.map +1 -0
  105. package/dist/shims/amp.js +21 -0
  106. package/dist/shims/amp.js.map +1 -0
  107. package/dist/shims/app.d.ts +12 -0
  108. package/dist/shims/app.d.ts.map +1 -0
  109. package/dist/shims/app.js +2 -0
  110. package/dist/shims/app.js.map +1 -0
  111. package/dist/shims/cache-runtime.d.ts +68 -0
  112. package/dist/shims/cache-runtime.d.ts.map +1 -0
  113. package/dist/shims/cache-runtime.js +437 -0
  114. package/dist/shims/cache-runtime.js.map +1 -0
  115. package/dist/shims/cache.d.ts +243 -0
  116. package/dist/shims/cache.d.ts.map +1 -0
  117. package/dist/shims/cache.js +415 -0
  118. package/dist/shims/cache.js.map +1 -0
  119. package/dist/shims/client-only.d.ts +18 -0
  120. package/dist/shims/client-only.d.ts.map +1 -0
  121. package/dist/shims/client-only.js +18 -0
  122. package/dist/shims/client-only.js.map +1 -0
  123. package/dist/shims/config.d.ts +27 -0
  124. package/dist/shims/config.d.ts.map +1 -0
  125. package/dist/shims/config.js +30 -0
  126. package/dist/shims/config.js.map +1 -0
  127. package/dist/shims/constants.d.ts +13 -0
  128. package/dist/shims/constants.d.ts.map +1 -0
  129. package/dist/shims/constants.js +13 -0
  130. package/dist/shims/constants.js.map +1 -0
  131. package/dist/shims/document.d.ts +33 -0
  132. package/dist/shims/document.d.ts.map +1 -0
  133. package/dist/shims/document.js +32 -0
  134. package/dist/shims/document.js.map +1 -0
  135. package/dist/shims/dynamic.d.ts +33 -0
  136. package/dist/shims/dynamic.d.ts.map +1 -0
  137. package/dist/shims/dynamic.js +148 -0
  138. package/dist/shims/dynamic.js.map +1 -0
  139. package/dist/shims/error-boundary.d.ts +33 -0
  140. package/dist/shims/error-boundary.d.ts.map +1 -0
  141. package/dist/shims/error-boundary.js +88 -0
  142. package/dist/shims/error-boundary.js.map +1 -0
  143. package/dist/shims/error.d.ts +16 -0
  144. package/dist/shims/error.d.ts.map +1 -0
  145. package/dist/shims/error.js +45 -0
  146. package/dist/shims/error.js.map +1 -0
  147. package/dist/shims/fetch-cache.d.ts +61 -0
  148. package/dist/shims/fetch-cache.d.ts.map +1 -0
  149. package/dist/shims/fetch-cache.js +307 -0
  150. package/dist/shims/fetch-cache.js.map +1 -0
  151. package/dist/shims/font-google.d.ts +122 -0
  152. package/dist/shims/font-google.d.ts.map +1 -0
  153. package/dist/shims/font-google.js +387 -0
  154. package/dist/shims/font-google.js.map +1 -0
  155. package/dist/shims/font-local.d.ts +61 -0
  156. package/dist/shims/font-local.d.ts.map +1 -0
  157. package/dist/shims/font-local.js +303 -0
  158. package/dist/shims/font-local.js.map +1 -0
  159. package/dist/shims/form.d.ts +30 -0
  160. package/dist/shims/form.d.ts.map +1 -0
  161. package/dist/shims/form.js +78 -0
  162. package/dist/shims/form.js.map +1 -0
  163. package/dist/shims/head-state.d.ts +11 -0
  164. package/dist/shims/head-state.d.ts.map +1 -0
  165. package/dist/shims/head-state.js +47 -0
  166. package/dist/shims/head-state.js.map +1 -0
  167. package/dist/shims/head.d.ts +28 -0
  168. package/dist/shims/head.d.ts.map +1 -0
  169. package/dist/shims/head.js +148 -0
  170. package/dist/shims/head.js.map +1 -0
  171. package/dist/shims/headers.d.ts +150 -0
  172. package/dist/shims/headers.d.ts.map +1 -0
  173. package/dist/shims/headers.js +412 -0
  174. package/dist/shims/headers.js.map +1 -0
  175. package/dist/shims/image-config.d.ts +30 -0
  176. package/dist/shims/image-config.d.ts.map +1 -0
  177. package/dist/shims/image-config.js +91 -0
  178. package/dist/shims/image-config.js.map +1 -0
  179. package/dist/shims/image.d.ts +63 -0
  180. package/dist/shims/image.d.ts.map +1 -0
  181. package/dist/shims/image.js +284 -0
  182. package/dist/shims/image.js.map +1 -0
  183. package/dist/shims/internal/api-utils.d.ts +12 -0
  184. package/dist/shims/internal/api-utils.d.ts.map +1 -0
  185. package/dist/shims/internal/api-utils.js +7 -0
  186. package/dist/shims/internal/api-utils.js.map +1 -0
  187. package/dist/shims/internal/app-router-context.d.ts +21 -0
  188. package/dist/shims/internal/app-router-context.d.ts.map +1 -0
  189. package/dist/shims/internal/app-router-context.js +15 -0
  190. package/dist/shims/internal/app-router-context.js.map +1 -0
  191. package/dist/shims/internal/cookies.d.ts +9 -0
  192. package/dist/shims/internal/cookies.d.ts.map +1 -0
  193. package/dist/shims/internal/cookies.js +9 -0
  194. package/dist/shims/internal/cookies.js.map +1 -0
  195. package/dist/shims/internal/router-context.d.ts +2 -0
  196. package/dist/shims/internal/router-context.d.ts.map +1 -0
  197. package/dist/shims/internal/router-context.js +9 -0
  198. package/dist/shims/internal/router-context.js.map +1 -0
  199. package/dist/shims/internal/utils.d.ts +48 -0
  200. package/dist/shims/internal/utils.d.ts.map +1 -0
  201. package/dist/shims/internal/utils.js +35 -0
  202. package/dist/shims/internal/utils.js.map +1 -0
  203. package/dist/shims/internal/work-unit-async-storage.d.ts +12 -0
  204. package/dist/shims/internal/work-unit-async-storage.d.ts.map +1 -0
  205. package/dist/shims/internal/work-unit-async-storage.js +13 -0
  206. package/dist/shims/internal/work-unit-async-storage.js.map +1 -0
  207. package/dist/shims/layout-segment-context.d.ts +21 -0
  208. package/dist/shims/layout-segment-context.d.ts.map +1 -0
  209. package/dist/shims/layout-segment-context.js +27 -0
  210. package/dist/shims/layout-segment-context.js.map +1 -0
  211. package/dist/shims/legacy-image.d.ts +52 -0
  212. package/dist/shims/legacy-image.d.ts.map +1 -0
  213. package/dist/shims/legacy-image.js +46 -0
  214. package/dist/shims/legacy-image.js.map +1 -0
  215. package/dist/shims/link.d.ts +48 -0
  216. package/dist/shims/link.d.ts.map +1 -0
  217. package/dist/shims/link.js +395 -0
  218. package/dist/shims/link.js.map +1 -0
  219. package/dist/shims/metadata.d.ts +184 -0
  220. package/dist/shims/metadata.d.ts.map +1 -0
  221. package/dist/shims/metadata.js +472 -0
  222. package/dist/shims/metadata.js.map +1 -0
  223. package/dist/shims/navigation-state.d.ts +14 -0
  224. package/dist/shims/navigation-state.d.ts.map +1 -0
  225. package/dist/shims/navigation-state.js +77 -0
  226. package/dist/shims/navigation-state.js.map +1 -0
  227. package/dist/shims/navigation.d.ts +201 -0
  228. package/dist/shims/navigation.d.ts.map +1 -0
  229. package/dist/shims/navigation.js +672 -0
  230. package/dist/shims/navigation.js.map +1 -0
  231. package/dist/shims/og.d.ts +20 -0
  232. package/dist/shims/og.d.ts.map +1 -0
  233. package/dist/shims/og.js +19 -0
  234. package/dist/shims/og.js.map +1 -0
  235. package/dist/shims/router-state.d.ts +11 -0
  236. package/dist/shims/router-state.d.ts.map +1 -0
  237. package/dist/shims/router-state.js +56 -0
  238. package/dist/shims/router-state.js.map +1 -0
  239. package/dist/shims/router.d.ts +103 -0
  240. package/dist/shims/router.d.ts.map +1 -0
  241. package/dist/shims/router.js +536 -0
  242. package/dist/shims/router.js.map +1 -0
  243. package/dist/shims/script.d.ts +58 -0
  244. package/dist/shims/script.d.ts.map +1 -0
  245. package/dist/shims/script.js +163 -0
  246. package/dist/shims/script.js.map +1 -0
  247. package/dist/shims/server-only.d.ts +19 -0
  248. package/dist/shims/server-only.d.ts.map +1 -0
  249. package/dist/shims/server-only.js +19 -0
  250. package/dist/shims/server-only.js.map +1 -0
  251. package/dist/shims/server.d.ts +178 -0
  252. package/dist/shims/server.d.ts.map +1 -0
  253. package/dist/shims/server.js +377 -0
  254. package/dist/shims/server.js.map +1 -0
  255. package/dist/shims/web-vitals.d.ts +24 -0
  256. package/dist/shims/web-vitals.d.ts.map +1 -0
  257. package/dist/shims/web-vitals.js +17 -0
  258. package/dist/shims/web-vitals.js.map +1 -0
  259. package/dist/utils/hash.d.ts +6 -0
  260. package/dist/utils/hash.d.ts.map +1 -0
  261. package/dist/utils/hash.js +20 -0
  262. package/dist/utils/hash.js.map +1 -0
  263. package/dist/utils/project.d.ts +36 -0
  264. package/dist/utils/project.d.ts.map +1 -0
  265. package/dist/utils/project.js +112 -0
  266. package/dist/utils/project.js.map +1 -0
  267. package/dist/utils/query.d.ts +10 -0
  268. package/dist/utils/query.d.ts.map +1 -0
  269. package/dist/utils/query.js +27 -0
  270. package/dist/utils/query.js.map +1 -0
  271. package/package.json +65 -7
  272. package/index.js +0 -1
@@ -0,0 +1,758 @@
1
+ import { matchRoute, patternToNextFormat } from "../routing/pages-router.js";
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";
6
+ // Import server-only state modules to register ALS-backed accessors.
7
+ // These modules must be imported before any rendering occurs.
8
+ import "../shims/router-state.js";
9
+ import "../shims/head-state.js";
10
+ import { reportRequestError } from "./instrumentation.js";
11
+ import { safeJsonStringify } from "./html.js";
12
+ import { parseQueryString as parseQuery } from "../utils/query.js";
13
+ import path from "node:path";
14
+ import fs from "node:fs";
15
+ import React from "react";
16
+ import { renderToReadableStream } from "react-dom/server.edge";
17
+ const PAGE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
18
+ /**
19
+ * Render a React element to a string using renderToReadableStream.
20
+ *
21
+ * Uses the edge-compatible Web Streams API. Waits for all Suspense
22
+ * boundaries to resolve via stream.allReady before collecting output.
23
+ * Used for _document rendering and error pages (small, non-streaming).
24
+ */
25
+ async function renderToStringAsync(element) {
26
+ const stream = await renderToReadableStream(element);
27
+ await stream.allReady;
28
+ return new Response(stream).text();
29
+ }
30
+ /** Body placeholder used to split the document shell for streaming. */
31
+ const STREAM_BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
32
+ /**
33
+ * Stream a Pages Router page response using progressive SSR.
34
+ *
35
+ * Sends the HTML shell (head, layout, Suspense fallbacks) immediately
36
+ * when the React shell is ready, then streams Suspense content as it
37
+ * resolves. This gives the browser content to render while slow data
38
+ * loads are still in flight.
39
+ *
40
+ * `__NEXT_DATA__` and the hydration script are appended after the body
41
+ * stream completes (the data is known before rendering starts, but
42
+ * deferring them reduces TTFB and lets the browser start parsing the
43
+ * shell sooner).
44
+ */
45
+ async function streamPageToResponse(res, element, options) {
46
+ const { url, server, fontHeadHTML, scripts, DocumentComponent, statusCode = 200, extraHeaders, getHeadHTML, } = options;
47
+ // Start the React body stream FIRST — the promise resolves when the
48
+ // shell is ready (synchronous content outside Suspense boundaries).
49
+ // This triggers the render which populates <Head> tags.
50
+ const bodyStream = await renderToReadableStream(element);
51
+ // Now that the shell has rendered, collect head HTML
52
+ const headHTML = getHeadHTML();
53
+ // Build the document shell with a placeholder for the body
54
+ let shellTemplate;
55
+ if (DocumentComponent) {
56
+ const docElement = React.createElement(DocumentComponent);
57
+ let docHtml = await renderToStringAsync(docElement);
58
+ // Replace __NEXT_MAIN__ with our stream marker
59
+ docHtml = docHtml.replace("__NEXT_MAIN__", STREAM_BODY_MARKER);
60
+ // Inject head tags
61
+ if (headHTML || fontHeadHTML) {
62
+ docHtml = docHtml.replace("</head>", ` ${fontHeadHTML}${headHTML}\n</head>`);
63
+ }
64
+ // Inject scripts: replace placeholder or append before </body>
65
+ docHtml = docHtml.replace("<!-- __NEXT_SCRIPTS__ -->", scripts);
66
+ if (!docHtml.includes("__NEXT_DATA__")) {
67
+ docHtml = docHtml.replace("</body>", ` ${scripts}\n</body>`);
68
+ }
69
+ shellTemplate = docHtml;
70
+ }
71
+ else {
72
+ shellTemplate = `<!DOCTYPE html>
73
+ <html>
74
+ <head>
75
+ <meta charset="utf-8" />
76
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
77
+ ${fontHeadHTML}${headHTML}
78
+ </head>
79
+ <body>
80
+ <div id="__next">${STREAM_BODY_MARKER}</div>
81
+ ${scripts}
82
+ </body>
83
+ </html>`;
84
+ }
85
+ // Apply Vite's HTML transforms (injects HMR client, etc.) on the full
86
+ // shell template, then split at the body marker.
87
+ const transformedShell = await server.transformIndexHtml(url, shellTemplate);
88
+ const markerIdx = transformedShell.indexOf(STREAM_BODY_MARKER);
89
+ const prefix = transformedShell.slice(0, markerIdx);
90
+ const suffix = transformedShell.slice(markerIdx + STREAM_BODY_MARKER.length);
91
+ // Send headers and start streaming
92
+ const headers = {
93
+ "Content-Type": "text/html",
94
+ "Transfer-Encoding": "chunked",
95
+ ...extraHeaders,
96
+ };
97
+ res.writeHead(statusCode, headers);
98
+ // Write the document prefix (head, opening body)
99
+ res.write(prefix);
100
+ // Pipe the React body stream through (Suspense content streams progressively)
101
+ const reader = bodyStream.getReader();
102
+ try {
103
+ for (;;) {
104
+ const { done, value } = await reader.read();
105
+ if (done)
106
+ break;
107
+ res.write(value);
108
+ }
109
+ }
110
+ finally {
111
+ reader.releaseLock();
112
+ }
113
+ // Write the document suffix (closing tags, scripts)
114
+ res.end(suffix);
115
+ }
116
+ /** Check if a file exists with any page extension (tsx, ts, jsx, js). */
117
+ function findFileWithExtensions(basePath) {
118
+ return PAGE_EXTENSIONS.some((ext) => fs.existsSync(basePath + ext));
119
+ }
120
+ /**
121
+ * Extract locale prefix from a URL path.
122
+ * e.g. /fr/about -> { locale: "fr", url: "/about", hadPrefix: true }
123
+ * /about -> { locale: "en", url: "/about", hadPrefix: false } (defaultLocale)
124
+ */
125
+ export function extractLocaleFromUrl(url, i18nConfig) {
126
+ const pathname = url.split("?")[0];
127
+ const parts = pathname.split("/").filter(Boolean);
128
+ const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
129
+ if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
130
+ const locale = parts[0];
131
+ const rest = "/" + parts.slice(1).join("/");
132
+ return { locale, url: (rest || "/") + query, hadPrefix: true };
133
+ }
134
+ return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
135
+ }
136
+ /**
137
+ * Detect the preferred locale from the Accept-Language header.
138
+ * Returns the best matching locale or null.
139
+ */
140
+ export function detectLocaleFromHeaders(req, i18nConfig) {
141
+ const acceptLang = req.headers["accept-language"];
142
+ if (!acceptLang)
143
+ return null;
144
+ // Parse Accept-Language: en-US,en;q=0.9,fr;q=0.8
145
+ const langs = acceptLang
146
+ .split(",")
147
+ .map((part) => {
148
+ const [lang, qPart] = part.trim().split(";");
149
+ const q = qPart ? parseFloat(qPart.replace("q=", "")) : 1;
150
+ return { lang: lang.trim().toLowerCase(), q };
151
+ })
152
+ .sort((a, b) => b.q - a.q);
153
+ for (const { lang } of langs) {
154
+ // Exact match
155
+ const exactMatch = i18nConfig.locales.find((l) => l.toLowerCase() === lang);
156
+ if (exactMatch)
157
+ return exactMatch;
158
+ // Prefix match (e.g. "en-US" matches "en")
159
+ const prefix = lang.split("-")[0];
160
+ const prefixMatch = i18nConfig.locales.find((l) => l.toLowerCase() === prefix || l.toLowerCase().startsWith(prefix + "-"));
161
+ if (prefixMatch)
162
+ return prefixMatch;
163
+ }
164
+ return null;
165
+ }
166
+ /**
167
+ * Parse the NEXT_LOCALE cookie from a request.
168
+ * Returns the cookie value if it matches a configured locale, otherwise null.
169
+ */
170
+ export function parseCookieLocale(req, i18nConfig) {
171
+ const cookieHeader = req.headers.cookie;
172
+ if (!cookieHeader)
173
+ return null;
174
+ // Simple cookie parsing — find NEXT_LOCALE=value
175
+ const match = cookieHeader.match(/(?:^|;\s*)NEXT_LOCALE=([^;]*)/);
176
+ if (!match)
177
+ return null;
178
+ let value;
179
+ try {
180
+ value = decodeURIComponent(match[1].trim());
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ // Only return if it's a valid configured locale
186
+ if (i18nConfig.locales.includes(value))
187
+ return value;
188
+ return null;
189
+ }
190
+ /**
191
+ * Create an SSR request handler for the Pages Router.
192
+ *
193
+ * For each request:
194
+ * 1. Match the URL against discovered routes
195
+ * 2. Load the page module via Vite's SSR module loader
196
+ * 3. Call getServerSideProps/getStaticProps if present
197
+ * 4. Render the component to HTML
198
+ * 5. Wrap in _document shell and send response
199
+ */
200
+ export function createSSRHandler(server, routes, pagesDir, i18nConfig) {
201
+ return async (req, res, url,
202
+ /** Status code override — propagated from middleware rewrite status. */
203
+ statusCode) => {
204
+ // --- i18n: extract locale from URL prefix ---
205
+ let locale;
206
+ let localeStrippedUrl = url;
207
+ if (i18nConfig) {
208
+ const parsed = extractLocaleFromUrl(url, i18nConfig);
209
+ locale = parsed.locale;
210
+ localeStrippedUrl = parsed.url;
211
+ // If no locale prefix, check NEXT_LOCALE cookie first, then Accept-Language
212
+ if (!parsed.hadPrefix) {
213
+ const cookieLocale = parseCookieLocale(req, i18nConfig);
214
+ if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
215
+ // NEXT_LOCALE cookie overrides Accept-Language — redirect to cookie locale
216
+ const redirectUrl = `/${cookieLocale}${url === "/" ? "" : url}`;
217
+ res.writeHead(307, { Location: redirectUrl });
218
+ res.end();
219
+ return;
220
+ }
221
+ // If no cookie or cookie matches default, fall through to Accept-Language detection
222
+ if (!cookieLocale && i18nConfig.localeDetection !== false) {
223
+ const detectedLocale = detectLocaleFromHeaders(req, i18nConfig);
224
+ if (detectedLocale && detectedLocale !== i18nConfig.defaultLocale) {
225
+ const redirectUrl = `/${detectedLocale}${url === "/" ? "" : url}`;
226
+ res.writeHead(307, { Location: redirectUrl });
227
+ res.end();
228
+ return;
229
+ }
230
+ }
231
+ }
232
+ }
233
+ const match = matchRoute(localeStrippedUrl, routes);
234
+ if (!match) {
235
+ // No route matched — try to render custom 404 page
236
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
237
+ return;
238
+ }
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
+ });
297
+ });
298
+ if (!isValidPath) {
299
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
300
+ return;
301
+ }
302
+ }
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;
332
+ }
333
+ if (result && "notFound" in result && result.notFound) {
334
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
335
+ return;
336
+ }
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());
347
+ }
348
+ const fontLocalEarly = await server.ssrLoadModule("next/font/local");
349
+ if (typeof fontLocalEarly.getSSRFontPreloads === "function") {
350
+ earlyPreloads.push(...fontLocalEarly.getSSRFontPreloads());
351
+ }
352
+ if (earlyPreloads.length > 0) {
353
+ earlyFontLinkHeader = earlyPreloads
354
+ .map((p) => `<${p.href}>; rel=preload; as=font; type=${p.type}; crossorigin`)
355
+ .join(", ");
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;
381
+ }
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);
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`,
403
+ };
404
+ if (earlyFontLinkHeader)
405
+ staleHeaders["Link"] = earlyFontLinkHeader;
406
+ res.writeHead(200, staleHeaders);
407
+ res.end(transformedHtml);
408
+ return;
409
+ }
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,
426
+ });
427
+ res.end();
428
+ return;
429
+ }
430
+ if (result && "notFound" in result && result.notFound) {
431
+ await renderErrorPage(server, req, res, url, pagesDir, 404);
432
+ return;
433
+ }
434
+ // Extract revalidate period for ISR caching after render
435
+ if (typeof result?.revalidate === "number" && result.revalidate > 0) {
436
+ isrRevalidateSeconds = result.revalidate;
437
+ }
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)) {
444
+ try {
445
+ const appModule = await server.ssrLoadModule(appPath);
446
+ AppComponent = appModule.default ?? null;
447
+ }
448
+ catch {
449
+ // _app exists but failed to load
450
+ }
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 `;
490
+ }
491
+ }
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());
498
+ }
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());
511
+ }
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 = `
536
+ <script type="module">
537
+ import React from "react";
538
+ import { hydrateRoot } from "react-dom/client";
539
+
540
+ const nextData = window.__NEXT_DATA__;
541
+ const { pageProps } = nextData.props;
542
+
543
+ async function hydrate() {
544
+ const pageModule = await import("${pageModuleUrl}");
545
+ const PageComponent = pageModule.default;
546
+ let element;
547
+ ${appModuleUrl
548
+ ? `
549
+ const appModule = await import("${appModuleUrl}");
550
+ const AppComponent = appModule.default;
551
+ window.__VINEXT_APP__ = AppComponent;
552
+ element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
553
+ `
554
+ : `
555
+ element = React.createElement(PageComponent, pageProps);
556
+ `}
557
+ const root = hydrateRoot(document.getElementById("__next"), element);
558
+ window.__VINEXT_ROOT__ = root;
559
+ }
560
+ hydrate();
561
+ </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;
583
+ }
584
+ catch {
585
+ // _document exists but failed to load
586
+ }
587
+ }
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);
651
+ }
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}`);
656
+ }
657
+ }
658
+ finally {
659
+ cleanupFetchCache();
660
+ }
661
+ };
662
+ }
663
+ /**
664
+ * Render a custom error page (404.tsx, 500.tsx, or _error.tsx).
665
+ *
666
+ * Next.js resolution order:
667
+ * - 404: pages/404.tsx -> pages/_error.tsx -> default
668
+ * - 500: pages/500.tsx -> pages/_error.tsx -> default
669
+ * - other: pages/_error.tsx -> default
670
+ */
671
+ async function renderErrorPage(server, _req, res, url, pagesDir, statusCode) {
672
+ // Try specific status page first, then _error, then fallback
673
+ const candidates = statusCode === 404
674
+ ? ["404", "_error"]
675
+ : statusCode === 500
676
+ ? ["500", "_error"]
677
+ : ["_error"];
678
+ for (const candidate of candidates) {
679
+ try {
680
+ const candidatePath = path.join(pagesDir, candidate);
681
+ if (!findFileWithExtensions(candidatePath))
682
+ continue;
683
+ const errorModule = await server.ssrLoadModule(candidatePath);
684
+ const ErrorComponent = errorModule.default;
685
+ if (!ErrorComponent)
686
+ continue;
687
+ // Try to load _app.tsx to wrap the error page
688
+ let AppComponent = null;
689
+ const appPathErr = path.join(pagesDir, "_app");
690
+ if (findFileWithExtensions(appPathErr)) {
691
+ try {
692
+ const appModule = await server.ssrLoadModule(appPathErr);
693
+ AppComponent = appModule.default ?? null;
694
+ }
695
+ catch {
696
+ // _app exists but failed to load
697
+ }
698
+ }
699
+ const createElement = React.createElement;
700
+ const errorProps = { statusCode };
701
+ let element;
702
+ if (AppComponent) {
703
+ element = createElement(AppComponent, {
704
+ Component: ErrorComponent,
705
+ pageProps: errorProps,
706
+ });
707
+ }
708
+ else {
709
+ element = createElement(ErrorComponent, errorProps);
710
+ }
711
+ const bodyHtml = await renderToStringAsync(element);
712
+ // Try custom _document
713
+ let html;
714
+ let DocumentComponent = null;
715
+ const docPathErr = path.join(pagesDir, "_document");
716
+ if (findFileWithExtensions(docPathErr)) {
717
+ try {
718
+ const docModule = await server.ssrLoadModule(docPathErr);
719
+ DocumentComponent = docModule.default ?? null;
720
+ }
721
+ catch {
722
+ // _document exists but failed to load
723
+ }
724
+ }
725
+ if (DocumentComponent) {
726
+ const docElement = createElement(DocumentComponent);
727
+ let docHtml = await renderToStringAsync(docElement);
728
+ docHtml = docHtml.replace("__NEXT_MAIN__", bodyHtml);
729
+ docHtml = docHtml.replace("<!-- __NEXT_SCRIPTS__ -->", "");
730
+ html = docHtml;
731
+ }
732
+ else {
733
+ html = `<!DOCTYPE html>
734
+ <html>
735
+ <head>
736
+ <meta charset="utf-8" />
737
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
738
+ </head>
739
+ <body>
740
+ <div id="__next">${bodyHtml}</div>
741
+ </body>
742
+ </html>`;
743
+ }
744
+ const transformedHtml = await server.transformIndexHtml(url, html);
745
+ res.writeHead(statusCode, { "Content-Type": "text/html" });
746
+ res.end(transformedHtml);
747
+ return;
748
+ }
749
+ catch {
750
+ // This candidate doesn't exist, try next
751
+ continue;
752
+ }
753
+ }
754
+ // No custom error page found — use plain text fallback
755
+ res.writeHead(statusCode, { "Content-Type": "text/plain" });
756
+ res.end(`${statusCode} - ${statusCode === 404 ? "Page not found" : "Internal Server Error"}`);
757
+ }
758
+ //# sourceMappingURL=dev-server.js.map