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,1107 @@
1
+ /**
2
+ * Entry point for Convex App Demo
3
+ * This file is loaded by the HTML and bootstraps the demo
4
+ */
5
+
6
+ import { VirtualFS } from './virtual-fs';
7
+ import { Runtime } from './runtime';
8
+ import { NextDevServer } from './frameworks/next-dev-server';
9
+ import { getServerBridge } from './server-bridge';
10
+ import { Buffer } from './shims/stream';
11
+ import { createConvexAppProject } from './convex-app-demo';
12
+ import { PackageManager } from './npm/index';
13
+
14
+ // DOM elements
15
+ const logsEl = document.getElementById('logs') as HTMLDivElement;
16
+ const previewContainer = document.getElementById('previewContainer') as HTMLDivElement;
17
+ const statusDot = document.getElementById('statusDot') as HTMLSpanElement;
18
+ const statusText = document.getElementById('statusText') as HTMLSpanElement;
19
+ const refreshBtn = document.getElementById('refreshBtn') as HTMLButtonElement;
20
+ const openBtn = document.getElementById('openBtn') as HTMLButtonElement;
21
+ const convexKeyInput = document.getElementById('convexKey') as HTMLInputElement;
22
+ const deployBtn = document.getElementById('deployBtn') as HTMLButtonElement;
23
+ const convexStatusEl = document.getElementById('convexStatus') as HTMLDivElement;
24
+ const convexStatusText = document.getElementById('convexStatusText') as HTMLSpanElement;
25
+ const fileTreeEl = document.getElementById('fileTree') as HTMLDivElement;
26
+ const editorTabsEl = document.getElementById('editorTabs') as HTMLDivElement;
27
+ const editorContentEl = document.getElementById('editorContent') as HTMLDivElement;
28
+ const saveBtn = document.getElementById('saveBtn') as HTMLButtonElement;
29
+ const watchModeCheckbox = document.getElementById('watchModeCheckbox') as HTMLInputElement;
30
+ const watchModeLabel = document.getElementById('watchModeLabel') as HTMLLabelElement;
31
+ const watchModeText = document.getElementById('watchModeText') as HTMLSpanElement;
32
+
33
+ let serverUrl: string | null = null;
34
+ let iframe: HTMLIFrameElement | null = null;
35
+ let vfs: VirtualFS | null = null;
36
+ let convexUrl: string | null = null;
37
+ let cliRuntime: Runtime | null = null;
38
+ let devServer: NextDevServer | null = null;
39
+
40
+ // Watch mode state
41
+ let watchModeEnabled = false;
42
+ let convexWatcher: { close: () => void } | null = null;
43
+ let isDeploying = false;
44
+ let deployDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
45
+ const DEPLOY_DEBOUNCE_MS = 800;
46
+
47
+ // Editor state
48
+ interface OpenFile {
49
+ path: string;
50
+ content: string;
51
+ originalContent: string;
52
+ modified: boolean;
53
+ }
54
+ let openFiles: OpenFile[] = [];
55
+ let activeFilePath: string | null = null;
56
+
57
+ // Status codes for test automation
58
+ type StatusCode =
59
+ | 'DEPLOYING'
60
+ | 'INSTALLED'
61
+ | 'CLI_RUNNING'
62
+ | 'WAITING'
63
+ | 'COMPLETE'
64
+ | 'ERROR';
65
+
66
+ function log(message: string, type: 'info' | 'error' | 'warn' | 'success' = 'info') {
67
+ const line = document.createElement('div');
68
+ const time = new Date().toLocaleTimeString();
69
+ line.textContent = `[${time}] ${message}`;
70
+ if (type === 'error') line.className = 'error';
71
+ if (type === 'warn') line.className = 'warn';
72
+ if (type === 'success') line.className = 'success';
73
+ logsEl.appendChild(line);
74
+ logsEl.scrollTop = logsEl.scrollHeight;
75
+ }
76
+
77
+ function logStatus(status: StatusCode, message: string) {
78
+ log(`[STATUS:${status}] ${message}`, status === 'ERROR' ? 'error' : status === 'COMPLETE' ? 'success' : 'info');
79
+ }
80
+
81
+ function setStatus(text: string, state: 'loading' | 'running' | 'error' = 'loading') {
82
+ statusText.textContent = text;
83
+ statusDot.className = 'status-dot ' + state;
84
+ }
85
+
86
+ // ============ File Tree Functions ============
87
+
88
+ /**
89
+ * Build the file tree UI for the given directories
90
+ */
91
+ function buildFileTree(): void {
92
+ if (!vfs) return;
93
+
94
+ fileTreeEl.innerHTML = '';
95
+
96
+ // Directories to show in the file tree
97
+ const rootDirs = ['/app', '/convex', '/components', '/lib'];
98
+
99
+ for (const dir of rootDirs) {
100
+ if (vfs.existsSync(dir)) {
101
+ const folderEl = createFolderElement(dir, true);
102
+ fileTreeEl.appendChild(folderEl);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Create a folder element with its children
109
+ */
110
+ function createFolderElement(path: string, expanded = false): HTMLElement {
111
+ if (!vfs) return document.createElement('div');
112
+
113
+ const folder = document.createElement('div');
114
+ folder.className = 'tree-folder' + (expanded ? ' expanded' : '');
115
+
116
+ const name = path.split('/').pop() || path;
117
+
118
+ // Folder header
119
+ const header = document.createElement('div');
120
+ header.className = 'tree-item';
121
+ header.innerHTML = `
122
+ <span class="icon">${expanded ? '▼' : '▶'}</span>
123
+ <span class="name">${name}</span>
124
+ `;
125
+ header.onclick = (e) => {
126
+ e.stopPropagation();
127
+ folder.classList.toggle('expanded');
128
+ const icon = header.querySelector('.icon') as HTMLSpanElement;
129
+ icon.textContent = folder.classList.contains('expanded') ? '▼' : '▶';
130
+ };
131
+ folder.appendChild(header);
132
+
133
+ // Children container
134
+ const children = document.createElement('div');
135
+ children.className = 'tree-children';
136
+
137
+ try {
138
+ const entries = vfs.readdirSync(path);
139
+
140
+ // Sort: folders first, then files
141
+ const sorted = entries.sort((a, b) => {
142
+ const aIsDir = isDirectory(path + '/' + a);
143
+ const bIsDir = isDirectory(path + '/' + b);
144
+ if (aIsDir && !bIsDir) return -1;
145
+ if (!aIsDir && bIsDir) return 1;
146
+ return a.localeCompare(b);
147
+ });
148
+
149
+ for (const entry of sorted) {
150
+ const fullPath = path + '/' + entry;
151
+ if (isDirectory(fullPath)) {
152
+ children.appendChild(createFolderElement(fullPath, false));
153
+ } else {
154
+ children.appendChild(createFileElement(fullPath));
155
+ }
156
+ }
157
+ } catch (e) {
158
+ // Directory might not exist or be readable
159
+ }
160
+
161
+ folder.appendChild(children);
162
+ return folder;
163
+ }
164
+
165
+ /**
166
+ * Create a file element
167
+ */
168
+ function createFileElement(path: string): HTMLElement {
169
+ const file = document.createElement('div');
170
+ file.className = 'tree-item';
171
+ file.dataset.path = path;
172
+
173
+ const name = path.split('/').pop() || path;
174
+ file.innerHTML = `
175
+ <span class="icon">📄</span>
176
+ <span class="name">${name}</span>
177
+ `;
178
+
179
+ file.onclick = (e) => {
180
+ e.stopPropagation();
181
+ openFile(path);
182
+ };
183
+
184
+ return file;
185
+ }
186
+
187
+ /**
188
+ * Check if a path is a directory
189
+ */
190
+ function isDirectory(path: string): boolean {
191
+ if (!vfs) return false;
192
+ try {
193
+ return vfs.statSync(path).isDirectory();
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ // ============ Editor Functions ============
200
+
201
+ /**
202
+ * Open a file in the editor
203
+ */
204
+ function openFile(path: string): void {
205
+ if (!vfs) return;
206
+
207
+ // Check if already open
208
+ let file = openFiles.find(f => f.path === path);
209
+
210
+ if (!file) {
211
+ // Read file content
212
+ try {
213
+ const content = vfs.readFileSync(path, 'utf8');
214
+ file = {
215
+ path,
216
+ content,
217
+ originalContent: content,
218
+ modified: false,
219
+ };
220
+ openFiles.push(file);
221
+ } catch (e) {
222
+ log(`Failed to open file: ${path}`, 'error');
223
+ return;
224
+ }
225
+ }
226
+
227
+ activeFilePath = path;
228
+ renderTabs();
229
+ renderEditor();
230
+ updateFileTreeSelection();
231
+ }
232
+
233
+ /**
234
+ * Close a file tab
235
+ */
236
+ function closeFile(path: string): void {
237
+ const index = openFiles.findIndex(f => f.path === path);
238
+ if (index === -1) return;
239
+
240
+ openFiles.splice(index, 1);
241
+
242
+ // If we closed the active file, switch to another
243
+ if (activeFilePath === path) {
244
+ activeFilePath = openFiles.length > 0 ? openFiles[openFiles.length - 1].path : null;
245
+ }
246
+
247
+ renderTabs();
248
+ renderEditor();
249
+ updateFileTreeSelection();
250
+ }
251
+
252
+ /**
253
+ * Save the currently active file and trigger HMR
254
+ */
255
+ function saveFile(): void {
256
+ if (!vfs || !activeFilePath) return;
257
+
258
+ const file = openFiles.find(f => f.path === activeFilePath);
259
+ if (!file) return;
260
+
261
+ try {
262
+ vfs.writeFileSync(file.path, file.content);
263
+ file.originalContent = file.content;
264
+ file.modified = false;
265
+ log(`Saved: ${file.path}`, 'success');
266
+ renderTabs();
267
+ saveBtn.disabled = true;
268
+
269
+ // Manually trigger HMR since automatic watcher may not work in all cases
270
+ triggerHMR(file.path);
271
+ } catch (e) {
272
+ log(`Failed to save: ${file.path} - ${e}`, 'error');
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Manually trigger HMR update via postMessage
278
+ * This mimics what the dev server's handleFileChange() does
279
+ * Uses postMessage instead of BroadcastChannel to work with sandboxed iframes
280
+ */
281
+ function triggerHMR(path: string): void {
282
+ const isJS = /\.(jsx?|tsx?)$/.test(path);
283
+ const isCSS = path.endsWith('.css');
284
+
285
+ if (!isJS && !isCSS) {
286
+ return;
287
+ }
288
+
289
+ const update = {
290
+ type: 'update' as const,
291
+ path,
292
+ timestamp: Date.now(),
293
+ channel: 'next-hmr' as const,
294
+ };
295
+
296
+ // Send via postMessage to iframe (works with sandboxed iframes)
297
+ if (iframe?.contentWindow) {
298
+ try {
299
+ iframe.contentWindow.postMessage(update, '*');
300
+ log(`HMR: ${path}`, 'success');
301
+ } catch (e) {
302
+ log(`HMR postMessage failed: ${e}`, 'warn');
303
+ }
304
+ } else {
305
+ log(`HMR: no iframe to send update to`, 'warn');
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Render the editor tabs
311
+ */
312
+ function renderTabs(): void {
313
+ editorTabsEl.innerHTML = '';
314
+
315
+ for (const file of openFiles) {
316
+ const tab = document.createElement('div');
317
+ tab.className = 'editor-tab' + (file.path === activeFilePath ? ' active' : '');
318
+
319
+ const name = file.path.split('/').pop() || file.path;
320
+ tab.innerHTML = `
321
+ <span>${name}</span>
322
+ ${file.modified ? '<span class="modified">●</span>' : ''}
323
+ <span class="close">×</span>
324
+ `;
325
+
326
+ tab.onclick = (e) => {
327
+ if ((e.target as HTMLElement).classList.contains('close')) {
328
+ closeFile(file.path);
329
+ } else {
330
+ activeFilePath = file.path;
331
+ renderTabs();
332
+ renderEditor();
333
+ updateFileTreeSelection();
334
+ }
335
+ };
336
+
337
+ editorTabsEl.appendChild(tab);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Render the editor content
343
+ */
344
+ function renderEditor(): void {
345
+ if (!activeFilePath) {
346
+ editorContentEl.innerHTML = '<div class="editor-empty">Select a file to edit</div>';
347
+ saveBtn.disabled = true;
348
+ return;
349
+ }
350
+
351
+ const file = openFiles.find(f => f.path === activeFilePath);
352
+ if (!file) {
353
+ editorContentEl.innerHTML = '<div class="editor-empty">File not found</div>';
354
+ saveBtn.disabled = true;
355
+ return;
356
+ }
357
+
358
+ // Create textarea
359
+ const textarea = document.createElement('textarea');
360
+ textarea.className = 'editor-textarea';
361
+ textarea.value = file.content;
362
+ textarea.spellcheck = false;
363
+
364
+ textarea.oninput = () => {
365
+ file.content = textarea.value;
366
+ file.modified = file.content !== file.originalContent;
367
+ saveBtn.disabled = !file.modified;
368
+ renderTabs();
369
+ };
370
+
371
+ // Handle Ctrl+S
372
+ textarea.onkeydown = (e) => {
373
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
374
+ e.preventDefault();
375
+ saveFile();
376
+ }
377
+ // Handle Tab key for indentation
378
+ if (e.key === 'Tab') {
379
+ e.preventDefault();
380
+ const start = textarea.selectionStart;
381
+ const end = textarea.selectionEnd;
382
+ textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
383
+ textarea.selectionStart = textarea.selectionEnd = start + 2;
384
+ file.content = textarea.value;
385
+ file.modified = file.content !== file.originalContent;
386
+ saveBtn.disabled = !file.modified;
387
+ renderTabs();
388
+ }
389
+ };
390
+
391
+ // Auto-save on blur
392
+ textarea.onblur = () => {
393
+ if (file.modified) {
394
+ saveFile();
395
+ }
396
+ };
397
+
398
+ editorContentEl.innerHTML = '';
399
+ editorContentEl.appendChild(textarea);
400
+
401
+ saveBtn.disabled = !file.modified;
402
+
403
+ // Focus the textarea
404
+ textarea.focus();
405
+ }
406
+
407
+ /**
408
+ * Update file tree selection highlight
409
+ */
410
+ function updateFileTreeSelection(): void {
411
+ // Remove all selected classes
412
+ fileTreeEl.querySelectorAll('.tree-item.selected').forEach(el => {
413
+ el.classList.remove('selected');
414
+ });
415
+
416
+ // Add selected class to active file
417
+ if (activeFilePath) {
418
+ const fileEl = fileTreeEl.querySelector(`[data-path="${activeFilePath}"]`);
419
+ if (fileEl) {
420
+ fileEl.classList.add('selected');
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Expose VFS functions to window for debugging
427
+ */
428
+ function exposeVfsToWindow(): void {
429
+ if (!vfs) return;
430
+
431
+ (window as any).__vfs__ = vfs;
432
+ (window as any).__readFile__ = (path: string) => vfs!.readFileSync(path, 'utf8');
433
+ (window as any).__writeFile__ = (path: string, content: string) => vfs!.writeFileSync(path, content);
434
+ (window as any).__listDir__ = (path: string) => vfs!.readdirSync(path);
435
+ (window as any).__isDir__ = (path: string) => vfs!.statSync(path).isDirectory();
436
+ }
437
+
438
+ /**
439
+ * Parse Convex deploy key to extract deployment name and URL
440
+ */
441
+ function parseConvexKey(key: string): { deploymentName: string; url: string; adminKey: string } | null {
442
+ // Format: dev:deployment-name|token or prod:deployment-name|token
443
+ const match = key.match(/^(dev|prod):([^|]+)\|(.+)$/);
444
+ if (!match) return null;
445
+
446
+ const [, env, deploymentName] = match;
447
+ const url = `https://${deploymentName}.convex.cloud`;
448
+ return { deploymentName, url, adminKey: key };
449
+ }
450
+
451
+ /**
452
+ * Wait for deployment to complete by polling for .env.local creation
453
+ * This replaces the fixed 10s timeout with smart polling
454
+ */
455
+ async function waitForDeployment(vfs: VirtualFS, maxWait = 30000, pollInterval = 500): Promise<boolean> {
456
+ const startTime = Date.now();
457
+ while (Date.now() - startTime < maxWait) {
458
+ if (vfs.existsSync('/project/.env.local')) {
459
+ return true;
460
+ }
461
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
462
+ }
463
+ return false;
464
+ }
465
+
466
+ /**
467
+ * Wait for _generated directory to be created (indicates functions were bundled)
468
+ */
469
+ async function waitForGenerated(vfs: VirtualFS, maxWait = 15000, pollInterval = 500): Promise<boolean> {
470
+ const startTime = Date.now();
471
+ while (Date.now() - startTime < maxWait) {
472
+ if (vfs.existsSync('/project/convex/_generated')) {
473
+ const files = vfs.readdirSync('/project/convex/_generated');
474
+ if (files.length > 0) {
475
+ return true;
476
+ }
477
+ }
478
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
479
+ }
480
+ return false;
481
+ }
482
+
483
+ /**
484
+ * Deploy Convex schema and functions to Convex cloud using the Convex CLI
485
+ *
486
+ * This approach is documented in examples/convex-todo/src/hooks/useConvexRuntime.ts
487
+ * Key requirements:
488
+ * 1. Use /project/ as the working directory (CLI expects this structure)
489
+ * 2. Use runtime.execute() with inline code that sets process.env and process.argv
490
+ * 3. Use require() with relative path to the CLI bundle
491
+ * 4. Create both .ts AND .js versions of convex/convex.config
492
+ * 5. Wait for async operations after CLI runs
493
+ */
494
+ async function deployToConvex(adminKey: string): Promise<void> {
495
+ if (!vfs) throw new Error('VFS not initialized');
496
+
497
+ const parsed = parseConvexKey(adminKey);
498
+ if (!parsed) {
499
+ throw new Error('Invalid deploy key format. Expected: dev:name|token');
500
+ }
501
+
502
+ logStatus('DEPLOYING', `Starting deployment to ${parsed.deploymentName}...`);
503
+
504
+ // Create /project directory structure for CLI (matching working example)
505
+ log('Setting up project structure for CLI...');
506
+ vfs.mkdirSync('/project', { recursive: true });
507
+ vfs.mkdirSync('/project/convex', { recursive: true });
508
+
509
+ // Create package.json in /project (and root - CLI looks for both)
510
+ const packageJson = JSON.stringify({
511
+ name: 'convex-app-demo',
512
+ version: '1.0.0',
513
+ dependencies: { convex: '^1.0.0' }
514
+ }, null, 2);
515
+ vfs.writeFileSync('/project/package.json', packageJson);
516
+ vfs.writeFileSync('/package.json', packageJson);
517
+
518
+ // Create convex.json in /project
519
+ vfs.writeFileSync('/project/convex.json', JSON.stringify({
520
+ functions: "convex/"
521
+ }, null, 2));
522
+
523
+ // Clean up /project/convex/ completely to ensure fresh state
524
+ // This prevents stale cached files from being used
525
+ if (vfs.existsSync('/project/convex')) {
526
+ log('Cleaning /project/convex/ directory...');
527
+ try {
528
+ const existingFiles = vfs.readdirSync('/project/convex');
529
+ for (const file of existingFiles) {
530
+ const filePath = `/project/convex/${file}`;
531
+ try {
532
+ const stat = vfs.statSync(filePath);
533
+ if (stat.isDirectory()) {
534
+ // Remove directory contents first
535
+ const subFiles = vfs.readdirSync(filePath);
536
+ for (const subFile of subFiles) {
537
+ vfs.unlinkSync(`${filePath}/${subFile}`);
538
+ }
539
+ vfs.rmdirSync(filePath);
540
+ } else {
541
+ vfs.unlinkSync(filePath);
542
+ }
543
+ } catch (e) {
544
+ log(` Warning: Could not remove ${filePath}: ${e}`, 'warn');
545
+ }
546
+ }
547
+ } catch (e) {
548
+ log(`Warning: Could not clean /project/convex/: ${e}`, 'warn');
549
+ }
550
+ }
551
+ vfs.mkdirSync('/project/convex', { recursive: true });
552
+
553
+ // Also clean /convex/_generated to ensure fresh generation
554
+ if (vfs.existsSync('/convex/_generated')) {
555
+ log('Cleaning /convex/_generated directory...');
556
+ try {
557
+ const files = vfs.readdirSync('/convex/_generated');
558
+ for (const file of files) {
559
+ vfs.unlinkSync(`/convex/_generated/${file}`);
560
+ }
561
+ vfs.rmdirSync('/convex/_generated');
562
+ } catch (e) {
563
+ log(`Warning: Could not remove /convex/_generated: ${e}`, 'warn');
564
+ }
565
+ }
566
+
567
+ // Create convex config files (BOTH .ts and .js required!)
568
+ const convexConfig = `import { defineApp } from "convex/server";
569
+ const app = defineApp();
570
+ export default app;
571
+ `;
572
+ vfs.writeFileSync('/project/convex/convex.config.ts', convexConfig);
573
+ vfs.writeFileSync('/project/convex/convex.config.js', convexConfig);
574
+
575
+ // Copy ALL convex files from root to /project/convex/ (dynamically, not hardcoded)
576
+ // Read files fresh from VFS to ensure we get the latest content
577
+ log('Copying convex files...');
578
+ if (vfs.existsSync('/convex')) {
579
+ const convexFiles = vfs.readdirSync('/convex');
580
+ for (const file of convexFiles) {
581
+ const srcPath = `/convex/${file}`;
582
+ const destPath = `/project/convex/${file}`;
583
+ // Skip _generated directory and only copy files (not directories)
584
+ if (file === '_generated') continue;
585
+ try {
586
+ const stat = vfs.statSync(srcPath);
587
+ if (stat.isFile()) {
588
+ const content = vfs.readFileSync(srcPath, 'utf8');
589
+ vfs.writeFileSync(destPath, content);
590
+ log(` Copied ${file}`);
591
+ }
592
+ } catch (e) {
593
+ log(` Warning: Could not copy ${srcPath}: ${e}`, 'warn');
594
+ }
595
+ }
596
+ }
597
+
598
+ // Install convex package in /project
599
+ const convexPkgPath = '/project/node_modules/convex/package.json';
600
+ if (!vfs.existsSync(convexPkgPath)) {
601
+ log('Installing convex package...');
602
+ const npm = new PackageManager(vfs, { cwd: '/project' });
603
+ try {
604
+ await npm.install('convex', {
605
+ onProgress: (msg) => log(` ${msg}`),
606
+ });
607
+ logStatus('INSTALLED', 'Convex package installed');
608
+ } catch (error) {
609
+ logStatus('ERROR', `Failed to install convex: ${error}`);
610
+ throw error;
611
+ }
612
+ } else {
613
+ logStatus('INSTALLED', 'Convex package already installed');
614
+ }
615
+
616
+ // Run Convex CLI using runtime.execute() with cwd /project
617
+ logStatus('CLI_RUNNING', 'Running convex dev --once');
618
+
619
+ // Always create fresh Runtime for each deployment
620
+ // This ensures no stale caches or closures from previous deployments
621
+ log('Creating fresh CLI Runtime...');
622
+ cliRuntime = new Runtime(vfs, { cwd: '/project' });
623
+
624
+ // Debug: verify files exist and show content preview
625
+ log('Verifying project structure...');
626
+ const requiredFiles = [
627
+ '/project/package.json',
628
+ '/project/convex.json',
629
+ '/project/convex/convex.config.ts',
630
+ '/project/convex/convex.config.js',
631
+ '/project/convex/schema.ts',
632
+ '/project/convex/todos.ts',
633
+ '/project/node_modules/convex/package.json',
634
+ '/project/node_modules/convex/dist/cli.bundle.cjs',
635
+ ];
636
+ for (const file of requiredFiles) {
637
+ if (vfs.existsSync(file)) {
638
+ // For convex source files, show content preview to verify it's fresh
639
+ if (file.includes('/project/convex/') && (file.endsWith('.ts') || file.endsWith('.js'))) {
640
+ const content = vfs.readFileSync(file, 'utf8');
641
+ const preview = content.substring(0, 60).replace(/\n/g, '\\n');
642
+ log(` ✓ ${file} (${content.length}b): "${preview}..."`, 'success');
643
+ } else {
644
+ log(` ✓ ${file}`, 'success');
645
+ }
646
+ } else {
647
+ log(` ✗ ${file} MISSING`, 'error');
648
+ }
649
+ }
650
+
651
+ // Match working example exactly
652
+ const cliCode = `
653
+ // Set environment for Convex CLI
654
+ process.env.CONVEX_DEPLOY_KEY = '${adminKey}';
655
+
656
+ // Set CLI arguments
657
+ process.argv = ['node', 'convex', 'dev', '--once'];
658
+
659
+ // Run the CLI
660
+ require('./node_modules/convex/dist/cli.bundle.cjs');
661
+ `;
662
+
663
+ try {
664
+ cliRuntime.execute(cliCode, '/project/cli-runner.js');
665
+ } catch (cliError) {
666
+ // Some errors are expected (like process.exit or stack overflow in watcher)
667
+ // The important work (deployment) happens before these errors
668
+ log(`CLI completed with: ${(cliError as Error).message}`, 'warn');
669
+ }
670
+
671
+ // Wait for async operations to complete using smart polling
672
+ // Poll for .env.local creation instead of fixed timeout
673
+ logStatus('WAITING', 'Waiting for deployment to complete...');
674
+ const deploymentSucceeded = await waitForDeployment(vfs, 30000, 500);
675
+
676
+ if (!deploymentSucceeded) {
677
+ log('Deployment may still be in progress, waiting additional time...', 'warn');
678
+ await new Promise(resolve => setTimeout(resolve, 5000));
679
+ } else {
680
+ // .env.local was found, now wait for _generated directory
681
+ // The CLI creates .env.local first, then bundles functions asynchronously
682
+ log('Environment configured, waiting for function bundling...');
683
+ const generatedCreated = await waitForGenerated(vfs, 15000, 500);
684
+ if (!generatedCreated) {
685
+ log('_generated directory not created yet, waiting additional time...', 'warn');
686
+ await new Promise(resolve => setTimeout(resolve, 5000));
687
+ }
688
+ }
689
+
690
+ // Check if deployment succeeded by reading .env.local (CLI creates it in /project)
691
+ const envLocalPath = '/project/.env.local';
692
+ if (vfs.existsSync(envLocalPath)) {
693
+ const envContent = vfs.readFileSync(envLocalPath, 'utf8');
694
+ log('.env.local created - deployment succeeded!', 'success');
695
+ log(` Contents: ${envContent.trim()}`);
696
+
697
+ // Check if _generated directory was created (indicates functions were pushed)
698
+ if (vfs.existsSync('/project/convex/_generated')) {
699
+ const generated = vfs.readdirSync('/project/convex/_generated');
700
+ log(` Generated files: ${generated.join(', ')}`, 'success');
701
+
702
+ // Show the contents of api.js to verify function references
703
+ if (vfs.existsSync('/project/convex/_generated/api.js')) {
704
+ const apiContent = vfs.readFileSync('/project/convex/_generated/api.js', 'utf8');
705
+ log(' Generated api.js content:', 'info');
706
+ // Show first 500 chars
707
+ log(` ${apiContent.substring(0, 500)}...`, 'info');
708
+ }
709
+
710
+ // Copy generated files to /convex/_generated/ for the Next.js app to use
711
+ // CLI generates .js/.d.ts files, but Next.js imports .ts files
712
+ // So we copy api.js as both api.js AND api.ts
713
+ log('Copying generated files to /convex/_generated/...');
714
+ vfs.mkdirSync('/convex/_generated', { recursive: true });
715
+ for (const file of generated) {
716
+ const srcPath = `/project/convex/_generated/${file}`;
717
+ const destPath = `/convex/_generated/${file}`;
718
+ if (vfs.existsSync(srcPath)) {
719
+ const content = vfs.readFileSync(srcPath, 'utf8');
720
+ vfs.writeFileSync(destPath, content);
721
+ log(` Copied ${file}`, 'success');
722
+
723
+ // Also copy .js files as .ts for Next.js imports
724
+ if (file.endsWith('.js') && !file.endsWith('.d.js')) {
725
+ const tsDestPath = destPath.replace(/\.js$/, '.ts');
726
+ vfs.writeFileSync(tsDestPath, content);
727
+ log(` Also copied as ${file.replace(/\.js$/, '.ts')}`, 'success');
728
+ }
729
+ }
730
+ }
731
+ } else {
732
+ log(' WARNING: _generated directory not created - functions may not be deployed!', 'error');
733
+ }
734
+
735
+ // Parse the Convex URL from .env.local
736
+ const match = envContent.match(/CONVEX_URL=(.+)/);
737
+ if (match) {
738
+ convexUrl = match[1].trim();
739
+ logStatus('COMPLETE', `Deployment successful - connected to ${convexUrl}`);
740
+ } else {
741
+ convexUrl = parsed.url;
742
+ logStatus('COMPLETE', `Deployment successful - Convex URL set: ${convexUrl}`);
743
+ }
744
+ } else {
745
+ log('.env.local not found - checking root...', 'warn');
746
+ // Also check root in case CLI wrote there
747
+ if (vfs.existsSync('/.env.local')) {
748
+ const envContent = vfs.readFileSync('/.env.local', 'utf8');
749
+ log(`Found .env.local at root: ${envContent.trim()}`);
750
+ const match = envContent.match(/CONVEX_URL=(.+)/);
751
+ if (match) {
752
+ convexUrl = match[1].trim();
753
+ }
754
+ }
755
+ if (!convexUrl) {
756
+ convexUrl = parsed.url;
757
+ log(`Using fallback URL: ${convexUrl}`, 'warn');
758
+ }
759
+ }
760
+
761
+ // Set the env var on the dev server (idiomatic Next.js pattern)
762
+ // This makes it available via process.env.NEXT_PUBLIC_CONVEX_URL in browser code
763
+ if (devServer && convexUrl) {
764
+ devServer.setEnv('NEXT_PUBLIC_CONVEX_URL', convexUrl);
765
+ log(`Set NEXT_PUBLIC_CONVEX_URL=${convexUrl}`);
766
+ }
767
+
768
+ // Also set on parent window for backwards compatibility
769
+ (window as any).__CONVEX_URL__ = convexUrl;
770
+
771
+ // Wait a moment for things to settle before refreshing
772
+ log('Waiting for iframe refresh...');
773
+ await new Promise(resolve => setTimeout(resolve, 1000));
774
+
775
+ // Refresh the iframe to pick up the new Convex connection
776
+ if (iframe) {
777
+ const iframeSrc = iframe.src;
778
+ log(`Refreshing preview: ${iframeSrc}`);
779
+
780
+ // Add load handler to track iframe state
781
+ iframe.onload = () => {
782
+ log('Iframe loaded successfully', 'success');
783
+ // The env var is now injected via the HTML, so we only need the fallback
784
+ if (convexUrl && iframe?.contentWindow) {
785
+ (iframe.contentWindow as any).__CONVEX_URL__ = convexUrl;
786
+ }
787
+ };
788
+
789
+ iframe.onerror = (e) => {
790
+ log(`Iframe error: ${e}`, 'error');
791
+ };
792
+
793
+ // Clear and reload
794
+ iframe.src = 'about:blank';
795
+ await new Promise(resolve => setTimeout(resolve, 500));
796
+ iframe.src = iframeSrc;
797
+ log('Preview refresh initiated', 'success');
798
+ } else {
799
+ log('No iframe found!', 'error');
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Set up file watcher for /convex directory to auto-deploy on changes
805
+ */
806
+ function setupConvexWatcher(): void {
807
+ if (!vfs || convexWatcher) return;
808
+
809
+ log('Setting up watch mode for /convex directory...', 'info');
810
+
811
+ convexWatcher = vfs.watch('/convex', { recursive: true }, (eventType, filename) => {
812
+ // Ignore if watch mode is disabled or already deploying
813
+ if (!watchModeEnabled || isDeploying) return;
814
+
815
+ // Ignore generated files and dotfiles
816
+ if (!filename || filename.includes('_generated') || filename.startsWith('.')) return;
817
+
818
+ // Only watch source files
819
+ if (!/\.(ts|tsx|js|json)$/.test(filename)) return;
820
+
821
+ log(`Watch: detected change in ${filename}`, 'info');
822
+
823
+ // Debounce to handle rapid edits
824
+ if (deployDebounceTimeout) {
825
+ clearTimeout(deployDebounceTimeout);
826
+ }
827
+
828
+ deployDebounceTimeout = setTimeout(() => {
829
+ const key = convexKeyInput.value.trim();
830
+ if (!key) {
831
+ log('Watch: no deploy key, skipping auto-deploy', 'warn');
832
+ return;
833
+ }
834
+
835
+ triggerAutoDeployment(key);
836
+ }, DEPLOY_DEBOUNCE_MS);
837
+ });
838
+
839
+ log('Watch mode enabled - changes to /convex files will auto-deploy', 'success');
840
+ }
841
+
842
+ /**
843
+ * Trigger an automatic deployment from watch mode
844
+ */
845
+ async function triggerAutoDeployment(key: string): Promise<void> {
846
+ if (isDeploying) {
847
+ log('Watch: deployment already in progress, skipping', 'warn');
848
+ return;
849
+ }
850
+
851
+ isDeploying = true;
852
+
853
+ // Update UI to show auto-deploying state
854
+ deployBtn.classList.add('auto-deploying');
855
+ deployBtn.textContent = 'Auto-deploying...';
856
+ watchModeLabel.classList.add('watching');
857
+ watchModeText.textContent = 'Deploying...';
858
+
859
+ try {
860
+ await deployToConvex(key);
861
+ log('Watch: auto-deployment complete', 'success');
862
+ } catch (error) {
863
+ log(`Watch: auto-deployment failed: ${error}`, 'error');
864
+ } finally {
865
+ isDeploying = false;
866
+
867
+ // Restore UI
868
+ deployBtn.classList.remove('auto-deploying');
869
+ deployBtn.textContent = 'Re-deploy';
870
+ watchModeLabel.classList.remove('watching');
871
+ watchModeText.textContent = 'Watching';
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Stop watching for file changes
877
+ */
878
+ function stopConvexWatcher(): void {
879
+ if (convexWatcher) {
880
+ convexWatcher.close();
881
+ convexWatcher = null;
882
+ log('Watch mode disabled', 'info');
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Update watch mode UI state
888
+ */
889
+ function updateWatchModeUI(): void {
890
+ if (watchModeEnabled) {
891
+ watchModeLabel.classList.add('active');
892
+ watchModeText.textContent = 'Watching';
893
+ } else {
894
+ watchModeLabel.classList.remove('active');
895
+ watchModeLabel.classList.remove('watching');
896
+ watchModeText.textContent = 'Watch';
897
+ }
898
+ }
899
+
900
+ async function main() {
901
+ try {
902
+ setStatus('Creating virtual file system...', 'loading');
903
+ log('Creating virtual file system...');
904
+ vfs = new VirtualFS();
905
+
906
+ setStatus('Setting up project...', 'loading');
907
+ log('Creating Convex App project structure...');
908
+ createConvexAppProject(vfs);
909
+ log('Project files created', 'success');
910
+
911
+ // Expose VFS to window and build file tree
912
+ exposeVfsToWindow();
913
+ buildFileTree();
914
+ log('File editor ready', 'success');
915
+
916
+ setStatus('Initializing runtime...', 'loading');
917
+ log('Initializing runtime...');
918
+ const runtime = new Runtime(vfs, {
919
+ cwd: '/',
920
+ env: { NODE_ENV: 'development' },
921
+ onConsole: (method, args) => {
922
+ const msg = args.map(a => String(a)).join(' ');
923
+ if (method === 'error') log(msg, 'error');
924
+ else if (method === 'warn') log(msg, 'warn');
925
+ else log(msg);
926
+ },
927
+ });
928
+
929
+ setStatus('Starting dev server...', 'loading');
930
+ log('Starting Next.js dev server...');
931
+
932
+ const port = 3002;
933
+ devServer = new NextDevServer(vfs, {
934
+ port,
935
+ root: '/',
936
+ preferAppRouter: true,
937
+ });
938
+ const server = devServer;
939
+
940
+ const bridge = getServerBridge();
941
+
942
+ try {
943
+ log('Initializing Service Worker...');
944
+ await bridge.initServiceWorker();
945
+ log('Service Worker ready', 'success');
946
+ } catch (error) {
947
+ log(`Service Worker warning: ${error}`, 'warn');
948
+ }
949
+
950
+ // Create HTTP server wrapper
951
+ const httpServer = {
952
+ listening: true,
953
+ address: () => ({ port, address: '0.0.0.0', family: 'IPv4' }),
954
+ async handleRequest(
955
+ method: string,
956
+ url: string,
957
+ headers: Record<string, string>,
958
+ body?: string | Buffer
959
+ ) {
960
+ const bodyBuffer = body
961
+ ? typeof body === 'string' ? Buffer.from(body) : body
962
+ : undefined;
963
+ return server.handleRequest(method, url, headers, bodyBuffer);
964
+ },
965
+ };
966
+
967
+ bridge.registerServer(httpServer as any, port);
968
+ server.start();
969
+
970
+ serverUrl = bridge.getServerUrl(port) + '/';
971
+ log(`Server running at: ${serverUrl}`, 'success');
972
+
973
+ setStatus('Running', 'running');
974
+
975
+ // Show iframe
976
+ previewContainer.innerHTML = '';
977
+ iframe = document.createElement('iframe');
978
+ iframe.src = serverUrl;
979
+ iframe.id = 'preview-iframe';
980
+ iframe.name = 'preview-iframe';
981
+ // Sandbox the iframe for security - postMessage-based HMR works with sandboxed iframes
982
+ iframe.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin allow-popups allow-pointer-lock allow-modals allow-downloads allow-orientation-lock allow-presentation allow-popups-to-escape-sandbox');
983
+
984
+ // Set up onload handler to inject Convex URL into iframe's window
985
+ // and register the iframe as HMR target
986
+ iframe.onload = () => {
987
+ if (iframe?.contentWindow) {
988
+ // Register iframe as HMR target (for postMessage-based HMR)
989
+ if (devServer) {
990
+ devServer.setHMRTarget(iframe.contentWindow);
991
+ }
992
+ // Inject Convex URL if available
993
+ if (convexUrl) {
994
+ (iframe.contentWindow as any).__CONVEX_URL__ = convexUrl;
995
+ log(`Injected Convex URL into iframe: ${convexUrl}`);
996
+ }
997
+ }
998
+ };
999
+
1000
+ previewContainer.appendChild(iframe);
1001
+
1002
+ // Enable buttons
1003
+ refreshBtn.disabled = false;
1004
+ openBtn.disabled = false;
1005
+ deployBtn.disabled = false;
1006
+
1007
+ refreshBtn.onclick = () => {
1008
+ if (iframe) {
1009
+ log('Refreshing preview...');
1010
+ iframe.src = iframe.src;
1011
+ }
1012
+ };
1013
+
1014
+ openBtn.onclick = () => {
1015
+ if (serverUrl) {
1016
+ window.open(serverUrl, '_blank');
1017
+ }
1018
+ };
1019
+
1020
+ saveBtn.onclick = () => {
1021
+ saveFile();
1022
+ };
1023
+
1024
+ deployBtn.onclick = async () => {
1025
+ const key = convexKeyInput.value.trim();
1026
+ if (!key) {
1027
+ logStatus('ERROR', 'Please enter a Convex deploy key');
1028
+ return;
1029
+ }
1030
+
1031
+ const isRedeployment = deployBtn.classList.contains('success');
1032
+ deployBtn.disabled = true;
1033
+ deployBtn.textContent = isRedeployment ? 'Re-deploying...' : 'Deploying...';
1034
+ // Remove success class during deployment
1035
+ deployBtn.classList.remove('success');
1036
+ isDeploying = true;
1037
+
1038
+ try {
1039
+ await deployToConvex(key);
1040
+
1041
+ // Show connected status
1042
+ const parsed = parseConvexKey(key);
1043
+ if (parsed && convexStatusEl && convexStatusText) {
1044
+ convexStatusText.textContent = parsed.deploymentName;
1045
+ convexStatusEl.style.display = 'inline-flex';
1046
+ }
1047
+
1048
+ // Update input to show connected state
1049
+ convexKeyInput.classList.add('connected');
1050
+
1051
+ // Change button to "Re-deploy" for subsequent deployments
1052
+ deployBtn.textContent = 'Re-deploy';
1053
+ deployBtn.classList.add('success');
1054
+ deployBtn.disabled = false;
1055
+
1056
+ // Enable watch mode checkbox after first successful deployment
1057
+ watchModeCheckbox.disabled = false;
1058
+
1059
+ // Set up watcher if watch mode is enabled
1060
+ if (watchModeEnabled && !convexWatcher) {
1061
+ setupConvexWatcher();
1062
+ }
1063
+
1064
+ log('Convex connected! The app will now use real-time data.', 'success');
1065
+ if (!watchModeEnabled) {
1066
+ log('Edit /convex files and click "Re-deploy" to push changes.', 'info');
1067
+ log('Or enable "Watch" mode for automatic re-deployment.', 'info');
1068
+ }
1069
+ } catch (error) {
1070
+ logStatus('ERROR', `Deployment failed: ${error}`);
1071
+ // Keep "Re-deploy" text if already connected, otherwise show "Deploy"
1072
+ deployBtn.textContent = convexStatusEl?.style.display === 'inline-flex' ? 'Re-deploy' : 'Deploy';
1073
+ deployBtn.disabled = false;
1074
+ } finally {
1075
+ isDeploying = false;
1076
+ }
1077
+ };
1078
+
1079
+ // Watch mode checkbox handler
1080
+ watchModeCheckbox.onchange = () => {
1081
+ watchModeEnabled = watchModeCheckbox.checked;
1082
+ updateWatchModeUI();
1083
+
1084
+ if (watchModeEnabled) {
1085
+ // Only set up watcher if we've already deployed (have a key)
1086
+ if (convexKeyInput.value.trim() && deployBtn.classList.contains('success')) {
1087
+ setupConvexWatcher();
1088
+ }
1089
+ } else {
1090
+ stopConvexWatcher();
1091
+ }
1092
+ };
1093
+
1094
+ log('Demo ready!', 'success');
1095
+ log('Edit files on the left, preview updates via HMR.');
1096
+ log('Enter Convex deploy key and click Deploy to connect.');
1097
+
1098
+ } catch (error) {
1099
+ const errorMessage = error instanceof Error ? error.message : String(error);
1100
+ log(`Error: ${errorMessage}`, 'error');
1101
+ console.error(error);
1102
+ setStatus('Error', 'error');
1103
+ }
1104
+ }
1105
+
1106
+ // Start the demo
1107
+ main();