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,702 @@
1
+ /**
2
+ * ViteDevServer - Vite-compatible dev server for browser environment
3
+ * Serves files from VirtualFS with JSX/TypeScript transformation
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
+ // jsdom has window but doesn't have ServiceWorker or SharedArrayBuffer
12
+ const isBrowser = typeof window !== 'undefined' &&
13
+ typeof window.navigator !== 'undefined' &&
14
+ 'serviceWorker' in window.navigator;
15
+
16
+ // Window.__esbuild type is declared in src/types/external.d.ts
17
+
18
+ /**
19
+ * Initialize esbuild-wasm for browser transforms
20
+ * Uses window-level singleton to prevent "Cannot call initialize more than once" errors
21
+ */
22
+ async function initEsbuild(): Promise<void> {
23
+ if (!isBrowser) return;
24
+
25
+ // Check if already initialized (survives HMR)
26
+ if (window.__esbuild) {
27
+ return;
28
+ }
29
+
30
+ // Check if initialization is in progress
31
+ if (window.__esbuildInitPromise) {
32
+ return window.__esbuildInitPromise;
33
+ }
34
+
35
+ window.__esbuildInitPromise = (async () => {
36
+ try {
37
+ const mod = await import(
38
+ /* @vite-ignore */
39
+ 'https://esm.sh/esbuild-wasm@0.20.0'
40
+ );
41
+
42
+ const esbuildMod = mod.default || mod;
43
+
44
+ try {
45
+ await esbuildMod.initialize({
46
+ wasmURL: 'https://unpkg.com/esbuild-wasm@0.20.0/esbuild.wasm',
47
+ });
48
+ console.log('[ViteDevServer] esbuild-wasm initialized');
49
+ } catch (initError) {
50
+ // If esbuild is already initialized (e.g., from a previous HMR cycle),
51
+ // the WASM is still loaded and the module is usable
52
+ if (initError instanceof Error && initError.message.includes('Cannot call "initialize" more than once')) {
53
+ console.log('[ViteDevServer] esbuild-wasm already initialized, reusing');
54
+ } else {
55
+ throw initError;
56
+ }
57
+ }
58
+
59
+ window.__esbuild = esbuildMod;
60
+ } catch (error) {
61
+ console.error('[ViteDevServer] Failed to initialize esbuild:', error);
62
+ window.__esbuildInitPromise = undefined;
63
+ throw error;
64
+ }
65
+ })();
66
+
67
+ return window.__esbuildInitPromise;
68
+ }
69
+
70
+ /**
71
+ * Get the esbuild instance (after initialization)
72
+ */
73
+ function getEsbuild(): typeof import('esbuild-wasm') | undefined {
74
+ return isBrowser ? window.__esbuild : undefined;
75
+ }
76
+
77
+ export interface ViteDevServerOptions extends DevServerOptions {
78
+ /**
79
+ * Enable JSX transformation (default: true)
80
+ */
81
+ jsx?: boolean;
82
+
83
+ /**
84
+ * JSX factory function (default: 'React.createElement')
85
+ */
86
+ jsxFactory?: string;
87
+
88
+ /**
89
+ * JSX fragment function (default: 'React.Fragment')
90
+ */
91
+ jsxFragment?: string;
92
+
93
+ /**
94
+ * Auto-inject React import for JSX files (default: true)
95
+ */
96
+ jsxAutoImport?: boolean;
97
+ }
98
+
99
+ /**
100
+ * React Refresh preamble - MUST run before React is loaded
101
+ * This script is blocking to ensure injectIntoGlobalHook runs first
102
+ */
103
+ const REACT_REFRESH_PREAMBLE = `
104
+ <script type="module">
105
+ // Block until React Refresh is loaded and initialized
106
+ // This MUST happen before React is imported
107
+ const RefreshRuntime = await import('https://esm.sh/react-refresh@0.14.0/runtime').then(m => m.default || m);
108
+
109
+ // Hook into React BEFORE it's loaded
110
+ RefreshRuntime.injectIntoGlobalHook(window);
111
+ window.$RefreshRuntime$ = RefreshRuntime;
112
+
113
+ // Track registrations for debugging
114
+ window.$RefreshRegCount$ = 0;
115
+
116
+ // Register function called by transformed modules
117
+ window.$RefreshReg$ = (type, id) => {
118
+ window.$RefreshRegCount$++;
119
+ RefreshRuntime.register(type, id);
120
+ };
121
+
122
+ // Signature function (simplified - always returns identity)
123
+ window.$RefreshSig$ = () => (type) => type;
124
+
125
+ console.log('[HMR] React Refresh initialized');
126
+ </script>
127
+ `;
128
+
129
+ /**
130
+ * HMR client script injected into index.html
131
+ * Implements the import.meta.hot API and handles HMR updates
132
+ */
133
+ const HMR_CLIENT_SCRIPT = `
134
+ <script type="module">
135
+ (function() {
136
+ // Track hot modules and their callbacks
137
+ const hotModules = new Map();
138
+ const pendingUpdates = new Map();
139
+
140
+ // Implement import.meta.hot API (Vite-compatible)
141
+ window.__vite_hot_context__ = function createHotContext(ownerPath) {
142
+ // Return existing context if already created
143
+ if (hotModules.has(ownerPath)) {
144
+ return hotModules.get(ownerPath);
145
+ }
146
+
147
+ const hot = {
148
+ // Persisted data between updates
149
+ data: {},
150
+
151
+ // Accept self-updates
152
+ accept(callback) {
153
+ hot._acceptCallback = callback;
154
+ },
155
+
156
+ // Cleanup before update
157
+ dispose(callback) {
158
+ hot._disposeCallback = callback;
159
+ },
160
+
161
+ // Force full reload
162
+ invalidate() {
163
+ location.reload();
164
+ },
165
+
166
+ // Prune callback (called when module is no longer imported)
167
+ prune(callback) {
168
+ hot._pruneCallback = callback;
169
+ },
170
+
171
+ // Event handlers (not implemented)
172
+ on(event, cb) {},
173
+ off(event, cb) {},
174
+ send(event, data) {},
175
+
176
+ // Internal callbacks
177
+ _acceptCallback: null,
178
+ _disposeCallback: null,
179
+ _pruneCallback: null,
180
+ };
181
+
182
+ hotModules.set(ownerPath, hot);
183
+ return hot;
184
+ };
185
+
186
+ // Listen for HMR updates via postMessage (works with sandboxed iframes)
187
+ window.addEventListener('message', async (event) => {
188
+ // Filter for HMR messages only
189
+ if (!event.data || event.data.channel !== 'vite-hmr') return;
190
+ const { type, path, timestamp } = event.data;
191
+
192
+ if (type === 'update') {
193
+ console.log('[HMR] Update:', path);
194
+
195
+ if (path.endsWith('.css')) {
196
+ // CSS hot reload - update stylesheet href
197
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
198
+ links.forEach(link => {
199
+ const href = link.getAttribute('href');
200
+ if (href && href.includes(path.replace(/^\\//, ''))) {
201
+ link.href = href.split('?')[0] + '?t=' + timestamp;
202
+ }
203
+ });
204
+
205
+ // Also update any injected style tags
206
+ const styles = document.querySelectorAll('style[data-vite-dev-id]');
207
+ styles.forEach(style => {
208
+ const id = style.getAttribute('data-vite-dev-id');
209
+ if (id && id.includes(path.replace(/^\\//, ''))) {
210
+ // Re-import the CSS module to get updated styles
211
+ import(path + '?t=' + timestamp).catch(() => {});
212
+ }
213
+ });
214
+ } else if (path.match(/\\.(jsx?|tsx?)$/)) {
215
+ // JS/JSX hot reload with React Refresh
216
+ await handleJSUpdate(path, timestamp);
217
+ }
218
+ } else if (type === 'full-reload') {
219
+ console.log('[HMR] Full reload');
220
+ location.reload();
221
+ }
222
+ });
223
+
224
+ // Handle JS/JSX module updates
225
+ async function handleJSUpdate(path, timestamp) {
226
+ // Normalize path to match module keys
227
+ const normalizedPath = path.startsWith('/') ? path : '/' + path;
228
+ const hot = hotModules.get(normalizedPath);
229
+
230
+ try {
231
+ // Call dispose callback if registered
232
+ if (hot && hot._disposeCallback) {
233
+ hot._disposeCallback(hot.data);
234
+ }
235
+
236
+ // Enqueue React Refresh (batches multiple updates)
237
+ if (window.$RefreshRuntime$) {
238
+ pendingUpdates.set(normalizedPath, timestamp);
239
+
240
+ // Schedule refresh after a short delay to batch updates
241
+ if (pendingUpdates.size === 1) {
242
+ setTimeout(async () => {
243
+ try {
244
+ // Re-import all pending modules
245
+ for (const [modulePath, ts] of pendingUpdates) {
246
+ const moduleUrl = '.' + modulePath + '?t=' + ts;
247
+ await import(moduleUrl);
248
+ }
249
+
250
+ // Perform React Refresh
251
+ window.$RefreshRuntime$.performReactRefresh();
252
+ console.log('[HMR] Updated', pendingUpdates.size, 'module(s)');
253
+
254
+ pendingUpdates.clear();
255
+ } catch (error) {
256
+ console.error('[HMR] Failed to apply update:', error);
257
+ pendingUpdates.clear();
258
+ location.reload();
259
+ }
260
+ }, 30);
261
+ }
262
+ } else {
263
+ // No React Refresh available, fall back to page reload
264
+ console.log('[HMR] React Refresh not available, reloading page');
265
+ location.reload();
266
+ }
267
+ } catch (error) {
268
+ console.error('[HMR] Update failed:', error);
269
+ location.reload();
270
+ }
271
+ }
272
+
273
+ console.log('[HMR] Client ready with React Refresh support');
274
+ })();
275
+ </script>
276
+ `;
277
+
278
+ export class ViteDevServer extends DevServer {
279
+ private watcherCleanup: (() => void) | null = null;
280
+ private options: ViteDevServerOptions;
281
+ private hmrTargetWindow: Window | null = null;
282
+
283
+ constructor(vfs: VirtualFS, options: ViteDevServerOptions) {
284
+ super(vfs, options);
285
+ this.options = {
286
+ jsx: true,
287
+ jsxFactory: 'React.createElement',
288
+ jsxFragment: 'React.Fragment',
289
+ jsxAutoImport: true,
290
+ ...options,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Set the target window for HMR updates (typically iframe.contentWindow)
296
+ * This enables HMR to work with sandboxed iframes via postMessage
297
+ */
298
+ setHMRTarget(targetWindow: Window): void {
299
+ this.hmrTargetWindow = targetWindow;
300
+ }
301
+
302
+ /**
303
+ * Handle an incoming HTTP request
304
+ */
305
+ async handleRequest(
306
+ method: string,
307
+ url: string,
308
+ headers: Record<string, string>,
309
+ body?: Buffer
310
+ ): Promise<ResponseData> {
311
+ // Parse URL
312
+ const urlObj = new URL(url, 'http://localhost');
313
+ let pathname = urlObj.pathname;
314
+
315
+ // Handle root path - serve index.html
316
+ if (pathname === '/') {
317
+ pathname = '/index.html';
318
+ }
319
+
320
+ // Resolve the full path
321
+ const filePath = this.resolvePath(pathname);
322
+
323
+ // Check if file exists
324
+ if (!this.exists(filePath)) {
325
+ // Try with .html extension
326
+ if (this.exists(filePath + '.html')) {
327
+ return this.serveFile(filePath + '.html');
328
+ }
329
+ // Try index.html in directory
330
+ if (this.isDirectory(filePath) && this.exists(filePath + '/index.html')) {
331
+ return this.serveFile(filePath + '/index.html');
332
+ }
333
+ return this.notFound(pathname);
334
+ }
335
+
336
+ // If it's a directory, redirect to index.html
337
+ if (this.isDirectory(filePath)) {
338
+ if (this.exists(filePath + '/index.html')) {
339
+ return this.serveFile(filePath + '/index.html');
340
+ }
341
+ return this.notFound(pathname);
342
+ }
343
+
344
+ // Check if file needs transformation (JSX/TS)
345
+ if (this.needsTransform(pathname)) {
346
+ return this.transformAndServe(filePath, pathname);
347
+ }
348
+
349
+ // Check if CSS is being imported as a module (needs to be converted to JS)
350
+ // In browser context with ES modules, CSS imports need to be served as JS
351
+ if (pathname.endsWith('.css')) {
352
+ // Check various header formats for sec-fetch-dest
353
+ const secFetchDest =
354
+ headers['sec-fetch-dest'] ||
355
+ headers['Sec-Fetch-Dest'] ||
356
+ headers['SEC-FETCH-DEST'] ||
357
+ '';
358
+
359
+ // In browser, serve CSS as module when:
360
+ // 1. Requested as a script (sec-fetch-dest: script)
361
+ // 2. Empty dest (sec-fetch-dest: empty) - fetch() calls
362
+ // 3. No sec-fetch-dest but in browser context - assume module import
363
+ const isModuleImport =
364
+ secFetchDest === 'script' ||
365
+ secFetchDest === 'empty' ||
366
+ (isBrowser && secFetchDest === '');
367
+
368
+ if (isModuleImport) {
369
+ return this.serveCssAsModule(filePath);
370
+ }
371
+ // Otherwise serve as regular CSS (e.g., <link> tags with sec-fetch-dest: style)
372
+ return this.serveFile(filePath);
373
+ }
374
+
375
+ // Check if it's HTML that needs HMR client injection
376
+ if (pathname.endsWith('.html')) {
377
+ return this.serveHtmlWithHMR(filePath);
378
+ }
379
+
380
+ // Serve static file
381
+ return this.serveFile(filePath);
382
+ }
383
+
384
+ /**
385
+ * Start file watching for HMR
386
+ */
387
+ startWatching(): void {
388
+ // Watch /src directory for changes
389
+ const srcPath = this.root === '/' ? '/src' : `${this.root}/src`;
390
+
391
+ try {
392
+ const watcher = this.vfs.watch(srcPath, { recursive: true }, (eventType, filename) => {
393
+ if (eventType === 'change' && filename) {
394
+ const fullPath = filename.startsWith('/') ? filename : `${srcPath}/${filename}`;
395
+ this.handleFileChange(fullPath);
396
+ }
397
+ });
398
+
399
+ this.watcherCleanup = () => {
400
+ watcher.close();
401
+ };
402
+ } catch (error) {
403
+ console.warn('[ViteDevServer] Could not watch /src directory:', error);
404
+ }
405
+
406
+ // Also watch for CSS files in root
407
+ try {
408
+ const rootWatcher = this.vfs.watch(this.root, { recursive: false }, (eventType, filename) => {
409
+ if (eventType === 'change' && filename && filename.endsWith('.css')) {
410
+ this.handleFileChange(`${this.root}/${filename}`);
411
+ }
412
+ });
413
+
414
+ const originalCleanup = this.watcherCleanup;
415
+ this.watcherCleanup = () => {
416
+ originalCleanup?.();
417
+ rootWatcher.close();
418
+ };
419
+ } catch {
420
+ // Ignore if root watching fails
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Handle file change event
426
+ */
427
+ private handleFileChange(path: string): void {
428
+ // Determine update type:
429
+ // - CSS and JS/JSX/TSX files: 'update' (handled by HMR client)
430
+ // - Other files: 'full-reload'
431
+ const isCSS = path.endsWith('.css');
432
+ const isJS = /\.(jsx?|tsx?)$/.test(path);
433
+ const updateType = (isCSS || isJS) ? 'update' : 'full-reload';
434
+
435
+ const update: HMRUpdate = {
436
+ type: updateType,
437
+ path,
438
+ timestamp: Date.now(),
439
+ };
440
+
441
+ // Emit event for ServerBridge
442
+ this.emitHMRUpdate(update);
443
+
444
+ // Send HMR update via postMessage (works with sandboxed iframes)
445
+ if (this.hmrTargetWindow) {
446
+ try {
447
+ this.hmrTargetWindow.postMessage({ ...update, channel: 'vite-hmr' }, '*');
448
+ } catch (e) {
449
+ // Window may be closed or unavailable
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Stop the server
456
+ */
457
+ stop(): void {
458
+ if (this.watcherCleanup) {
459
+ this.watcherCleanup();
460
+ this.watcherCleanup = null;
461
+ }
462
+
463
+ this.hmrTargetWindow = null;
464
+
465
+ super.stop();
466
+ }
467
+
468
+ /**
469
+ * Check if a file needs transformation
470
+ */
471
+ private needsTransform(path: string): boolean {
472
+ return /\.(jsx|tsx|ts)$/.test(path);
473
+ }
474
+
475
+ /**
476
+ * Transform and serve a JSX/TS file
477
+ */
478
+ private async transformAndServe(filePath: string, urlPath: string): Promise<ResponseData> {
479
+ try {
480
+ const content = this.vfs.readFileSync(filePath, 'utf8');
481
+ const transformed = await this.transformCode(content, urlPath);
482
+
483
+ const buffer = Buffer.from(transformed);
484
+ return {
485
+ statusCode: 200,
486
+ statusMessage: 'OK',
487
+ headers: {
488
+ 'Content-Type': 'application/javascript; charset=utf-8',
489
+ 'Content-Length': String(buffer.length),
490
+ 'Cache-Control': 'no-cache',
491
+ 'X-Transformed': 'true',
492
+ },
493
+ body: buffer,
494
+ };
495
+ } catch (error) {
496
+ console.error('[ViteDevServer] Transform error:', error);
497
+ const message = error instanceof Error ? error.message : 'Transform failed';
498
+ const body = `// Transform Error: ${message}\nconsole.error(${JSON.stringify(message)});`;
499
+ return {
500
+ statusCode: 200, // Return 200 with error in code to show in browser console
501
+ statusMessage: 'OK',
502
+ headers: {
503
+ 'Content-Type': 'application/javascript; charset=utf-8',
504
+ 'X-Transform-Error': 'true',
505
+ },
506
+ body: Buffer.from(body),
507
+ };
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Transform JSX/TS code to browser-compatible JavaScript
513
+ */
514
+ private async transformCode(code: string, filename: string): Promise<string> {
515
+ if (!isBrowser) {
516
+ // In test environment, just return code as-is
517
+ return code;
518
+ }
519
+
520
+ // Initialize esbuild if needed
521
+ await initEsbuild();
522
+
523
+ const esbuild = getEsbuild();
524
+ if (!esbuild) {
525
+ throw new Error('esbuild not available');
526
+ }
527
+
528
+ // Determine loader based on extension
529
+ let loader: 'js' | 'jsx' | 'ts' | 'tsx' = 'js';
530
+ if (filename.endsWith('.jsx')) loader = 'jsx';
531
+ else if (filename.endsWith('.tsx')) loader = 'tsx';
532
+ else if (filename.endsWith('.ts')) loader = 'ts';
533
+
534
+ const result = await esbuild.transform(code, {
535
+ loader,
536
+ format: 'esm', // Keep as ES modules for browser
537
+ target: 'esnext',
538
+ jsx: 'automatic', // Use React 17+ automatic runtime
539
+ jsxImportSource: 'react',
540
+ sourcemap: 'inline',
541
+ sourcefile: filename,
542
+ });
543
+
544
+ // Add React Refresh registration for JSX/TSX files
545
+ if (/\.(jsx|tsx)$/.test(filename)) {
546
+ return this.addReactRefresh(result.code, filename);
547
+ }
548
+
549
+ return result.code;
550
+ }
551
+
552
+ /**
553
+ * Add React Refresh registration to transformed code
554
+ * This enables true HMR (state-preserving) for React components
555
+ */
556
+ private addReactRefresh(code: string, filename: string): string {
557
+ // Find React components (functions starting with uppercase letter)
558
+ const components: string[] = [];
559
+
560
+ // Match function declarations: function App() { ... }
561
+ // Also handles: export function App() { ... }
562
+ const funcDeclRegex = /(?:^|\n)(?:export\s+)?function\s+([A-Z][a-zA-Z0-9]*)\s*\(/g;
563
+ let match;
564
+ while ((match = funcDeclRegex.exec(code)) !== null) {
565
+ if (!components.includes(match[1])) {
566
+ components.push(match[1]);
567
+ }
568
+ }
569
+
570
+ // Match arrow function components: const App = () => { ... }
571
+ // Also handles: export const App = () => { ... }
572
+ // And: const App = function() { ... }
573
+ const arrowRegex = /(?:^|\n)(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9]*)\s*=/g;
574
+ while ((match = arrowRegex.exec(code)) !== null) {
575
+ if (!components.includes(match[1])) {
576
+ components.push(match[1]);
577
+ }
578
+ }
579
+
580
+ // If no components found, just add hot module setup without refresh
581
+ if (components.length === 0) {
582
+ return `// HMR Setup
583
+ import.meta.hot = window.__vite_hot_context__("${filename}");
584
+
585
+ ${code}
586
+
587
+ // HMR Accept
588
+ if (import.meta.hot) {
589
+ import.meta.hot.accept();
590
+ }
591
+ `;
592
+ }
593
+
594
+ // Build React Refresh registration calls
595
+ const registrations = components
596
+ .map(name => ` $RefreshReg$(${name}, "${filename} ${name}");`)
597
+ .join('\n');
598
+
599
+ return `// HMR Setup
600
+ import.meta.hot = window.__vite_hot_context__("${filename}");
601
+
602
+ ${code}
603
+
604
+ // React Refresh Registration
605
+ if (import.meta.hot) {
606
+ ${registrations}
607
+ import.meta.hot.accept(() => {
608
+ if (window.$RefreshRuntime$) {
609
+ window.$RefreshRuntime$.performReactRefresh();
610
+ }
611
+ });
612
+ }
613
+ `;
614
+ }
615
+
616
+ /**
617
+ * Serve CSS file as a JavaScript module that injects styles
618
+ * This is needed because ES module imports of CSS files need to return JS
619
+ */
620
+ private serveCssAsModule(filePath: string): ResponseData {
621
+ try {
622
+ const css = this.vfs.readFileSync(filePath, 'utf8');
623
+
624
+ // Create JavaScript that injects the CSS into the document
625
+ const js = `
626
+ // CSS Module: ${filePath}
627
+ const css = ${JSON.stringify(css)};
628
+ const style = document.createElement('style');
629
+ style.setAttribute('data-vite-dev-id', ${JSON.stringify(filePath)});
630
+ style.textContent = css;
631
+ document.head.appendChild(style);
632
+ export default css;
633
+ `;
634
+
635
+ const buffer = Buffer.from(js);
636
+ return {
637
+ statusCode: 200,
638
+ statusMessage: 'OK',
639
+ headers: {
640
+ 'Content-Type': 'application/javascript; charset=utf-8',
641
+ 'Content-Length': String(buffer.length),
642
+ 'Cache-Control': 'no-cache',
643
+ },
644
+ body: buffer,
645
+ };
646
+ } catch (error) {
647
+ return this.serverError(error);
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Serve HTML file with HMR client script injected
653
+ *
654
+ * IMPORTANT: React Refresh preamble MUST be injected before any module scripts.
655
+ * The preamble uses top-level await to block until React Refresh is loaded
656
+ * and injectIntoGlobalHook is called. This ensures React Refresh hooks into
657
+ * React BEFORE React is imported by any module.
658
+ */
659
+ private serveHtmlWithHMR(filePath: string): ResponseData {
660
+ try {
661
+ let content = this.vfs.readFileSync(filePath, 'utf8');
662
+
663
+ // Inject React Refresh preamble right after <head> to ensure it runs first
664
+ // The preamble blocks (via top-level await) until React Refresh is initialized
665
+ if (content.includes('<head>')) {
666
+ content = content.replace('<head>', `<head>${REACT_REFRESH_PREAMBLE}`);
667
+ } else if (content.includes('<html')) {
668
+ // If no <head>, inject after <html...>
669
+ content = content.replace(/<html[^>]*>/, `$&${REACT_REFRESH_PREAMBLE}`);
670
+ } else {
671
+ // Prepend if no html tag
672
+ content = REACT_REFRESH_PREAMBLE + content;
673
+ }
674
+
675
+ // Inject HMR client script before </head> or </body>
676
+ if (content.includes('</head>')) {
677
+ content = content.replace('</head>', `${HMR_CLIENT_SCRIPT}</head>`);
678
+ } else if (content.includes('</body>')) {
679
+ content = content.replace('</body>', `${HMR_CLIENT_SCRIPT}</body>`);
680
+ } else {
681
+ // Append at the end if no closing tag found
682
+ content += HMR_CLIENT_SCRIPT;
683
+ }
684
+
685
+ const buffer = Buffer.from(content);
686
+ return {
687
+ statusCode: 200,
688
+ statusMessage: 'OK',
689
+ headers: {
690
+ 'Content-Type': 'text/html; charset=utf-8',
691
+ 'Content-Length': String(buffer.length),
692
+ 'Cache-Control': 'no-cache',
693
+ },
694
+ body: buffer,
695
+ };
696
+ } catch (error) {
697
+ return this.serverError(error);
698
+ }
699
+ }
700
+ }
701
+
702
+ export default ViteDevServer;