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
package/dist/index.js ADDED
@@ -0,0 +1,3287 @@
1
+ import { parseAst } from "vite";
2
+ import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, patternToNextFormat as pagesPatternToNextFormat } from "./routing/pages-router.js";
3
+ import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
4
+ import { createSSRHandler } from "./server/dev-server.js";
5
+ import { handleApiRoute } from "./server/api-handler.js";
6
+ import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.js";
7
+ import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
8
+ import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
9
+ import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
10
+ import { safeRegExp, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
11
+ import { scanMetadataFiles } from "./server/metadata-routes.js";
12
+ import tsconfigPaths from "vite-tsconfig-paths";
13
+ import MagicString from "magic-string";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { createRequire } from "node:module";
17
+ import fs from "node:fs";
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ /**
20
+ * Fetch Google Fonts CSS, download .woff2 files, cache locally, and return
21
+ * @font-face CSS with local file references.
22
+ *
23
+ * Cache dir structure: .vinext/fonts/<family-hash>/
24
+ * - style.css (the rewritten @font-face CSS)
25
+ * - *.woff2 (downloaded font files)
26
+ */
27
+ async function fetchAndCacheFont(cssUrl, family, cacheDir) {
28
+ // Use a hash of the URL for the cache key
29
+ const { createHash } = await import("node:crypto");
30
+ const urlHash = createHash("md5").update(cssUrl).digest("hex").slice(0, 12);
31
+ const fontDir = path.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${urlHash}`);
32
+ // Check if already cached
33
+ const cachedCSSPath = path.join(fontDir, "style.css");
34
+ if (fs.existsSync(cachedCSSPath)) {
35
+ return fs.readFileSync(cachedCSSPath, "utf-8");
36
+ }
37
+ // Fetch CSS from Google Fonts (woff2 user-agent gives woff2 URLs)
38
+ const cssResponse = await fetch(cssUrl, {
39
+ headers: {
40
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
41
+ },
42
+ });
43
+ if (!cssResponse.ok) {
44
+ throw new Error(`Failed to fetch Google Fonts CSS: ${cssResponse.status}`);
45
+ }
46
+ let css = await cssResponse.text();
47
+ // Extract all font file URLs
48
+ const urlRe = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/g;
49
+ const urls = new Map(); // original URL -> local filename
50
+ let urlMatch;
51
+ while ((urlMatch = urlRe.exec(css)) !== null) {
52
+ const fontUrl = urlMatch[1];
53
+ if (!urls.has(fontUrl)) {
54
+ const ext = fontUrl.includes(".woff2") ? ".woff2" : fontUrl.includes(".woff") ? ".woff" : ".ttf";
55
+ const fileHash = createHash("md5").update(fontUrl).digest("hex").slice(0, 8);
56
+ urls.set(fontUrl, `${family.toLowerCase().replace(/\s+/g, "-")}-${fileHash}${ext}`);
57
+ }
58
+ }
59
+ // Download font files
60
+ fs.mkdirSync(fontDir, { recursive: true });
61
+ for (const [fontUrl, filename] of urls) {
62
+ const filePath = path.join(fontDir, filename);
63
+ if (!fs.existsSync(filePath)) {
64
+ const fontResponse = await fetch(fontUrl);
65
+ if (fontResponse.ok) {
66
+ const buffer = Buffer.from(await fontResponse.arrayBuffer());
67
+ fs.writeFileSync(filePath, buffer);
68
+ }
69
+ }
70
+ // Rewrite CSS to use relative path (Vite will resolve /@fs/ for dev, or asset for build)
71
+ css = css.split(fontUrl).join(filePath);
72
+ }
73
+ // Cache the rewritten CSS
74
+ fs.writeFileSync(cachedCSSPath, css);
75
+ return css;
76
+ }
77
+ /**
78
+ * Safely parse a static JS object literal string into a plain object.
79
+ * Uses Vite's parseAst (Rollup/acorn) so no code is ever evaluated.
80
+ * Returns null if the expression contains anything dynamic (function calls,
81
+ * template literals, identifiers, computed properties, etc.).
82
+ *
83
+ * Supports: string literals, numeric literals, boolean literals,
84
+ * arrays of the above, and nested object literals.
85
+ */
86
+ function parseStaticObjectLiteral(objectStr) {
87
+ let ast;
88
+ try {
89
+ // Wrap in parens so the parser treats `{…}` as an expression, not a block
90
+ ast = parseAst(`(${objectStr})`);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ // The AST should be: Program > ExpressionStatement > ObjectExpression
96
+ const body = ast.body;
97
+ if (body.length !== 1 || body[0].type !== "ExpressionStatement")
98
+ return null;
99
+ const expr = body[0].expression;
100
+ if (expr.type !== "ObjectExpression")
101
+ return null;
102
+ const result = extractStaticValue(expr);
103
+ return result === undefined ? null : result;
104
+ }
105
+ /**
106
+ * Recursively extract a static value from an ESTree AST node.
107
+ * Returns undefined (not null) if the node contains any dynamic expression.
108
+ *
109
+ * Uses `any` for the node parameter because Rollup's internal ESTree types
110
+ * (estree.Expression, estree.ObjectExpression, etc.) aren't re-exported by Vite,
111
+ * and the recursive traversal touches many different node shapes.
112
+ */
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ function extractStaticValue(node) {
115
+ switch (node.type) {
116
+ case "Literal":
117
+ // String, number, boolean, null
118
+ return node.value;
119
+ case "UnaryExpression":
120
+ // Handle negative numbers: -1, -3.14
121
+ if (node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number") {
122
+ return -node.argument.value;
123
+ }
124
+ return undefined;
125
+ case "ArrayExpression": {
126
+ const arr = [];
127
+ for (const elem of node.elements) {
128
+ if (!elem)
129
+ return undefined; // sparse array
130
+ const val = extractStaticValue(elem);
131
+ if (val === undefined)
132
+ return undefined;
133
+ arr.push(val);
134
+ }
135
+ return arr;
136
+ }
137
+ case "ObjectExpression": {
138
+ const obj = {};
139
+ for (const prop of node.properties) {
140
+ if (prop.type !== "Property")
141
+ return undefined; // SpreadElement etc.
142
+ if (prop.computed)
143
+ return undefined; // [expr]: val
144
+ // Key can be Identifier (unquoted) or Literal (quoted)
145
+ let key;
146
+ if (prop.key.type === "Identifier") {
147
+ key = prop.key.name;
148
+ }
149
+ else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
150
+ key = prop.key.value;
151
+ }
152
+ else {
153
+ return undefined;
154
+ }
155
+ const val = extractStaticValue(prop.value);
156
+ if (val === undefined)
157
+ return undefined;
158
+ obj[key] = val;
159
+ }
160
+ return obj;
161
+ }
162
+ default:
163
+ // TemplateLiteral, CallExpression, Identifier, etc. — reject
164
+ return undefined;
165
+ }
166
+ }
167
+ /**
168
+ * Detect Vite major version at runtime by resolving from cwd.
169
+ * The plugin may be installed in a workspace root with Vite 7 but used
170
+ * by a project that has Vite 8 — so we resolve from cwd, not from
171
+ * the plugin's own location.
172
+ */
173
+ function getViteMajorVersion() {
174
+ try {
175
+ const require = createRequire(path.join(process.cwd(), "package.json"));
176
+ const vitePkg = require("vite/package.json");
177
+ return parseInt(vitePkg.version, 10);
178
+ }
179
+ catch {
180
+ return 7; // default to Vite 7
181
+ }
182
+ }
183
+ /**
184
+ * PostCSS config file names to search for, in priority order.
185
+ * Matches the same search order as postcss-load-config / lilconfig.
186
+ */
187
+ const POSTCSS_CONFIG_FILES = [
188
+ "postcss.config.js",
189
+ "postcss.config.cjs",
190
+ "postcss.config.mjs",
191
+ "postcss.config.ts",
192
+ "postcss.config.cts",
193
+ "postcss.config.mts",
194
+ ".postcssrc",
195
+ ".postcssrc.js",
196
+ ".postcssrc.cjs",
197
+ ".postcssrc.mjs",
198
+ ".postcssrc.ts",
199
+ ".postcssrc.cts",
200
+ ".postcssrc.mts",
201
+ ".postcssrc.json",
202
+ ".postcssrc.yaml",
203
+ ".postcssrc.yml",
204
+ ];
205
+ /**
206
+ * Resolve PostCSS string plugin names in a project's PostCSS config.
207
+ *
208
+ * Next.js (via postcss-load-config) resolves string plugin names in the
209
+ * object form `{ plugins: { "pkg-name": opts } }` but NOT in the array form
210
+ * `{ plugins: ["pkg-name"] }`. Since many Next.js projects use the array
211
+ * form (particularly with Tailwind CSS v4), we detect this case and resolve
212
+ * the string names to actual plugin functions so Vite can use them.
213
+ *
214
+ * Returns the resolved PostCSS config object to inject into Vite's
215
+ * `css.postcss`, or `undefined` if no resolution is needed.
216
+ */
217
+ async function resolvePostcssStringPlugins(projectRoot) {
218
+ // Find the PostCSS config file
219
+ let configPath = null;
220
+ for (const name of POSTCSS_CONFIG_FILES) {
221
+ const candidate = path.join(projectRoot, name);
222
+ if (fs.existsSync(candidate)) {
223
+ configPath = candidate;
224
+ break;
225
+ }
226
+ }
227
+ if (!configPath)
228
+ return undefined;
229
+ // Load the config file
230
+ let config;
231
+ try {
232
+ if (configPath.endsWith(".json") || configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
233
+ // JSON/YAML configs use object form — postcss-load-config handles these fine
234
+ return undefined;
235
+ }
236
+ // For .postcssrc without extension, check if it's JSON
237
+ if (configPath.endsWith(".postcssrc")) {
238
+ const content = fs.readFileSync(configPath, "utf-8").trim();
239
+ if (content.startsWith("{")) {
240
+ // JSON format — postcss-load-config handles object form
241
+ return undefined;
242
+ }
243
+ }
244
+ const mod = await import(configPath);
245
+ config = mod.default ?? mod;
246
+ }
247
+ catch {
248
+ // If we can't load the config, let Vite/postcss-load-config handle it
249
+ return undefined;
250
+ }
251
+ // Only process array-form plugins that contain string entries
252
+ // (either bare strings or tuple form ["plugin-name", { options }])
253
+ if (!config || !Array.isArray(config.plugins))
254
+ return undefined;
255
+ const hasStringPlugins = config.plugins.some((p) => typeof p === "string" ||
256
+ (Array.isArray(p) && typeof p[0] === "string"));
257
+ if (!hasStringPlugins)
258
+ return undefined;
259
+ // Resolve string plugin names to actual plugin functions
260
+ const req = createRequire(path.join(projectRoot, "package.json"));
261
+ const resolved = await Promise.all(config.plugins.filter(Boolean).map(async (plugin) => {
262
+ if (typeof plugin === "string") {
263
+ const resolved = req.resolve(plugin);
264
+ const mod = await import(resolved);
265
+ const fn = mod.default ?? mod;
266
+ // If the export is a function, call it to get the plugin instance
267
+ return typeof fn === "function" ? fn() : fn;
268
+ }
269
+ // Array tuple form: ["plugin-name", { options }]
270
+ if (Array.isArray(plugin) && typeof plugin[0] === "string") {
271
+ const [name, options] = plugin;
272
+ const resolved = req.resolve(name);
273
+ const mod = await import(resolved);
274
+ const fn = mod.default ?? mod;
275
+ return typeof fn === "function" ? fn(options) : fn;
276
+ }
277
+ // Already a function or plugin object — pass through
278
+ return plugin;
279
+ }));
280
+ return { plugins: resolved };
281
+ }
282
+ // Virtual module IDs for Pages Router production build
283
+ const VIRTUAL_SERVER_ENTRY = "virtual:vinext-server-entry";
284
+ const RESOLVED_SERVER_ENTRY = "\0" + VIRTUAL_SERVER_ENTRY;
285
+ const VIRTUAL_CLIENT_ENTRY = "virtual:vinext-client-entry";
286
+ const RESOLVED_CLIENT_ENTRY = "\0" + VIRTUAL_CLIENT_ENTRY;
287
+ // Virtual module IDs for App Router entries
288
+ const VIRTUAL_RSC_ENTRY = "virtual:vinext-rsc-entry";
289
+ const RESOLVED_RSC_ENTRY = "\0" + VIRTUAL_RSC_ENTRY;
290
+ const VIRTUAL_APP_SSR_ENTRY = "virtual:vinext-app-ssr-entry";
291
+ const RESOLVED_APP_SSR_ENTRY = "\0" + VIRTUAL_APP_SSR_ENTRY;
292
+ const VIRTUAL_APP_BROWSER_ENTRY = "virtual:vinext-app-browser-entry";
293
+ const RESOLVED_APP_BROWSER_ENTRY = "\0" + VIRTUAL_APP_BROWSER_ENTRY;
294
+ /** Image file extensions handled by the vinext:image-imports plugin.
295
+ * Shared between the Rolldown hook filter and the transform handler regex. */
296
+ const IMAGE_EXTS = "png|jpe?g|gif|webp|avif|svg|ico|bmp|tiff?";
297
+ /**
298
+ * Extract the npm package name from a module ID (file path).
299
+ * Returns null if not in node_modules.
300
+ *
301
+ * Handles scoped packages (@org/pkg) and pnpm-style paths
302
+ * (node_modules/.pnpm/pkg@ver/node_modules/pkg).
303
+ */
304
+ function getPackageName(id) {
305
+ const nmIdx = id.lastIndexOf("node_modules/");
306
+ if (nmIdx === -1)
307
+ return null;
308
+ const rest = id.slice(nmIdx + "node_modules/".length);
309
+ if (rest.startsWith("@")) {
310
+ // Scoped package: @org/pkg
311
+ const parts = rest.split("/");
312
+ return parts.length >= 2 ? parts[0] + "/" + parts[1] : null;
313
+ }
314
+ return rest.split("/")[0] || null;
315
+ }
316
+ /** Absolute path to vinext's shims directory, used by clientManualChunks. */
317
+ const _shimsDir = path.resolve(__dirname, "shims") + "/";
318
+ /**
319
+ * manualChunks function for client builds.
320
+ *
321
+ * Splits the client bundle into:
322
+ * - "framework" — React, ReactDOM, and scheduler (loaded on every page)
323
+ * - "vinext" — vinext shims (router, head, link, etc.)
324
+ *
325
+ * All other vendor code is left to Rollup's default chunk-splitting
326
+ * algorithm. Rollup automatically deduplicates shared modules into
327
+ * common chunks based on the import graph — no manual intervention
328
+ * needed.
329
+ *
330
+ * Why not split every npm package into its own chunk?
331
+ * - Per-package splitting (`vendor-X`) creates 50-200+ chunks for a
332
+ * typical app, far exceeding the ~25-request sweet spot for HTTP/2.
333
+ * - gzip/brotli compress small files poorly — each file restarts with
334
+ * an empty dictionary, losing ~5-15% total compressed size vs fewer
335
+ * larger chunks (Khan Academy measured +2.5% wire size with 10x
336
+ * more files containing less raw code).
337
+ * - ES module evaluation has per-module overhead that compounds on
338
+ * mobile devices.
339
+ * - No major Vite-based framework (Remix, SvelteKit, Astro, TanStack)
340
+ * uses per-package splitting. Next.js only isolates packages >160KB.
341
+ * - Rollup's graph-based splitting already handles the common case
342
+ * well: shared dependencies between routes get their own chunks,
343
+ * and route-specific code stays in route chunks.
344
+ */
345
+ function clientManualChunks(id) {
346
+ // React framework — always loaded, shared across all pages.
347
+ // Isolating React into its own chunk is the single highest-value
348
+ // split: it's ~130KB compressed, loaded on every page, and its
349
+ // content hash rarely changes between deploys.
350
+ if (id.includes("node_modules")) {
351
+ const pkg = getPackageName(id);
352
+ if (!pkg)
353
+ return undefined;
354
+ if (pkg === "react" ||
355
+ pkg === "react-dom" ||
356
+ pkg === "scheduler") {
357
+ return "framework";
358
+ }
359
+ // Let Rollup handle all other vendor code via its default
360
+ // graph-based splitting. This produces a reasonable number of
361
+ // shared chunks (typically 5-15) based on actual import patterns,
362
+ // with good compression efficiency.
363
+ return undefined;
364
+ }
365
+ // vinext shims — small runtime, shared across all pages.
366
+ // Use the absolute shims directory path to avoid matching user files
367
+ // that happen to have "/shims/" in their path.
368
+ if (id.startsWith(_shimsDir)) {
369
+ return "vinext";
370
+ }
371
+ return undefined;
372
+ }
373
+ /**
374
+ * Rollup output config with manualChunks for client code-splitting.
375
+ * Used by both CLI builds and multi-environment builds.
376
+ *
377
+ * experimentalMinChunkSize merges tiny shared chunks (< 10KB) back into
378
+ * their importers. This reduces HTTP request count and improves gzip
379
+ * compression efficiency — small files restart the compression dictionary,
380
+ * adding ~5-15% wire overhead vs fewer larger chunks.
381
+ */
382
+ const clientOutputConfig = {
383
+ manualChunks: clientManualChunks,
384
+ experimentalMinChunkSize: 10_000,
385
+ };
386
+ /**
387
+ * Rollup treeshake configuration for production client builds.
388
+ *
389
+ * Uses the 'recommended' preset as a safe base, then overrides
390
+ * moduleSideEffects to strip unused re-exports from npm packages.
391
+ *
392
+ * The 'no-external' value for moduleSideEffects means:
393
+ * - Local project modules: preserve side effects (CSS imports, polyfills)
394
+ * - node_modules packages: treat as side-effect-free unless exports are used
395
+ *
396
+ * This is the single highest-impact optimization for large barrel-exporting
397
+ * libraries like mermaid, @mui/material, lucide-react, etc. These libraries
398
+ * re-export hundreds of sub-modules through barrel files. Without this,
399
+ * Rollup preserves every sub-module even when only a few exports are consumed.
400
+ *
401
+ * Why 'no-external' instead of false (global side-effect-free)?
402
+ * - User code may rely on import-time side effects (e.g., `import './global.css'`)
403
+ * - 'no-external' is safe for app code while still enabling aggressive DCE for deps
404
+ *
405
+ * Why not the 'smallest' preset?
406
+ * - 'smallest' also sets propertyReadSideEffects: false and
407
+ * tryCatchDeoptimization: false, which can break specific libraries
408
+ * that rely on property access side effects or try/catch for feature detection
409
+ * - 'recommended' + 'no-external' gives most of the benefit with less risk
410
+ */
411
+ const clientTreeshakeConfig = {
412
+ preset: "recommended",
413
+ moduleSideEffects: "no-external",
414
+ };
415
+ /**
416
+ * Compute the set of chunk filenames that are ONLY reachable through dynamic
417
+ * imports (i.e. behind React.lazy(), next/dynamic, or manual import()).
418
+ *
419
+ * These chunks should NOT be modulepreloaded in the HTML — they will be
420
+ * fetched on demand when the dynamic import executes.
421
+ *
422
+ * Algorithm: Starting from all entry chunks in the build manifest, walk the
423
+ * static `imports` tree (breadth-first). Any chunk file NOT reached by this
424
+ * walk is only reachable through `dynamicImports` and is therefore "lazy".
425
+ *
426
+ * @param buildManifest - Vite's build manifest (manifest.json), which is a
427
+ * Record<string, ManifestChunk> where each chunk has `file`, `imports`,
428
+ * `dynamicImports`, `isEntry`, and `isDynamicEntry` fields.
429
+ * @returns Array of chunk filenames (e.g. "assets/mermaid-NOHMQCX5.js") that
430
+ * should be excluded from modulepreload hints.
431
+ */
432
+ function computeLazyChunks(buildManifest) {
433
+ // Collect all chunk files that are statically reachable from entries
434
+ const eagerFiles = new Set();
435
+ const visited = new Set();
436
+ const queue = [];
437
+ // Start BFS from all entry chunks
438
+ for (const key of Object.keys(buildManifest)) {
439
+ const chunk = buildManifest[key];
440
+ if (chunk.isEntry) {
441
+ queue.push(key);
442
+ }
443
+ }
444
+ while (queue.length > 0) {
445
+ const key = queue.shift();
446
+ if (visited.has(key))
447
+ continue;
448
+ visited.add(key);
449
+ const chunk = buildManifest[key];
450
+ if (!chunk)
451
+ continue;
452
+ // Mark this chunk's file as eager
453
+ eagerFiles.add(chunk.file);
454
+ // Also mark its CSS as eager (CSS should always be preloaded to avoid FOUC)
455
+ if (chunk.css) {
456
+ for (const cssFile of chunk.css) {
457
+ eagerFiles.add(cssFile);
458
+ }
459
+ }
460
+ // Follow only static imports — NOT dynamicImports
461
+ if (chunk.imports) {
462
+ for (const imp of chunk.imports) {
463
+ if (!visited.has(imp)) {
464
+ queue.push(imp);
465
+ }
466
+ }
467
+ }
468
+ }
469
+ // Any JS file in the manifest that's NOT in eagerFiles is a lazy chunk
470
+ const lazyChunks = [];
471
+ const allFiles = new Set();
472
+ for (const key of Object.keys(buildManifest)) {
473
+ const chunk = buildManifest[key];
474
+ if (chunk.file && !allFiles.has(chunk.file)) {
475
+ allFiles.add(chunk.file);
476
+ if (!eagerFiles.has(chunk.file) && chunk.file.endsWith(".js")) {
477
+ lazyChunks.push(chunk.file);
478
+ }
479
+ }
480
+ }
481
+ return lazyChunks;
482
+ }
483
+ export default function vinext(options = {}) {
484
+ let root;
485
+ let pagesDir;
486
+ let appDir;
487
+ let hasAppDir = false;
488
+ let hasPagesDir = false;
489
+ let nextConfig;
490
+ let middlewarePath = null;
491
+ let instrumentationPath = null;
492
+ let hasCloudflarePlugin = false;
493
+ // Resolve shim paths - works both from source (.ts) and built (.js)
494
+ const shimsDir = path.resolve(__dirname, "shims");
495
+ // Shim alias map — populated in config(), used by resolveId() for .js variants
496
+ let nextShimMap = {};
497
+ /**
498
+ * Generate the virtual SSR server entry module.
499
+ * This is the entry point for `vite build --ssr`.
500
+ */
501
+ async function generateServerEntry() {
502
+ const pageRoutes = await pagesRouter(pagesDir);
503
+ const apiRoutes = await apiRouter(pagesDir);
504
+ // Generate import statements using absolute paths since virtual
505
+ // modules don't have a real file location for relative resolution.
506
+ const pageImports = pageRoutes.map((r, i) => {
507
+ const absPath = r.filePath.replace(/\\/g, "/");
508
+ return `import * as page_${i} from ${JSON.stringify(absPath)};`;
509
+ });
510
+ const apiImports = apiRoutes.map((r, i) => {
511
+ const absPath = r.filePath.replace(/\\/g, "/");
512
+ return `import * as api_${i} from ${JSON.stringify(absPath)};`;
513
+ });
514
+ // Build the route table — include filePath for SSR manifest lookup
515
+ const pageRouteEntries = pageRoutes.map((r, i) => {
516
+ const absPath = r.filePath.replace(/\\/g, "/");
517
+ return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`;
518
+ });
519
+ const apiRouteEntries = apiRoutes.map((r, i) => {
520
+ return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
521
+ });
522
+ // Check for _app and _document
523
+ const hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
524
+ const hasDoc = fs.existsSync(path.join(pagesDir, "_document.tsx")) || fs.existsSync(path.join(pagesDir, "_document.jsx")) || fs.existsSync(path.join(pagesDir, "_document.ts")) || fs.existsSync(path.join(pagesDir, "_document.js"));
525
+ // Use absolute paths for _app and _document too
526
+ const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
527
+ const docFileBase = path.join(pagesDir, "_document").replace(/\\/g, "/");
528
+ const appImportCode = hasApp
529
+ ? `import { default as AppComponent } from ${JSON.stringify(appFileBase)};`
530
+ : `const AppComponent = null;`;
531
+ const docImportCode = hasDoc
532
+ ? `import { default as DocumentComponent } from ${JSON.stringify(docFileBase)};`
533
+ : `const DocumentComponent = null;`;
534
+ // Serialize i18n config for embedding in the server entry
535
+ const i18nConfigJson = nextConfig?.i18n
536
+ ? JSON.stringify({
537
+ locales: nextConfig.i18n.locales,
538
+ defaultLocale: nextConfig.i18n.defaultLocale,
539
+ localeDetection: nextConfig.i18n.localeDetection,
540
+ })
541
+ : "null";
542
+ // Serialize the full resolved config for the production server.
543
+ // This embeds redirects, rewrites, headers, basePath, trailingSlash
544
+ // so prod-server.ts can apply them without loading next.config.js at runtime.
545
+ const vinextConfigJson = JSON.stringify({
546
+ basePath: nextConfig?.basePath ?? "",
547
+ trailingSlash: nextConfig?.trailingSlash ?? false,
548
+ redirects: nextConfig?.redirects ?? [],
549
+ rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
550
+ headers: nextConfig?.headers ?? [],
551
+ i18n: nextConfig?.i18n ?? null,
552
+ });
553
+ // Generate middleware code if middleware.ts exists
554
+ const middlewareImportCode = middlewarePath
555
+ ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
556
+ import { NextRequest } from "next/server";`
557
+ : "";
558
+ // The matcher config is read from the middleware module at import time.
559
+ // We inline the matching + execution logic so the prod server can call it.
560
+ const middlewareExportCode = middlewarePath
561
+ ? `
562
+ // --- Middleware support ---
563
+ function matchesMiddleware(pathname, matcher) {
564
+ if (!matcher) {
565
+ return !pathname.startsWith("/_next") && !pathname.startsWith("/api") && !pathname.includes(".") && pathname !== "/favicon.ico";
566
+ }
567
+ var patterns = [];
568
+ if (typeof matcher === "string") { patterns.push(matcher); }
569
+ else if (Array.isArray(matcher)) {
570
+ for (var m of matcher) {
571
+ if (typeof m === "string") patterns.push(m);
572
+ else if (m && typeof m === "object" && "source" in m) patterns.push(m.source);
573
+ }
574
+ }
575
+ return patterns.some(function(p) { return matchMiddlewarePattern(pathname, p); });
576
+ }
577
+
578
+ function __isSafeRegex(pattern) {
579
+ var quantifierAtDepth = [];
580
+ var depth = 0;
581
+ var i = 0;
582
+ while (i < pattern.length) {
583
+ var ch = pattern[i];
584
+ if (ch === "\\\\") { i += 2; continue; }
585
+ if (ch === "[") {
586
+ i++;
587
+ while (i < pattern.length && pattern[i] !== "]") {
588
+ if (pattern[i] === "\\\\") i++;
589
+ i++;
590
+ }
591
+ i++;
592
+ continue;
593
+ }
594
+ if (ch === "(") {
595
+ depth++;
596
+ if (quantifierAtDepth.length <= depth) quantifierAtDepth.push(false);
597
+ else quantifierAtDepth[depth] = false;
598
+ i++;
599
+ continue;
600
+ }
601
+ if (ch === ")") {
602
+ var hadQ = depth > 0 && quantifierAtDepth[depth];
603
+ if (depth > 0) depth--;
604
+ var next = pattern[i + 1];
605
+ if (next === "+" || next === "*" || next === "{") {
606
+ if (hadQ) return false;
607
+ if (depth >= 0 && depth < quantifierAtDepth.length) quantifierAtDepth[depth] = true;
608
+ }
609
+ i++;
610
+ continue;
611
+ }
612
+ if (ch === "+" || ch === "*") {
613
+ if (depth > 0) quantifierAtDepth[depth] = true;
614
+ i++;
615
+ continue;
616
+ }
617
+ if (ch === "?") {
618
+ var prev = i > 0 ? pattern[i - 1] : "";
619
+ if (prev !== "+" && prev !== "*" && prev !== "?" && prev !== "}") {
620
+ if (depth > 0) quantifierAtDepth[depth] = true;
621
+ }
622
+ i++;
623
+ continue;
624
+ }
625
+ if (ch === "{") {
626
+ var j = i + 1;
627
+ while (j < pattern.length && /[\\d,]/.test(pattern[j])) j++;
628
+ if (j < pattern.length && pattern[j] === "}" && j > i + 1) {
629
+ if (depth > 0) quantifierAtDepth[depth] = true;
630
+ i = j + 1;
631
+ continue;
632
+ }
633
+ }
634
+ i++;
635
+ }
636
+ return true;
637
+ }
638
+ function __safeRegExp(pattern, flags) {
639
+ if (!__isSafeRegex(pattern)) {
640
+ console.warn("[vinext] Ignoring potentially unsafe regex pattern (ReDoS risk): " + pattern);
641
+ return null;
642
+ }
643
+ try { return new RegExp(pattern, flags); } catch { return null; }
644
+ }
645
+
646
+ function matchMiddlewarePattern(pathname, pattern) {
647
+ if (pattern.includes("(") || pattern.includes("\\\\")) {
648
+ var re = __safeRegExp("^" + pattern + "$");
649
+ if (re) return re.test(pathname);
650
+ }
651
+ var regexStr = pattern
652
+ .replace(/\\./g, "\\\\.")
653
+ .replace(/\\/:([\\w]+)\\*/g, "(?:/.*)?")
654
+ .replace(/\\/:([\\w]+)\\+/g, "(?:/.+)")
655
+ .replace(/:([\\w]+)/g, "([^/]+)");
656
+ var re2 = __safeRegExp("^" + regexStr + "$");
657
+ return re2 ? re2.test(pathname) : pathname === pattern;
658
+ }
659
+
660
+ export async function runMiddleware(request) {
661
+ var middlewareFn = middlewareModule.default || middlewareModule.middleware;
662
+ if (typeof middlewareFn !== "function") return { continue: true };
663
+
664
+ var config = middlewareModule.config;
665
+ var matcher = config && config.matcher;
666
+ var url = new URL(request.url);
667
+
668
+ if (!matchesMiddleware(url.pathname, matcher)) return { continue: true };
669
+
670
+ var nextRequest = request instanceof NextRequest ? request : new NextRequest(request);
671
+ var response;
672
+ try { response = await middlewareFn(nextRequest); }
673
+ catch (e) {
674
+ console.error("[vinext] Middleware error:", e);
675
+ return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
676
+ }
677
+
678
+ if (!response) return { continue: true };
679
+
680
+ if (response.headers.get("x-middleware-next") === "1") {
681
+ var rHeaders = new Headers();
682
+ for (var [key, value] of response.headers) {
683
+ if (key !== "x-middleware-next" && key !== "x-middleware-rewrite") rHeaders.set(key, value);
684
+ }
685
+ return { continue: true, responseHeaders: rHeaders };
686
+ }
687
+
688
+ if (response.status >= 300 && response.status < 400) {
689
+ var location = response.headers.get("Location") || response.headers.get("location");
690
+ if (location) return { continue: false, redirectUrl: location, redirectStatus: response.status };
691
+ }
692
+
693
+ var rewriteUrl = response.headers.get("x-middleware-rewrite");
694
+ if (rewriteUrl) {
695
+ var rwHeaders = new Headers();
696
+ for (var [k, v] of response.headers) { if (k !== "x-middleware-rewrite") rwHeaders.set(k, v); }
697
+ var rewritePath;
698
+ try { var parsed = new URL(rewriteUrl, request.url); rewritePath = parsed.pathname + parsed.search; }
699
+ catch { rewritePath = rewriteUrl; }
700
+ return { continue: true, rewriteUrl: rewritePath, rewriteStatus: response.status !== 200 ? response.status : undefined, responseHeaders: rwHeaders };
701
+ }
702
+
703
+ return { continue: false, response: response };
704
+ }
705
+ `
706
+ : `
707
+ export async function runMiddleware() { return { continue: true }; }
708
+ `;
709
+ // The server entry is a self-contained module that uses Web-standard APIs
710
+ // (Request/Response, renderToReadableStream) so it runs on Cloudflare Workers.
711
+ return `
712
+ import React from "react";
713
+ import { renderToReadableStream } from "react-dom/server.edge";
714
+ import { resetSSRHead, getSSRHeadHTML } from "next/head";
715
+ import { flushPreloads } from "next/dynamic";
716
+ import { setSSRContext } from "next/router";
717
+ import { getCacheHandler } from "next/cache";
718
+ import { withFetchCache } from "vinext/fetch-cache";
719
+ import { safeJsonStringify } from "vinext/html";
720
+ import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
721
+ import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
722
+ ${middlewareImportCode}
723
+
724
+ // i18n config (embedded at build time)
725
+ const i18nConfig = ${i18nConfigJson};
726
+
727
+ // Full resolved config for production server (embedded at build time)
728
+ export const vinextConfig = ${vinextConfigJson};
729
+
730
+ // ISR cache helpers (inlined for the server entry)
731
+ async function isrGet(key) {
732
+ const handler = getCacheHandler();
733
+ const result = await handler.get(key);
734
+ if (!result || !result.value) return null;
735
+ return { value: result, isStale: result.cacheState === "stale" };
736
+ }
737
+ async function isrSet(key, data, revalidateSeconds, tags) {
738
+ const handler = getCacheHandler();
739
+ await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
740
+ }
741
+ const pendingRegenerations = new Map();
742
+ function triggerBackgroundRegeneration(key, renderFn) {
743
+ if (pendingRegenerations.has(key)) return;
744
+ const promise = renderFn()
745
+ .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
746
+ .finally(() => pendingRegenerations.delete(key));
747
+ pendingRegenerations.set(key, promise);
748
+ }
749
+
750
+ async function renderToStringAsync(element) {
751
+ const stream = await renderToReadableStream(element);
752
+ await stream.allReady;
753
+ return new Response(stream).text();
754
+ }
755
+
756
+ ${pageImports.join("\n")}
757
+ ${apiImports.join("\n")}
758
+
759
+ ${appImportCode}
760
+ ${docImportCode}
761
+
762
+ const pageRoutes = [
763
+ ${pageRouteEntries.join(",\n")}
764
+ ];
765
+
766
+ const apiRoutes = [
767
+ ${apiRouteEntries.join(",\n")}
768
+ ];
769
+
770
+ function matchRoute(url, routes) {
771
+ const pathname = url.split("?")[0];
772
+ let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
773
+ try { normalizedUrl = decodeURIComponent(normalizedUrl); } catch {}
774
+ for (const route of routes) {
775
+ const params = matchPattern(normalizedUrl, route.pattern);
776
+ if (params !== null) return { route, params };
777
+ }
778
+ return null;
779
+ }
780
+
781
+ function matchPattern(url, pattern) {
782
+ const urlParts = url.split("/").filter(Boolean);
783
+ const patternParts = pattern.split("/").filter(Boolean);
784
+ const params = Object.create(null);
785
+ for (let i = 0; i < patternParts.length; i++) {
786
+ const pp = patternParts[i];
787
+ if (pp.endsWith("+")) {
788
+ const paramName = pp.slice(1, -1);
789
+ const remaining = urlParts.slice(i);
790
+ if (remaining.length === 0) return null;
791
+ params[paramName] = remaining;
792
+ return params;
793
+ }
794
+ if (pp.endsWith("*")) {
795
+ const paramName = pp.slice(1, -1);
796
+ params[paramName] = urlParts.slice(i);
797
+ return params;
798
+ }
799
+ if (pp.startsWith(":")) {
800
+ if (i >= urlParts.length) return null;
801
+ params[pp.slice(1)] = urlParts[i];
802
+ continue;
803
+ }
804
+ if (i >= urlParts.length || urlParts[i] !== pp) return null;
805
+ }
806
+ if (urlParts.length !== patternParts.length) return null;
807
+ return params;
808
+ }
809
+
810
+ function parseQuery(url) {
811
+ const qs = url.split("?")[1];
812
+ if (!qs) return {};
813
+ const p = new URLSearchParams(qs);
814
+ const q = {};
815
+ for (const [k, v] of p) {
816
+ if (k in q) {
817
+ q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
818
+ } else {
819
+ q[k] = v;
820
+ }
821
+ }
822
+ return q;
823
+ }
824
+
825
+ function patternToNextFormat(pattern) {
826
+ return pattern
827
+ .replace(/:([\\w]+)\\*/g, "[[...$1]]")
828
+ .replace(/:([\\w]+)\\+/g, "[...$1]")
829
+ .replace(/:([\\w]+)/g, "[$1]");
830
+ }
831
+
832
+ function collectAssetTags(manifest, moduleIds) {
833
+ // Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
834
+ const m = (manifest && Object.keys(manifest).length > 0)
835
+ ? manifest
836
+ : (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
837
+ const tags = [];
838
+ const seen = new Set();
839
+
840
+ // Load the set of lazy chunk filenames (only reachable via dynamic imports).
841
+ // These should NOT get <link rel="modulepreload"> or <script type="module">
842
+ // tags — they are fetched on demand when the dynamic import() executes (e.g.
843
+ // chunks behind React.lazy() or next/dynamic boundaries).
844
+ var lazyChunks = (typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__) || null;
845
+ var lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
846
+
847
+ // Inject the client entry script if embedded by vinext:cloudflare-build
848
+ if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
849
+ const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
850
+ seen.add(entry);
851
+ tags.push('<link rel="modulepreload" href="/' + entry + '" />');
852
+ tags.push('<script type="module" src="/' + entry + '" crossorigin></script>');
853
+ }
854
+ if (m) {
855
+ // Always inject shared chunks (framework, vinext runtime, entry) and
856
+ // page-specific chunks. The manifest maps module file paths to their
857
+ // associated JS/CSS assets.
858
+ //
859
+ // For page-specific injection, the module IDs may be absolute paths
860
+ // while the manifest uses relative paths. Try both the original ID
861
+ // and a suffix match to find the correct manifest entry.
862
+ var allFiles = [];
863
+
864
+ if (moduleIds && moduleIds.length > 0) {
865
+ // Collect assets for the requested page modules
866
+ for (var mi = 0; mi < moduleIds.length; mi++) {
867
+ var id = moduleIds[mi];
868
+ var files = m[id];
869
+ if (!files) {
870
+ // Absolute path didn't match — try matching by suffix.
871
+ // Manifest keys are relative (e.g. "pages/about.tsx") while
872
+ // moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
873
+ for (var mk in m) {
874
+ if (id.endsWith("/" + mk) || id === mk) {
875
+ files = m[mk];
876
+ break;
877
+ }
878
+ }
879
+ }
880
+ if (files) {
881
+ for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
882
+ }
883
+ }
884
+
885
+ // Also inject shared chunks that every page needs: framework,
886
+ // vinext runtime, and the entry bootstrap. These are identified
887
+ // by scanning all manifest values for chunk filenames containing
888
+ // known prefixes.
889
+ for (var key in m) {
890
+ var vals = m[key];
891
+ if (!vals) continue;
892
+ for (var vi = 0; vi < vals.length; vi++) {
893
+ var file = vals[vi];
894
+ var basename = file.split("/").pop() || "";
895
+ if (
896
+ basename.startsWith("framework-") ||
897
+ basename.startsWith("vinext-") ||
898
+ basename.includes("vinext-client-entry") ||
899
+ basename.includes("vinext-app-browser-entry")
900
+ ) {
901
+ allFiles.push(file);
902
+ }
903
+ }
904
+ }
905
+ } else {
906
+ // No specific modules — include all assets from manifest
907
+ for (var akey in m) {
908
+ var avals = m[akey];
909
+ if (avals) {
910
+ for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
911
+ }
912
+ }
913
+ }
914
+
915
+ for (var ti = 0; ti < allFiles.length; ti++) {
916
+ var tf = allFiles[ti];
917
+ // Normalize: Vite's SSR manifest values include a leading '/'
918
+ // (from base path), but we prepend '/' ourselves when building
919
+ // href/src attributes. Strip any existing leading slash to avoid
920
+ // producing protocol-relative URLs like "//assets/chunk.js".
921
+ // This also ensures consistent keys for the seen-set dedup and
922
+ // lazySet.has() checks (which use values without leading slash).
923
+ if (tf.charAt(0) === '/') tf = tf.slice(1);
924
+ if (seen.has(tf)) continue;
925
+ seen.add(tf);
926
+ if (tf.endsWith(".css")) {
927
+ tags.push('<link rel="stylesheet" href="/' + tf + '" />');
928
+ } else if (tf.endsWith(".js")) {
929
+ // Skip lazy chunks — they are behind dynamic import() boundaries
930
+ // (React.lazy, next/dynamic) and should only be fetched on demand.
931
+ if (lazySet && lazySet.has(tf)) continue;
932
+ tags.push('<link rel="modulepreload" href="/' + tf + '" />');
933
+ tags.push('<script type="module" src="/' + tf + '" crossorigin></script>');
934
+ }
935
+ }
936
+ }
937
+ return tags.join("\\n ");
938
+ }
939
+
940
+ // i18n helpers
941
+ function extractLocale(url) {
942
+ if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
943
+ const pathname = url.split("?")[0];
944
+ const parts = pathname.split("/").filter(Boolean);
945
+ const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
946
+ if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
947
+ const locale = parts[0];
948
+ const rest = "/" + parts.slice(1).join("/");
949
+ return { locale, url: (rest || "/") + query, hadPrefix: true };
950
+ }
951
+ return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
952
+ }
953
+
954
+ function detectLocaleFromHeaders(headers) {
955
+ if (!i18nConfig) return null;
956
+ const acceptLang = headers.get("accept-language");
957
+ if (!acceptLang) return null;
958
+ const langs = acceptLang.split(",").map(function(part) {
959
+ const pieces = part.trim().split(";");
960
+ const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
961
+ return { lang: pieces[0].trim().toLowerCase(), q: q };
962
+ }).sort(function(a, b) { return b.q - a.q; });
963
+ for (let k = 0; k < langs.length; k++) {
964
+ const lang = langs[k].lang;
965
+ for (let j = 0; j < i18nConfig.locales.length; j++) {
966
+ if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
967
+ }
968
+ const prefix = lang.split("-")[0];
969
+ for (let j = 0; j < i18nConfig.locales.length; j++) {
970
+ const loc = i18nConfig.locales[j].toLowerCase();
971
+ if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
972
+ }
973
+ }
974
+ return null;
975
+ }
976
+
977
+ function parseCookieLocaleFromHeader(cookieHeader) {
978
+ if (!i18nConfig || !cookieHeader) return null;
979
+ const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
980
+ if (!match) return null;
981
+ const value = decodeURIComponent(match[1].trim());
982
+ if (i18nConfig.locales.indexOf(value) !== -1) return value;
983
+ return null;
984
+ }
985
+
986
+ function parseCookies(cookieHeader) {
987
+ const cookies = {};
988
+ if (!cookieHeader) return cookies;
989
+ for (const part of cookieHeader.split(";")) {
990
+ const [key, ...rest] = part.split("=");
991
+ if (key) cookies[key.trim()] = rest.join("=").trim();
992
+ }
993
+ return cookies;
994
+ }
995
+
996
+ // Lightweight req/res facade for getServerSideProps and API routes.
997
+ // Next.js pages expect ctx.req/ctx.res with Node-like shapes.
998
+ function createReqRes(request, url, query, body) {
999
+ const headersObj = {};
1000
+ for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
1001
+
1002
+ const req = {
1003
+ method: request.method,
1004
+ url: url,
1005
+ headers: headersObj,
1006
+ query: query,
1007
+ body: body,
1008
+ cookies: parseCookies(request.headers.get("cookie")),
1009
+ };
1010
+
1011
+ let resStatusCode = 200;
1012
+ const resHeaders = {};
1013
+ // set-cookie needs array support (multiple Set-Cookie headers are common)
1014
+ const setCookieHeaders = [];
1015
+ let resBody = null;
1016
+ let ended = false;
1017
+ let resolveResponse;
1018
+ const responsePromise = new Promise(function(r) { resolveResponse = r; });
1019
+
1020
+ const res = {
1021
+ get statusCode() { return resStatusCode; },
1022
+ set statusCode(code) { resStatusCode = code; },
1023
+ writeHead: function(code, headers) {
1024
+ resStatusCode = code;
1025
+ if (headers) {
1026
+ for (const [k, v] of Object.entries(headers)) {
1027
+ if (k.toLowerCase() === "set-cookie") {
1028
+ if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
1029
+ else { setCookieHeaders.push(v); }
1030
+ } else {
1031
+ resHeaders[k] = v;
1032
+ }
1033
+ }
1034
+ }
1035
+ return res;
1036
+ },
1037
+ setHeader: function(name, value) {
1038
+ if (name.toLowerCase() === "set-cookie") {
1039
+ if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
1040
+ else { setCookieHeaders.push(value); }
1041
+ } else {
1042
+ resHeaders[name.toLowerCase()] = value;
1043
+ }
1044
+ return res;
1045
+ },
1046
+ getHeader: function(name) {
1047
+ if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
1048
+ return resHeaders[name.toLowerCase()];
1049
+ },
1050
+ end: function(data) {
1051
+ if (ended) return;
1052
+ ended = true;
1053
+ if (data !== undefined && data !== null) resBody = data;
1054
+ const h = new Headers(resHeaders);
1055
+ for (const c of setCookieHeaders) h.append("set-cookie", c);
1056
+ resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
1057
+ },
1058
+ status: function(code) { resStatusCode = code; return res; },
1059
+ json: function(data) {
1060
+ resHeaders["content-type"] = "application/json";
1061
+ res.end(JSON.stringify(data));
1062
+ },
1063
+ send: function(data) {
1064
+ if (typeof data === "object" && data !== null) { res.json(data); }
1065
+ else { if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain"; res.end(String(data)); }
1066
+ },
1067
+ redirect: function(statusOrUrl, url2) {
1068
+ if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
1069
+ else { res.writeHead(statusOrUrl, { Location: url2 }); }
1070
+ res.end();
1071
+ },
1072
+ };
1073
+
1074
+ return { req, res, responsePromise };
1075
+ }
1076
+
1077
+ /**
1078
+ * Read request body as text with a size limit.
1079
+ * Throws if the body exceeds maxBytes. This prevents DoS via chunked
1080
+ * transfer encoding where Content-Length is absent or spoofed.
1081
+ */
1082
+ async function readBodyWithLimit(request, maxBytes) {
1083
+ if (!request.body) return "";
1084
+ var reader = request.body.getReader();
1085
+ var decoder = new TextDecoder();
1086
+ var chunks = [];
1087
+ var totalSize = 0;
1088
+ for (;;) {
1089
+ var result = await reader.read();
1090
+ if (result.done) break;
1091
+ totalSize += result.value.byteLength;
1092
+ if (totalSize > maxBytes) {
1093
+ reader.cancel();
1094
+ throw new Error("Request body too large");
1095
+ }
1096
+ chunks.push(decoder.decode(result.value, { stream: true }));
1097
+ }
1098
+ chunks.push(decoder.decode());
1099
+ return chunks.join("");
1100
+ }
1101
+
1102
+ export async function renderPage(request, url, manifest) {
1103
+ const localeInfo = extractLocale(url);
1104
+ const locale = localeInfo.locale;
1105
+ const routeUrl = localeInfo.url;
1106
+ const cookieHeader = request.headers.get("cookie") || "";
1107
+
1108
+ // i18n redirect: check NEXT_LOCALE cookie first, then Accept-Language
1109
+ if (i18nConfig && !localeInfo.hadPrefix) {
1110
+ const cookieLocale = parseCookieLocaleFromHeader(cookieHeader);
1111
+ if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
1112
+ return new Response(null, { status: 307, headers: { Location: "/" + cookieLocale + routeUrl } });
1113
+ }
1114
+ if (!cookieLocale && i18nConfig.localeDetection !== false) {
1115
+ const detected = detectLocaleFromHeaders(request.headers);
1116
+ if (detected && detected !== i18nConfig.defaultLocale) {
1117
+ return new Response(null, { status: 307, headers: { Location: "/" + detected + routeUrl } });
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ const match = matchRoute(routeUrl, pageRoutes);
1123
+ if (!match) {
1124
+ return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
1125
+ { status: 404, headers: { "Content-Type": "text/html" } });
1126
+ }
1127
+
1128
+ const { route, params } = match;
1129
+ const cleanupFetchCache = withFetchCache();
1130
+ try {
1131
+ if (typeof setSSRContext === "function") {
1132
+ setSSRContext({
1133
+ pathname: routeUrl.split("?")[0],
1134
+ query: { ...params, ...parseQuery(routeUrl) },
1135
+ asPath: routeUrl,
1136
+ locale: locale,
1137
+ locales: i18nConfig ? i18nConfig.locales : undefined,
1138
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1139
+ });
1140
+ }
1141
+
1142
+ if (i18nConfig) {
1143
+ globalThis.__VINEXT_LOCALE__ = locale;
1144
+ globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
1145
+ globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
1146
+ }
1147
+
1148
+ const pageModule = route.module;
1149
+ const PageComponent = pageModule.default;
1150
+ if (!PageComponent) {
1151
+ return new Response("Page has no default export", { status: 500 });
1152
+ }
1153
+
1154
+ // Handle getStaticPaths for dynamic routes
1155
+ if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
1156
+ const pathsResult = await pageModule.getStaticPaths({
1157
+ locales: i18nConfig ? i18nConfig.locales : [],
1158
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : "",
1159
+ });
1160
+ const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
1161
+
1162
+ if (fallback === false) {
1163
+ const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
1164
+ const isValidPath = paths.some(function(p) {
1165
+ return Object.entries(p.params).every(function(entry) {
1166
+ var key = entry[0], val = entry[1];
1167
+ var actual = params[key];
1168
+ if (Array.isArray(val)) {
1169
+ return Array.isArray(actual) && val.join("/") === actual.join("/");
1170
+ }
1171
+ return String(val) === String(actual);
1172
+ });
1173
+ });
1174
+ if (!isValidPath) {
1175
+ return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
1176
+ { status: 404, headers: { "Content-Type": "text/html" } });
1177
+ }
1178
+ }
1179
+ }
1180
+
1181
+ let pageProps = {};
1182
+ if (typeof pageModule.getServerSideProps === "function") {
1183
+ const { req, res } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
1184
+ const ctx = {
1185
+ params, req, res,
1186
+ query: parseQuery(routeUrl),
1187
+ resolvedUrl: routeUrl,
1188
+ locale: locale,
1189
+ locales: i18nConfig ? i18nConfig.locales : undefined,
1190
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1191
+ };
1192
+ const result = await pageModule.getServerSideProps(ctx);
1193
+ if (result && result.props) pageProps = result.props;
1194
+ if (result && result.redirect) {
1195
+ var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1196
+ return new Response(null, { status: gsspStatus, headers: { Location: result.redirect.destination } });
1197
+ }
1198
+ if (result && result.notFound) {
1199
+ return new Response("404", { status: 404 });
1200
+ }
1201
+ }
1202
+ // Build font Link header early so it's available for ISR cached responses too.
1203
+ // Font preloads are module-level state populated at import time and persist across requests.
1204
+ var _fontLinkHeader = "";
1205
+ var _allFp = [];
1206
+ try {
1207
+ var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
1208
+ var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
1209
+ _allFp = _fpGoogle.concat(_fpLocal);
1210
+ if (_allFp.length > 0) {
1211
+ _fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
1212
+ }
1213
+ } catch (e) { /* font preloads not available */ }
1214
+
1215
+ let isrRevalidateSeconds = null;
1216
+ if (typeof pageModule.getStaticProps === "function") {
1217
+ const pathname = routeUrl.split("?")[0];
1218
+ const cacheKey = "pages:" + (pathname === "/" ? "/" : pathname.replace(/\\/$/, ""));
1219
+ const cached = await isrGet(cacheKey);
1220
+
1221
+ if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
1222
+ var _hitHeaders = {
1223
+ "Content-Type": "text/html", "X-Vinext-Cache": "HIT",
1224
+ "Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
1225
+ };
1226
+ if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
1227
+ return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
1228
+ }
1229
+
1230
+ if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
1231
+ triggerBackgroundRegeneration(cacheKey, async function() {
1232
+ const freshResult = await pageModule.getStaticProps({ params });
1233
+ if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
1234
+ await isrSet(cacheKey, { kind: "PAGES", html: cached.value.value.html, pageData: freshResult.props, headers: undefined, status: undefined }, freshResult.revalidate);
1235
+ }
1236
+ });
1237
+ var _staleHeaders = {
1238
+ "Content-Type": "text/html", "X-Vinext-Cache": "STALE",
1239
+ "Cache-Control": "s-maxage=0, stale-while-revalidate",
1240
+ };
1241
+ if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
1242
+ return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
1243
+ }
1244
+
1245
+ const ctx = {
1246
+ params,
1247
+ locale: locale,
1248
+ locales: i18nConfig ? i18nConfig.locales : undefined,
1249
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
1250
+ };
1251
+ const result = await pageModule.getStaticProps(ctx);
1252
+ if (result && result.props) pageProps = result.props;
1253
+ if (result && result.redirect) {
1254
+ var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1255
+ return new Response(null, { status: gspStatus, headers: { Location: result.redirect.destination } });
1256
+ }
1257
+ if (result && result.notFound) {
1258
+ return new Response("404", { status: 404 });
1259
+ }
1260
+ if (typeof result.revalidate === "number" && result.revalidate > 0) {
1261
+ isrRevalidateSeconds = result.revalidate;
1262
+ }
1263
+ }
1264
+
1265
+ let element;
1266
+ if (AppComponent) {
1267
+ element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1268
+ } else {
1269
+ element = React.createElement(PageComponent, pageProps);
1270
+ }
1271
+
1272
+ if (typeof resetSSRHead === "function") resetSSRHead();
1273
+ if (typeof flushPreloads === "function") await flushPreloads();
1274
+
1275
+ const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
1276
+
1277
+ // Collect SSR font data (Google Font links, font preloads, font-face styles)
1278
+ var fontHeadHTML = "";
1279
+ function _escAttr(s) { return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
1280
+ try {
1281
+ var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
1282
+ for (var fl of fontLinks) { fontHeadHTML += '<link rel="stylesheet" href="' + _escAttr(fl) + '" />\\n '; }
1283
+ } catch (e) { /* next/font/google not used */ }
1284
+ // Emit <link rel="preload"> for all font files (reuse _allFp collected earlier for Link header)
1285
+ for (var fp of _allFp) { fontHeadHTML += '<link rel="preload" href="' + _escAttr(fp.href) + '" as="font" type="' + _escAttr(fp.type) + '" crossorigin />\\n '; }
1286
+ try {
1287
+ var allFontStyles = [];
1288
+ if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
1289
+ if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
1290
+ if (allFontStyles.length > 0) { fontHeadHTML += '<style data-vinext-fonts>' + allFontStyles.join("\\n") + '</style>\\n '; }
1291
+ } catch (e) { /* font styles not available */ }
1292
+
1293
+ const pageModuleIds = route.filePath ? [route.filePath] : [];
1294
+ const assetTags = collectAssetTags(manifest, pageModuleIds);
1295
+ const nextDataPayload = {
1296
+ props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, isFallback: false,
1297
+ };
1298
+ if (i18nConfig) {
1299
+ nextDataPayload.locale = locale;
1300
+ nextDataPayload.locales = i18nConfig.locales;
1301
+ nextDataPayload.defaultLocale = i18nConfig.defaultLocale;
1302
+ }
1303
+ const localeGlobals = i18nConfig
1304
+ ? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
1305
+ ";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
1306
+ ";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(i18nConfig.defaultLocale)
1307
+ : "";
1308
+ const nextDataScript = "<script>window.__NEXT_DATA__ = " + safeJsonStringify(nextDataPayload) + localeGlobals + "</script>";
1309
+
1310
+ // Build the document shell with a placeholder for the streamed body
1311
+ var BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
1312
+ var shellHtml;
1313
+ if (DocumentComponent) {
1314
+ const docElement = React.createElement(DocumentComponent);
1315
+ shellHtml = await renderToStringAsync(docElement);
1316
+ shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
1317
+ if (ssrHeadHTML || assetTags || fontHeadHTML) {
1318
+ shellHtml = shellHtml.replace("</head>", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>");
1319
+ }
1320
+ shellHtml = shellHtml.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
1321
+ if (!shellHtml.includes("__NEXT_DATA__")) {
1322
+ shellHtml = shellHtml.replace("</body>", " " + nextDataScript + "\\n</body>");
1323
+ }
1324
+ } else {
1325
+ shellHtml = "<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"utf-8\\" />\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />\\n " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>\\n<body>\\n <div id=\\"__next\\">" + BODY_MARKER + "</div>\\n " + nextDataScript + "\\n</body>\\n</html>";
1326
+ }
1327
+
1328
+ if (typeof setSSRContext === "function") setSSRContext(null);
1329
+
1330
+ // Split the shell at the body marker
1331
+ var markerIdx = shellHtml.indexOf(BODY_MARKER);
1332
+ var shellPrefix = shellHtml.slice(0, markerIdx);
1333
+ var shellSuffix = shellHtml.slice(markerIdx + BODY_MARKER.length);
1334
+
1335
+ // Start the React body stream — progressive SSR (no allReady wait)
1336
+ var bodyStream = await renderToReadableStream(element);
1337
+ var encoder = new TextEncoder();
1338
+
1339
+ // Create a composite stream: prefix + body + suffix
1340
+ var compositeStream = new ReadableStream({
1341
+ async start(controller) {
1342
+ controller.enqueue(encoder.encode(shellPrefix));
1343
+ var reader = bodyStream.getReader();
1344
+ try {
1345
+ for (;;) {
1346
+ var chunk = await reader.read();
1347
+ if (chunk.done) break;
1348
+ controller.enqueue(chunk.value);
1349
+ }
1350
+ } finally {
1351
+ reader.releaseLock();
1352
+ }
1353
+ controller.enqueue(encoder.encode(shellSuffix));
1354
+ controller.close();
1355
+ }
1356
+ });
1357
+
1358
+ // Cache the rendered HTML for ISR (needs the full string — re-render synchronously)
1359
+ if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
1360
+ // Tee the stream so we can cache and respond simultaneously would be ideal,
1361
+ // but ISR responses are rare on first hit. Re-render to get complete HTML for cache.
1362
+ var isrElement;
1363
+ if (AppComponent) {
1364
+ isrElement = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1365
+ } else {
1366
+ isrElement = React.createElement(PageComponent, pageProps);
1367
+ }
1368
+ var isrHtml = await renderToStringAsync(isrElement);
1369
+ var fullHtml = shellPrefix + isrHtml + shellSuffix;
1370
+ var isrPathname = url.split("?")[0];
1371
+ var isrCacheKey = "pages:" + (isrPathname === "/" ? "/" : isrPathname.replace(/\\/$/, ""));
1372
+ await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
1373
+ }
1374
+
1375
+ const responseHeaders = { "Content-Type": "text/html" };
1376
+ if (isrRevalidateSeconds) {
1377
+ responseHeaders["Cache-Control"] = "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate";
1378
+ responseHeaders["X-Vinext-Cache"] = "MISS";
1379
+ }
1380
+ // Set HTTP Link header for font preloading
1381
+ if (_fontLinkHeader) {
1382
+ responseHeaders["Link"] = _fontLinkHeader;
1383
+ }
1384
+ return new Response(compositeStream, { status: 200, headers: responseHeaders });
1385
+ } catch (e) {
1386
+ console.error("[vinext] SSR error:", e);
1387
+ return new Response("Internal Server Error", { status: 500 });
1388
+ } finally {
1389
+ cleanupFetchCache();
1390
+ }
1391
+ }
1392
+
1393
+ export async function handleApiRoute(request, url) {
1394
+ const match = matchRoute(url, apiRoutes);
1395
+ if (!match) {
1396
+ return new Response("404 - API route not found", { status: 404 });
1397
+ }
1398
+
1399
+ const { route, params } = match;
1400
+ const handler = route.module.default;
1401
+ if (typeof handler !== "function") {
1402
+ return new Response("API route does not export a default function", { status: 500 });
1403
+ }
1404
+
1405
+ const query = { ...params };
1406
+ const qs = url.split("?")[1];
1407
+ if (qs) {
1408
+ for (const [k, v] of new URLSearchParams(qs)) {
1409
+ if (k in query) {
1410
+ // Multi-value: promote to array (Next.js returns string[] for duplicate keys)
1411
+ query[k] = Array.isArray(query[k]) ? query[k].concat(v) : [query[k], v];
1412
+ } else {
1413
+ query[k] = v;
1414
+ }
1415
+ }
1416
+ }
1417
+
1418
+ // Parse request body (enforce 1MB limit to prevent memory exhaustion,
1419
+ // matching Next.js default bodyParser sizeLimit).
1420
+ // Check Content-Length first as a fast path, then enforce on the actual
1421
+ // stream to prevent bypasses via chunked transfer encoding.
1422
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
1423
+ if (contentLength > 1 * 1024 * 1024) {
1424
+ return new Response("Request body too large", { status: 413 });
1425
+ }
1426
+ let body;
1427
+ const ct = request.headers.get("content-type") || "";
1428
+ let rawBody;
1429
+ try { rawBody = await readBodyWithLimit(request, 1 * 1024 * 1024); }
1430
+ catch { return new Response("Request body too large", { status: 413 }); }
1431
+ if (!rawBody) {
1432
+ body = undefined;
1433
+ } else if (ct.includes("application/json")) {
1434
+ try { body = JSON.parse(rawBody); } catch { body = rawBody; }
1435
+ } else {
1436
+ body = rawBody;
1437
+ }
1438
+
1439
+ const { req, res, responsePromise } = createReqRes(request, url, query, body);
1440
+
1441
+ try {
1442
+ await handler(req, res);
1443
+ // If handler didn't call res.end(), end it now.
1444
+ // The end() method is idempotent — safe to call twice.
1445
+ res.end();
1446
+ return await responsePromise;
1447
+ } catch (e) {
1448
+ console.error("[vinext] API error:", e);
1449
+ return new Response("Internal Server Error", { status: 500 });
1450
+ }
1451
+ }
1452
+
1453
+ ${middlewareExportCode}
1454
+ `;
1455
+ }
1456
+ /**
1457
+ * Generate the virtual client hydration entry module.
1458
+ * This is the entry point for `vite build` (client bundle).
1459
+ *
1460
+ * It maps route patterns to dynamic imports of page modules so Vite
1461
+ * code-splits each page into its own chunk. At runtime it reads
1462
+ * __NEXT_DATA__ to determine which page to hydrate.
1463
+ */
1464
+ async function generateClientEntry() {
1465
+ const pageRoutes = await pagesRouter(pagesDir);
1466
+ const hasApp = fs.existsSync(path.join(pagesDir, "_app.tsx")) || fs.existsSync(path.join(pagesDir, "_app.jsx")) || fs.existsSync(path.join(pagesDir, "_app.ts")) || fs.existsSync(path.join(pagesDir, "_app.js"));
1467
+ // Build a map of route pattern -> dynamic import.
1468
+ // Keys must use Next.js bracket format (e.g. "/user/[id]") to match
1469
+ // __NEXT_DATA__.page which is set via patternToNextFormat() during SSR.
1470
+ const loaderEntries = pageRoutes.map((r) => {
1471
+ const absPath = r.filePath.replace(/\\/g, "/");
1472
+ const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
1473
+ return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
1474
+ });
1475
+ const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
1476
+ return `
1477
+ import React from "react";
1478
+ import { hydrateRoot } from "react-dom/client";
1479
+ // Eagerly import the router shim so its module-level popstate listener is
1480
+ // registered. Without this, browser back/forward buttons do nothing because
1481
+ // navigateClient() is never invoked on history changes.
1482
+ import "next/router";
1483
+
1484
+ const pageLoaders = {
1485
+ ${loaderEntries.join(",\n")}
1486
+ };
1487
+
1488
+ async function hydrate() {
1489
+ const nextData = window.__NEXT_DATA__;
1490
+ if (!nextData) {
1491
+ console.error("[vinext] No __NEXT_DATA__ found");
1492
+ return;
1493
+ }
1494
+
1495
+ const { pageProps } = nextData.props;
1496
+ const loader = pageLoaders[nextData.page];
1497
+ if (!loader) {
1498
+ console.error("[vinext] No page loader for route:", nextData.page);
1499
+ return;
1500
+ }
1501
+
1502
+ const pageModule = await loader();
1503
+ const PageComponent = pageModule.default;
1504
+ if (!PageComponent) {
1505
+ console.error("[vinext] Page module has no default export");
1506
+ return;
1507
+ }
1508
+
1509
+ let element;
1510
+ ${hasApp ? `
1511
+ try {
1512
+ const appModule = await import(${JSON.stringify(appFileBase)});
1513
+ const AppComponent = appModule.default;
1514
+ window.__VINEXT_APP__ = AppComponent;
1515
+ element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
1516
+ } catch {
1517
+ element = React.createElement(PageComponent, pageProps);
1518
+ }
1519
+ ` : `
1520
+ element = React.createElement(PageComponent, pageProps);
1521
+ `}
1522
+
1523
+ const container = document.getElementById("__next");
1524
+ if (!container) {
1525
+ console.error("[vinext] No #__next element found");
1526
+ return;
1527
+ }
1528
+
1529
+ const root = hydrateRoot(container, element);
1530
+ window.__VINEXT_ROOT__ = root;
1531
+ }
1532
+
1533
+ hydrate();
1534
+ `;
1535
+ }
1536
+ // Auto-register @vitejs/plugin-rsc when App Router is detected.
1537
+ // Check eagerly at call time using the same heuristic as config().
1538
+ // Must mirror the full detection logic: check {base}/app then {base}/src/app.
1539
+ const autoRsc = options.rsc !== false;
1540
+ const earlyBaseDir = options.appDir ?? process.cwd();
1541
+ const earlyAppDirExists = fs.existsSync(path.join(earlyBaseDir, "app")) ||
1542
+ fs.existsSync(path.join(earlyBaseDir, "src", "app"));
1543
+ // IMPORTANT: Resolve @vitejs/plugin-rsc subpath imports from the user's
1544
+ // project root, not from vinext's own package location. When vinext is
1545
+ // installed via symlink (npm file: deps, pnpm workspace:*), a bare
1546
+ // import() resolves from vinext's realpath, which can find a different
1547
+ // copy of the RSC plugin (and transitively a different copy of vite).
1548
+ // This causes instanceof RunnableDevEnvironment checks to fail at
1549
+ // runtime because the Vite server and the RSC plugin end up with
1550
+ // different class identities. Resolving from the project root ensures a
1551
+ // single shared vite instance.
1552
+ //
1553
+ // Pre-resolve both the main plugin and the /transforms subpath eagerly
1554
+ // so all import() calls in this module use consistent resolution.
1555
+ const earlyRequire = createRequire(path.join(earlyBaseDir, "package.json"));
1556
+ let resolvedRscPath = null;
1557
+ let resolvedRscTransformsPath = null;
1558
+ try {
1559
+ resolvedRscPath = earlyRequire.resolve("@vitejs/plugin-rsc");
1560
+ resolvedRscTransformsPath = earlyRequire.resolve("@vitejs/plugin-rsc/transforms");
1561
+ }
1562
+ catch {
1563
+ // @vitejs/plugin-rsc not installed — that's fine for Pages Router
1564
+ // projects. If App Router is detected, the error is thrown below.
1565
+ }
1566
+ // If app/ exists and auto-RSC is enabled, create a lazy Promise that
1567
+ // resolves to the configured RSC plugin array. Vite's asyncFlatten
1568
+ // will resolve this before processing the plugin list.
1569
+ let rscPluginPromise = null;
1570
+ if (earlyAppDirExists && autoRsc) {
1571
+ if (!resolvedRscPath) {
1572
+ throw new Error("vinext: App Router detected but @vitejs/plugin-rsc is not installed.\n" +
1573
+ "Run: npm install -D @vitejs/plugin-rsc");
1574
+ }
1575
+ const rscImport = import(resolvedRscPath);
1576
+ rscPluginPromise = rscImport
1577
+ .then((mod) => {
1578
+ const rsc = mod.default;
1579
+ return rsc({
1580
+ entries: {
1581
+ rsc: VIRTUAL_RSC_ENTRY,
1582
+ ssr: VIRTUAL_APP_SSR_ENTRY,
1583
+ client: VIRTUAL_APP_BROWSER_ENTRY,
1584
+ },
1585
+ });
1586
+ });
1587
+ }
1588
+ const plugins = [
1589
+ // Resolve tsconfig paths/baseUrl aliases so real-world Next.js repos
1590
+ // that use @/*, #/*, or baseUrl imports work out of the box.
1591
+ tsconfigPaths(),
1592
+ {
1593
+ name: "vinext:config",
1594
+ enforce: "pre",
1595
+ async config(config) {
1596
+ root = config.root ?? process.cwd();
1597
+ // Resolve the base directory for app/pages detection.
1598
+ // If appDir is provided, resolve it (supports both relative and absolute paths).
1599
+ // If not provided, auto-detect: check root first, then src/ subdirectory.
1600
+ let baseDir;
1601
+ if (options.appDir) {
1602
+ baseDir = path.isAbsolute(options.appDir)
1603
+ ? options.appDir
1604
+ : path.resolve(root, options.appDir);
1605
+ }
1606
+ else {
1607
+ // Auto-detect: prefer root-level app/ and pages/, fall back to src/
1608
+ const hasRootApp = fs.existsSync(path.join(root, "app"));
1609
+ const hasRootPages = fs.existsSync(path.join(root, "pages"));
1610
+ const hasSrcApp = fs.existsSync(path.join(root, "src", "app"));
1611
+ const hasSrcPages = fs.existsSync(path.join(root, "src", "pages"));
1612
+ if (hasRootApp || hasRootPages) {
1613
+ baseDir = root;
1614
+ }
1615
+ else if (hasSrcApp || hasSrcPages) {
1616
+ baseDir = path.join(root, "src");
1617
+ }
1618
+ else {
1619
+ baseDir = root;
1620
+ }
1621
+ }
1622
+ pagesDir = path.join(baseDir, "pages");
1623
+ appDir = path.join(baseDir, "app");
1624
+ hasPagesDir = fs.existsSync(pagesDir);
1625
+ hasAppDir = fs.existsSync(appDir);
1626
+ middlewarePath = findMiddlewareFile(root);
1627
+ instrumentationPath = findInstrumentationFile(root);
1628
+ // Load next.config.js if present (always from project root, not src/)
1629
+ const rawConfig = await loadNextConfig(root);
1630
+ nextConfig = await resolveNextConfig(rawConfig);
1631
+ // Merge env from next.config.js with NEXT_PUBLIC_* env vars
1632
+ const defines = getNextPublicEnvDefines();
1633
+ for (const [key, value] of Object.entries(nextConfig.env)) {
1634
+ defines[`process.env.${key}`] = JSON.stringify(value);
1635
+ }
1636
+ // Expose basePath to client-side code
1637
+ defines["process.env.__NEXT_ROUTER_BASEPATH"] = JSON.stringify(nextConfig.basePath);
1638
+ // Expose image remote patterns for validation in next/image shim
1639
+ defines["process.env.__VINEXT_IMAGE_REMOTE_PATTERNS"] = JSON.stringify(JSON.stringify(nextConfig.images?.remotePatterns ?? []));
1640
+ defines["process.env.__VINEXT_IMAGE_DOMAINS"] = JSON.stringify(JSON.stringify(nextConfig.images?.domains ?? []));
1641
+ // Draft mode secret — generated once at build time so the
1642
+ // __prerender_bypass cookie is consistent across all server
1643
+ // instances (e.g. multiple Cloudflare Workers isolates).
1644
+ defines["process.env.__VINEXT_DRAFT_SECRET"] = JSON.stringify(crypto.randomUUID());
1645
+ // Build the shim alias map — used by both resolve.alias and resolveId
1646
+ // (resolveId handles .js extension variants for libraries like nuqs)
1647
+ nextShimMap = {
1648
+ "next/link": path.join(shimsDir, "link"),
1649
+ "next/head": path.join(shimsDir, "head"),
1650
+ "next/router": path.join(shimsDir, "router"),
1651
+ "next/image": path.join(shimsDir, "image"),
1652
+ "next/legacy/image": path.join(shimsDir, "legacy-image"),
1653
+ "next/dynamic": path.join(shimsDir, "dynamic"),
1654
+ "next/app": path.join(shimsDir, "app"),
1655
+ "next/document": path.join(shimsDir, "document"),
1656
+ "next/config": path.join(shimsDir, "config"),
1657
+ "next/script": path.join(shimsDir, "script"),
1658
+ "next/server": path.join(shimsDir, "server"),
1659
+ "next/navigation": path.join(shimsDir, "navigation"),
1660
+ "next/headers": path.join(shimsDir, "headers"),
1661
+ "next/font/google": path.join(shimsDir, "font-google"),
1662
+ "next/font/local": path.join(shimsDir, "font-local"),
1663
+ "next/cache": path.join(shimsDir, "cache"),
1664
+ "next/form": path.join(shimsDir, "form"),
1665
+ "next/og": path.join(shimsDir, "og"),
1666
+ "next/web-vitals": path.join(shimsDir, "web-vitals"),
1667
+ "next/amp": path.join(shimsDir, "amp"),
1668
+ "next/error": path.join(shimsDir, "error"),
1669
+ "next/constants": path.join(shimsDir, "constants"),
1670
+ // Internal next/dist/* paths used by popular libraries
1671
+ // (next-intl, @clerk/nextjs, @sentry/nextjs, next-nprogress-bar, etc.)
1672
+ "next/dist/shared/lib/app-router-context.shared-runtime": path.join(shimsDir, "internal", "app-router-context"),
1673
+ "next/dist/shared/lib/app-router-context": path.join(shimsDir, "internal", "app-router-context"),
1674
+ "next/dist/shared/lib/router-context.shared-runtime": path.join(shimsDir, "internal", "router-context"),
1675
+ "next/dist/shared/lib/utils": path.join(shimsDir, "internal", "utils"),
1676
+ "next/dist/server/api-utils": path.join(shimsDir, "internal", "api-utils"),
1677
+ "next/dist/server/web/spec-extension/cookies": path.join(shimsDir, "internal", "cookies"),
1678
+ "next/dist/compiled/@edge-runtime/cookies": path.join(shimsDir, "internal", "cookies"),
1679
+ "next/dist/server/app-render/work-unit-async-storage.external": path.join(shimsDir, "internal", "work-unit-async-storage"),
1680
+ "next/dist/client/components/work-unit-async-storage.external": path.join(shimsDir, "internal", "work-unit-async-storage"),
1681
+ "next/dist/client/components/request-async-storage.external": path.join(shimsDir, "internal", "work-unit-async-storage"),
1682
+ "next/dist/client/components/request-async-storage": path.join(shimsDir, "internal", "work-unit-async-storage"),
1683
+ // Re-export public modules for internal path imports
1684
+ "next/dist/client/components/navigation": path.join(shimsDir, "navigation"),
1685
+ "next/dist/server/config-shared": path.join(shimsDir, "internal", "utils"),
1686
+ // server-only / client-only marker packages
1687
+ "server-only": path.join(shimsDir, "server-only"),
1688
+ "client-only": path.join(shimsDir, "client-only"),
1689
+ "vinext/error-boundary": path.join(shimsDir, "error-boundary"),
1690
+ "vinext/layout-segment-context": path.join(shimsDir, "layout-segment-context"),
1691
+ "vinext/metadata": path.join(shimsDir, "metadata"),
1692
+ "vinext/fetch-cache": path.join(shimsDir, "fetch-cache"),
1693
+ "vinext/cache-runtime": path.join(shimsDir, "cache-runtime"),
1694
+ "vinext/navigation-state": path.join(shimsDir, "navigation-state"),
1695
+ "vinext/router-state": path.join(shimsDir, "router-state"),
1696
+ "vinext/head-state": path.join(shimsDir, "head-state"),
1697
+ "vinext/instrumentation": path.resolve(__dirname, "server", "instrumentation"),
1698
+ "vinext/html": path.resolve(__dirname, "server", "html"),
1699
+ };
1700
+ // Detect if Cloudflare's vite plugin is present — if so, skip
1701
+ // SSR externals (Workers bundle everything, can't have Node.js externals).
1702
+ const pluginsFlat = [];
1703
+ function flattenPlugins(arr) {
1704
+ for (const p of arr) {
1705
+ if (Array.isArray(p))
1706
+ flattenPlugins(p);
1707
+ else if (p)
1708
+ pluginsFlat.push(p);
1709
+ }
1710
+ }
1711
+ flattenPlugins(config.plugins ?? []);
1712
+ hasCloudflarePlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" && (p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")));
1713
+ // Resolve PostCSS string plugin names that Vite can't handle.
1714
+ // Next.js projects commonly use array-form plugins like
1715
+ // `plugins: ["@tailwindcss/postcss"]` which postcss-load-config
1716
+ // doesn't resolve (only object-form keys are resolved). We detect
1717
+ // this and resolve the strings to actual plugin functions, then
1718
+ // inject via css.postcss so Vite uses the resolved plugins.
1719
+ // Only do this if the user hasn't already set css.postcss inline.
1720
+ let postcssOverride;
1721
+ if (!config.css?.postcss || typeof config.css.postcss === "string") {
1722
+ postcssOverride = await resolvePostcssStringPlugins(root);
1723
+ }
1724
+ // Auto-inject @mdx-js/rollup when MDX files exist and no MDX plugin is
1725
+ // already configured. Applies remark/rehype plugins from next.config.
1726
+ const hasMdxPlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" &&
1727
+ (p.name === "@mdx-js/rollup" || p.name === "mdx"));
1728
+ const mdxPlugins = [];
1729
+ if (!hasMdxPlugin && hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null)) {
1730
+ try {
1731
+ const mdxRollup = await import("@mdx-js/rollup");
1732
+ const mdxPlugin = mdxRollup.default ?? mdxRollup;
1733
+ const mdxOpts = {};
1734
+ if (nextConfig.mdx) {
1735
+ if (nextConfig.mdx.remarkPlugins)
1736
+ mdxOpts.remarkPlugins = nextConfig.mdx.remarkPlugins;
1737
+ if (nextConfig.mdx.rehypePlugins)
1738
+ mdxOpts.rehypePlugins = nextConfig.mdx.rehypePlugins;
1739
+ if (nextConfig.mdx.recmaPlugins)
1740
+ mdxOpts.recmaPlugins = nextConfig.mdx.recmaPlugins;
1741
+ }
1742
+ mdxPlugins.push(mdxPlugin(mdxOpts));
1743
+ if (nextConfig.mdx) {
1744
+ console.log("[vinext] Auto-injected @mdx-js/rollup with remark/rehype plugins from next.config");
1745
+ }
1746
+ else {
1747
+ console.log("[vinext] Auto-injected @mdx-js/rollup for MDX support");
1748
+ }
1749
+ }
1750
+ catch {
1751
+ // @mdx-js/rollup not installed — warn but don't fail
1752
+ console.warn("[vinext] MDX files detected but @mdx-js/rollup is not installed. " +
1753
+ "Install it with: npm install -D @mdx-js/rollup");
1754
+ }
1755
+ }
1756
+ // Detect if this is a standalone SSR build (set by `vite build --ssr`
1757
+ // or `build.ssr` in config). SSR builds must NOT use manualChunks
1758
+ // because they use inlineDynamicImports which is incompatible.
1759
+ const isSSR = !!config.build?.ssr;
1760
+ // Detect if this is a multi-environment build (App Router or Cloudflare).
1761
+ // In multi-env builds, manualChunks must only be set per-environment
1762
+ // (on the client env), not globally — otherwise it leaks into RSC/SSR
1763
+ // environments where it can cause asset resolution issues.
1764
+ const isMultiEnv = hasAppDir || hasCloudflarePlugin;
1765
+ const viteConfig = {
1766
+ // Disable Vite's default HTML serving - we handle all routing
1767
+ appType: "custom",
1768
+ build: {
1769
+ rollupOptions: {
1770
+ // Suppress "Module level directives cause errors when bundled"
1771
+ // warnings for "use client" / "use server" directives. Our shims
1772
+ // and third-party libraries legitimately use these directives;
1773
+ // they are handled by the RSC plugin and are harmless in the
1774
+ // final bundle. We preserve any user-supplied onwarn so custom
1775
+ // warning handling is not lost.
1776
+ onwarn: (() => {
1777
+ const userOnwarn = config.build?.rollupOptions?.onwarn;
1778
+ return (warning, defaultHandler) => {
1779
+ if (warning.code === "MODULE_LEVEL_DIRECTIVE" &&
1780
+ (warning.message?.includes('"use client"') ||
1781
+ warning.message?.includes('"use server"'))) {
1782
+ return;
1783
+ }
1784
+ if (userOnwarn) {
1785
+ userOnwarn(warning, defaultHandler);
1786
+ }
1787
+ else {
1788
+ defaultHandler(warning);
1789
+ }
1790
+ };
1791
+ })(),
1792
+ // Enable aggressive tree-shaking for client builds.
1793
+ // See clientTreeshakeConfig for rationale.
1794
+ // Only apply globally for standalone client builds (Pages Router
1795
+ // CLI). For multi-environment builds (App Router, Cloudflare),
1796
+ // treeshake is set per-environment on the client env below to
1797
+ // avoid leaking into RSC/SSR environments where
1798
+ // moduleSideEffects: 'no-external' could drop server packages
1799
+ // that rely on module-level side effects.
1800
+ ...(!isSSR && !isMultiEnv ? { treeshake: clientTreeshakeConfig } : {}),
1801
+ // Code-split client bundles: separate framework (React/ReactDOM),
1802
+ // vinext runtime (shims), and vendor packages into their own
1803
+ // chunks so pages only load the JS they need.
1804
+ // Only apply globally for standalone client builds (CLI Pages
1805
+ // Router). For multi-environment builds (App Router, Cloudflare),
1806
+ // manualChunks is set per-environment on the client env below
1807
+ // to avoid leaking into RSC/SSR environments.
1808
+ ...(!isSSR && !isMultiEnv ? { output: clientOutputConfig } : {}),
1809
+ },
1810
+ },
1811
+ // Let OPTIONS requests pass through Vite's CORS middleware to our
1812
+ // route handlers so they can set the Allow header and run user-defined
1813
+ // OPTIONS handlers. Without this, Vite's CORS middleware responds to
1814
+ // OPTIONS with a 204 before the request reaches vinext's handler.
1815
+ server: { cors: { preflightContinue: true } },
1816
+ // Externalize React packages from SSR transform — they are CJS and
1817
+ // must be loaded natively by Node, not through Vite's ESM evaluator.
1818
+ // Skip when targeting Cloudflare Workers (they bundle everything).
1819
+ ...(hasCloudflarePlugin ? {} : {
1820
+ ssr: {
1821
+ external: ["react", "react-dom", "react-dom/server"],
1822
+ },
1823
+ }),
1824
+ resolve: {
1825
+ alias: nextShimMap,
1826
+ // Dedupe React packages to prevent dual-instance errors.
1827
+ // When vinext is linked (npm link / bun link) or any dependency
1828
+ // brings its own React copy, multiple React instances can load,
1829
+ // causing cryptic "Invalid hook call" errors. This is a no-op
1830
+ // when only one copy exists.
1831
+ dedupe: [
1832
+ "react",
1833
+ "react-dom",
1834
+ "react/jsx-runtime",
1835
+ "react/jsx-dev-runtime",
1836
+ ],
1837
+ },
1838
+ // Enable JSX in .tsx/.jsx files
1839
+ // Vite 7 uses `esbuild` for transforms, Vite 8+ uses `oxc`
1840
+ ...(getViteMajorVersion() >= 8
1841
+ ? { oxc: { jsx: { runtime: "automatic" } } }
1842
+ : { esbuild: { jsx: "automatic" } }),
1843
+ // Define env vars for client bundle
1844
+ define: defines,
1845
+ // Set base path if configured
1846
+ ...(nextConfig.basePath ? { base: nextConfig.basePath + "/" } : {}),
1847
+ // Inject resolved PostCSS plugins if string names were found
1848
+ ...(postcssOverride ? { css: { postcss: postcssOverride } } : {}),
1849
+ };
1850
+ // If app/ directory exists, configure RSC environments
1851
+ if (hasAppDir) {
1852
+ // Compute optimizeDeps.entries so Vite discovers server-side
1853
+ // dependencies at startup instead of on first request. Without
1854
+ // this, deps imported in rsc/ssr environments are found lazily,
1855
+ // causing re-optimisation cascades and runtime errors (e.g.
1856
+ // "Invalid hook call" from duplicate React instances).
1857
+ // The entries must be relative to the project root.
1858
+ const relAppDir = path.relative(root, appDir);
1859
+ const appEntries = [
1860
+ `${relAppDir}/**/*.{tsx,ts,jsx,js}`,
1861
+ ];
1862
+ viteConfig.environments = {
1863
+ rsc: {
1864
+ ...(hasCloudflarePlugin ? {} : {
1865
+ resolve: {
1866
+ // Externalize native/heavy packages so the RSC environment
1867
+ // loads them natively via Node rather than through Vite's
1868
+ // ESM module evaluator (which can't handle native addons).
1869
+ // Note: Do NOT externalize react/react-dom here — they must
1870
+ // be bundled with the "react-server" condition for RSC.
1871
+ // Skip when targeting Cloudflare Workers.
1872
+ external: [
1873
+ "satori",
1874
+ "@resvg/resvg-js",
1875
+ "yoga-wasm-web",
1876
+ ],
1877
+ },
1878
+ }),
1879
+ optimizeDeps: {
1880
+ entries: appEntries,
1881
+ },
1882
+ build: {
1883
+ outDir: "dist/server",
1884
+ rollupOptions: {
1885
+ input: { index: VIRTUAL_RSC_ENTRY },
1886
+ },
1887
+ },
1888
+ },
1889
+ ssr: {
1890
+ optimizeDeps: {
1891
+ entries: appEntries,
1892
+ },
1893
+ build: {
1894
+ outDir: "dist/server/ssr",
1895
+ rollupOptions: {
1896
+ input: { index: VIRTUAL_APP_SSR_ENTRY },
1897
+ },
1898
+ },
1899
+ },
1900
+ client: {
1901
+ optimizeDeps: {
1902
+ // react and react-dom are framework dependencies used for
1903
+ // hydration. They aren't crawled from app/ source files so
1904
+ // must be pre-included to prevent late discovery and page
1905
+ // reloads during development.
1906
+ include: ["react", "react-dom", "react-dom/client"],
1907
+ },
1908
+ build: {
1909
+ // When targeting Cloudflare Workers, enable manifest generation
1910
+ // so the vinext:cloudflare-build closeBundle hook can read the
1911
+ // client build manifest, compute lazy chunks (only reachable
1912
+ // via dynamic imports), and inject __VINEXT_LAZY_CHUNKS__ into
1913
+ // the worker entry. Without this, all chunks are modulepreloaded
1914
+ // on every page — defeating code-splitting for React.lazy() and
1915
+ // next/dynamic boundaries.
1916
+ ...(hasCloudflarePlugin ? { manifest: true } : {}),
1917
+ rollupOptions: {
1918
+ input: { index: VIRTUAL_APP_BROWSER_ENTRY },
1919
+ output: clientOutputConfig,
1920
+ treeshake: clientTreeshakeConfig,
1921
+ },
1922
+ },
1923
+ },
1924
+ };
1925
+ }
1926
+ else if (hasCloudflarePlugin) {
1927
+ // Pages Router on Cloudflare Workers: add a client environment
1928
+ // so the multi-environment build produces client JS bundles
1929
+ // alongside the worker. Without this, only the worker is built
1930
+ // and there's no client-side hydration.
1931
+ viteConfig.environments = {
1932
+ client: {
1933
+ build: {
1934
+ manifest: true,
1935
+ ssrManifest: true,
1936
+ rollupOptions: {
1937
+ input: { index: VIRTUAL_CLIENT_ENTRY },
1938
+ output: clientOutputConfig,
1939
+ treeshake: clientTreeshakeConfig,
1940
+ },
1941
+ },
1942
+ },
1943
+ };
1944
+ }
1945
+ // Add auto-injected MDX plugin if needed
1946
+ if (mdxPlugins.length > 0) {
1947
+ viteConfig.plugins = mdxPlugins;
1948
+ }
1949
+ return viteConfig;
1950
+ },
1951
+ configResolved(config) {
1952
+ // Detect double RSC plugin registration. When vinext auto-injects
1953
+ // @vitejs/plugin-rsc AND the user also registers it manually, the
1954
+ // RSC transform pipeline runs twice — doubling build time.
1955
+ // Rather than trying to magically fix this at runtime, fail fast
1956
+ // with a clear error telling the user how to fix their config.
1957
+ if (rscPluginPromise) {
1958
+ // Count top-level RSC plugins (name === "rsc") — each call to
1959
+ // the rsc() factory produces exactly one plugin with this name.
1960
+ const rscRootPlugins = config.plugins.filter((p) => p && p.name === "rsc");
1961
+ if (rscRootPlugins.length > 1) {
1962
+ throw new Error("[vinext] Duplicate @vitejs/plugin-rsc detected.\n" +
1963
+ " vinext auto-registers @vitejs/plugin-rsc when app/ is detected.\n" +
1964
+ " Your config also registers it manually, which doubles build time.\n\n" +
1965
+ " Fix: remove the explicit rsc() call from your plugins array.\n" +
1966
+ " Or: pass rsc: false to vinext() if you want to configure rsc() yourself.");
1967
+ }
1968
+ }
1969
+ },
1970
+ resolveId: {
1971
+ // Hook filter: only invoke JS for next/* imports and virtual:vinext-* modules.
1972
+ // Matches "next/navigation", "next/router.js", "virtual:vinext-rsc-entry",
1973
+ // and \0-prefixed re-imports from @vitejs/plugin-rsc.
1974
+ filter: {
1975
+ id: /(?:next\/|virtual:vinext-)/,
1976
+ },
1977
+ handler(id) {
1978
+ // Strip \0 prefix if present — @vitejs/plugin-rsc's generated
1979
+ // browser entry imports our virtual module using the already-resolved
1980
+ // ID (with \0 prefix). We need to re-resolve it so the client
1981
+ // environment's import-analysis can find it.
1982
+ const cleanId = id.startsWith("\0") ? id.slice(1) : id;
1983
+ // Handle next/* imports with .js extension (e.g. "next/navigation.js")
1984
+ // Libraries like nuqs import "next/navigation.js" which doesn't match
1985
+ // our resolve.alias for "next/navigation". Strip the .js and resolve
1986
+ // through our shim map, appending .js to the resolved path.
1987
+ if (cleanId.startsWith("next/") && cleanId.endsWith(".js")) {
1988
+ const withoutExt = cleanId.slice(0, -3);
1989
+ if (nextShimMap[withoutExt]) {
1990
+ const shimPath = nextShimMap[withoutExt];
1991
+ // Alias values don't include .js — append it for resolveId
1992
+ return shimPath.endsWith(".js") ? shimPath : shimPath + ".js";
1993
+ }
1994
+ }
1995
+ // Pages Router virtual modules
1996
+ if (cleanId === VIRTUAL_SERVER_ENTRY)
1997
+ return RESOLVED_SERVER_ENTRY;
1998
+ if (cleanId === VIRTUAL_CLIENT_ENTRY)
1999
+ return RESOLVED_CLIENT_ENTRY;
2000
+ if (cleanId.endsWith("/" + VIRTUAL_SERVER_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_SERVER_ENTRY)) {
2001
+ return RESOLVED_SERVER_ENTRY;
2002
+ }
2003
+ if (cleanId.endsWith("/" + VIRTUAL_CLIENT_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_CLIENT_ENTRY)) {
2004
+ return RESOLVED_CLIENT_ENTRY;
2005
+ }
2006
+ // App Router virtual modules
2007
+ if (cleanId === VIRTUAL_RSC_ENTRY)
2008
+ return RESOLVED_RSC_ENTRY;
2009
+ if (cleanId === VIRTUAL_APP_SSR_ENTRY)
2010
+ return RESOLVED_APP_SSR_ENTRY;
2011
+ if (cleanId === VIRTUAL_APP_BROWSER_ENTRY)
2012
+ return RESOLVED_APP_BROWSER_ENTRY;
2013
+ if (cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_RSC_ENTRY)) {
2014
+ return RESOLVED_RSC_ENTRY;
2015
+ }
2016
+ if (cleanId.endsWith("/" + VIRTUAL_APP_SSR_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_APP_SSR_ENTRY)) {
2017
+ return RESOLVED_APP_SSR_ENTRY;
2018
+ }
2019
+ if (cleanId.endsWith("/" + VIRTUAL_APP_BROWSER_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_APP_BROWSER_ENTRY)) {
2020
+ return RESOLVED_APP_BROWSER_ENTRY;
2021
+ }
2022
+ },
2023
+ },
2024
+ async load(id) {
2025
+ // Pages Router virtual modules
2026
+ if (id === RESOLVED_SERVER_ENTRY) {
2027
+ return await generateServerEntry();
2028
+ }
2029
+ if (id === RESOLVED_CLIENT_ENTRY) {
2030
+ return await generateClientEntry();
2031
+ }
2032
+ // App Router virtual modules
2033
+ if (id === RESOLVED_RSC_ENTRY && hasAppDir) {
2034
+ const routes = await appRouter(appDir);
2035
+ const metaRoutes = scanMetadataFiles(appDir);
2036
+ // Check for global-error.tsx at app root
2037
+ const globalErrorPath = findFileWithExts(appDir, "global-error");
2038
+ return generateRscEntry(appDir, routes, middlewarePath, metaRoutes, globalErrorPath, nextConfig?.basePath, nextConfig?.trailingSlash, {
2039
+ redirects: nextConfig?.redirects,
2040
+ rewrites: nextConfig?.rewrites,
2041
+ headers: nextConfig?.headers,
2042
+ allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
2043
+ });
2044
+ }
2045
+ if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
2046
+ return generateSsrEntry();
2047
+ }
2048
+ if (id === RESOLVED_APP_BROWSER_ENTRY && hasAppDir) {
2049
+ return generateBrowserEntry();
2050
+ }
2051
+ },
2052
+ },
2053
+ // Shim React canary/experimental APIs (ViewTransition, addTransitionType)
2054
+ // that exist in Next.js's bundled React canary but not in stable React 19.
2055
+ // Provides graceful no-op fallbacks so projects using these APIs degrade
2056
+ // instead of crashing with "does not provide an export named 'ViewTransition'".
2057
+ {
2058
+ name: "vinext:react-canary",
2059
+ enforce: "pre",
2060
+ resolveId(id) {
2061
+ if (id === "virtual:vinext-react-canary")
2062
+ return "\0virtual:vinext-react-canary";
2063
+ },
2064
+ load(id) {
2065
+ if (id === "\0virtual:vinext-react-canary") {
2066
+ return [
2067
+ `export * from "react";`,
2068
+ `export { default } from "react";`,
2069
+ `import * as _React from "react";`,
2070
+ `export const ViewTransition = _React.ViewTransition || function ViewTransition({ children }) { return children; };`,
2071
+ `export const addTransitionType = _React.addTransitionType || function addTransitionType() {};`,
2072
+ ].join("\n");
2073
+ }
2074
+ },
2075
+ transform(code, id) {
2076
+ // Only transform user source files, not node_modules or virtual modules
2077
+ if (id.includes("node_modules"))
2078
+ return null;
2079
+ if (id.startsWith("\0"))
2080
+ return null;
2081
+ if (!/\.(tsx?|jsx?|mjs)$/.test(id))
2082
+ return null;
2083
+ // Quick check: does this file reference canary APIs and import from "react"?
2084
+ if (!(code.includes("ViewTransition") || code.includes("addTransitionType")) ||
2085
+ !/from\s+['"]react['"]/.test(code)) {
2086
+ return null;
2087
+ }
2088
+ // Only rewrite if the import actually destructures a canary API
2089
+ const canaryImportRegex = /import\s*\{[^}]*(ViewTransition|addTransitionType)[^}]*\}\s*from\s*['"]react['"]/;
2090
+ if (!canaryImportRegex.test(code))
2091
+ return null;
2092
+ // Rewrite all `from "react"` / `from 'react'` to use the canary shim.
2093
+ // This is safe because the virtual module re-exports everything from
2094
+ // react, so non-canary imports continue to work.
2095
+ const result = code.replace(/from\s*['"]react['"]/g, 'from "virtual:vinext-react-canary"');
2096
+ if (result !== code) {
2097
+ return { code: result, map: null };
2098
+ }
2099
+ return null;
2100
+ },
2101
+ },
2102
+ {
2103
+ name: "vinext:pages-router",
2104
+ // HMR: trigger full-reload for Pages Router page changes.
2105
+ // Without @vitejs/plugin-react (React Fast Refresh), component edits
2106
+ // can't be hot-updated. In theory Vite's default propagation should
2107
+ // reach the root and trigger a full-reload, but the Pages Router
2108
+ // injects hydration via inline <script type="module"> which may not
2109
+ // be tracked in the module graph. Explicitly sending full-reload
2110
+ // ensures changes are always reflected in the browser.
2111
+ hotUpdate(options) {
2112
+ if (!hasPagesDir || hasAppDir)
2113
+ return;
2114
+ const ext = /\.(tsx?|jsx?|mdx)$/;
2115
+ if (options.file.startsWith(pagesDir) && ext.test(options.file)) {
2116
+ options.server.environments.client.hot.send({ type: "full-reload" });
2117
+ return [];
2118
+ }
2119
+ },
2120
+ configureServer(server) {
2121
+ // Watch pages directory for file additions/removals to invalidate route cache.
2122
+ const pageExtensions = /\.(tsx?|jsx?|mdx)$/;
2123
+ server.watcher.on("add", (filePath) => {
2124
+ if (hasPagesDir && filePath.startsWith(pagesDir) && pageExtensions.test(filePath)) {
2125
+ invalidateRouteCache(pagesDir);
2126
+ }
2127
+ if (hasAppDir && filePath.startsWith(appDir) && pageExtensions.test(filePath)) {
2128
+ invalidateAppRouteCache();
2129
+ }
2130
+ });
2131
+ server.watcher.on("unlink", (filePath) => {
2132
+ if (hasPagesDir && filePath.startsWith(pagesDir) && pageExtensions.test(filePath)) {
2133
+ invalidateRouteCache(pagesDir);
2134
+ }
2135
+ if (hasAppDir && filePath.startsWith(appDir) && pageExtensions.test(filePath)) {
2136
+ invalidateAppRouteCache();
2137
+ }
2138
+ });
2139
+ // Run instrumentation.ts register() if present (once at server startup)
2140
+ if (instrumentationPath) {
2141
+ runInstrumentation(server, instrumentationPath).catch((err) => {
2142
+ console.error("[vinext] Instrumentation error:", err);
2143
+ });
2144
+ }
2145
+ // Return a function to register middleware AFTER Vite's built-in middleware
2146
+ return () => {
2147
+ server.middlewares.use(async (req, res, next) => {
2148
+ try {
2149
+ let url = req.url ?? "/";
2150
+ // If no pages directory, skip this middleware entirely
2151
+ // (app router is handled by @vitejs/plugin-rsc's built-in middleware)
2152
+ if (!hasPagesDir)
2153
+ return next();
2154
+ // Skip Vite internal requests and static files
2155
+ if (url.startsWith("/@") ||
2156
+ url.startsWith("/__vite") ||
2157
+ url.startsWith("/node_modules")) {
2158
+ return next();
2159
+ }
2160
+ // Skip .rsc requests — those are for the App Router RSC handler
2161
+ if (url.split("?")[0].endsWith(".rsc")) {
2162
+ return next();
2163
+ }
2164
+ // ── Image optimization passthrough (dev mode) ─────────────
2165
+ // In dev, redirect to the original asset URL so Vite serves it.
2166
+ if (url.split("?")[0] === "/_vinext/image") {
2167
+ const imgParams = new URLSearchParams(url.split("?")[1] ?? "");
2168
+ const imgUrl = imgParams.get("url");
2169
+ // Allowlist: must start with "/" but not "//" — blocks absolute
2170
+ // URLs, protocol-relative, and exotic schemes (data:, javascript:, etc.).
2171
+ if (!imgUrl || !imgUrl.startsWith("/") || imgUrl.startsWith("//")) {
2172
+ res.writeHead(400);
2173
+ res.end(!imgUrl ? "Missing url parameter" : "Only relative URLs allowed");
2174
+ return;
2175
+ }
2176
+ res.writeHead(302, { Location: imgUrl });
2177
+ res.end();
2178
+ return;
2179
+ }
2180
+ // Vite's built-in middleware may rewrite "/" to "/index.html".
2181
+ // Normalize it back so our router can match correctly.
2182
+ const rawPathname = url.split("?")[0];
2183
+ if (rawPathname.endsWith("/index.html")) {
2184
+ url = url.replace("/index.html", "/");
2185
+ }
2186
+ else if (rawPathname.endsWith(".html")) {
2187
+ // Strip .html extensions (e.g. "/about.html" -> "/about")
2188
+ url = url.replace(/\.html(?=\?|$)/, "");
2189
+ }
2190
+ // Skip requests for files with extensions (static assets)
2191
+ let pathname = url.split("?")[0];
2192
+ if (pathname.includes(".") && !pathname.endsWith(".html")) {
2193
+ return next();
2194
+ }
2195
+ // Guard against protocol-relative URL open redirect attacks.
2196
+ // Paths like //example.com/ would be redirected to //example.com
2197
+ // by the trailing-slash normalizer, which browsers interpret as
2198
+ // http://example.com — an open redirect. Next.js returns 404 for
2199
+ // double-slash paths.
2200
+ if (pathname.startsWith("//")) {
2201
+ res.writeHead(404);
2202
+ res.end("404 Not Found");
2203
+ return;
2204
+ }
2205
+ // Strip basePath prefix from URL for route matching.
2206
+ // All internal routing uses basePath-free paths.
2207
+ //
2208
+ // NOTE: When basePath is set, we also set Vite's `base` config to
2209
+ // `basePath + "/"`. Vite's connect middleware stack strips the base
2210
+ // prefix from req.url before passing it to our middleware, so the
2211
+ // URL will already lack the basePath prefix. We still attempt to
2212
+ // strip it (for robustness) but don't reject paths that don't start
2213
+ // with basePath — Vite has already done the filtering.
2214
+ const bp = nextConfig?.basePath ?? "";
2215
+ if (bp && pathname.startsWith(bp)) {
2216
+ const stripped = pathname.slice(bp.length) || "/";
2217
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
2218
+ url = stripped + qs;
2219
+ pathname = stripped;
2220
+ }
2221
+ // Normalize trailing slash based on next.config.js trailingSlash setting.
2222
+ // Redirect to the canonical form if needed.
2223
+ if (nextConfig && pathname !== "/" && !pathname.startsWith("/api")) {
2224
+ const hasTrailing = pathname.endsWith("/");
2225
+ if (nextConfig.trailingSlash && !hasTrailing) {
2226
+ // trailingSlash: true — redirect /about → /about/
2227
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
2228
+ const dest = bp + pathname + "/" + qs;
2229
+ res.writeHead(308, { Location: dest });
2230
+ res.end();
2231
+ return;
2232
+ }
2233
+ else if (!nextConfig.trailingSlash && hasTrailing) {
2234
+ // trailingSlash: false (default) — redirect /about/ → /about
2235
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
2236
+ const dest = bp + pathname.replace(/\/+$/, "") + qs;
2237
+ res.writeHead(308, { Location: dest });
2238
+ res.end();
2239
+ return;
2240
+ }
2241
+ }
2242
+ // Run middleware.ts if present
2243
+ if (middlewarePath) {
2244
+ // Only trust X-Forwarded-Proto when behind a trusted proxy
2245
+ const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" || (process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some(h => h.trim());
2246
+ const rawProto = devTrustProxy
2247
+ ? String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim()
2248
+ : "";
2249
+ const mwProto = rawProto === "https" || rawProto === "http" ? rawProto : "http";
2250
+ const origin = `${mwProto}://${req.headers.host || "localhost"}`;
2251
+ const middlewareRequest = new Request(new URL(url, origin), {
2252
+ method: req.method,
2253
+ headers: Object.fromEntries(Object.entries(req.headers)
2254
+ .filter(([, v]) => v !== undefined)
2255
+ .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])),
2256
+ });
2257
+ const result = await runMiddleware(server, middlewarePath, middlewareRequest);
2258
+ if (!result.continue) {
2259
+ if (result.redirectUrl) {
2260
+ res.writeHead(result.redirectStatus ?? 307, {
2261
+ Location: result.redirectUrl,
2262
+ });
2263
+ res.end();
2264
+ return;
2265
+ }
2266
+ if (result.response) {
2267
+ res.statusCode = result.response.status;
2268
+ for (const [key, value] of result.response.headers) {
2269
+ res.setHeader(key, value);
2270
+ }
2271
+ const body = await result.response.text();
2272
+ res.end(body);
2273
+ return;
2274
+ }
2275
+ }
2276
+ // Apply middleware response headers
2277
+ if (result.responseHeaders) {
2278
+ for (const [key, value] of result.responseHeaders) {
2279
+ res.setHeader(key, value);
2280
+ }
2281
+ }
2282
+ // Apply middleware rewrite (URL and optional status code)
2283
+ if (result.rewriteUrl) {
2284
+ url = result.rewriteUrl;
2285
+ }
2286
+ if (result.rewriteStatus) {
2287
+ req.__vinextRewriteStatus = result.rewriteStatus;
2288
+ }
2289
+ }
2290
+ // Apply custom headers from next.config.js
2291
+ if (nextConfig?.headers.length) {
2292
+ applyHeaders(pathname, res, nextConfig.headers);
2293
+ }
2294
+ // Apply redirects from next.config.js
2295
+ if (nextConfig?.redirects.length) {
2296
+ const redirected = applyRedirects(pathname, res, nextConfig.redirects);
2297
+ if (redirected)
2298
+ return;
2299
+ }
2300
+ // Apply rewrites from next.config.js (beforeFiles)
2301
+ let resolvedUrl = url;
2302
+ if (nextConfig?.rewrites.beforeFiles.length) {
2303
+ resolvedUrl =
2304
+ applyRewrites(pathname, nextConfig.rewrites.beforeFiles) ??
2305
+ url;
2306
+ }
2307
+ // External rewrite from beforeFiles — proxy to external URL
2308
+ if (isExternalUrl(resolvedUrl)) {
2309
+ await proxyExternalRewriteNode(req, res, resolvedUrl);
2310
+ return;
2311
+ }
2312
+ // Handle API routes first (pages/api/*)
2313
+ const resolvedPathname = resolvedUrl.split("?")[0];
2314
+ if (resolvedPathname.startsWith("/api/") ||
2315
+ resolvedPathname === "/api") {
2316
+ const apiRoutes = await apiRouter(pagesDir);
2317
+ const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
2318
+ if (handled)
2319
+ return;
2320
+ // No API route matched — fall through to 404
2321
+ res.statusCode = 404;
2322
+ res.end("404 - API route not found");
2323
+ return;
2324
+ }
2325
+ const routes = await pagesRouter(pagesDir);
2326
+ // Apply afterFiles rewrites — these run after initial route matching
2327
+ // If beforeFiles already rewrote the URL, afterFiles still run on the
2328
+ // *resolved* pathname. Next.js applies these when route matching succeeds
2329
+ // but allows overriding with rewrites.
2330
+ if (nextConfig?.rewrites.afterFiles.length) {
2331
+ const afterRewrite = applyRewrites(resolvedUrl.split("?")[0], nextConfig.rewrites.afterFiles);
2332
+ if (afterRewrite)
2333
+ resolvedUrl = afterRewrite;
2334
+ }
2335
+ // External rewrite from afterFiles — proxy to external URL
2336
+ if (isExternalUrl(resolvedUrl)) {
2337
+ await proxyExternalRewriteNode(req, res, resolvedUrl);
2338
+ return;
2339
+ }
2340
+ const handler = createSSRHandler(server, routes, pagesDir, nextConfig?.i18n);
2341
+ const mwStatus = req.__vinextRewriteStatus;
2342
+ // Try rendering the resolved URL
2343
+ const match = matchRoute(resolvedUrl.split("?")[0], routes);
2344
+ if (match) {
2345
+ await handler(req, res, resolvedUrl, mwStatus);
2346
+ return;
2347
+ }
2348
+ // No route matched — try fallback rewrites
2349
+ if (nextConfig?.rewrites.fallback.length) {
2350
+ const fallbackRewrite = applyRewrites(resolvedUrl.split("?")[0], nextConfig.rewrites.fallback);
2351
+ if (fallbackRewrite) {
2352
+ // External fallback rewrite — proxy to external URL
2353
+ if (isExternalUrl(fallbackRewrite)) {
2354
+ await proxyExternalRewriteNode(req, res, fallbackRewrite);
2355
+ return;
2356
+ }
2357
+ await handler(req, res, fallbackRewrite, mwStatus);
2358
+ return;
2359
+ }
2360
+ }
2361
+ // No fallback matched — render as-is (will hit 404 handler)
2362
+ await handler(req, res, resolvedUrl, mwStatus);
2363
+ }
2364
+ catch (e) {
2365
+ next(e);
2366
+ }
2367
+ });
2368
+ };
2369
+ },
2370
+ },
2371
+ // Local image import transform:
2372
+ // When a source file imports a local image (e.g., `import hero from './hero.jpg'`),
2373
+ // this plugin transforms the default import to a StaticImageData object with
2374
+ // { src, width, height } so the next/image shim can set correct dimensions
2375
+ // on <img> tags, preventing CLS.
2376
+ //
2377
+ // Vite's default image import returns a URL string. We intercept this by
2378
+ // adding a `?vinext-meta` suffix: the original import gets the URL from Vite,
2379
+ // and we resolve the `?vinext-meta` virtual module to provide dimensions.
2380
+ {
2381
+ name: "vinext:image-imports",
2382
+ enforce: "pre",
2383
+ // Cache of image dimensions to avoid re-reading files
2384
+ _dimCache: new Map(),
2385
+ resolveId: {
2386
+ filter: { id: /\?vinext-meta$/ },
2387
+ handler(source, _importer) {
2388
+ if (!source.endsWith("?vinext-meta"))
2389
+ return null;
2390
+ // Resolve the real image path from the importer
2391
+ const realPath = source.replace("?vinext-meta", "");
2392
+ return `\0vinext-image-meta:${realPath}`;
2393
+ },
2394
+ },
2395
+ async load(id) {
2396
+ if (!id.startsWith("\0vinext-image-meta:"))
2397
+ return null;
2398
+ const imagePath = id.replace("\0vinext-image-meta:", "");
2399
+ // Read from cache first
2400
+ const cache = this._dimCache;
2401
+ let dims = cache.get(imagePath);
2402
+ if (!dims) {
2403
+ try {
2404
+ const { imageSize } = await import("image-size");
2405
+ const buffer = fs.readFileSync(imagePath);
2406
+ const result = imageSize(buffer);
2407
+ dims = { width: result.width ?? 0, height: result.height ?? 0 };
2408
+ cache.set(imagePath, dims);
2409
+ }
2410
+ catch {
2411
+ dims = { width: 0, height: 0 };
2412
+ }
2413
+ }
2414
+ return `export default ${JSON.stringify(dims)};`;
2415
+ },
2416
+ transform: {
2417
+ // Hook filter: Rolldown evaluates these on the Rust side, skipping
2418
+ // the JS handler entirely for files that don't match.
2419
+ filter: {
2420
+ id: {
2421
+ include: /\.(tsx?|jsx?|mjs)$/,
2422
+ exclude: /node_modules/,
2423
+ },
2424
+ code: new RegExp(`import\\s+\\w+\\s+from\\s+['"][^'"]+\\.(${IMAGE_EXTS})['"]`),
2425
+ },
2426
+ async handler(code, id) {
2427
+ // Defensive guard — duplicates filter logic
2428
+ if (id.includes("node_modules"))
2429
+ return null;
2430
+ if (id.startsWith("\0"))
2431
+ return null;
2432
+ if (!id.match(/\.(tsx?|jsx?|mjs)$/))
2433
+ return null;
2434
+ const imageImportRe = new RegExp(`import\\s+(\\w+)\\s+from\\s+['"]([^'"]+\\.(${IMAGE_EXTS}))['"];?`, "g");
2435
+ if (!imageImportRe.test(code))
2436
+ return null;
2437
+ imageImportRe.lastIndex = 0;
2438
+ const s = new MagicString(code);
2439
+ let hasChanges = false;
2440
+ let match;
2441
+ while ((match = imageImportRe.exec(code)) !== null) {
2442
+ const [fullMatch, varName, importPath] = match;
2443
+ const matchStart = match.index;
2444
+ const matchEnd = matchStart + fullMatch.length;
2445
+ // Resolve the absolute path of the image
2446
+ const dir = path.dirname(id);
2447
+ const absImagePath = path.resolve(dir, importPath);
2448
+ if (!fs.existsSync(absImagePath))
2449
+ continue;
2450
+ // Replace the single import with two:
2451
+ // 1. Original import (Vite gives us the URL string)
2452
+ // 2. Meta import (we provide { width, height })
2453
+ // Combined into a StaticImageData object
2454
+ const urlVar = `__vinext_img_url_${varName}`;
2455
+ const metaVar = `__vinext_img_meta_${varName}`;
2456
+ const replacement = `import ${urlVar} from ${JSON.stringify(importPath)};\n` +
2457
+ `import ${metaVar} from ${JSON.stringify(absImagePath + "?vinext-meta")};\n` +
2458
+ `const ${varName} = { src: ${urlVar}, width: ${metaVar}.width, height: ${metaVar}.height };`;
2459
+ s.overwrite(matchStart, matchEnd, replacement);
2460
+ hasChanges = true;
2461
+ }
2462
+ if (!hasChanges)
2463
+ return null;
2464
+ return {
2465
+ code: s.toString(),
2466
+ map: s.generateMap({ hires: "boundary" }),
2467
+ };
2468
+ },
2469
+ },
2470
+ },
2471
+ // Google Fonts self-hosting:
2472
+ // During production builds, fetches Google Fonts CSS + .woff2 files,
2473
+ // caches them locally in .vinext/fonts/, and rewrites font constructor
2474
+ // calls to pass _selfHostedCSS with @font-face rules pointing at local assets.
2475
+ // In dev mode, this plugin is a no-op (CDN loading is used instead).
2476
+ {
2477
+ name: "vinext:google-fonts",
2478
+ enforce: "pre",
2479
+ _isBuild: false,
2480
+ _fontCache: new Map(), // url -> local @font-face CSS
2481
+ _cacheDir: "",
2482
+ configResolved(config) {
2483
+ this._isBuild = config.command === "build";
2484
+ this._cacheDir = path.join(config.root, ".vinext", "fonts");
2485
+ },
2486
+ transform: {
2487
+ // Hook filter: only invoke JS when code contains 'next/font/google'.
2488
+ // The _isBuild runtime check can't be expressed as a filter, but this
2489
+ // still eliminates nearly all Rust-to-JS calls since very few files
2490
+ // import from next/font/google.
2491
+ filter: {
2492
+ id: {
2493
+ include: /\.(tsx?|jsx?|mjs)$/,
2494
+ exclude: /node_modules/,
2495
+ },
2496
+ code: "next/font/google",
2497
+ },
2498
+ async handler(code, id) {
2499
+ if (!this._isBuild)
2500
+ return null;
2501
+ // Defensive guard — duplicates filter logic
2502
+ if (id.includes("node_modules"))
2503
+ return null;
2504
+ if (id.startsWith("\0"))
2505
+ return null;
2506
+ if (!id.match(/\.(tsx?|jsx?|mjs)$/))
2507
+ return null;
2508
+ if (!code.includes("next/font/google"))
2509
+ return null;
2510
+ // Match font constructor calls: Inter({ weight: ..., subsets: ... })
2511
+ // We look for PascalCase or Name_Name identifiers followed by ({...})
2512
+ // This regex captures the font name and the options object literal
2513
+ const fontCallRe = /\b([A-Z][A-Za-z]*(?:_[A-Z][A-Za-z]*)*)\s*\(\s*(\{[^}]*\})\s*\)/g;
2514
+ // Also need to verify these names came from next/font/google import
2515
+ const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]next\/font\/google['"]/;
2516
+ const importMatch = code.match(importRe);
2517
+ if (!importMatch)
2518
+ return null;
2519
+ const importedNames = new Set(importMatch[1].split(",").map((s) => s.trim()).filter(Boolean));
2520
+ const s = new MagicString(code);
2521
+ let hasChanges = false;
2522
+ const cacheDir = this._cacheDir;
2523
+ const fontCache = this._fontCache;
2524
+ let match;
2525
+ while ((match = fontCallRe.exec(code)) !== null) {
2526
+ const [fullMatch, fontName, optionsStr] = match;
2527
+ if (!importedNames.has(fontName))
2528
+ continue;
2529
+ // Convert PascalCase/Underscore to font family
2530
+ const family = fontName.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2");
2531
+ // Parse options safely via AST — no eval/new Function
2532
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2533
+ let options = {};
2534
+ try {
2535
+ const parsed = parseStaticObjectLiteral(optionsStr);
2536
+ if (!parsed)
2537
+ continue; // Contains dynamic expressions, skip
2538
+ options = parsed;
2539
+ }
2540
+ catch {
2541
+ continue; // Can't parse options statically, skip
2542
+ }
2543
+ // Build the Google Fonts CSS URL
2544
+ const weights = options.weight
2545
+ ? Array.isArray(options.weight) ? options.weight : [options.weight]
2546
+ : [];
2547
+ const styles = options.style
2548
+ ? Array.isArray(options.style) ? options.style : [options.style]
2549
+ : [];
2550
+ const display = options.display ?? "swap";
2551
+ let spec = family.replace(/\s+/g, "+");
2552
+ if (weights.length > 0) {
2553
+ const hasItalic = styles.includes("italic");
2554
+ if (hasItalic) {
2555
+ const pairs = [];
2556
+ for (const w of weights) {
2557
+ pairs.push(`0,${w}`);
2558
+ pairs.push(`1,${w}`);
2559
+ }
2560
+ spec += `:ital,wght@${pairs.join(";")}`;
2561
+ }
2562
+ else {
2563
+ spec += `:wght@${weights.join(";")}`;
2564
+ }
2565
+ }
2566
+ else if (styles.length === 0) {
2567
+ // Request full variable weight range when no weight specified.
2568
+ // Without this, Google Fonts returns only weight 400.
2569
+ spec += `:wght@100..900`;
2570
+ }
2571
+ const params = new URLSearchParams();
2572
+ params.set("family", spec);
2573
+ params.set("display", display);
2574
+ const cssUrl = `https://fonts.googleapis.com/css2?${params.toString()}`;
2575
+ // Check cache
2576
+ let localCSS = fontCache.get(cssUrl);
2577
+ if (!localCSS) {
2578
+ try {
2579
+ localCSS = await fetchAndCacheFont(cssUrl, family, cacheDir);
2580
+ fontCache.set(cssUrl, localCSS);
2581
+ }
2582
+ catch {
2583
+ // Fetch failed (offline?) — fall back to CDN mode
2584
+ continue;
2585
+ }
2586
+ }
2587
+ // Inject _selfHostedCSS into the options object
2588
+ const matchStart = match.index;
2589
+ const matchEnd = matchStart + fullMatch.length;
2590
+ const escapedCSS = JSON.stringify(localCSS);
2591
+ const closingBrace = optionsStr.lastIndexOf("}");
2592
+ const optionsWithCSS = optionsStr.slice(0, closingBrace) +
2593
+ (optionsStr.slice(0, closingBrace).trim().endsWith("{") ? "" : ", ") +
2594
+ `_selfHostedCSS: ${escapedCSS}` +
2595
+ optionsStr.slice(closingBrace);
2596
+ const replacement = `${fontName}(${optionsWithCSS})`;
2597
+ s.overwrite(matchStart, matchEnd, replacement);
2598
+ hasChanges = true;
2599
+ }
2600
+ if (!hasChanges)
2601
+ return null;
2602
+ return {
2603
+ code: s.toString(),
2604
+ map: s.generateMap({ hires: "boundary" }),
2605
+ };
2606
+ },
2607
+ },
2608
+ },
2609
+ // Local font path resolution:
2610
+ // When a source file calls localFont({ src: "./font.woff2" }) or
2611
+ // localFont({ src: [{ path: "./font.woff2" }] }), the relative paths
2612
+ // won't resolve in the browser because the CSS is injected at runtime.
2613
+ // This plugin rewrites those path strings into Vite asset import
2614
+ // references so that both dev (/@fs/...) and prod (/assets/font-xxx.woff2)
2615
+ // URLs are correct.
2616
+ {
2617
+ name: "vinext:local-fonts",
2618
+ enforce: "pre",
2619
+ transform: {
2620
+ filter: {
2621
+ id: {
2622
+ include: /\.(tsx?|jsx?|mjs)$/,
2623
+ exclude: /node_modules/,
2624
+ },
2625
+ code: "next/font/local",
2626
+ },
2627
+ handler(code, id) {
2628
+ // Defensive guards — duplicate filter logic
2629
+ if (id.includes("node_modules"))
2630
+ return null;
2631
+ if (id.startsWith("\0"))
2632
+ return null;
2633
+ if (!id.match(/\.(tsx?|jsx?|mjs)$/))
2634
+ return null;
2635
+ if (!code.includes("next/font/local"))
2636
+ return null;
2637
+ // Skip vinext's own font-local shim — it contains example paths
2638
+ // in comments that would be incorrectly rewritten.
2639
+ if (id.includes("font-local"))
2640
+ return null;
2641
+ // Verify there's actually an import from next/font/local
2642
+ const importRe = /import\s+\w+\s+from\s*['"]next\/font\/local['"]/;
2643
+ if (!importRe.test(code))
2644
+ return null;
2645
+ const s = new MagicString(code);
2646
+ let hasChanges = false;
2647
+ let fontImportCounter = 0;
2648
+ const imports = [];
2649
+ // Match font file paths in `path: "..."` or `src: "..."` properties.
2650
+ // Captures: (1) property+colon prefix, (2) quote char, (3) the path.
2651
+ const fontPathRe = /((?:path|src)\s*:\s*)(['"])([^'"]+\.(?:woff2?|ttf|otf|eot))\2/g;
2652
+ let match;
2653
+ while ((match = fontPathRe.exec(code)) !== null) {
2654
+ const [fullMatch, prefix, _quote, fontPath] = match;
2655
+ const varName = `__vinext_local_font_${fontImportCounter++}`;
2656
+ // Add an import for this font file — Vite resolves it as a static
2657
+ // asset and returns the correct URL for both dev and prod.
2658
+ imports.push(`import ${varName} from ${JSON.stringify(fontPath)};`);
2659
+ // Replace: path: "./font.woff2" -> path: __vinext_local_font_0
2660
+ const matchStart = match.index;
2661
+ const matchEnd = matchStart + fullMatch.length;
2662
+ s.overwrite(matchStart, matchEnd, `${prefix}${varName}`);
2663
+ hasChanges = true;
2664
+ }
2665
+ if (!hasChanges)
2666
+ return null;
2667
+ // Prepend the asset imports at the top of the file
2668
+ s.prepend(imports.join("\n") + "\n");
2669
+ return {
2670
+ code: s.toString(),
2671
+ map: s.generateMap({ hires: "boundary" }),
2672
+ };
2673
+ },
2674
+ },
2675
+ },
2676
+ // "use cache" directive transform:
2677
+ // Detects "use cache" at file-level or function-level and wraps the
2678
+ // exports/functions with registerCachedFunction() from vinext/cache-runtime.
2679
+ // Runs without enforce so it executes after JSX transform (parseAst needs plain JS).
2680
+ {
2681
+ name: "vinext:use-cache",
2682
+ transform: {
2683
+ // Hook filter: only invoke JS when code contains 'use cache'.
2684
+ // The vast majority of files don't use this directive.
2685
+ filter: {
2686
+ id: {
2687
+ include: /\.(tsx?|jsx?|mjs)$/,
2688
+ exclude: /node_modules/,
2689
+ },
2690
+ code: "use cache",
2691
+ },
2692
+ async handler(code, id) {
2693
+ // Defensive guard — duplicates filter logic
2694
+ if (id.includes("node_modules"))
2695
+ return null;
2696
+ if (id.startsWith("\0"))
2697
+ return null;
2698
+ if (!id.match(/\.(tsx?|jsx?|mjs)$/))
2699
+ return null;
2700
+ if (!code.includes("use cache"))
2701
+ return null;
2702
+ if (!resolvedRscTransformsPath) {
2703
+ throw new Error("vinext: 'use cache' requires @vitejs/plugin-rsc to be installed.\n" +
2704
+ "Run: npm install -D @vitejs/plugin-rsc");
2705
+ }
2706
+ const { transformWrapExport, transformHoistInlineDirective } = await import(resolvedRscTransformsPath);
2707
+ const ast = parseAst(code);
2708
+ // Check for file-level "use cache" directive
2709
+ const cacheDirective = ast.body.find((node) => node.type === "ExpressionStatement" &&
2710
+ node.expression?.type === "Literal" &&
2711
+ typeof node.expression.value === "string" &&
2712
+ node.expression.value.startsWith("use cache"));
2713
+ if (cacheDirective) {
2714
+ // File-level "use cache" — wrap function exports with
2715
+ // registerCachedFunction. Page default exports are wrapped directly
2716
+ // (they're leaf components). Layout/template defaults are excluded
2717
+ // because they receive {children} from the framework.
2718
+ const directiveValue = cacheDirective.expression.value;
2719
+ const variant = directiveValue === "use cache" ? "" : directiveValue.replace("use cache:", "").replace("use cache: ", "").trim();
2720
+ // Only skip default export wrapping for layouts and templates —
2721
+ // they receive {children} from the framework which requires
2722
+ // temporary reference handling that registerCachedFunction doesn't
2723
+ // support yet. Pages, not-found, loading, error, and default are
2724
+ // leaf components with no {children} prop and can be cached directly.
2725
+ const isLayoutOrTemplate = /\/(layout|template)\.(tsx?|jsx?|mjs)$/.test(id);
2726
+ const runtimeModulePath = path.join(shimsDir, "cache-runtime.js");
2727
+ const result = transformWrapExport(code, ast, {
2728
+ runtime: (value, name) => `(await import(${JSON.stringify(runtimeModulePath)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`,
2729
+ rejectNonAsyncFunction: false,
2730
+ filter: (name, meta) => {
2731
+ // Skip non-functions (constants, types, etc.)
2732
+ if (meta.isFunction === false)
2733
+ return false;
2734
+ // Skip the default export on layout/template files — these
2735
+ // receive {children} from the framework, and caching them
2736
+ // requires temporary reference handling for the children slot.
2737
+ // Named exports (e.g. generateMetadata) are still wrapped.
2738
+ if (isLayoutOrTemplate && name === "default")
2739
+ return false;
2740
+ return true;
2741
+ },
2742
+ });
2743
+ if (result.exportNames.length > 0) {
2744
+ // Remove the directive itself so it doesn't cause runtime errors
2745
+ const output = result.output;
2746
+ output.overwrite(cacheDirective.start, cacheDirective.end, `/* "use cache" — wrapped by vinext */`);
2747
+ return {
2748
+ code: output.toString(),
2749
+ map: output.generateMap({ hires: "boundary" }),
2750
+ };
2751
+ }
2752
+ // Even if no exports were wrapped, still strip the directive
2753
+ // (e.g., layout/template file with only a default export)
2754
+ const output = new MagicString(code);
2755
+ output.overwrite(cacheDirective.start, cacheDirective.end, `/* "use cache" — handled by vinext */`);
2756
+ return {
2757
+ code: output.toString(),
2758
+ map: output.generateMap({ hires: "boundary" }),
2759
+ };
2760
+ }
2761
+ // Check for function-level "use cache" directives
2762
+ // (e.g., async function getData() { "use cache"; ... })
2763
+ const hasInlineCache = code.includes("use cache") && !cacheDirective;
2764
+ if (hasInlineCache) {
2765
+ const runtimeModulePath = path.join(shimsDir, "cache-runtime.js");
2766
+ try {
2767
+ const result = transformHoistInlineDirective(code, ast, {
2768
+ directive: /^use cache(:\s*\w+)?$/,
2769
+ runtime: (value, name, meta) => {
2770
+ const directiveMatch = meta.directiveMatch[0];
2771
+ const variant = directiveMatch === "use cache" ? "" : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim();
2772
+ return `(await import(${JSON.stringify(runtimeModulePath)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`;
2773
+ },
2774
+ rejectNonAsyncFunction: false,
2775
+ });
2776
+ if (result.names.length > 0) {
2777
+ return {
2778
+ code: result.output.toString(),
2779
+ map: result.output.generateMap({ hires: "boundary" }),
2780
+ };
2781
+ }
2782
+ }
2783
+ catch {
2784
+ // If hoisting fails (e.g., complex closure), fall through
2785
+ }
2786
+ }
2787
+ return null;
2788
+ },
2789
+ },
2790
+ },
2791
+ // Copy @vercel/og assets (font, WASM) to the RSC output directory.
2792
+ // @vercel/og uses readFileSync(new URL("./font.ttf", import.meta.url)) which
2793
+ // breaks when the module is bundled because Vite doesn't process
2794
+ // new URL(..., import.meta.url) for server-side (SSR/RSC) builds.
2795
+ // This plugin copies the required assets so they exist alongside the bundle.
2796
+ {
2797
+ name: "vinext:og-assets",
2798
+ apply: "build",
2799
+ enforce: "post",
2800
+ writeBundle: {
2801
+ sequential: true,
2802
+ order: "post",
2803
+ async handler(options) {
2804
+ const envName = this.environment?.name;
2805
+ if (envName !== "rsc")
2806
+ return;
2807
+ const outDir = options.dir;
2808
+ if (!outDir)
2809
+ return;
2810
+ // Check if the bundle references @vercel/og assets
2811
+ const indexPath = path.join(outDir, "index.js");
2812
+ if (!fs.existsSync(indexPath))
2813
+ return;
2814
+ const content = fs.readFileSync(indexPath, "utf-8");
2815
+ const ogAssets = [
2816
+ "noto-sans-v27-latin-regular.ttf",
2817
+ "resvg.wasm",
2818
+ ];
2819
+ // Only copy if the bundle actually references these files
2820
+ const referencedAssets = ogAssets.filter(asset => content.includes(asset));
2821
+ if (referencedAssets.length === 0)
2822
+ return;
2823
+ // Find @vercel/og in node_modules
2824
+ try {
2825
+ const require = createRequire(import.meta.url);
2826
+ const ogPkgPath = require.resolve("@vercel/og/package.json");
2827
+ const ogDistDir = path.join(path.dirname(ogPkgPath), "dist");
2828
+ for (const asset of referencedAssets) {
2829
+ const src = path.join(ogDistDir, asset);
2830
+ const dest = path.join(outDir, asset);
2831
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
2832
+ fs.copyFileSync(src, dest);
2833
+ }
2834
+ }
2835
+ }
2836
+ catch {
2837
+ // @vercel/og not installed — nothing to copy
2838
+ }
2839
+ },
2840
+ },
2841
+ },
2842
+ // Cloudflare Workers production build integration:
2843
+ // After all environments are built, compute lazy chunks from the client
2844
+ // build manifest and inject globals into the worker entry.
2845
+ //
2846
+ // Pages Router: injects __VINEXT_CLIENT_ENTRY__, __VINEXT_SSR_MANIFEST__,
2847
+ // and __VINEXT_LAZY_CHUNKS__ into the worker entry (found via wrangler.json).
2848
+ // App Router: the RSC plugin handles __VINEXT_CLIENT_ENTRY__ via
2849
+ // loadBootstrapScriptContent(), but we still inject __VINEXT_LAZY_CHUNKS__
2850
+ // and __VINEXT_SSR_MANIFEST__ into the worker entry at dist/server/index.js.
2851
+ // Both: generates _headers file for immutable asset caching.
2852
+ {
2853
+ name: "vinext:cloudflare-build",
2854
+ apply: "build",
2855
+ enforce: "post",
2856
+ closeBundle: {
2857
+ sequential: true,
2858
+ order: "post",
2859
+ async handler() {
2860
+ const envName = this.environment?.name;
2861
+ if (!envName || !hasCloudflarePlugin)
2862
+ return;
2863
+ if (envName !== "client")
2864
+ return;
2865
+ const envConfig = this.environment?.config;
2866
+ if (!envConfig)
2867
+ return;
2868
+ const buildRoot = envConfig.root ?? process.cwd();
2869
+ const distDir = path.resolve(buildRoot, "dist");
2870
+ if (!fs.existsSync(distDir))
2871
+ return;
2872
+ const clientDir = path.resolve(buildRoot, "dist", "client");
2873
+ // Read build manifest and compute lazy chunks (only reachable via
2874
+ // dynamic imports). This runs for BOTH App Router and Pages Router.
2875
+ // clientEntryFile is only used by the Pages Router path below —
2876
+ // App Router gets its client entry via the RSC plugin instead.
2877
+ let lazyChunksData = null;
2878
+ let clientEntryFile = null;
2879
+ const buildManifestPath = path.join(clientDir, ".vite", "manifest.json");
2880
+ if (fs.existsSync(buildManifestPath)) {
2881
+ try {
2882
+ const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"));
2883
+ for (const [, value] of Object.entries(buildManifest)) {
2884
+ if (value && value.isEntry && value.file) {
2885
+ clientEntryFile = value.file;
2886
+ break;
2887
+ }
2888
+ }
2889
+ const lazy = computeLazyChunks(buildManifest);
2890
+ if (lazy.length > 0)
2891
+ lazyChunksData = lazy;
2892
+ }
2893
+ catch { /* ignore parse errors */ }
2894
+ }
2895
+ // Read SSR manifest for per-page CSS/JS injection
2896
+ let ssrManifestData = null;
2897
+ const ssrManifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
2898
+ if (fs.existsSync(ssrManifestPath)) {
2899
+ try {
2900
+ ssrManifestData = JSON.parse(fs.readFileSync(ssrManifestPath, "utf-8"));
2901
+ }
2902
+ catch { /* ignore parse errors */ }
2903
+ }
2904
+ if (hasAppDir) {
2905
+ // App Router: the RSC plugin handles __VINEXT_CLIENT_ENTRY__
2906
+ // via loadBootstrapScriptContent(), but we still need to inject
2907
+ // __VINEXT_LAZY_CHUNKS__ and __VINEXT_SSR_MANIFEST__ into the
2908
+ // worker entry at dist/server/index.js.
2909
+ const workerEntry = path.resolve(distDir, "server", "index.js");
2910
+ if (fs.existsSync(workerEntry) && (lazyChunksData || ssrManifestData)) {
2911
+ let code = fs.readFileSync(workerEntry, "utf-8");
2912
+ const globals = [];
2913
+ if (ssrManifestData) {
2914
+ globals.push(`globalThis.__VINEXT_SSR_MANIFEST__ = ${JSON.stringify(ssrManifestData)};`);
2915
+ }
2916
+ if (lazyChunksData) {
2917
+ globals.push(`globalThis.__VINEXT_LAZY_CHUNKS__ = ${JSON.stringify(lazyChunksData)};`);
2918
+ }
2919
+ code = globals.join("\n") + "\n" + code;
2920
+ fs.writeFileSync(workerEntry, code);
2921
+ }
2922
+ }
2923
+ else {
2924
+ // Pages Router: find worker output by scanning dist/ for a
2925
+ // directory containing wrangler.json (Cloudflare plugin default).
2926
+ let workerOutDir = null;
2927
+ for (const entry of fs.readdirSync(distDir)) {
2928
+ const candidate = path.join(distDir, entry);
2929
+ if (entry === "client")
2930
+ continue;
2931
+ if (fs.statSync(candidate).isDirectory() &&
2932
+ fs.existsSync(path.join(candidate, "wrangler.json"))) {
2933
+ workerOutDir = candidate;
2934
+ break;
2935
+ }
2936
+ }
2937
+ if (!workerOutDir)
2938
+ return;
2939
+ const workerEntry = path.join(workerOutDir, "index.js");
2940
+ if (!fs.existsSync(workerEntry))
2941
+ return;
2942
+ // Fallback: scan dist/client/assets/ for the client entry chunk.
2943
+ // Pages Router uses "vinext-client-entry", App Router uses
2944
+ // "vinext-app-browser-entry".
2945
+ if (!clientEntryFile) {
2946
+ const assetsDir = path.join(clientDir, "assets");
2947
+ if (fs.existsSync(assetsDir)) {
2948
+ const files = fs.readdirSync(assetsDir);
2949
+ const entry = files.find((f) => (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) && f.endsWith(".js"));
2950
+ if (entry)
2951
+ clientEntryFile = "assets/" + entry;
2952
+ }
2953
+ }
2954
+ // Prepend globals to worker entry
2955
+ if (clientEntryFile || ssrManifestData || lazyChunksData) {
2956
+ let code = fs.readFileSync(workerEntry, "utf-8");
2957
+ const globals = [];
2958
+ if (clientEntryFile) {
2959
+ globals.push(`globalThis.__VINEXT_CLIENT_ENTRY__ = ${JSON.stringify(clientEntryFile)};`);
2960
+ }
2961
+ if (ssrManifestData) {
2962
+ globals.push(`globalThis.__VINEXT_SSR_MANIFEST__ = ${JSON.stringify(ssrManifestData)};`);
2963
+ }
2964
+ if (lazyChunksData) {
2965
+ globals.push(`globalThis.__VINEXT_LAZY_CHUNKS__ = ${JSON.stringify(lazyChunksData)};`);
2966
+ }
2967
+ code = globals.join("\n") + "\n" + code;
2968
+ fs.writeFileSync(workerEntry, code);
2969
+ }
2970
+ }
2971
+ // Generate _headers file for Cloudflare Workers static asset caching.
2972
+ // Vite outputs content-hashed files (JS, CSS, fonts) to the assetsDir
2973
+ // (defaults to "assets"). These are safe to cache indefinitely since
2974
+ // the hash changes on any content change. Without this, Cloudflare
2975
+ // serves them with max-age=0 which forces unnecessary revalidation
2976
+ // on every page load.
2977
+ const headersPath = path.join(clientDir, "_headers");
2978
+ if (!fs.existsSync(headersPath)) {
2979
+ const assetsDir = envConfig.build?.assetsDir ?? "assets";
2980
+ const headersContent = [
2981
+ "# Cache content-hashed assets immutably (generated by vinext)",
2982
+ `/${assetsDir}/*`,
2983
+ " Cache-Control: public, max-age=31536000, immutable",
2984
+ "",
2985
+ ].join("\n");
2986
+ fs.writeFileSync(headersPath, headersContent);
2987
+ }
2988
+ },
2989
+ },
2990
+ },
2991
+ ];
2992
+ // Append auto-injected RSC plugins if applicable
2993
+ if (rscPluginPromise) {
2994
+ plugins.push(rscPluginPromise);
2995
+ }
2996
+ return plugins;
2997
+ }
2998
+ /**
2999
+ * Collect all NEXT_PUBLIC_* env vars and create Vite define entries
3000
+ * so they get inlined into the client bundle.
3001
+ */
3002
+ function getNextPublicEnvDefines() {
3003
+ const defines = {};
3004
+ for (const [key, value] of Object.entries(process.env)) {
3005
+ if (key.startsWith("NEXT_PUBLIC_") && value !== undefined) {
3006
+ defines[`process.env.${key}`] = JSON.stringify(value);
3007
+ }
3008
+ }
3009
+ return defines;
3010
+ }
3011
+ /**
3012
+ * Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
3013
+ * Returns matched params or null.
3014
+ *
3015
+ * Supports:
3016
+ * :param — matches a single path segment
3017
+ * :param* — matches zero or more segments (catch-all)
3018
+ * :param+ — matches one or more segments
3019
+ * (regex) — inline regex patterns in the source
3020
+ */
3021
+ export function matchConfigPattern(pathname, pattern) {
3022
+ // If the pattern contains regex groups like (\\d+) or (.*), use regex matching.
3023
+ // Also enter this branch when a catch-all parameter (:param* or :param+) is
3024
+ // followed by a literal suffix (e.g. "/:path*.md"). Without this, the suffix
3025
+ // pattern falls through to the simple segment matcher which incorrectly treats
3026
+ // the whole segment (":path*.md") as a named parameter and matches everything.
3027
+ if (pattern.includes("(") ||
3028
+ pattern.includes("\\") ||
3029
+ /:\w+[*+][^/]/.test(pattern)) {
3030
+ try {
3031
+ // Extract named params and their constraints from the pattern.
3032
+ // :param(constraint) -> use constraint as the regex group
3033
+ // :param -> ([^/]+)
3034
+ // :param* -> (.*)
3035
+ // :param+ -> (.+)
3036
+ const paramNames = [];
3037
+ const regexStr = pattern
3038
+ .replace(/\./g, "\\.")
3039
+ // :param* with optional constraint
3040
+ .replace(/:(\w+)\*(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3041
+ paramNames.push(name);
3042
+ return constraint ? `(${constraint})` : "(.*)";
3043
+ })
3044
+ // :param+ with optional constraint
3045
+ .replace(/:(\w+)\+(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3046
+ paramNames.push(name);
3047
+ return constraint ? `(${constraint})` : "(.+)";
3048
+ })
3049
+ // :param(constraint) — named param with inline regex constraint
3050
+ .replace(/:(\w+)\(([^)]+)\)/g, (_m, name, constraint) => {
3051
+ paramNames.push(name);
3052
+ return `(${constraint})`;
3053
+ })
3054
+ // :param — plain named param
3055
+ .replace(/:(\w+)/g, (_m, name) => {
3056
+ paramNames.push(name);
3057
+ return "([^/]+)";
3058
+ });
3059
+ const re = safeRegExp("^" + regexStr + "$");
3060
+ if (!re)
3061
+ return null;
3062
+ const match = re.exec(pathname);
3063
+ if (!match)
3064
+ return null;
3065
+ const params = {};
3066
+ for (let i = 0; i < paramNames.length; i++) {
3067
+ params[paramNames[i]] = match[i + 1] ?? "";
3068
+ }
3069
+ return params;
3070
+ }
3071
+ catch {
3072
+ // Fall through to segment-based matching
3073
+ }
3074
+ }
3075
+ // Check for catch-all patterns (:param* or :param+) without regex groups
3076
+ const catchAllMatch = pattern.match(/:(\w+)(\*|\+)$/);
3077
+ if (catchAllMatch) {
3078
+ const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
3079
+ const paramName = catchAllMatch[1];
3080
+ const isPlus = catchAllMatch[2] === "+";
3081
+ if (!pathname.startsWith(prefix.replace(/\/$/, "")))
3082
+ return null;
3083
+ const rest = pathname.slice(prefix.replace(/\/$/, "").length);
3084
+ // For :path+ we need at least one segment (non-empty after the prefix)
3085
+ if (isPlus && (!rest || rest === "/"))
3086
+ return null;
3087
+ // For :path* zero segments is fine
3088
+ return { [paramName]: rest.startsWith("/") ? rest.slice(1) : rest };
3089
+ }
3090
+ // Simple segment-based matching for exact patterns and :param
3091
+ const parts = pattern.split("/");
3092
+ const pathParts = pathname.split("/");
3093
+ if (parts.length !== pathParts.length)
3094
+ return null;
3095
+ const params = {};
3096
+ for (let i = 0; i < parts.length; i++) {
3097
+ if (parts[i].startsWith(":")) {
3098
+ params[parts[i].slice(1)] = pathParts[i];
3099
+ }
3100
+ else if (parts[i] !== pathParts[i]) {
3101
+ return null;
3102
+ }
3103
+ }
3104
+ return params;
3105
+ }
3106
+ /**
3107
+ * Apply redirect rules from next.config.js.
3108
+ * Returns true if a redirect was applied.
3109
+ */
3110
+ function applyRedirects(pathname, res, redirects) {
3111
+ for (const redirect of redirects) {
3112
+ const params = matchConfigPattern(pathname, redirect.source);
3113
+ if (params) {
3114
+ let dest = redirect.destination;
3115
+ for (const [key, value] of Object.entries(params)) {
3116
+ dest = dest.replace(`:${key}*`, value);
3117
+ dest = dest.replace(`:${key}+`, value);
3118
+ dest = dest.replace(`:${key}`, value);
3119
+ }
3120
+ res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
3121
+ res.end();
3122
+ return true;
3123
+ }
3124
+ }
3125
+ return false;
3126
+ }
3127
+ /**
3128
+ * Proxy an external rewrite in the Node.js dev server context.
3129
+ *
3130
+ * Converts the Node.js IncomingMessage into a Web Request, calls
3131
+ * proxyExternalRequest(), and pipes the response back to the Node.js
3132
+ * ServerResponse.
3133
+ */
3134
+ async function proxyExternalRewriteNode(req, res, externalUrl) {
3135
+ try {
3136
+ const proto = "http";
3137
+ const host = req.headers.host || "localhost";
3138
+ const origin = `${proto}://${host}`;
3139
+ const method = req.method ?? "GET";
3140
+ const hasBody = method !== "GET" && method !== "HEAD";
3141
+ const init = {
3142
+ method,
3143
+ headers: Object.fromEntries(Object.entries(req.headers)
3144
+ .filter(([, v]) => v !== undefined)
3145
+ .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])),
3146
+ };
3147
+ if (hasBody) {
3148
+ const { Readable } = await import("node:stream");
3149
+ init.body = Readable.toWeb(req);
3150
+ init.duplex = "half";
3151
+ }
3152
+ const webRequest = new Request(new URL(req.url ?? "/", origin), init);
3153
+ const proxyResponse = await proxyExternalRequest(webRequest, externalUrl);
3154
+ // Preserve multi-value headers (e.g. Set-Cookie) — Object.fromEntries()
3155
+ // would collapse them into a single value.
3156
+ const nodeHeaders = {};
3157
+ proxyResponse.headers.forEach((value, key) => {
3158
+ const existing = nodeHeaders[key];
3159
+ if (existing !== undefined) {
3160
+ nodeHeaders[key] = Array.isArray(existing)
3161
+ ? [...existing, value]
3162
+ : [existing, value];
3163
+ }
3164
+ else {
3165
+ nodeHeaders[key] = value;
3166
+ }
3167
+ });
3168
+ res.writeHead(proxyResponse.status, nodeHeaders);
3169
+ if (proxyResponse.body) {
3170
+ const { Readable: ReadableImport } = await import("node:stream");
3171
+ const nodeStream = ReadableImport.fromWeb(proxyResponse.body);
3172
+ nodeStream.pipe(res);
3173
+ }
3174
+ else {
3175
+ res.end();
3176
+ }
3177
+ }
3178
+ catch (e) {
3179
+ console.error("[vinext] External rewrite proxy error:", e);
3180
+ if (!res.headersSent) {
3181
+ res.writeHead(502);
3182
+ res.end("Bad Gateway");
3183
+ }
3184
+ }
3185
+ }
3186
+ /**
3187
+ * Apply rewrite rules from next.config.js.
3188
+ * Returns the rewritten URL or null if no rewrite matched.
3189
+ */
3190
+ function applyRewrites(pathname, rewrites) {
3191
+ for (const rewrite of rewrites) {
3192
+ const params = matchConfigPattern(pathname, rewrite.source);
3193
+ if (params) {
3194
+ let dest = rewrite.destination;
3195
+ for (const [key, value] of Object.entries(params)) {
3196
+ dest = dest.replace(`:${key}*`, value);
3197
+ dest = dest.replace(`:${key}+`, value);
3198
+ dest = dest.replace(`:${key}`, value);
3199
+ }
3200
+ return dest;
3201
+ }
3202
+ }
3203
+ return null;
3204
+ }
3205
+ /**
3206
+ * Apply custom header rules from next.config.js.
3207
+ */
3208
+ function applyHeaders(pathname, res, headers) {
3209
+ for (const rule of headers) {
3210
+ // Escape regex metacharacters in the source, then convert Next.js patterns.
3211
+ // Strategy: extract regex groups first, process the rest, then restore groups.
3212
+ const groups = [];
3213
+ const withPlaceholders = rule.source.replace(/\(([^)]+)\)/g, (_m, inner) => {
3214
+ groups.push(inner);
3215
+ return `___GROUP_${groups.length - 1}___`;
3216
+ });
3217
+ const escaped = withPlaceholders
3218
+ // Escape dots and other metacharacters
3219
+ .replace(/\./g, "\\.")
3220
+ .replace(/\+/g, "\\+")
3221
+ .replace(/\?/g, "\\?")
3222
+ // Convert glob * to .*
3223
+ .replace(/\*/g, ".*")
3224
+ // Convert :param to [^/]+
3225
+ .replace(/:\w+/g, "[^/]+")
3226
+ // Restore regex groups (contents are untouched)
3227
+ .replace(/___GROUP_(\d+)___/g, (_m, idx) => `(${groups[Number(idx)]})`);
3228
+ const sourceRegex = safeRegExp("^" + escaped + "$");
3229
+ if (sourceRegex && sourceRegex.test(pathname)) {
3230
+ for (const header of rule.headers) {
3231
+ res.setHeader(header.key, header.value);
3232
+ }
3233
+ }
3234
+ }
3235
+ }
3236
+ /**
3237
+ * Find a file by name (without extension) in a directory.
3238
+ * Checks .tsx, .ts, .jsx, .js extensions.
3239
+ */
3240
+ function findFileWithExts(dir, name) {
3241
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
3242
+ for (const ext of extensions) {
3243
+ const filePath = path.join(dir, name + ext);
3244
+ if (fs.existsSync(filePath))
3245
+ return filePath;
3246
+ }
3247
+ return null;
3248
+ }
3249
+ /**
3250
+ * Check if the project has .mdx files in app/ or pages/ directories.
3251
+ */
3252
+ function hasMdxFiles(root, appDir, pagesDir) {
3253
+ const dirs = [appDir, pagesDir].filter(Boolean);
3254
+ for (const dir of dirs) {
3255
+ if (fs.existsSync(dir) && scanDirForMdx(dir))
3256
+ return true;
3257
+ }
3258
+ return false;
3259
+ }
3260
+ function scanDirForMdx(dir) {
3261
+ try {
3262
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
3263
+ for (const entry of entries) {
3264
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
3265
+ continue;
3266
+ const full = path.join(dir, entry.name);
3267
+ if (entry.isDirectory()) {
3268
+ if (scanDirForMdx(full))
3269
+ return true;
3270
+ }
3271
+ else if (entry.isFile() && entry.name.endsWith(".mdx")) {
3272
+ return true;
3273
+ }
3274
+ }
3275
+ }
3276
+ catch {
3277
+ // ignore unreadable dirs
3278
+ }
3279
+ return false;
3280
+ }
3281
+ // Public exports for static export
3282
+ export { staticExportPages, staticExportApp } from "./build/static-export.js";
3283
+ // Exported for CLI and testing
3284
+ export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks };
3285
+ export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins };
3286
+ export { parseStaticObjectLiteral as _parseStaticObjectLiteral };
3287
+ //# sourceMappingURL=index.js.map