almostnode 0.1.0

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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +731 -0
  3. package/dist/__sw__.js +394 -0
  4. package/dist/ai-chatbot-demo-entry.d.ts +6 -0
  5. package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
  6. package/dist/ai-chatbot-demo.d.ts +42 -0
  7. package/dist/ai-chatbot-demo.d.ts.map +1 -0
  8. package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
  9. package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
  10. package/dist/convex-app-demo-entry.d.ts +6 -0
  11. package/dist/convex-app-demo-entry.d.ts.map +1 -0
  12. package/dist/convex-app-demo.d.ts +68 -0
  13. package/dist/convex-app-demo.d.ts.map +1 -0
  14. package/dist/cors-proxy.d.ts +46 -0
  15. package/dist/cors-proxy.d.ts.map +1 -0
  16. package/dist/create-runtime.d.ts +42 -0
  17. package/dist/create-runtime.d.ts.map +1 -0
  18. package/dist/demo.d.ts +6 -0
  19. package/dist/demo.d.ts.map +1 -0
  20. package/dist/dev-server.d.ts +97 -0
  21. package/dist/dev-server.d.ts.map +1 -0
  22. package/dist/frameworks/next-dev-server.d.ts +202 -0
  23. package/dist/frameworks/next-dev-server.d.ts.map +1 -0
  24. package/dist/frameworks/vite-dev-server.d.ts +85 -0
  25. package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
  26. package/dist/index.cjs +14965 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.ts +71 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +14867 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/next-demo.d.ts +49 -0
  33. package/dist/next-demo.d.ts.map +1 -0
  34. package/dist/npm/index.d.ts +71 -0
  35. package/dist/npm/index.d.ts.map +1 -0
  36. package/dist/npm/registry.d.ts +66 -0
  37. package/dist/npm/registry.d.ts.map +1 -0
  38. package/dist/npm/resolver.d.ts +52 -0
  39. package/dist/npm/resolver.d.ts.map +1 -0
  40. package/dist/npm/tarball.d.ts +29 -0
  41. package/dist/npm/tarball.d.ts.map +1 -0
  42. package/dist/runtime-interface.d.ts +90 -0
  43. package/dist/runtime-interface.d.ts.map +1 -0
  44. package/dist/runtime.d.ts +103 -0
  45. package/dist/runtime.d.ts.map +1 -0
  46. package/dist/sandbox-helpers.d.ts +43 -0
  47. package/dist/sandbox-helpers.d.ts.map +1 -0
  48. package/dist/sandbox-runtime.d.ts +65 -0
  49. package/dist/sandbox-runtime.d.ts.map +1 -0
  50. package/dist/server-bridge.d.ts +89 -0
  51. package/dist/server-bridge.d.ts.map +1 -0
  52. package/dist/shims/assert.d.ts +51 -0
  53. package/dist/shims/assert.d.ts.map +1 -0
  54. package/dist/shims/async_hooks.d.ts +37 -0
  55. package/dist/shims/async_hooks.d.ts.map +1 -0
  56. package/dist/shims/buffer.d.ts +20 -0
  57. package/dist/shims/buffer.d.ts.map +1 -0
  58. package/dist/shims/child_process-browser.d.ts +92 -0
  59. package/dist/shims/child_process-browser.d.ts.map +1 -0
  60. package/dist/shims/child_process.d.ts +93 -0
  61. package/dist/shims/child_process.d.ts.map +1 -0
  62. package/dist/shims/chokidar.d.ts +55 -0
  63. package/dist/shims/chokidar.d.ts.map +1 -0
  64. package/dist/shims/cluster.d.ts +52 -0
  65. package/dist/shims/cluster.d.ts.map +1 -0
  66. package/dist/shims/crypto.d.ts +122 -0
  67. package/dist/shims/crypto.d.ts.map +1 -0
  68. package/dist/shims/dgram.d.ts +34 -0
  69. package/dist/shims/dgram.d.ts.map +1 -0
  70. package/dist/shims/diagnostics_channel.d.ts +80 -0
  71. package/dist/shims/diagnostics_channel.d.ts.map +1 -0
  72. package/dist/shims/dns.d.ts +87 -0
  73. package/dist/shims/dns.d.ts.map +1 -0
  74. package/dist/shims/domain.d.ts +25 -0
  75. package/dist/shims/domain.d.ts.map +1 -0
  76. package/dist/shims/esbuild.d.ts +105 -0
  77. package/dist/shims/esbuild.d.ts.map +1 -0
  78. package/dist/shims/events.d.ts +37 -0
  79. package/dist/shims/events.d.ts.map +1 -0
  80. package/dist/shims/fs.d.ts +115 -0
  81. package/dist/shims/fs.d.ts.map +1 -0
  82. package/dist/shims/fsevents.d.ts +67 -0
  83. package/dist/shims/fsevents.d.ts.map +1 -0
  84. package/dist/shims/http.d.ts +217 -0
  85. package/dist/shims/http.d.ts.map +1 -0
  86. package/dist/shims/http2.d.ts +81 -0
  87. package/dist/shims/http2.d.ts.map +1 -0
  88. package/dist/shims/https.d.ts +36 -0
  89. package/dist/shims/https.d.ts.map +1 -0
  90. package/dist/shims/inspector.d.ts +25 -0
  91. package/dist/shims/inspector.d.ts.map +1 -0
  92. package/dist/shims/module.d.ts +22 -0
  93. package/dist/shims/module.d.ts.map +1 -0
  94. package/dist/shims/net.d.ts +100 -0
  95. package/dist/shims/net.d.ts.map +1 -0
  96. package/dist/shims/os.d.ts +159 -0
  97. package/dist/shims/os.d.ts.map +1 -0
  98. package/dist/shims/path.d.ts +72 -0
  99. package/dist/shims/path.d.ts.map +1 -0
  100. package/dist/shims/perf_hooks.d.ts +50 -0
  101. package/dist/shims/perf_hooks.d.ts.map +1 -0
  102. package/dist/shims/process.d.ts +93 -0
  103. package/dist/shims/process.d.ts.map +1 -0
  104. package/dist/shims/querystring.d.ts +23 -0
  105. package/dist/shims/querystring.d.ts.map +1 -0
  106. package/dist/shims/readdirp.d.ts +52 -0
  107. package/dist/shims/readdirp.d.ts.map +1 -0
  108. package/dist/shims/readline.d.ts +62 -0
  109. package/dist/shims/readline.d.ts.map +1 -0
  110. package/dist/shims/rollup.d.ts +34 -0
  111. package/dist/shims/rollup.d.ts.map +1 -0
  112. package/dist/shims/sentry.d.ts +163 -0
  113. package/dist/shims/sentry.d.ts.map +1 -0
  114. package/dist/shims/stream.d.ts +181 -0
  115. package/dist/shims/stream.d.ts.map +1 -0
  116. package/dist/shims/tls.d.ts +53 -0
  117. package/dist/shims/tls.d.ts.map +1 -0
  118. package/dist/shims/tty.d.ts +30 -0
  119. package/dist/shims/tty.d.ts.map +1 -0
  120. package/dist/shims/url.d.ts +64 -0
  121. package/dist/shims/url.d.ts.map +1 -0
  122. package/dist/shims/util.d.ts +106 -0
  123. package/dist/shims/util.d.ts.map +1 -0
  124. package/dist/shims/v8.d.ts +73 -0
  125. package/dist/shims/v8.d.ts.map +1 -0
  126. package/dist/shims/vfs-adapter.d.ts +126 -0
  127. package/dist/shims/vfs-adapter.d.ts.map +1 -0
  128. package/dist/shims/vm.d.ts +45 -0
  129. package/dist/shims/vm.d.ts.map +1 -0
  130. package/dist/shims/worker_threads.d.ts +66 -0
  131. package/dist/shims/worker_threads.d.ts.map +1 -0
  132. package/dist/shims/ws.d.ts +66 -0
  133. package/dist/shims/ws.d.ts.map +1 -0
  134. package/dist/shims/zlib.d.ts +161 -0
  135. package/dist/shims/zlib.d.ts.map +1 -0
  136. package/dist/transform.d.ts +24 -0
  137. package/dist/transform.d.ts.map +1 -0
  138. package/dist/virtual-fs.d.ts +226 -0
  139. package/dist/virtual-fs.d.ts.map +1 -0
  140. package/dist/vite-demo.d.ts +35 -0
  141. package/dist/vite-demo.d.ts.map +1 -0
  142. package/dist/vite-sw.js +132 -0
  143. package/dist/worker/runtime-worker.d.ts +8 -0
  144. package/dist/worker/runtime-worker.d.ts.map +1 -0
  145. package/dist/worker-runtime.d.ts +50 -0
  146. package/dist/worker-runtime.d.ts.map +1 -0
  147. package/package.json +85 -0
  148. package/src/ai-chatbot-demo-entry.ts +244 -0
  149. package/src/ai-chatbot-demo.ts +509 -0
  150. package/src/convex-app-demo-entry.ts +1107 -0
  151. package/src/convex-app-demo.ts +1316 -0
  152. package/src/cors-proxy.ts +81 -0
  153. package/src/create-runtime.ts +147 -0
  154. package/src/demo.ts +304 -0
  155. package/src/dev-server.ts +274 -0
  156. package/src/frameworks/next-dev-server.ts +2224 -0
  157. package/src/frameworks/vite-dev-server.ts +702 -0
  158. package/src/index.ts +101 -0
  159. package/src/next-demo.ts +1784 -0
  160. package/src/npm/index.ts +347 -0
  161. package/src/npm/registry.ts +152 -0
  162. package/src/npm/resolver.ts +385 -0
  163. package/src/npm/tarball.ts +209 -0
  164. package/src/runtime-interface.ts +103 -0
  165. package/src/runtime.ts +1046 -0
  166. package/src/sandbox-helpers.ts +173 -0
  167. package/src/sandbox-runtime.ts +252 -0
  168. package/src/server-bridge.ts +426 -0
  169. package/src/shims/assert.ts +664 -0
  170. package/src/shims/async_hooks.ts +86 -0
  171. package/src/shims/buffer.ts +75 -0
  172. package/src/shims/child_process-browser.ts +217 -0
  173. package/src/shims/child_process.ts +463 -0
  174. package/src/shims/chokidar.ts +313 -0
  175. package/src/shims/cluster.ts +67 -0
  176. package/src/shims/crypto.ts +830 -0
  177. package/src/shims/dgram.ts +47 -0
  178. package/src/shims/diagnostics_channel.ts +196 -0
  179. package/src/shims/dns.ts +172 -0
  180. package/src/shims/domain.ts +58 -0
  181. package/src/shims/esbuild.ts +805 -0
  182. package/src/shims/events.ts +195 -0
  183. package/src/shims/fs.ts +803 -0
  184. package/src/shims/fsevents.ts +63 -0
  185. package/src/shims/http.ts +904 -0
  186. package/src/shims/http2.ts +96 -0
  187. package/src/shims/https.ts +86 -0
  188. package/src/shims/inspector.ts +30 -0
  189. package/src/shims/module.ts +82 -0
  190. package/src/shims/net.ts +359 -0
  191. package/src/shims/os.ts +195 -0
  192. package/src/shims/path.ts +199 -0
  193. package/src/shims/perf_hooks.ts +92 -0
  194. package/src/shims/process.ts +346 -0
  195. package/src/shims/querystring.ts +97 -0
  196. package/src/shims/readdirp.ts +228 -0
  197. package/src/shims/readline.ts +110 -0
  198. package/src/shims/rollup.ts +80 -0
  199. package/src/shims/sentry.ts +133 -0
  200. package/src/shims/stream.ts +1126 -0
  201. package/src/shims/tls.ts +95 -0
  202. package/src/shims/tty.ts +64 -0
  203. package/src/shims/url.ts +171 -0
  204. package/src/shims/util.ts +312 -0
  205. package/src/shims/v8.ts +113 -0
  206. package/src/shims/vfs-adapter.ts +402 -0
  207. package/src/shims/vm.ts +83 -0
  208. package/src/shims/worker_threads.ts +111 -0
  209. package/src/shims/ws.ts +382 -0
  210. package/src/shims/zlib.ts +289 -0
  211. package/src/transform.ts +313 -0
  212. package/src/types/external.d.ts +67 -0
  213. package/src/virtual-fs.ts +903 -0
  214. package/src/vite-demo.ts +577 -0
  215. package/src/worker/runtime-worker.ts +128 -0
  216. package/src/worker-runtime.ts +145 -0
