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,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();
|