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.
- package/LICENSE +21 -0
- package/README.md +731 -0
- package/dist/__sw__.js +394 -0
- package/dist/ai-chatbot-demo-entry.d.ts +6 -0
- package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
- package/dist/ai-chatbot-demo.d.ts +42 -0
- package/dist/ai-chatbot-demo.d.ts.map +1 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
- package/dist/convex-app-demo-entry.d.ts +6 -0
- package/dist/convex-app-demo-entry.d.ts.map +1 -0
- package/dist/convex-app-demo.d.ts +68 -0
- package/dist/convex-app-demo.d.ts.map +1 -0
- package/dist/cors-proxy.d.ts +46 -0
- package/dist/cors-proxy.d.ts.map +1 -0
- package/dist/create-runtime.d.ts +42 -0
- package/dist/create-runtime.d.ts.map +1 -0
- package/dist/demo.d.ts +6 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/dev-server.d.ts +97 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +202 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +85 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
- package/dist/index.cjs +14965 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +14867 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next-demo.d.ts +49 -0
- package/dist/next-demo.d.ts.map +1 -0
- package/dist/npm/index.d.ts +71 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/registry.d.ts +66 -0
- package/dist/npm/registry.d.ts.map +1 -0
- package/dist/npm/resolver.d.ts +52 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/tarball.d.ts +29 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/runtime-interface.d.ts +90 -0
- package/dist/runtime-interface.d.ts.map +1 -0
- package/dist/runtime.d.ts +103 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/sandbox-helpers.d.ts +43 -0
- package/dist/sandbox-helpers.d.ts.map +1 -0
- package/dist/sandbox-runtime.d.ts +65 -0
- package/dist/sandbox-runtime.d.ts.map +1 -0
- package/dist/server-bridge.d.ts +89 -0
- package/dist/server-bridge.d.ts.map +1 -0
- package/dist/shims/assert.d.ts +51 -0
- package/dist/shims/assert.d.ts.map +1 -0
- package/dist/shims/async_hooks.d.ts +37 -0
- package/dist/shims/async_hooks.d.ts.map +1 -0
- package/dist/shims/buffer.d.ts +20 -0
- package/dist/shims/buffer.d.ts.map +1 -0
- package/dist/shims/child_process-browser.d.ts +92 -0
- package/dist/shims/child_process-browser.d.ts.map +1 -0
- package/dist/shims/child_process.d.ts +93 -0
- package/dist/shims/child_process.d.ts.map +1 -0
- package/dist/shims/chokidar.d.ts +55 -0
- package/dist/shims/chokidar.d.ts.map +1 -0
- package/dist/shims/cluster.d.ts +52 -0
- package/dist/shims/cluster.d.ts.map +1 -0
- package/dist/shims/crypto.d.ts +122 -0
- package/dist/shims/crypto.d.ts.map +1 -0
- package/dist/shims/dgram.d.ts +34 -0
- package/dist/shims/dgram.d.ts.map +1 -0
- package/dist/shims/diagnostics_channel.d.ts +80 -0
- package/dist/shims/diagnostics_channel.d.ts.map +1 -0
- package/dist/shims/dns.d.ts +87 -0
- package/dist/shims/dns.d.ts.map +1 -0
- package/dist/shims/domain.d.ts +25 -0
- package/dist/shims/domain.d.ts.map +1 -0
- package/dist/shims/esbuild.d.ts +105 -0
- package/dist/shims/esbuild.d.ts.map +1 -0
- package/dist/shims/events.d.ts +37 -0
- package/dist/shims/events.d.ts.map +1 -0
- package/dist/shims/fs.d.ts +115 -0
- package/dist/shims/fs.d.ts.map +1 -0
- package/dist/shims/fsevents.d.ts +67 -0
- package/dist/shims/fsevents.d.ts.map +1 -0
- package/dist/shims/http.d.ts +217 -0
- package/dist/shims/http.d.ts.map +1 -0
- package/dist/shims/http2.d.ts +81 -0
- package/dist/shims/http2.d.ts.map +1 -0
- package/dist/shims/https.d.ts +36 -0
- package/dist/shims/https.d.ts.map +1 -0
- package/dist/shims/inspector.d.ts +25 -0
- package/dist/shims/inspector.d.ts.map +1 -0
- package/dist/shims/module.d.ts +22 -0
- package/dist/shims/module.d.ts.map +1 -0
- package/dist/shims/net.d.ts +100 -0
- package/dist/shims/net.d.ts.map +1 -0
- package/dist/shims/os.d.ts +159 -0
- package/dist/shims/os.d.ts.map +1 -0
- package/dist/shims/path.d.ts +72 -0
- package/dist/shims/path.d.ts.map +1 -0
- package/dist/shims/perf_hooks.d.ts +50 -0
- package/dist/shims/perf_hooks.d.ts.map +1 -0
- package/dist/shims/process.d.ts +93 -0
- package/dist/shims/process.d.ts.map +1 -0
- package/dist/shims/querystring.d.ts +23 -0
- package/dist/shims/querystring.d.ts.map +1 -0
- package/dist/shims/readdirp.d.ts +52 -0
- package/dist/shims/readdirp.d.ts.map +1 -0
- package/dist/shims/readline.d.ts +62 -0
- package/dist/shims/readline.d.ts.map +1 -0
- package/dist/shims/rollup.d.ts +34 -0
- package/dist/shims/rollup.d.ts.map +1 -0
- package/dist/shims/sentry.d.ts +163 -0
- package/dist/shims/sentry.d.ts.map +1 -0
- package/dist/shims/stream.d.ts +181 -0
- package/dist/shims/stream.d.ts.map +1 -0
- package/dist/shims/tls.d.ts +53 -0
- package/dist/shims/tls.d.ts.map +1 -0
- package/dist/shims/tty.d.ts +30 -0
- package/dist/shims/tty.d.ts.map +1 -0
- package/dist/shims/url.d.ts +64 -0
- package/dist/shims/url.d.ts.map +1 -0
- package/dist/shims/util.d.ts +106 -0
- package/dist/shims/util.d.ts.map +1 -0
- package/dist/shims/v8.d.ts +73 -0
- package/dist/shims/v8.d.ts.map +1 -0
- package/dist/shims/vfs-adapter.d.ts +126 -0
- package/dist/shims/vfs-adapter.d.ts.map +1 -0
- package/dist/shims/vm.d.ts +45 -0
- package/dist/shims/vm.d.ts.map +1 -0
- package/dist/shims/worker_threads.d.ts +66 -0
- package/dist/shims/worker_threads.d.ts.map +1 -0
- package/dist/shims/ws.d.ts +66 -0
- package/dist/shims/ws.d.ts.map +1 -0
- package/dist/shims/zlib.d.ts +161 -0
- package/dist/shims/zlib.d.ts.map +1 -0
- package/dist/transform.d.ts +24 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +226 -0
- package/dist/virtual-fs.d.ts.map +1 -0
- package/dist/vite-demo.d.ts +35 -0
- package/dist/vite-demo.d.ts.map +1 -0
- package/dist/vite-sw.js +132 -0
- package/dist/worker/runtime-worker.d.ts +8 -0
- package/dist/worker/runtime-worker.d.ts.map +1 -0
- package/dist/worker-runtime.d.ts +50 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/ai-chatbot-demo-entry.ts +244 -0
- package/src/ai-chatbot-demo.ts +509 -0
- package/src/convex-app-demo-entry.ts +1107 -0
- package/src/convex-app-demo.ts +1316 -0
- package/src/cors-proxy.ts +81 -0
- package/src/create-runtime.ts +147 -0
- package/src/demo.ts +304 -0
- package/src/dev-server.ts +274 -0
- package/src/frameworks/next-dev-server.ts +2224 -0
- package/src/frameworks/vite-dev-server.ts +702 -0
- package/src/index.ts +101 -0
- package/src/next-demo.ts +1784 -0
- package/src/npm/index.ts +347 -0
- package/src/npm/registry.ts +152 -0
- package/src/npm/resolver.ts +385 -0
- package/src/npm/tarball.ts +209 -0
- package/src/runtime-interface.ts +103 -0
- package/src/runtime.ts +1046 -0
- package/src/sandbox-helpers.ts +173 -0
- package/src/sandbox-runtime.ts +252 -0
- package/src/server-bridge.ts +426 -0
- package/src/shims/assert.ts +664 -0
- package/src/shims/async_hooks.ts +86 -0
- package/src/shims/buffer.ts +75 -0
- package/src/shims/child_process-browser.ts +217 -0
- package/src/shims/child_process.ts +463 -0
- package/src/shims/chokidar.ts +313 -0
- package/src/shims/cluster.ts +67 -0
- package/src/shims/crypto.ts +830 -0
- package/src/shims/dgram.ts +47 -0
- package/src/shims/diagnostics_channel.ts +196 -0
- package/src/shims/dns.ts +172 -0
- package/src/shims/domain.ts +58 -0
- package/src/shims/esbuild.ts +805 -0
- package/src/shims/events.ts +195 -0
- package/src/shims/fs.ts +803 -0
- package/src/shims/fsevents.ts +63 -0
- package/src/shims/http.ts +904 -0
- package/src/shims/http2.ts +96 -0
- package/src/shims/https.ts +86 -0
- package/src/shims/inspector.ts +30 -0
- package/src/shims/module.ts +82 -0
- package/src/shims/net.ts +359 -0
- package/src/shims/os.ts +195 -0
- package/src/shims/path.ts +199 -0
- package/src/shims/perf_hooks.ts +92 -0
- package/src/shims/process.ts +346 -0
- package/src/shims/querystring.ts +97 -0
- package/src/shims/readdirp.ts +228 -0
- package/src/shims/readline.ts +110 -0
- package/src/shims/rollup.ts +80 -0
- package/src/shims/sentry.ts +133 -0
- package/src/shims/stream.ts +1126 -0
- package/src/shims/tls.ts +95 -0
- package/src/shims/tty.ts +64 -0
- package/src/shims/url.ts +171 -0
- package/src/shims/util.ts +312 -0
- package/src/shims/v8.ts +113 -0
- package/src/shims/vfs-adapter.ts +402 -0
- package/src/shims/vm.ts +83 -0
- package/src/shims/worker_threads.ts +111 -0
- package/src/shims/ws.ts +382 -0
- package/src/shims/zlib.ts +289 -0
- package/src/transform.ts +313 -0
- package/src/types/external.d.ts +67 -0
- package/src/virtual-fs.ts +903 -0
- package/src/vite-demo.ts +577 -0
- package/src/worker/runtime-worker.ts +128 -0
- 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;
|