@@ -0,0 +1,2224 @@
1
+ /**
2
+ * NextDevServer - Next.js-compatible dev server for browser environment
3
+ * Implements file-based routing, API routes, and HMR
4
+ */
5
+
6
+ import { DevServer, DevServerOptions, ResponseData, HMRUpdate } from '../dev-server';
7
+ import { VirtualFS } from '../virtual-fs';
8
+ import { Buffer } from '../shims/stream';
9
+
10
+ // Check if we're in a real browser environment (not jsdom or Node.js)
11
+ const isBrowser = typeof window !== 'undefined' &&
12
+ typeof window.navigator !== 'undefined' &&
13
+ 'serviceWorker' in window.navigator;
14
+
15
+ // Window.__esbuild type is declared in src/types/external.d.ts
16
+
17
+ /**
18
+ * Initialize esbuild-wasm for browser transforms
19
+ */
20
+ async function initEsbuild(): Promise<void> {
21
+ if (!isBrowser) return;
22
+
23
+ if (window.__esbuild) {
24
+ return;
25
+ }
26
+
27
+ if (window.__esbuildInitPromise) {
28
+ return window.__esbuildInitPromise;
29
+ }
30
+
31
+ window.__esbuildInitPromise = (async () => {
32
+ try {
33
+ const mod = await import(
34
+ /* @vite-ignore */
35
+ 'https://esm.sh/esbuild-wasm@0.20.0'
36
+ );
37
+
38
+ const esbuildMod = mod.default || mod;
39
+
40
+ try {
41
+ await esbuildMod.initialize({
42
+ wasmURL: 'https://unpkg.com/esbuild-wasm@0.20.0/esbuild.wasm',
43
+ });
44
+ console.log('[NextDevServer] esbuild-wasm initialized');
45
+ } catch (initError) {
46
+ if (initError instanceof Error && initError.message.includes('Cannot call "initialize" more than once')) {
47
+ console.log('[NextDevServer] esbuild-wasm already initialized, reusing');
48
+ } else {
49
+ throw initError;
50
+ }
51
+ }
52
+
53
+ window.__esbuild = esbuildMod;
54
+ } catch (error) {
55
+ console.error('[NextDevServer] Failed to initialize esbuild:', error);
56
+ window.__esbuildInitPromise = undefined;
57
+ throw error;
58
+ }
59
+ })();
60
+
61
+ return window.__esbuildInitPromise;
62
+ }
63
+
64
+ function getEsbuild(): typeof import('esbuild-wasm') | undefined {
65
+ return isBrowser ? window.__esbuild : undefined;
66
+ }
67
+
68
+ export interface NextDevServerOptions extends DevServerOptions {
69
+ /** Pages directory (default: '/pages') */
70
+ pagesDir?: string;
71
+ /** App directory for App Router (default: '/app') */
72
+ appDir?: string;
73
+ /** Public directory for static assets (default: '/public') */
74
+ publicDir?: string;
75
+ /** Prefer App Router over Pages Router (default: auto-detect) */
76
+ preferAppRouter?: boolean;
77
+ /** Environment variables (NEXT_PUBLIC_* are available in browser code via process.env) */
78
+ env?: Record<string, string>;
79
+ }
80
+
81
+ /**
82
+ * Tailwind CSS CDN script for runtime JIT compilation
83
+ */
84
+ const TAILWIND_CDN_SCRIPT = `<script src="https://cdn.tailwindcss.com"></script>`;
85
+
86
+ /**
87
+ * CORS Proxy script - provides proxyFetch function in the iframe
88
+ * Reads proxy URL from localStorage (set by parent window)
89
+ */
90
+ const CORS_PROXY_SCRIPT = `
91
+ <script>
92
+ // CORS Proxy support for external API calls
93
+ window.__getCorsProxy = function() {
94
+ return localStorage.getItem('__corsProxyUrl') || null;
95
+ };
96
+
97
+ window.__setCorsProxy = function(url) {
98
+ if (url) {
99
+ localStorage.setItem('__corsProxyUrl', url);
100
+ } else {
101
+ localStorage.removeItem('__corsProxyUrl');
102
+ }
103
+ };
104
+
105
+ window.__proxyFetch = async function(url, options) {
106
+ const proxyUrl = window.__getCorsProxy();
107
+ if (proxyUrl) {
108
+ const proxiedUrl = proxyUrl + encodeURIComponent(url);
109
+ return fetch(proxiedUrl, options);
110
+ }
111
+ return fetch(url, options);
112
+ };
113
+ </script>
114
+ `;
115
+
116
+ /**
117
+ * React Refresh preamble - MUST run before React is loaded
118
+ */
119
+ const REACT_REFRESH_PREAMBLE = `
120
+ <script type="module">
121
+ // Block until React Refresh is loaded and initialized
122
+ const RefreshRuntime = await import('https://esm.sh/react-refresh@0.14.0/runtime').then(m => m.default || m);
123
+
124
+ RefreshRuntime.injectIntoGlobalHook(window);
125
+ window.$RefreshRuntime$ = RefreshRuntime;
126
+ window.$RefreshRegCount$ = 0;
127
+
128
+ window.$RefreshReg$ = (type, id) => {
129
+ window.$RefreshRegCount$++;
130
+ RefreshRuntime.register(type, id);
131
+ };
132
+
133
+ window.$RefreshSig$ = () => (type) => type;
134
+
135
+ console.log('[HMR] React Refresh initialized');
136
+ </script>
137
+ `;
138
+
139
+ /**
140
+ * HMR client script for Next.js
141
+ */
142
+ const HMR_CLIENT_SCRIPT = `
143
+ <script type="module">
144
+ (function() {
145
+ const hotModules = new Map();
146
+ const pendingUpdates = new Map();
147
+
148
+ window.__vite_hot_context__ = function createHotContext(ownerPath) {
149
+ if (hotModules.has(ownerPath)) {
150
+ return hotModules.get(ownerPath);
151
+ }
152
+
153
+ const hot = {
154
+ data: {},
155
+ accept(callback) {
156
+ hot._acceptCallback = callback;
157
+ },
158
+ dispose(callback) {
159
+ hot._disposeCallback = callback;
160
+ },
161
+ invalidate() {
162
+ location.reload();
163
+ },
164
+ prune(callback) {
165
+ hot._pruneCallback = callback;
166
+ },
167
+ on(event, cb) {},
168
+ off(event, cb) {},
169
+ send(event, data) {},
170
+ _acceptCallback: null,
171
+ _disposeCallback: null,
172
+ _pruneCallback: null,
173
+ };
174
+
175
+ hotModules.set(ownerPath, hot);
176
+ return hot;
177
+ };
178
+
179
+ // Listen for HMR updates via postMessage (works with sandboxed iframes)
180
+ window.addEventListener('message', async (event) => {
181
+ // Filter for HMR messages only
182
+ if (!event.data || event.data.channel !== 'next-hmr') return;
183
+ const { type, path, timestamp } = event.data;
184
+
185
+ if (type === 'update') {
186
+ console.log('[HMR] Update:', path);
187
+
188
+ if (path.endsWith('.css')) {
189
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
190
+ links.forEach(link => {
191
+ const href = link.getAttribute('href');
192
+ if (href && href.includes(path.replace(/^\\//, ''))) {
193
+ link.href = href.split('?')[0] + '?t=' + timestamp;
194
+ }
195
+ });
196
+
197
+ const styles = document.querySelectorAll('style[data-next-dev-id]');
198
+ styles.forEach(style => {
199
+ const id = style.getAttribute('data-next-dev-id');
200
+ if (id && id.includes(path.replace(/^\\//, ''))) {
201
+ import(path + '?t=' + timestamp).catch(() => {});
202
+ }
203
+ });
204
+ } else if (path.match(/\\.(jsx?|tsx?)$/)) {
205
+ await handleJSUpdate(path, timestamp);
206
+ }
207
+ } else if (type === 'full-reload') {
208
+ console.log('[HMR] Full reload');
209
+ location.reload();
210
+ }
211
+ });
212
+
213
+ async function handleJSUpdate(path, timestamp) {
214
+ const normalizedPath = path.startsWith('/') ? path : '/' + path;
215
+ const hot = hotModules.get(normalizedPath);
216
+
217
+ try {
218
+ if (hot && hot._disposeCallback) {
219
+ hot._disposeCallback(hot.data);
220
+ }
221
+
222
+ if (window.$RefreshRuntime$) {
223
+ pendingUpdates.set(normalizedPath, timestamp);
224
+
225
+ if (pendingUpdates.size === 1) {
226
+ setTimeout(async () => {
227
+ try {
228
+ for (const [modulePath, ts] of pendingUpdates) {
229
+ const moduleUrl = '.' + modulePath + '?t=' + ts;
230
+ await import(moduleUrl);
231
+ }
232
+
233
+ window.$RefreshRuntime$.performReactRefresh();
234
+ console.log('[HMR] Updated', pendingUpdates.size, 'module(s)');
235
+
236
+ pendingUpdates.clear();
237
+ } catch (error) {
238
+ console.error('[HMR] Failed to apply update:', error);
239
+ pendingUpdates.clear();
240
+ location.reload();
241
+ }
242
+ }, 30);
243
+ }
244
+ } else {
245
+ console.log('[HMR] React Refresh not available, reloading page');
246
+ location.reload();
247
+ }
248
+ } catch (error) {
249
+ console.error('[HMR] Update failed:', error);
250
+ location.reload();
251
+ }
252
+ }
253
+
254
+ console.log('[HMR] Next.js client ready');
255
+ })();
256
+ </script>
257
+ `;
258
+
259
+ /**
260
+ * Next.js Link shim code
261
+ */
262
+ const NEXT_LINK_SHIM = `
263
+ import React from 'react';
264
+
265
+ const getVirtualBasePath = () => {
266
+ const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
267
+ if (!match) return '';
268
+ return match[0].endsWith('/') ? match[0] : match[0] + '/';
269
+ };
270
+
271
+ const applyVirtualBase = (url) => {
272
+ if (typeof url !== 'string') return url;
273
+ if (!url || url.startsWith('#') || url.startsWith('?')) return url;
274
+ if (/^(https?:)?\\/\\//.test(url)) return url;
275
+
276
+ const base = getVirtualBasePath();
277
+ if (!base) return url;
278
+ if (url.startsWith(base)) return url;
279
+ if (url.startsWith('/')) return base + url.slice(1);
280
+ return base + url;
281
+ };
282
+
283
+ export default function Link({ href, children, ...props }) {
284
+ const handleClick = (e) => {
285
+ if (props.onClick) {
286
+ props.onClick(e);
287
+ }
288
+
289
+ // Allow cmd/ctrl click to open in new tab
290
+ if (e.metaKey || e.ctrlKey) {
291
+ return;
292
+ }
293
+
294
+ if (typeof href !== 'string' || !href || href.startsWith('#') || href.startsWith('?')) {
295
+ return;
296
+ }
297
+
298
+ if (/^(https?:)?\\/\\//.test(href)) {
299
+ return;
300
+ }
301
+
302
+ e.preventDefault();
303
+ const resolvedHref = applyVirtualBase(href);
304
+ window.history.pushState({}, '', resolvedHref);
305
+ window.dispatchEvent(new PopStateEvent('popstate'));
306
+ };
307
+
308
+ return React.createElement('a', { href, onClick: handleClick, ...props }, children);
309
+ }
310
+
311
+ export { Link };
312
+ `;
313
+
314
+ /**
315
+ * Next.js Router shim code
316
+ */
317
+ const NEXT_ROUTER_SHIM = `
318
+ import React, { useState, useEffect, createContext, useContext } from 'react';
319
+
320
+ const RouterContext = createContext(null);
321
+
322
+ const getVirtualBasePath = () => {
323
+ const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
324
+ if (!match) return '';
325
+ return match[0].endsWith('/') ? match[0] : match[0] + '/';
326
+ };
327
+
328
+ const applyVirtualBase = (url) => {
329
+ if (typeof url !== 'string') return url;
330
+ if (!url || url.startsWith('#') || url.startsWith('?')) return url;
331
+ if (/^(https?:)?\\/\\//.test(url)) return url;
332
+
333
+ const base = getVirtualBasePath();
334
+ if (!base) return url;
335
+ if (url.startsWith(base)) return url;
336
+ if (url.startsWith('/')) return base + url.slice(1);
337
+ return base + url;
338
+ };
339
+
340
+ const stripVirtualBase = (pathname) => {
341
+ const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
342
+ if (!match) return pathname;
343
+ return '/' + pathname.slice(match[0].length);
344
+ };
345
+
346
+ export function useRouter() {
347
+ const [pathname, setPathname] = useState(
348
+ typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
349
+ );
350
+ const [query, setQuery] = useState({});
351
+
352
+ useEffect(() => {
353
+ const updateRoute = () => {
354
+ setPathname(stripVirtualBase(window.location.pathname));
355
+ setQuery(Object.fromEntries(new URLSearchParams(window.location.search)));
356
+ };
357
+
358
+ window.addEventListener('popstate', updateRoute);
359
+ updateRoute();
360
+
361
+ return () => window.removeEventListener('popstate', updateRoute);
362
+ }, []);
363
+
364
+ return {
365
+ pathname,
366
+ query,
367
+ asPath: pathname + window.location.search,
368
+ push: (url, as, options) => {
369
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
370
+ window.location.href = url;
371
+ return Promise.resolve(true);
372
+ }
373
+ const resolvedUrl = applyVirtualBase(url);
374
+ window.history.pushState({}, '', resolvedUrl);
375
+ window.dispatchEvent(new PopStateEvent('popstate'));
376
+ return Promise.resolve(true);
377
+ },
378
+ replace: (url, as, options) => {
379
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
380
+ window.location.href = url;
381
+ return Promise.resolve(true);
382
+ }
383
+ const resolvedUrl = applyVirtualBase(url);
384
+ window.history.replaceState({}, '', resolvedUrl);
385
+ window.dispatchEvent(new PopStateEvent('popstate'));
386
+ return Promise.resolve(true);
387
+ },
388
+ prefetch: () => Promise.resolve(),
389
+ back: () => window.history.back(),
390
+ forward: () => window.history.forward(),
391
+ reload: () => window.location.reload(),
392
+ events: {
393
+ on: () => {},
394
+ off: () => {},
395
+ emit: () => {},
396
+ },
397
+ isFallback: false,
398
+ isReady: true,
399
+ isPreview: false,
400
+ };
401
+ }
402
+
403
+ export const Router = {
404
+ events: {
405
+ on: () => {},
406
+ off: () => {},
407
+ emit: () => {},
408
+ },
409
+ push: (url) => {
410
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
411
+ window.location.href = url;
412
+ return Promise.resolve(true);
413
+ }
414
+ const resolvedUrl = applyVirtualBase(url);
415
+ window.history.pushState({}, '', resolvedUrl);
416
+ window.dispatchEvent(new PopStateEvent('popstate'));
417
+ return Promise.resolve(true);
418
+ },
419
+ replace: (url) => {
420
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
421
+ window.location.href = url;
422
+ return Promise.resolve(true);
423
+ }
424
+ const resolvedUrl = applyVirtualBase(url);
425
+ window.history.replaceState({}, '', resolvedUrl);
426
+ window.dispatchEvent(new PopStateEvent('popstate'));
427
+ return Promise.resolve(true);
428
+ },
429
+ };
430
+
431
+ export default { useRouter, Router };
432
+ `;
433
+
434
+ /**
435
+ * Next.js Navigation shim code (App Router)
436
+ *
437
+ * This shim provides App Router-specific navigation hooks from 'next/navigation'.
438
+ * These are DIFFERENT from the Pages Router hooks in 'next/router':
439
+ *
440
+ * Pages Router (next/router):
441
+ * - useRouter() returns { pathname, query, push, replace, events, ... }
442
+ * - Has router.events for route change subscriptions
443
+ * - query object contains URL params
444
+ *
445
+ * App Router (next/navigation):
446
+ * - useRouter() returns { push, replace, back, forward, refresh, prefetch }
447
+ * - usePathname() for current path
448
+ * - useSearchParams() for URL search params
449
+ * - useParams() for dynamic route segments
450
+ * - No events - use useEffect with pathname/searchParams instead
451
+ *
452
+ * @see https://nextjs.org/docs/app/api-reference/functions/use-router
453
+ */
454
+ const NEXT_NAVIGATION_SHIM = `
455
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
456
+
457
+ const getVirtualBasePath = () => {
458
+ const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
459
+ if (!match) return '';
460
+ return match[0].endsWith('/') ? match[0] : match[0] + '/';
461
+ };
462
+
463
+ const applyVirtualBase = (url) => {
464
+ if (typeof url !== 'string') return url;
465
+ if (!url || url.startsWith('#') || url.startsWith('?')) return url;
466
+ if (/^(https?:)?\\/\\//.test(url)) return url;
467
+
468
+ const base = getVirtualBasePath();
469
+ if (!base) return url;
470
+ if (url.startsWith(base)) return url;
471
+ if (url.startsWith('/')) return base + url.slice(1);
472
+ return base + url;
473
+ };
474
+
475
+ const stripVirtualBase = (pathname) => {
476
+ const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
477
+ if (!match) return pathname;
478
+ return '/' + pathname.slice(match[0].length);
479
+ };
480
+
481
+ /**
482
+ * App Router's useRouter hook
483
+ * Returns navigation methods only (no pathname, no query)
484
+ * Use usePathname() and useSearchParams() for URL info
485
+ */
486
+ export function useRouter() {
487
+ const push = useCallback((url, options) => {
488
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
489
+ window.location.href = url;
490
+ return;
491
+ }
492
+ const resolvedUrl = applyVirtualBase(url);
493
+ window.history.pushState({}, '', resolvedUrl);
494
+ window.dispatchEvent(new PopStateEvent('popstate'));
495
+ }, []);
496
+
497
+ const replace = useCallback((url, options) => {
498
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
499
+ window.location.href = url;
500
+ return;
501
+ }
502
+ const resolvedUrl = applyVirtualBase(url);
503
+ window.history.replaceState({}, '', resolvedUrl);
504
+ window.dispatchEvent(new PopStateEvent('popstate'));
505
+ }, []);
506
+
507
+ const back = useCallback(() => window.history.back(), []);
508
+ const forward = useCallback(() => window.history.forward(), []);
509
+ const refresh = useCallback(() => window.location.reload(), []);
510
+ const prefetch = useCallback(() => Promise.resolve(), []);
511
+
512
+ return useMemo(() => ({
513
+ push,
514
+ replace,
515
+ back,
516
+ forward,
517
+ refresh,
518
+ prefetch,
519
+ }), [push, replace, back, forward, refresh, prefetch]);
520
+ }
521
+
522
+ /**
523
+ * usePathname - Returns the current URL pathname
524
+ * Reactively updates when navigation occurs
525
+ * @example const pathname = usePathname(); // '/dashboard/settings'
526
+ */
527
+ export function usePathname() {
528
+ const [pathname, setPathname] = useState(
529
+ typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
530
+ );
531
+
532
+ useEffect(() => {
533
+ const handler = () => setPathname(stripVirtualBase(window.location.pathname));
534
+ window.addEventListener('popstate', handler);
535
+ return () => window.removeEventListener('popstate', handler);
536
+ }, []);
537
+
538
+ return pathname;
539
+ }
540
+
541
+ /**
542
+ * useSearchParams - Returns the current URL search parameters
543
+ * @example const searchParams = useSearchParams();
544
+ * const query = searchParams.get('q'); // '?q=hello' -> 'hello'
545
+ */
546
+ export function useSearchParams() {
547
+ const [searchParams, setSearchParams] = useState(() => {
548
+ if (typeof window === 'undefined') return new URLSearchParams();
549
+ return new URLSearchParams(window.location.search);
550
+ });
551
+
552
+ useEffect(() => {
553
+ const handler = () => {
554
+ setSearchParams(new URLSearchParams(window.location.search));
555
+ };
556
+ window.addEventListener('popstate', handler);
557
+ return () => window.removeEventListener('popstate', handler);
558
+ }, []);
559
+
560
+ return searchParams;
561
+ }
562
+
563
+ /**
564
+ * useParams - Returns dynamic route parameters
565
+ * For route /users/[id]/page.jsx with URL /users/123:
566
+ * @example const { id } = useParams(); // { id: '123' }
567
+ *
568
+ * NOTE: This simplified implementation returns empty object.
569
+ * Full implementation would need route pattern matching.
570
+ */
571
+ export function useParams() {
572
+ // In a real implementation, this would parse the current route
573
+ // against the route pattern to extract params
574
+ // For now, return empty object - works for basic cases
575
+ return {};
576
+ }
577
+
578
+ /**
579
+ * useSelectedLayoutSegment - Returns the active child segment one level below
580
+ * Useful for styling active nav items in layouts
581
+ * @example For /dashboard/settings, returns 'settings' in dashboard layout
582
+ */
583
+ export function useSelectedLayoutSegment() {
584
+ const pathname = usePathname();
585
+ const segments = pathname.split('/').filter(Boolean);
586
+ return segments[0] || null;
587
+ }
588
+
589
+ /**
590
+ * useSelectedLayoutSegments - Returns all active child segments
591
+ * @example For /dashboard/settings/profile, returns ['dashboard', 'settings', 'profile']
592
+ */
593
+ export function useSelectedLayoutSegments() {
594
+ const pathname = usePathname();
595
+ return pathname.split('/').filter(Boolean);
596
+ }
597
+
598
+ /**
599
+ * redirect - Programmatic redirect (typically used in Server Components)
600
+ * In this browser implementation, performs immediate navigation
601
+ */
602
+ export function redirect(url) {
603
+ if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
604
+ window.location.href = url;
605
+ return;
606
+ }
607
+ window.location.href = applyVirtualBase(url);
608
+ }
609
+
610
+ /**
611
+ * notFound - Trigger the not-found UI
612
+ * In this browser implementation, throws an error
613
+ */
614
+ export function notFound() {
615
+ throw new Error('NEXT_NOT_FOUND');
616
+ }
617
+
618
+ // Re-export Link for convenience (can import from next/navigation or next/link)
619
+ export { default as Link } from 'next/link';
620
+ `;
621
+
622
+ /**
623
+ * Next.js Head shim code
624
+ */
625
+ const NEXT_HEAD_SHIM = `
626
+ import React, { useEffect } from 'react';
627
+
628
+ export default function Head({ children }) {
629
+ useEffect(() => {
630
+ // Process children and update document.head
631
+ React.Children.forEach(children, (child) => {
632
+ if (!React.isValidElement(child)) return;
633
+
634
+ const { type, props } = child;
635
+
636
+ if (type === 'title' && props.children) {
637
+ document.title = Array.isArray(props.children)
638
+ ? props.children.join('')
639
+ : props.children;
640
+ } else if (type === 'meta') {
641
+ const existingMeta = props.name
642
+ ? document.querySelector(\`meta[name="\${props.name}"]\`)
643
+ : props.property
644
+ ? document.querySelector(\`meta[property="\${props.property}"]\`)
645
+ : null;
646
+
647
+ if (existingMeta) {
648
+ Object.keys(props).forEach(key => {
649
+ existingMeta.setAttribute(key, props[key]);
650
+ });
651
+ } else {
652
+ const meta = document.createElement('meta');
653
+ Object.keys(props).forEach(key => {
654
+ meta.setAttribute(key, props[key]);
655
+ });
656
+ document.head.appendChild(meta);
657
+ }
658
+ } else if (type === 'link') {
659
+ const link = document.createElement('link');
660
+ Object.keys(props).forEach(key => {
661
+ link.setAttribute(key, props[key]);
662
+ });
663
+ document.head.appendChild(link);
664
+ }
665
+ });
666
+ }, [children]);
667
+
668
+ return null;
669
+ }
670
+ `;
671
+
672
+ /**
673
+ * NextDevServer - A lightweight Next.js-compatible development server
674
+ *
675
+ * Supports both routing paradigms:
676
+ *
677
+ * 1. PAGES ROUTER (legacy, /pages directory):
678
+ * - /pages/index.jsx -> /
679
+ * - /pages/about.jsx -> /about
680
+ * - /pages/users/[id].jsx -> /users/:id (dynamic)
681
+ * - /pages/api/hello.js -> /api/hello (API route)
682
+ * - Uses next/router for navigation
683
+ *
684
+ * 2. APP ROUTER (new, /app directory):
685
+ * - /app/page.jsx -> /
686
+ * - /app/about/page.jsx -> /about
687
+ * - /app/users/[id]/page.jsx -> /users/:id (dynamic)
688
+ * - /app/layout.jsx -> Root layout (wraps all pages)
689
+ * - /app/about/layout.jsx -> Nested layout (wraps /about/*)
690
+ * - Uses next/navigation for navigation
691
+ *
692
+ * The server auto-detects which router to use based on directory existence,
693
+ * preferring App Router if both exist. Can be overridden via options.
694
+ */
695
+ export class NextDevServer extends DevServer {
696
+ /** Pages Router directory (default: '/pages') */
697
+ private pagesDir: string;
698
+
699
+ /** App Router directory (default: '/app') */
700
+ private appDir: string;
701
+
702
+ /** Static assets directory (default: '/public') */
703
+ private publicDir: string;
704
+
705
+ /** Whether to use App Router (true) or Pages Router (false) */
706
+ private useAppRouter: boolean;
707
+
708
+ /** Cleanup function for file watchers */
709
+ private watcherCleanup: (() => void) | null = null;
710
+
711
+ /** Target window for HMR updates (iframe contentWindow) */
712
+ private hmrTargetWindow: Window | null = null;
713
+
714
+ /** Store options for later access (e.g., env vars) */
715
+ private options: NextDevServerOptions;
716
+
717
+ constructor(vfs: VirtualFS, options: NextDevServerOptions) {
718
+ super(vfs, options);
719
+ this.options = options;
720
+ this.pagesDir = options.pagesDir || '/pages';
721
+ this.appDir = options.appDir || '/app';
722
+ this.publicDir = options.publicDir || '/public';
723
+
724
+ // Auto-detect which router to use based on directory existence
725
+ // User can override with preferAppRouter option
726
+ if (options.preferAppRouter !== undefined) {
727
+ this.useAppRouter = options.preferAppRouter;
728
+ } else {
729
+ // Prefer App Router if /app directory exists with a page.jsx file
730
+ this.useAppRouter = this.hasAppRouter();
731
+ }
732
+ }
733
+
734
+ /**
735
+ * Set an environment variable at runtime
736
+ * NEXT_PUBLIC_* variables will be available via process.env in browser code
737
+ */
738
+ setEnv(key: string, value: string): void {
739
+ this.options.env = this.options.env || {};
740
+ this.options.env[key] = value;
741
+ }
742
+
743
+ /**
744
+ * Get current environment variables
745
+ */
746
+ getEnv(): Record<string, string> {
747
+ return { ...this.options.env };
748
+ }
749
+
750
+ /**
751
+ * Set the target window for HMR updates (typically iframe.contentWindow)
752
+ * This enables HMR to work with sandboxed iframes via postMessage
753
+ */
754
+ setHMRTarget(targetWindow: Window): void {
755
+ this.hmrTargetWindow = targetWindow;
756
+ }
757
+
758
+ /**
759
+ * Generate a script tag that defines process.env with NEXT_PUBLIC_* variables
760
+ * This makes environment variables available to browser code via process.env.NEXT_PUBLIC_*
761
+ */
762
+ private generateEnvScript(): string {
763
+ const env = this.options.env || {};
764
+
765
+ // Filter for NEXT_PUBLIC_* variables only (Next.js convention)
766
+ const publicEnvVars = Object.entries(env)
767
+ .filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
768
+ .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record<string, string>);
769
+
770
+ // Return empty string if no public env vars
771
+ if (Object.keys(publicEnvVars).length === 0) {
772
+ return '';
773
+ }
774
+
775
+ return `<script>
776
+ // NEXT_PUBLIC_* environment variables (injected by NextDevServer)
777
+ window.process = window.process || {};
778
+ window.process.env = window.process.env || {};
779
+ Object.assign(window.process.env, ${JSON.stringify(publicEnvVars)});
780
+ </script>`;
781
+ }
782
+
783
+ /**
784
+ * Check if App Router is available
785
+ */
786
+ private hasAppRouter(): boolean {
787
+ try {
788
+ // Check if /app directory exists and has a page file
789
+ if (!this.exists(this.appDir)) return false;
790
+
791
+ // Check for root page
792
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
793
+ for (const ext of extensions) {
794
+ if (this.exists(`${this.appDir}/page${ext}`)) return true;
795
+ }
796
+ return false;
797
+ } catch {
798
+ return false;
799
+ }
800
+ }
801
+
802
+ /**
803
+ * Handle an incoming HTTP request
804
+ */
805
+ async handleRequest(
806
+ method: string,
807
+ url: string,
808
+ headers: Record<string, string>,
809
+ body?: Buffer
810
+ ): Promise<ResponseData> {
811
+ const urlObj = new URL(url, 'http://localhost');
812
+ const pathname = urlObj.pathname;
813
+
814
+ // Serve Next.js shims
815
+ if (pathname.startsWith('/_next/shims/')) {
816
+ return this.serveNextShim(pathname);
817
+ }
818
+
819
+ // Static assets from /_next/static/*
820
+ if (pathname.startsWith('/_next/static/')) {
821
+ return this.serveStaticAsset(pathname);
822
+ }
823
+
824
+ // API routes: /api/*
825
+ if (pathname.startsWith('/api/')) {
826
+ return this.handleApiRoute(method, pathname, headers, body);
827
+ }
828
+
829
+ // Public directory files
830
+ const publicPath = this.publicDir + pathname;
831
+ if (this.exists(publicPath) && !this.isDirectory(publicPath)) {
832
+ return this.serveFile(publicPath);
833
+ }
834
+
835
+ // Direct file requests (e.g., /pages/index.jsx for HMR re-imports)
836
+ if (this.needsTransform(pathname) && this.exists(pathname)) {
837
+ return this.transformAndServe(pathname, pathname);
838
+ }
839
+
840
+ // Serve regular files directly if they exist
841
+ if (this.exists(pathname) && !this.isDirectory(pathname)) {
842
+ return this.serveFile(pathname);
843
+ }
844
+
845
+ // Page routes: everything else
846
+ return this.handlePageRoute(pathname, urlObj.search);
847
+ }
848
+
849
+ /**
850
+ * Serve Next.js shims (link, router, head, navigation)
851
+ */
852
+ private serveNextShim(pathname: string): ResponseData {
853
+ const shimName = pathname.replace('/_next/shims/', '').replace('.js', '');
854
+
855
+ let code: string;
856
+ switch (shimName) {
857
+ case 'link':
858
+ code = NEXT_LINK_SHIM;
859
+ break;
860
+ case 'router':
861
+ code = NEXT_ROUTER_SHIM;
862
+ break;
863
+ case 'head':
864
+ code = NEXT_HEAD_SHIM;
865
+ break;
866
+ case 'navigation':
867
+ code = NEXT_NAVIGATION_SHIM;
868
+ break;
869
+ default:
870
+ return this.notFound(pathname);
871
+ }
872
+
873
+ const buffer = Buffer.from(code);
874
+ return {
875
+ statusCode: 200,
876
+ statusMessage: 'OK',
877
+ headers: {
878
+ 'Content-Type': 'application/javascript; charset=utf-8',
879
+ 'Content-Length': String(buffer.length),
880
+ 'Cache-Control': 'no-cache',
881
+ },
882
+ body: buffer,
883
+ };
884
+ }
885
+
886
+ /**
887
+ * Serve static assets from /_next/static/
888
+ */
889
+ private serveStaticAsset(pathname: string): ResponseData {
890
+ // Map /_next/static/* to actual file location
891
+ const filePath = pathname.replace('/_next/static/', '/');
892
+ if (this.exists(filePath)) {
893
+ return this.serveFile(filePath);
894
+ }
895
+ return this.notFound(pathname);
896
+ }
897
+
898
+ /**
899
+ * Handle API route requests
900
+ */
901
+ private async handleApiRoute(
902
+ method: string,
903
+ pathname: string,
904
+ headers: Record<string, string>,
905
+ body?: Buffer
906
+ ): Promise<ResponseData> {
907
+ // Map /api/hello → /pages/api/hello.js or .ts
908
+ const apiFile = this.resolveApiFile(pathname);
909
+
910
+ if (!apiFile) {
911
+ return {
912
+ statusCode: 404,
913
+ statusMessage: 'Not Found',
914
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
915
+ body: Buffer.from(JSON.stringify({ error: 'API route not found' })),
916
+ };
917
+ }
918
+
919
+ try {
920
+ // Read and transform the API handler to CJS for eval execution
921
+ const code = this.vfs.readFileSync(apiFile, 'utf8');
922
+ const transformed = await this.transformApiHandler(code, apiFile);
923
+
924
+ // Create mock req/res objects
925
+ const req = this.createMockRequest(method, pathname, headers, body);
926
+ const res = this.createMockResponse();
927
+
928
+ // Execute the handler
929
+ await this.executeApiHandler(transformed, req, res);
930
+
931
+ // Wait for async handlers (like those using https.get with callbacks)
932
+ // with a reasonable timeout
933
+ if (!res.isEnded()) {
934
+ const timeout = new Promise<void>((_, reject) => {
935
+ setTimeout(() => reject(new Error('API handler timeout')), 30000);
936
+ });
937
+ await Promise.race([res.waitForEnd(), timeout]);
938
+ }
939
+
940
+ return res.toResponse();
941
+ } catch (error) {
942
+ console.error('[NextDevServer] API error:', error);
943
+ return {
944
+ statusCode: 500,
945
+ statusMessage: 'Internal Server Error',
946
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
947
+ body: Buffer.from(JSON.stringify({
948
+ error: error instanceof Error ? error.message : 'Internal Server Error'
949
+ })),
950
+ };
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Handle streaming API route requests
956
+ * This is called by the server bridge for requests that need streaming support
957
+ */
958
+ async handleStreamingRequest(
959
+ method: string,
960
+ url: string,
961
+ headers: Record<string, string>,
962
+ body: Buffer | undefined,
963
+ onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void,
964
+ onChunk: (chunk: string | Uint8Array) => void,
965
+ onEnd: () => void
966
+ ): Promise<void> {
967
+ const urlObj = new URL(url, 'http://localhost');
968
+ const pathname = urlObj.pathname;
969
+
970
+ // Only handle API routes
971
+ if (!pathname.startsWith('/api/')) {
972
+ onStart(404, 'Not Found', { 'Content-Type': 'application/json' });
973
+ onChunk(JSON.stringify({ error: 'Not found' }));
974
+ onEnd();
975
+ return;
976
+ }
977
+
978
+ const apiFile = this.resolveApiFile(pathname);
979
+
980
+ if (!apiFile) {
981
+ onStart(404, 'Not Found', { 'Content-Type': 'application/json' });
982
+ onChunk(JSON.stringify({ error: 'API route not found' }));
983
+ onEnd();
984
+ return;
985
+ }
986
+
987
+ try {
988
+ const code = this.vfs.readFileSync(apiFile, 'utf8');
989
+ const transformed = await this.transformApiHandler(code, apiFile);
990
+
991
+ const req = this.createMockRequest(method, pathname, headers, body);
992
+ const res = this.createStreamingMockResponse(onStart, onChunk, onEnd);
993
+
994
+ await this.executeApiHandler(transformed, req, res);
995
+
996
+ // Wait for the response to end
997
+ if (!res.isEnded()) {
998
+ const timeout = new Promise<void>((_, reject) => {
999
+ setTimeout(() => reject(new Error('API handler timeout')), 30000);
1000
+ });
1001
+ await Promise.race([res.waitForEnd(), timeout]);
1002
+ }
1003
+ } catch (error) {
1004
+ console.error('[NextDevServer] Streaming API error:', error);
1005
+ onStart(500, 'Internal Server Error', { 'Content-Type': 'application/json' });
1006
+ onChunk(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal Server Error' }));
1007
+ onEnd();
1008
+ }
1009
+ }
1010
+
1011
+ /**
1012
+ * Create a streaming mock response that calls callbacks as data is written
1013
+ */
1014
+ private createStreamingMockResponse(
1015
+ onStart: (statusCode: number, statusMessage: string, headers: Record<string, string>) => void,
1016
+ onChunk: (chunk: string | Uint8Array) => void,
1017
+ onEnd: () => void
1018
+ ) {
1019
+ let statusCode = 200;
1020
+ let statusMessage = 'OK';
1021
+ const headers: Record<string, string> = {};
1022
+ let ended = false;
1023
+ let headersSent = false;
1024
+ let resolveEnded: (() => void) | null = null;
1025
+
1026
+ const endedPromise = new Promise<void>((resolve) => {
1027
+ resolveEnded = resolve;
1028
+ });
1029
+
1030
+ const sendHeaders = () => {
1031
+ if (!headersSent) {
1032
+ headersSent = true;
1033
+ onStart(statusCode, statusMessage, headers);
1034
+ }
1035
+ };
1036
+
1037
+ const markEnded = () => {
1038
+ if (!ended) {
1039
+ sendHeaders();
1040
+ ended = true;
1041
+ onEnd();
1042
+ if (resolveEnded) resolveEnded();
1043
+ }
1044
+ };
1045
+
1046
+ return {
1047
+ headersSent: false,
1048
+
1049
+ status(code: number) {
1050
+ statusCode = code;
1051
+ return this;
1052
+ },
1053
+ setHeader(name: string, value: string) {
1054
+ headers[name] = value;
1055
+ return this;
1056
+ },
1057
+ getHeader(name: string) {
1058
+ return headers[name];
1059
+ },
1060
+ // Write data and stream it immediately
1061
+ write(chunk: string | Buffer): boolean {
1062
+ sendHeaders();
1063
+ const data = typeof chunk === 'string' ? chunk : chunk.toString();
1064
+ onChunk(data);
1065
+ return true;
1066
+ },
1067
+ get writable() {
1068
+ return true;
1069
+ },
1070
+ json(data: unknown) {
1071
+ headers['Content-Type'] = 'application/json; charset=utf-8';
1072
+ sendHeaders();
1073
+ onChunk(JSON.stringify(data));
1074
+ markEnded();
1075
+ return this;
1076
+ },
1077
+ send(data: string | object) {
1078
+ if (typeof data === 'object') {
1079
+ return this.json(data);
1080
+ }
1081
+ sendHeaders();
1082
+ onChunk(data);
1083
+ markEnded();
1084
+ return this;
1085
+ },
1086
+ end(data?: string) {
1087
+ if (data) {
1088
+ sendHeaders();
1089
+ onChunk(data);
1090
+ }
1091
+ markEnded();
1092
+ return this;
1093
+ },
1094
+ redirect(statusOrUrl: number | string, url?: string) {
1095
+ if (typeof statusOrUrl === 'number') {
1096
+ statusCode = statusOrUrl;
1097
+ headers['Location'] = url || '/';
1098
+ } else {
1099
+ statusCode = 307;
1100
+ headers['Location'] = statusOrUrl;
1101
+ }
1102
+ markEnded();
1103
+ return this;
1104
+ },
1105
+ isEnded() {
1106
+ return ended;
1107
+ },
1108
+ waitForEnd() {
1109
+ return endedPromise;
1110
+ },
1111
+ toResponse(): ResponseData {
1112
+ // This shouldn't be called for streaming responses
1113
+ return {
1114
+ statusCode,
1115
+ statusMessage,
1116
+ headers,
1117
+ body: Buffer.from(''),
1118
+ };
1119
+ },
1120
+ };
1121
+ }
1122
+
1123
+ /**
1124
+ * Resolve API route to file path
1125
+ */
1126
+ private resolveApiFile(pathname: string): string | null {
1127
+ // Remove /api prefix and look in /pages/api
1128
+ const apiPath = pathname.replace(/^\/api/, `${this.pagesDir}/api`);
1129
+
1130
+ const extensions = ['.js', '.ts', '.jsx', '.tsx'];
1131
+
1132
+ for (const ext of extensions) {
1133
+ const filePath = apiPath + ext;
1134
+ if (this.exists(filePath)) {
1135
+ return filePath;
1136
+ }
1137
+ }
1138
+
1139
+ // Try index file
1140
+ for (const ext of extensions) {
1141
+ const filePath = `${apiPath}/index${ext}`;
1142
+ if (this.exists(filePath)) {
1143
+ return filePath;
1144
+ }
1145
+ }
1146
+
1147
+ return null;
1148
+ }
1149
+
1150
+ /**
1151
+ * Create mock Next.js request object
1152
+ */
1153
+ private createMockRequest(
1154
+ method: string,
1155
+ pathname: string,
1156
+ headers: Record<string, string>,
1157
+ body?: Buffer
1158
+ ) {
1159
+ const url = new URL(pathname, 'http://localhost');
1160
+
1161
+ return {
1162
+ method,
1163
+ url: pathname,
1164
+ headers,
1165
+ query: Object.fromEntries(url.searchParams),
1166
+ body: body ? JSON.parse(body.toString()) : undefined,
1167
+ cookies: this.parseCookies(headers.cookie || ''),
1168
+ };
1169
+ }
1170
+
1171
+ /**
1172
+ * Parse cookie header
1173
+ */
1174
+ private parseCookies(cookieHeader: string): Record<string, string> {
1175
+ const cookies: Record<string, string> = {};
1176
+ if (!cookieHeader) return cookies;
1177
+
1178
+ cookieHeader.split(';').forEach(cookie => {
1179
+ const [name, value] = cookie.trim().split('=');
1180
+ if (name && value) {
1181
+ cookies[name] = decodeURIComponent(value);
1182
+ }
1183
+ });
1184
+
1185
+ return cookies;
1186
+ }
1187
+
1188
+ /**
1189
+ * Create mock Next.js response object with streaming support
1190
+ */
1191
+ private createMockResponse() {
1192
+ let statusCode = 200;
1193
+ let statusMessage = 'OK';
1194
+ const headers: Record<string, string> = {};
1195
+ let responseBody = '';
1196
+ let ended = false;
1197
+ let resolveEnded: (() => void) | null = null;
1198
+ let headersSent = false;
1199
+
1200
+ // Promise that resolves when response is ended
1201
+ const endedPromise = new Promise<void>((resolve) => {
1202
+ resolveEnded = resolve;
1203
+ });
1204
+
1205
+ const markEnded = () => {
1206
+ if (!ended) {
1207
+ ended = true;
1208
+ if (resolveEnded) resolveEnded();
1209
+ }
1210
+ };
1211
+
1212
+ return {
1213
+ // Track if headers have been sent (for streaming)
1214
+ headersSent: false,
1215
+
1216
+ status(code: number) {
1217
+ statusCode = code;
1218
+ return this;
1219
+ },
1220
+ setHeader(name: string, value: string) {
1221
+ headers[name] = value;
1222
+ return this;
1223
+ },
1224
+ getHeader(name: string) {
1225
+ return headers[name];
1226
+ },
1227
+ // Write data to response body (for streaming)
1228
+ write(chunk: string | Buffer): boolean {
1229
+ if (!headersSent) {
1230
+ headersSent = true;
1231
+ this.headersSent = true;
1232
+ }
1233
+ responseBody += typeof chunk === 'string' ? chunk : chunk.toString();
1234
+ return true;
1235
+ },
1236
+ // Writable stream interface for AI SDK compatibility
1237
+ get writable() {
1238
+ return true;
1239
+ },
1240
+ json(data: unknown) {
1241
+ headers['Content-Type'] = 'application/json; charset=utf-8';
1242
+ responseBody = JSON.stringify(data);
1243
+ markEnded();
1244
+ return this;
1245
+ },
1246
+ send(data: string | object) {
1247
+ if (typeof data === 'object') {
1248
+ return this.json(data);
1249
+ }
1250
+ responseBody = data;
1251
+ markEnded();
1252
+ return this;
1253
+ },
1254
+ end(data?: string) {
1255
+ if (data) responseBody += data;
1256
+ markEnded();
1257
+ return this;
1258
+ },
1259
+ redirect(statusOrUrl: number | string, url?: string) {
1260
+ if (typeof statusOrUrl === 'number') {
1261
+ statusCode = statusOrUrl;
1262
+ headers['Location'] = url || '/';
1263
+ } else {
1264
+ statusCode = 307;
1265
+ headers['Location'] = statusOrUrl;
1266
+ }
1267
+ markEnded();
1268
+ return this;
1269
+ },
1270
+ isEnded() {
1271
+ return ended;
1272
+ },
1273
+ waitForEnd() {
1274
+ return endedPromise;
1275
+ },
1276
+ toResponse(): ResponseData {
1277
+ const buffer = Buffer.from(responseBody);
1278
+ headers['Content-Length'] = String(buffer.length);
1279
+ return {
1280
+ statusCode,
1281
+ statusMessage,
1282
+ headers,
1283
+ body: buffer,
1284
+ };
1285
+ },
1286
+ };
1287
+ }
1288
+
1289
+ /**
1290
+ * Execute API handler code
1291
+ */
1292
+ private async executeApiHandler(
1293
+ code: string,
1294
+ req: ReturnType<typeof this.createMockRequest>,
1295
+ res: ReturnType<typeof this.createMockResponse>
1296
+ ): Promise<void> {
1297
+ try {
1298
+ // Create a minimal require function for built-in modules
1299
+ const builtinModules: Record<string, unknown> = {
1300
+ https: await import('../shims/https'),
1301
+ http: await import('../shims/http'),
1302
+ path: await import('../shims/path'),
1303
+ fs: await import('../shims/fs').then(m => m.createFsShim(this.vfs)),
1304
+ url: await import('../shims/url'),
1305
+ querystring: await import('../shims/querystring'),
1306
+ util: await import('../shims/util'),
1307
+ events: await import('../shims/events'),
1308
+ stream: await import('../shims/stream'),
1309
+ buffer: await import('../shims/buffer'),
1310
+ crypto: await import('../shims/crypto'),
1311
+ };
1312
+
1313
+ const require = (id: string): unknown => {
1314
+ // Handle node: prefix
1315
+ const modId = id.startsWith('node:') ? id.slice(5) : id;
1316
+ if (builtinModules[modId]) {
1317
+ return builtinModules[modId];
1318
+ }
1319
+ throw new Error(`Module not found: ${id}`);
1320
+ };
1321
+
1322
+ // Create module context
1323
+ const module = { exports: {} as Record<string, unknown> };
1324
+ const exports = module.exports;
1325
+
1326
+ // Create process object with environment variables
1327
+ const process = {
1328
+ env: { ...this.options.env },
1329
+ cwd: () => '/',
1330
+ platform: 'browser',
1331
+ version: 'v18.0.0',
1332
+ versions: { node: '18.0.0' },
1333
+ };
1334
+
1335
+ // Execute the transformed code
1336
+ // The code is already in CJS format from esbuild transform
1337
+ // Use Function constructor instead of eval with template literal
1338
+ // to avoid issues with backticks or ${} in the transformed code
1339
+ const fn = new Function('exports', 'require', 'module', 'process', code);
1340
+ fn(exports, require, module, process);
1341
+
1342
+ // Get the handler - check both module.exports and module.exports.default
1343
+ let handler: unknown = module.exports.default || module.exports;
1344
+
1345
+ // If handler is still an object with a default property, unwrap it
1346
+ if (typeof handler === 'object' && handler !== null && 'default' in handler) {
1347
+ handler = (handler as { default: unknown }).default;
1348
+ }
1349
+
1350
+ if (typeof handler !== 'function') {
1351
+ throw new Error('No default export handler found');
1352
+ }
1353
+
1354
+ // Call the handler - it may be async
1355
+ const result = (handler as (req: unknown, res: unknown) => unknown)(req, res);
1356
+
1357
+ // If the handler returns a promise, wait for it
1358
+ if (result instanceof Promise) {
1359
+ await result;
1360
+ }
1361
+ } catch (error) {
1362
+ console.error('[NextDevServer] API handler error:', error);
1363
+ throw error;
1364
+ }
1365
+ }
1366
+
1367
+ /**
1368
+ * Handle page route requests
1369
+ */
1370
+ private async handlePageRoute(pathname: string, search: string): Promise<ResponseData> {
1371
+ // Use App Router if available
1372
+ if (this.useAppRouter) {
1373
+ return this.handleAppRouterPage(pathname, search);
1374
+ }
1375
+
1376
+ // Resolve pathname to page file (Pages Router)
1377
+ const pageFile = this.resolvePageFile(pathname);
1378
+
1379
+ if (!pageFile) {
1380
+ // Try to serve 404 page if exists
1381
+ const notFoundPage = this.resolvePageFile('/404');
1382
+ if (notFoundPage) {
1383
+ const html = await this.generatePageHtml(notFoundPage, '/404');
1384
+ return {
1385
+ statusCode: 404,
1386
+ statusMessage: 'Not Found',
1387
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
1388
+ body: Buffer.from(html),
1389
+ };
1390
+ }
1391
+ return this.serve404Page();
1392
+ }
1393
+
1394
+ // Check if this is a direct request for a page file (e.g., /pages/index.jsx)
1395
+ if (this.needsTransform(pathname)) {
1396
+ return this.transformAndServe(pageFile, pathname);
1397
+ }
1398
+
1399
+ // Generate HTML shell with page component
1400
+ const html = await this.generatePageHtml(pageFile, pathname);
1401
+
1402
+ const buffer = Buffer.from(html);
1403
+ return {
1404
+ statusCode: 200,
1405
+ statusMessage: 'OK',
1406
+ headers: {
1407
+ 'Content-Type': 'text/html; charset=utf-8',
1408
+ 'Content-Length': String(buffer.length),
1409
+ 'Cache-Control': 'no-cache',
1410
+ },
1411
+ body: buffer,
1412
+ };
1413
+ }
1414
+
1415
+ /**
1416
+ * Handle App Router page requests
1417
+ */
1418
+ private async handleAppRouterPage(pathname: string, search: string): Promise<ResponseData> {
1419
+ // Resolve the route to page and layouts
1420
+ const route = this.resolveAppRoute(pathname);
1421
+
1422
+ if (!route) {
1423
+ // Try not-found page
1424
+ const notFoundRoute = this.resolveAppRoute('/not-found');
1425
+ if (notFoundRoute) {
1426
+ const html = await this.generateAppRouterHtml(notFoundRoute, '/not-found');
1427
+ return {
1428
+ statusCode: 404,
1429
+ statusMessage: 'Not Found',
1430
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
1431
+ body: Buffer.from(html),
1432
+ };
1433
+ }
1434
+ return this.serve404Page();
1435
+ }
1436
+
1437
+ const html = await this.generateAppRouterHtml(route, pathname);
1438
+
1439
+ const buffer = Buffer.from(html);
1440
+ return {
1441
+ statusCode: 200,
1442
+ statusMessage: 'OK',
1443
+ headers: {
1444
+ 'Content-Type': 'text/html; charset=utf-8',
1445
+ 'Content-Length': String(buffer.length),
1446
+ 'Cache-Control': 'no-cache',
1447
+ },
1448
+ body: buffer,
1449
+ };
1450
+ }
1451
+
1452
+ /**
1453
+ * Resolve App Router route to page and layout files
1454
+ */
1455
+ private resolveAppRoute(pathname: string): { page: string; layouts: string[] } | null {
1456
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
1457
+ const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
1458
+
1459
+ // Build the directory path
1460
+ let dirPath = this.appDir;
1461
+ const layouts: string[] = [];
1462
+
1463
+ // Collect layouts from root to current directory
1464
+ for (const ext of extensions) {
1465
+ const rootLayout = `${this.appDir}/layout${ext}`;
1466
+ if (this.exists(rootLayout)) {
1467
+ layouts.push(rootLayout);
1468
+ break;
1469
+ }
1470
+ }
1471
+
1472
+ // Walk through segments to find page and collect layouts
1473
+ for (const segment of segments) {
1474
+ dirPath = `${dirPath}/${segment}`;
1475
+
1476
+ // Check for layout in this segment
1477
+ for (const ext of extensions) {
1478
+ const layoutPath = `${dirPath}/layout${ext}`;
1479
+ if (this.exists(layoutPath)) {
1480
+ layouts.push(layoutPath);
1481
+ break;
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ // Find the page file
1487
+ for (const ext of extensions) {
1488
+ const pagePath = `${dirPath}/page${ext}`;
1489
+ if (this.exists(pagePath)) {
1490
+ return { page: pagePath, layouts };
1491
+ }
1492
+ }
1493
+
1494
+ // Try dynamic segments
1495
+ return this.resolveAppDynamicRoute(pathname, segments);
1496
+ }
1497
+
1498
+ /**
1499
+ * Resolve dynamic App Router routes like /app/[id]/page.jsx
1500
+ */
1501
+ private resolveAppDynamicRoute(
1502
+ pathname: string,
1503
+ segments: string[]
1504
+ ): { page: string; layouts: string[] } | null {
1505
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
1506
+
1507
+ const tryPath = (
1508
+ dirPath: string,
1509
+ remainingSegments: string[],
1510
+ layouts: string[]
1511
+ ): { page: string; layouts: string[] } | null => {
1512
+ // Check for layout at current level
1513
+ for (const ext of extensions) {
1514
+ const layoutPath = `${dirPath}/layout${ext}`;
1515
+ if (this.exists(layoutPath) && !layouts.includes(layoutPath)) {
1516
+ layouts = [...layouts, layoutPath];
1517
+ }
1518
+ }
1519
+
1520
+ if (remainingSegments.length === 0) {
1521
+ // Look for page file
1522
+ for (const ext of extensions) {
1523
+ const pagePath = `${dirPath}/page${ext}`;
1524
+ if (this.exists(pagePath)) {
1525
+ return { page: pagePath, layouts };
1526
+ }
1527
+ }
1528
+ return null;
1529
+ }
1530
+
1531
+ const [current, ...rest] = remainingSegments;
1532
+
1533
+ // Try exact match first
1534
+ const exactPath = `${dirPath}/${current}`;
1535
+ if (this.isDirectory(exactPath)) {
1536
+ const result = tryPath(exactPath, rest, layouts);
1537
+ if (result) return result;
1538
+ }
1539
+
1540
+ // Try dynamic segment [param]
1541
+ try {
1542
+ const entries = this.vfs.readdirSync(dirPath);
1543
+ for (const entry of entries) {
1544
+ if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
1545
+ const dynamicPath = `${dirPath}/${entry}`;
1546
+ if (this.isDirectory(dynamicPath)) {
1547
+ const result = tryPath(dynamicPath, rest, layouts);
1548
+ if (result) return result;
1549
+ }
1550
+ }
1551
+ }
1552
+ } catch {
1553
+ // Directory doesn't exist
1554
+ }
1555
+
1556
+ return null;
1557
+ };
1558
+
1559
+ // Collect root layout
1560
+ const layouts: string[] = [];
1561
+ for (const ext of extensions) {
1562
+ const rootLayout = `${this.appDir}/layout${ext}`;
1563
+ if (this.exists(rootLayout)) {
1564
+ layouts.push(rootLayout);
1565
+ break;
1566
+ }
1567
+ }
1568
+
1569
+ return tryPath(this.appDir, segments, layouts);
1570
+ }
1571
+
1572
+ /**
1573
+ * Generate HTML for App Router with nested layouts
1574
+ */
1575
+ private async generateAppRouterHtml(
1576
+ route: { page: string; layouts: string[] },
1577
+ pathname: string
1578
+ ): Promise<string> {
1579
+ // Use virtual server prefix for all file imports so the service worker can intercept them
1580
+ const virtualPrefix = `/__virtual__/${this.port}`;
1581
+
1582
+ // Check for global CSS files
1583
+ const globalCssLinks: string[] = [];
1584
+ const cssLocations = ['/app/globals.css', '/styles/globals.css', '/styles/global.css'];
1585
+ for (const cssPath of cssLocations) {
1586
+ if (this.exists(cssPath)) {
1587
+ globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
1588
+ }
1589
+ }
1590
+
1591
+ // Build the nested component structure
1592
+ // Layouts wrap the page from outside in
1593
+ const pageModulePath = virtualPrefix + route.page; // route.page already starts with /
1594
+ const layoutImports = route.layouts
1595
+ .map((layout, i) => `import Layout${i} from '${virtualPrefix}${layout}';`)
1596
+ .join('\n ');
1597
+
1598
+ // Build nested JSX: Layout0 > Layout1 > ... > Page
1599
+ let nestedJsx = 'React.createElement(Page)';
1600
+ for (let i = route.layouts.length - 1; i >= 0; i--) {
1601
+ nestedJsx = `React.createElement(Layout${i}, null, ${nestedJsx})`;
1602
+ }
1603
+
1604
+ // Generate env script for NEXT_PUBLIC_* variables
1605
+ const envScript = this.generateEnvScript();
1606
+
1607
+ return `<!DOCTYPE html>
1608
+ <html lang="en">
1609
+ <head>
1610
+ <meta charset="UTF-8">
1611
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1612
+ <base href="${virtualPrefix}/">
1613
+ <title>Next.js App</title>
1614
+ ${envScript}
1615
+ ${TAILWIND_CDN_SCRIPT}
1616
+ ${CORS_PROXY_SCRIPT}
1617
+ ${globalCssLinks.join('\n ')}
1618
+ ${REACT_REFRESH_PREAMBLE}
1619
+ <script type="importmap">
1620
+ {
1621
+ "imports": {
1622
+ "react": "https://esm.sh/react@18.2.0?dev",
1623
+ "react/": "https://esm.sh/react@18.2.0&dev/",
1624
+ "react-dom": "https://esm.sh/react-dom@18.2.0?dev",
1625
+ "react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
1626
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
1627
+ "convex/react": "https://esm.sh/convex@1.21.0/react?external=react",
1628
+ "convex/server": "https://esm.sh/convex@1.21.0/server",
1629
+ "convex/values": "https://esm.sh/convex@1.21.0/values",
1630
+ "convex/_generated/api": "${virtualPrefix}/convex/_generated/api.ts",
1631
+ "ai": "https://esm.sh/ai@4?external=react",
1632
+ "ai/react": "https://esm.sh/ai@4/react?external=react",
1633
+ "@ai-sdk/openai": "https://esm.sh/@ai-sdk/openai@1",
1634
+ "next/link": "${virtualPrefix}/_next/shims/link.js",
1635
+ "next/router": "${virtualPrefix}/_next/shims/router.js",
1636
+ "next/head": "${virtualPrefix}/_next/shims/head.js",
1637
+ "next/navigation": "${virtualPrefix}/_next/shims/navigation.js"
1638
+ }
1639
+ }
1640
+ </script>
1641
+ ${HMR_CLIENT_SCRIPT}
1642
+ </head>
1643
+ <body>
1644
+ <div id="__next"></div>
1645
+ <script type="module">
1646
+ import React from 'react';
1647
+ import ReactDOM from 'react-dom/client';
1648
+ import Page from '${pageModulePath}';
1649
+ ${layoutImports}
1650
+
1651
+ function App() {
1652
+ return ${nestedJsx};
1653
+ }
1654
+
1655
+ ReactDOM.createRoot(document.getElementById('__next')).render(
1656
+ React.createElement(React.StrictMode, null,
1657
+ React.createElement(App)
1658
+ )
1659
+ );
1660
+ </script>
1661
+ </body>
1662
+ </html>`;
1663
+ }
1664
+
1665
+ /**
1666
+ * Resolve URL pathname to page file
1667
+ */
1668
+ private resolvePageFile(pathname: string): string | null {
1669
+ // Handle root path
1670
+ if (pathname === '/') {
1671
+ pathname = '/index';
1672
+ }
1673
+
1674
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
1675
+
1676
+ // Try exact match: /about → /pages/about.jsx
1677
+ for (const ext of extensions) {
1678
+ const filePath = `${this.pagesDir}${pathname}${ext}`;
1679
+ if (this.exists(filePath)) {
1680
+ return filePath;
1681
+ }
1682
+ }
1683
+
1684
+ // Try index file: /about → /pages/about/index.jsx
1685
+ for (const ext of extensions) {
1686
+ const filePath = `${this.pagesDir}${pathname}/index${ext}`;
1687
+ if (this.exists(filePath)) {
1688
+ return filePath;
1689
+ }
1690
+ }
1691
+
1692
+ // Try dynamic route matching
1693
+ return this.resolveDynamicRoute(pathname);
1694
+ }
1695
+
1696
+ /**
1697
+ * Resolve dynamic routes like /users/[id]
1698
+ */
1699
+ private resolveDynamicRoute(pathname: string): string | null {
1700
+ const segments = pathname.split('/').filter(Boolean);
1701
+ if (segments.length === 0) return null;
1702
+
1703
+ const extensions = ['.jsx', '.tsx', '.js', '.ts'];
1704
+
1705
+ // Build possible paths with dynamic segments
1706
+ // e.g., /users/123 could match /pages/users/[id].jsx
1707
+ const tryPath = (dirPath: string, remainingSegments: string[]): string | null => {
1708
+ if (remainingSegments.length === 0) {
1709
+ // Try index file
1710
+ for (const ext of extensions) {
1711
+ const indexPath = `${dirPath}/index${ext}`;
1712
+ if (this.exists(indexPath)) {
1713
+ return indexPath;
1714
+ }
1715
+ }
1716
+ return null;
1717
+ }
1718
+
1719
+ const [current, ...rest] = remainingSegments;
1720
+
1721
+ // Try exact match first
1722
+ const exactPath = `${dirPath}/${current}`;
1723
+
1724
+ // Check if it's a file
1725
+ for (const ext of extensions) {
1726
+ if (rest.length === 0 && this.exists(exactPath + ext)) {
1727
+ return exactPath + ext;
1728
+ }
1729
+ }
1730
+
1731
+ // Check if it's a directory
1732
+ if (this.isDirectory(exactPath)) {
1733
+ const exactResult = tryPath(exactPath, rest);
1734
+ if (exactResult) return exactResult;
1735
+ }
1736
+
1737
+ // Try dynamic segment [param]
1738
+ try {
1739
+ const entries = this.vfs.readdirSync(dirPath);
1740
+ for (const entry of entries) {
1741
+ // Check for dynamic file like [id].jsx
1742
+ for (const ext of extensions) {
1743
+ const dynamicFilePattern = /^\[([^\]]+)\]$/;
1744
+ const nameWithoutExt = entry.replace(ext, '');
1745
+ if (entry.endsWith(ext) && dynamicFilePattern.test(nameWithoutExt)) {
1746
+ // It's a dynamic file like [id].jsx
1747
+ if (rest.length === 0) {
1748
+ const filePath = `${dirPath}/${entry}`;
1749
+ if (this.exists(filePath)) {
1750
+ return filePath;
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ // Check for dynamic directory like [id]
1757
+ if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
1758
+ const dynamicPath = `${dirPath}/${entry}`;
1759
+ if (this.isDirectory(dynamicPath)) {
1760
+ const dynamicResult = tryPath(dynamicPath, rest);
1761
+ if (dynamicResult) return dynamicResult;
1762
+ }
1763
+ }
1764
+
1765
+ // Check for catch-all [...param].jsx
1766
+ for (const ext of extensions) {
1767
+ if (entry.startsWith('[...') && entry.endsWith(']' + ext)) {
1768
+ const filePath = `${dirPath}/${entry}`;
1769
+ if (this.exists(filePath)) {
1770
+ return filePath;
1771
+ }
1772
+ }
1773
+ }
1774
+ }
1775
+ } catch {
1776
+ // Directory doesn't exist
1777
+ }
1778
+
1779
+ return null;
1780
+ };
1781
+
1782
+ return tryPath(this.pagesDir, segments);
1783
+ }
1784
+
1785
+ /**
1786
+ * Generate HTML shell for a page
1787
+ */
1788
+ private async generatePageHtml(pageFile: string, pathname: string): Promise<string> {
1789
+ // Use virtual server prefix for all file imports so the service worker can intercept them
1790
+ // Without this, /pages/index.jsx would go to localhost:5173/pages/index.jsx
1791
+ // instead of /__virtual__/3001/pages/index.jsx
1792
+ const virtualPrefix = `/__virtual__/${this.port}`;
1793
+ const pageModulePath = virtualPrefix + pageFile; // pageFile already starts with /
1794
+
1795
+ // Check for global CSS files
1796
+ const globalCssLinks: string[] = [];
1797
+ const cssLocations = ['/styles/globals.css', '/styles/global.css', '/app/globals.css'];
1798
+ for (const cssPath of cssLocations) {
1799
+ if (this.exists(cssPath)) {
1800
+ globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
1801
+ }
1802
+ }
1803
+
1804
+ // Generate env script for NEXT_PUBLIC_* variables
1805
+ const envScript = this.generateEnvScript();
1806
+
1807
+ return `<!DOCTYPE html>
1808
+ <html lang="en">
1809
+ <head>
1810
+ <meta charset="UTF-8">
1811
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1812
+ <base href="${virtualPrefix}/">
1813
+ <title>Next.js App</title>
1814
+ ${envScript}
1815
+ ${TAILWIND_CDN_SCRIPT}
1816
+ ${CORS_PROXY_SCRIPT}
1817
+ ${globalCssLinks.join('\n ')}
1818
+ ${REACT_REFRESH_PREAMBLE}
1819
+ <script type="importmap">
1820
+ {
1821
+ "imports": {
1822
+ "react": "https://esm.sh/react@18.2.0?dev",
1823
+ "react/": "https://esm.sh/react@18.2.0&dev/",
1824
+ "react-dom": "https://esm.sh/react-dom@18.2.0?dev",
1825
+ "react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
1826
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
1827
+ "next/link": "${virtualPrefix}/_next/shims/link.js",
1828
+ "next/router": "${virtualPrefix}/_next/shims/router.js",
1829
+ "next/head": "${virtualPrefix}/_next/shims/head.js"
1830
+ }
1831
+ }
1832
+ </script>
1833
+ ${HMR_CLIENT_SCRIPT}
1834
+ </head>
1835
+ <body>
1836
+ <div id="__next"></div>
1837
+ <script type="module">
1838
+ import React from 'react';
1839
+ import ReactDOM from 'react-dom/client';
1840
+ import Page from '${pageModulePath}';
1841
+
1842
+ // Handle client-side navigation
1843
+ function App() {
1844
+ const [currentPath, setCurrentPath] = React.useState(window.location.pathname);
1845
+
1846
+ React.useEffect(() => {
1847
+ const handlePopState = () => {
1848
+ setCurrentPath(window.location.pathname);
1849
+ // Re-render the page component
1850
+ window.location.reload();
1851
+ };
1852
+
1853
+ window.addEventListener('popstate', handlePopState);
1854
+ return () => window.removeEventListener('popstate', handlePopState);
1855
+ }, []);
1856
+
1857
+ return React.createElement(Page);
1858
+ }
1859
+
1860
+ ReactDOM.createRoot(document.getElementById('__next')).render(
1861
+ React.createElement(React.StrictMode, null,
1862
+ React.createElement(App)
1863
+ )
1864
+ );
1865
+ </script>
1866
+ </body>
1867
+ </html>`;
1868
+ }
1869
+
1870
+ /**
1871
+ * Serve a basic 404 page
1872
+ */
1873
+ private serve404Page(): ResponseData {
1874
+ const virtualPrefix = `/__virtual__/${this.port}`;
1875
+ const html = `<!DOCTYPE html>
1876
+ <html lang="en">
1877
+ <head>
1878
+ <meta charset="UTF-8">
1879
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1880
+ <base href="${virtualPrefix}/">
1881
+ <title>404 - Page Not Found</title>
1882
+ <style>
1883
+ body {
1884
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1885
+ display: flex;
1886
+ flex-direction: column;
1887
+ align-items: center;
1888
+ justify-content: center;
1889
+ min-height: 100vh;
1890
+ margin: 0;
1891
+ background: #fafafa;
1892
+ }
1893
+ h1 { font-size: 48px; margin: 0; }
1894
+ p { color: #666; margin-top: 10px; }
1895
+ a { color: #0070f3; text-decoration: none; }
1896
+ a:hover { text-decoration: underline; }
1897
+ </style>
1898
+ </head>
1899
+ <body>
1900
+ <h1>404</h1>
1901
+ <p>This page could not be found.</p>
1902
+ <p><a href="/">Go back home</a></p>
1903
+ </body>
1904
+ </html>`;
1905
+
1906
+ const buffer = Buffer.from(html);
1907
+ return {
1908
+ statusCode: 404,
1909
+ statusMessage: 'Not Found',
1910
+ headers: {
1911
+ 'Content-Type': 'text/html; charset=utf-8',
1912
+ 'Content-Length': String(buffer.length),
1913
+ },
1914
+ body: buffer,
1915
+ };
1916
+ }
1917
+
1918
+ /**
1919
+ * Check if a file needs transformation
1920
+ */
1921
+ private needsTransform(path: string): boolean {
1922
+ return /\.(jsx|tsx|ts)$/.test(path);
1923
+ }
1924
+
1925
+ /**
1926
+ * Transform and serve a JSX/TS file
1927
+ */
1928
+ private async transformAndServe(filePath: string, urlPath: string): Promise<ResponseData> {
1929
+ try {
1930
+ const content = this.vfs.readFileSync(filePath, 'utf8');
1931
+ const transformed = await this.transformCode(content, urlPath);
1932
+
1933
+ const buffer = Buffer.from(transformed);
1934
+ return {
1935
+ statusCode: 200,
1936
+ statusMessage: 'OK',
1937
+ headers: {
1938
+ 'Content-Type': 'application/javascript; charset=utf-8',
1939
+ 'Content-Length': String(buffer.length),
1940
+ 'Cache-Control': 'no-cache',
1941
+ 'X-Transformed': 'true',
1942
+ },
1943
+ body: buffer,
1944
+ };
1945
+ } catch (error) {
1946
+ console.error('[NextDevServer] Transform error:', error);
1947
+ const message = error instanceof Error ? error.message : 'Transform failed';
1948
+ const body = `// Transform Error: ${message}\nconsole.error(${JSON.stringify(message)});`;
1949
+ return {
1950
+ statusCode: 200,
1951
+ statusMessage: 'OK',
1952
+ headers: {
1953
+ 'Content-Type': 'application/javascript; charset=utf-8',
1954
+ 'X-Transform-Error': 'true',
1955
+ },
1956
+ body: Buffer.from(body),
1957
+ };
1958
+ }
1959
+ }
1960
+
1961
+ /**
1962
+ * Transform JSX/TS code to browser-compatible JavaScript (ESM for browser)
1963
+ */
1964
+ private async transformCode(code: string, filename: string): Promise<string> {
1965
+ if (!isBrowser) {
1966
+ return code;
1967
+ }
1968
+
1969
+ await initEsbuild();
1970
+
1971
+ const esbuild = getEsbuild();
1972
+ if (!esbuild) {
1973
+ throw new Error('esbuild not available');
1974
+ }
1975
+
1976
+ // Remove CSS imports before transformation - they are handled via <link> tags
1977
+ // CSS imports in ESM would fail with MIME type errors
1978
+ const codeWithoutCssImports = this.stripCssImports(code);
1979
+
1980
+ let loader: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
1981
+ if (filename.endsWith('.jsx')) loader = 'jsx';
1982
+ else if (filename.endsWith('.tsx')) loader = 'tsx';
1983
+ else if (filename.endsWith('.ts')) loader = 'ts';
1984
+
1985
+ const result = await esbuild.transform(codeWithoutCssImports, {
1986
+ loader,
1987
+ format: 'esm',
1988
+ target: 'esnext',
1989
+ jsx: 'automatic',
1990
+ jsxImportSource: 'react',
1991
+ sourcemap: 'inline',
1992
+ sourcefile: filename,
1993
+ });
1994
+
1995
+ // Add React Refresh registration for JSX/TSX files
1996
+ if (/\.(jsx|tsx)$/.test(filename)) {
1997
+ return this.addReactRefresh(result.code, filename);
1998
+ }
1999
+
2000
+ return result.code;
2001
+ }
2002
+
2003
+ /**
2004
+ * Strip CSS imports from code (they are loaded via <link> tags instead)
2005
+ * Handles: import './styles.css', import '../globals.css', etc.
2006
+ */
2007
+ private stripCssImports(code: string): string {
2008
+ // Match import statements for CSS files (with or without semicolon)
2009
+ // Handles: import './styles.css'; import "./globals.css" import '../path/file.css'
2010
+ return code.replace(/import\s+['"][^'"]+\.css['"]\s*;?/g, '// CSS import removed (loaded via <link>)');
2011
+ }
2012
+
2013
+ /**
2014
+ * Transform API handler code to CommonJS for eval execution
2015
+ */
2016
+ private async transformApiHandler(code: string, filename: string): Promise<string> {
2017
+ if (isBrowser) {
2018
+ // Use esbuild in browser
2019
+ await initEsbuild();
2020
+
2021
+ const esbuild = getEsbuild();
2022
+ if (!esbuild) {
2023
+ throw new Error('esbuild not available');
2024
+ }
2025
+
2026
+ let loader: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
2027
+ if (filename.endsWith('.jsx')) loader = 'jsx';
2028
+ else if (filename.endsWith('.tsx')) loader = 'tsx';
2029
+ else if (filename.endsWith('.ts')) loader = 'ts';
2030
+
2031
+ const result = await esbuild.transform(code, {
2032
+ loader,
2033
+ format: 'cjs', // CommonJS for eval execution
2034
+ target: 'esnext',
2035
+ platform: 'neutral',
2036
+ sourcefile: filename,
2037
+ });
2038
+
2039
+ return result.code;
2040
+ }
2041
+
2042
+ // Simple ESM to CJS transform for Node.js/test environment
2043
+ let transformed = code;
2044
+
2045
+ // Convert: import X from 'Y' -> const X = require('Y')
2046
+ transformed = transformed.replace(
2047
+ /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
2048
+ 'const $1 = require("$2")'
2049
+ );
2050
+
2051
+ // Convert: import { X } from 'Y' -> const { X } = require('Y')
2052
+ transformed = transformed.replace(
2053
+ /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
2054
+ 'const {$1} = require("$2")'
2055
+ );
2056
+
2057
+ // Convert: export default function X -> module.exports = function X
2058
+ transformed = transformed.replace(
2059
+ /export\s+default\s+function\s+(\w+)/g,
2060
+ 'module.exports = function $1'
2061
+ );
2062
+
2063
+ // Convert: export default function -> module.exports = function
2064
+ transformed = transformed.replace(
2065
+ /export\s+default\s+function\s*\(/g,
2066
+ 'module.exports = function('
2067
+ );
2068
+
2069
+ // Convert: export default X -> module.exports = X
2070
+ transformed = transformed.replace(
2071
+ /export\s+default\s+/g,
2072
+ 'module.exports = '
2073
+ );
2074
+
2075
+ return transformed;
2076
+ }
2077
+
2078
+ /**
2079
+ * Add React Refresh registration to transformed code
2080
+ */
2081
+ private addReactRefresh(code: string, filename: string): string {
2082
+ const components: string[] = [];
2083
+
2084
+ const funcDeclRegex = /(?:^|\n)(?:export\s+)?function\s+([A-Z][a-zA-Z0-9]*)\s*\(/g;
2085
+ let match;
2086
+ while ((match = funcDeclRegex.exec(code)) !== null) {
2087
+ if (!components.includes(match[1])) {
2088
+ components.push(match[1]);
2089
+ }
2090
+ }
2091
+
2092
+ const arrowRegex = /(?:^|\n)(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9]*)\s*=/g;
2093
+ while ((match = arrowRegex.exec(code)) !== null) {
2094
+ if (!components.includes(match[1])) {
2095
+ components.push(match[1]);
2096
+ }
2097
+ }
2098
+
2099
+ if (components.length === 0) {
2100
+ return `// HMR Setup
2101
+ import.meta.hot = window.__vite_hot_context__("${filename}");
2102
+
2103
+ ${code}
2104
+
2105
+ if (import.meta.hot) {
2106
+ import.meta.hot.accept();
2107
+ }
2108
+ `;
2109
+ }
2110
+
2111
+ const registrations = components
2112
+ .map(name => ` $RefreshReg$(${name}, "${filename} ${name}");`)
2113
+ .join('\n');
2114
+
2115
+ return `// HMR Setup
2116
+ import.meta.hot = window.__vite_hot_context__("${filename}");
2117
+
2118
+ ${code}
2119
+
2120
+ // React Refresh Registration
2121
+ if (import.meta.hot) {
2122
+ ${registrations}
2123
+ import.meta.hot.accept(() => {
2124
+ if (window.$RefreshRuntime$) {
2125
+ window.$RefreshRuntime$.performReactRefresh();
2126
+ }
2127
+ });
2128
+ }
2129
+ `;
2130
+ }
2131
+
2132
+ /**
2133
+ * Start file watching for HMR
2134
+ */
2135
+ startWatching(): void {
2136
+ const watchers: Array<{ close: () => void }> = [];
2137
+
2138
+ // Watch /pages directory
2139
+ try {
2140
+ const pagesWatcher = this.vfs.watch(this.pagesDir, { recursive: true }, (eventType, filename) => {
2141
+ if (eventType === 'change' && filename) {
2142
+ const fullPath = filename.startsWith('/') ? filename : `${this.pagesDir}/${filename}`;
2143
+ this.handleFileChange(fullPath);
2144
+ }
2145
+ });
2146
+ watchers.push(pagesWatcher);
2147
+ } catch (error) {
2148
+ console.warn('[NextDevServer] Could not watch pages directory:', error);
2149
+ }
2150
+
2151
+ // Watch /app directory for App Router
2152
+ if (this.useAppRouter) {
2153
+ try {
2154
+ const appWatcher = this.vfs.watch(this.appDir, { recursive: true }, (eventType, filename) => {
2155
+ if (eventType === 'change' && filename) {
2156
+ const fullPath = filename.startsWith('/') ? filename : `${this.appDir}/${filename}`;
2157
+ this.handleFileChange(fullPath);
2158
+ }
2159
+ });
2160
+ watchers.push(appWatcher);
2161
+ } catch (error) {
2162
+ console.warn('[NextDevServer] Could not watch app directory:', error);
2163
+ }
2164
+ }
2165
+
2166
+ // Watch /public directory for static assets
2167
+ try {
2168
+ const publicWatcher = this.vfs.watch(this.publicDir, { recursive: true }, (eventType, filename) => {
2169
+ if (eventType === 'change' && filename) {
2170
+ this.handleFileChange(`${this.publicDir}/${filename}`);
2171
+ }
2172
+ });
2173
+ watchers.push(publicWatcher);
2174
+ } catch {
2175
+ // Ignore if public directory doesn't exist
2176
+ }
2177
+
2178
+ this.watcherCleanup = () => {
2179
+ watchers.forEach(w => w.close());
2180
+ };
2181
+ }
2182
+
2183
+ /**
2184
+ * Handle file change event
2185
+ */
2186
+ private handleFileChange(path: string): void {
2187
+ const isCSS = path.endsWith('.css');
2188
+ const isJS = /\.(jsx?|tsx?)$/.test(path);
2189
+ const updateType = (isCSS || isJS) ? 'update' : 'full-reload';
2190
+
2191
+ const update: HMRUpdate = {
2192
+ type: updateType,
2193
+ path,
2194
+ timestamp: Date.now(),
2195
+ };
2196
+
2197
+ this.emitHMRUpdate(update);
2198
+
2199
+ // Send HMR update via postMessage (works with sandboxed iframes)
2200
+ if (this.hmrTargetWindow) {
2201
+ try {
2202
+ this.hmrTargetWindow.postMessage({ ...update, channel: 'next-hmr' }, '*');
2203
+ } catch (e) {
2204
+ // Window may be closed or unavailable
2205
+ }
2206
+ }
2207
+ }
2208
+
2209
+ /**
2210
+ * Stop the server
2211
+ */
2212
+ stop(): void {
2213
+ if (this.watcherCleanup) {
2214
+ this.watcherCleanup();
2215
+ this.watcherCleanup = null;
2216
+ }
2217
+
2218
+ this.hmrTargetWindow = null;
2219
+
2220
+ super.stop();
2221
+ }
2222
+ }
2223
+
2224
+ export default NextDevServer;