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,1316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realistic Next.js + Convex App Demo
|
|
3
|
+
*
|
|
4
|
+
* This demo creates a more realistic Next.js application structure
|
|
5
|
+
* with Radix UI components, Tailwind CSS, and a mocked Convex backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { VirtualFS } from './virtual-fs';
|
|
9
|
+
import { Runtime } from './runtime';
|
|
10
|
+
import { NextDevServer } from './frameworks/next-dev-server';
|
|
11
|
+
import { getServerBridge } from './server-bridge';
|
|
12
|
+
import { Buffer } from './shims/stream';
|
|
13
|
+
import { PackageManager, InstallOptions, InstallResult } from './npm';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Package.json for a realistic Next.js + Convex app
|
|
17
|
+
*/
|
|
18
|
+
const PACKAGE_JSON = {
|
|
19
|
+
name: "convex-app-demo",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
private: true,
|
|
22
|
+
scripts: {
|
|
23
|
+
dev: "next dev",
|
|
24
|
+
build: "next build",
|
|
25
|
+
start: "next start",
|
|
26
|
+
},
|
|
27
|
+
dependencies: {
|
|
28
|
+
// Core
|
|
29
|
+
"next": "^14.0.0",
|
|
30
|
+
"react": "^18.2.0",
|
|
31
|
+
"react-dom": "^18.2.0",
|
|
32
|
+
// UI
|
|
33
|
+
"clsx": "^2.1.1",
|
|
34
|
+
"tailwind-merge": "^3.1.0",
|
|
35
|
+
"lucide-react": "^0.400.0",
|
|
36
|
+
// Forms
|
|
37
|
+
"zod": "^3.24.2",
|
|
38
|
+
// Date
|
|
39
|
+
"date-fns": "^3.6.0",
|
|
40
|
+
},
|
|
41
|
+
devDependencies: {
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"@types/react": "^19",
|
|
44
|
+
"@types/react-dom": "^19",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Minimal packages to install for demo (others loaded from CDN)
|
|
51
|
+
*/
|
|
52
|
+
const DEMO_PACKAGES = [
|
|
53
|
+
'clsx',
|
|
54
|
+
'tailwind-merge',
|
|
55
|
+
'zod',
|
|
56
|
+
'date-fns',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create the project structure in the virtual filesystem
|
|
61
|
+
*/
|
|
62
|
+
export function createConvexAppProject(vfs: VirtualFS): void {
|
|
63
|
+
// Create package.json
|
|
64
|
+
vfs.writeFileSync('/package.json', JSON.stringify(PACKAGE_JSON, null, 2));
|
|
65
|
+
|
|
66
|
+
// Create directories - App Router structure
|
|
67
|
+
vfs.mkdirSync('/app', { recursive: true });
|
|
68
|
+
vfs.mkdirSync('/app/api', { recursive: true });
|
|
69
|
+
vfs.mkdirSync('/app/tasks', { recursive: true });
|
|
70
|
+
vfs.mkdirSync('/components', { recursive: true });
|
|
71
|
+
vfs.mkdirSync('/components/ui', { recursive: true });
|
|
72
|
+
vfs.mkdirSync('/lib', { recursive: true });
|
|
73
|
+
vfs.mkdirSync('/convex', { recursive: true });
|
|
74
|
+
vfs.mkdirSync('/public', { recursive: true });
|
|
75
|
+
|
|
76
|
+
// Create convex.json configuration (required by Convex CLI)
|
|
77
|
+
vfs.writeFileSync('/convex.json', JSON.stringify({
|
|
78
|
+
functions: "convex/"
|
|
79
|
+
}, null, 2));
|
|
80
|
+
|
|
81
|
+
// Create TypeScript config
|
|
82
|
+
vfs.writeFileSync('/tsconfig.json', JSON.stringify({
|
|
83
|
+
compilerOptions: {
|
|
84
|
+
target: "es5",
|
|
85
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
86
|
+
allowJs: true,
|
|
87
|
+
skipLibCheck: true,
|
|
88
|
+
strict: true,
|
|
89
|
+
noEmit: true,
|
|
90
|
+
esModuleInterop: true,
|
|
91
|
+
module: "esnext",
|
|
92
|
+
moduleResolution: "bundler",
|
|
93
|
+
resolveJsonModule: true,
|
|
94
|
+
isolatedModules: true,
|
|
95
|
+
jsx: "preserve",
|
|
96
|
+
incremental: true,
|
|
97
|
+
paths: {
|
|
98
|
+
"@/*": ["./*"]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
102
|
+
exclude: ["node_modules"]
|
|
103
|
+
}, null, 2));
|
|
104
|
+
|
|
105
|
+
// Create Tailwind config
|
|
106
|
+
vfs.writeFileSync('/tailwind.config.js', `/** @type {import('tailwindcss').Config} */
|
|
107
|
+
module.exports = {
|
|
108
|
+
darkMode: ["class"],
|
|
109
|
+
content: [
|
|
110
|
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
111
|
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
112
|
+
],
|
|
113
|
+
theme: {
|
|
114
|
+
extend: {
|
|
115
|
+
colors: {
|
|
116
|
+
border: "hsl(var(--border))",
|
|
117
|
+
background: "hsl(var(--background))",
|
|
118
|
+
foreground: "hsl(var(--foreground))",
|
|
119
|
+
primary: {
|
|
120
|
+
DEFAULT: "hsl(var(--primary))",
|
|
121
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
122
|
+
},
|
|
123
|
+
secondary: {
|
|
124
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
125
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
126
|
+
},
|
|
127
|
+
muted: {
|
|
128
|
+
DEFAULT: "hsl(var(--muted))",
|
|
129
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
130
|
+
},
|
|
131
|
+
accent: {
|
|
132
|
+
DEFAULT: "hsl(var(--accent))",
|
|
133
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
134
|
+
},
|
|
135
|
+
destructive: {
|
|
136
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
137
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
borderRadius: {
|
|
141
|
+
lg: "var(--radius)",
|
|
142
|
+
md: "calc(var(--radius) - 2px)",
|
|
143
|
+
sm: "calc(var(--radius) - 4px)",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
plugins: [],
|
|
148
|
+
}
|
|
149
|
+
`);
|
|
150
|
+
|
|
151
|
+
// Create global CSS with Tailwind and shadcn/ui CSS variables
|
|
152
|
+
vfs.writeFileSync('/app/globals.css', `@tailwind base;
|
|
153
|
+
@tailwind components;
|
|
154
|
+
@tailwind utilities;
|
|
155
|
+
|
|
156
|
+
@layer base {
|
|
157
|
+
:root {
|
|
158
|
+
--background: 0 0% 100%;
|
|
159
|
+
--foreground: 222.2 84% 4.9%;
|
|
160
|
+
--card: 0 0% 100%;
|
|
161
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
162
|
+
--popover: 0 0% 100%;
|
|
163
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
164
|
+
--primary: 222.2 47.4% 11.2%;
|
|
165
|
+
--primary-foreground: 210 40% 98%;
|
|
166
|
+
--secondary: 210 40% 96.1%;
|
|
167
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
168
|
+
--muted: 210 40% 96.1%;
|
|
169
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
170
|
+
--accent: 210 40% 96.1%;
|
|
171
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
172
|
+
--destructive: 0 84.2% 60.2%;
|
|
173
|
+
--destructive-foreground: 210 40% 98%;
|
|
174
|
+
--border: 214.3 31.8% 91.4%;
|
|
175
|
+
--input: 214.3 31.8% 91.4%;
|
|
176
|
+
--ring: 222.2 84% 4.9%;
|
|
177
|
+
--radius: 0.5rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.dark {
|
|
181
|
+
--background: 222.2 84% 4.9%;
|
|
182
|
+
--foreground: 210 40% 98%;
|
|
183
|
+
--card: 222.2 84% 4.9%;
|
|
184
|
+
--card-foreground: 210 40% 98%;
|
|
185
|
+
--popover: 222.2 84% 4.9%;
|
|
186
|
+
--popover-foreground: 210 40% 98%;
|
|
187
|
+
--primary: 210 40% 98%;
|
|
188
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
189
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
190
|
+
--secondary-foreground: 210 40% 98%;
|
|
191
|
+
--muted: 217.2 32.6% 17.5%;
|
|
192
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
193
|
+
--accent: 217.2 32.6% 17.5%;
|
|
194
|
+
--accent-foreground: 210 40% 98%;
|
|
195
|
+
--destructive: 0 62.8% 30.6%;
|
|
196
|
+
--destructive-foreground: 210 40% 98%;
|
|
197
|
+
--border: 217.2 32.6% 17.5%;
|
|
198
|
+
--input: 217.2 32.6% 17.5%;
|
|
199
|
+
--ring: 212.7 26.8% 83.9%;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@layer base {
|
|
204
|
+
* {
|
|
205
|
+
@apply border-border;
|
|
206
|
+
}
|
|
207
|
+
body {
|
|
208
|
+
@apply bg-background text-foreground;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
// Create utility lib (cn function from shadcn/ui)
|
|
214
|
+
vfs.writeFileSync('/lib/utils.ts', `// Utility functions
|
|
215
|
+
// Note: In production, use clsx and tailwind-merge packages
|
|
216
|
+
|
|
217
|
+
export function cn(...inputs: (string | undefined | null | false)[]) {
|
|
218
|
+
return inputs.filter(Boolean).join(' ');
|
|
219
|
+
}
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
// Create Convex config (required by CLI bundler)
|
|
223
|
+
// IMPORTANT: CLI needs BOTH .ts and .js versions!
|
|
224
|
+
vfs.writeFileSync('/convex/convex.config.ts', `import { defineApp } from "convex/server";
|
|
225
|
+
|
|
226
|
+
const app = defineApp();
|
|
227
|
+
export default app;
|
|
228
|
+
`);
|
|
229
|
+
vfs.writeFileSync('/convex/convex.config.js', `import { defineApp } from "convex/server";
|
|
230
|
+
|
|
231
|
+
const app = defineApp();
|
|
232
|
+
export default app;
|
|
233
|
+
`);
|
|
234
|
+
|
|
235
|
+
// Create Convex schema
|
|
236
|
+
vfs.writeFileSync('/convex/schema.ts', `import { defineSchema, defineTable } from "convex/server";
|
|
237
|
+
import { v } from "convex/values";
|
|
238
|
+
|
|
239
|
+
export default defineSchema({
|
|
240
|
+
todos: defineTable({
|
|
241
|
+
title: v.string(),
|
|
242
|
+
completed: v.boolean(),
|
|
243
|
+
priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
`);
|
|
247
|
+
|
|
248
|
+
// Create Convex functions for todos
|
|
249
|
+
vfs.writeFileSync('/convex/todos.ts', `import { query, mutation } from "./_generated/server";
|
|
250
|
+
import { v } from "convex/values";
|
|
251
|
+
|
|
252
|
+
export const list = query({
|
|
253
|
+
args: {},
|
|
254
|
+
handler: async (ctx) => {
|
|
255
|
+
return await ctx.db.query("todos").order("desc").collect();
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export const create = mutation({
|
|
260
|
+
args: {
|
|
261
|
+
title: v.string(),
|
|
262
|
+
priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
|
|
263
|
+
},
|
|
264
|
+
handler: async (ctx, args) => {
|
|
265
|
+
return await ctx.db.insert("todos", {
|
|
266
|
+
title: args.title,
|
|
267
|
+
completed: false,
|
|
268
|
+
priority: args.priority,
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export const toggle = mutation({
|
|
274
|
+
args: { id: v.id("todos") },
|
|
275
|
+
handler: async (ctx, args) => {
|
|
276
|
+
const todo = await ctx.db.get(args.id);
|
|
277
|
+
if (!todo) throw new Error("Todo not found");
|
|
278
|
+
await ctx.db.patch(args.id, { completed: !todo.completed });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export const remove = mutation({
|
|
283
|
+
args: { id: v.id("todos") },
|
|
284
|
+
handler: async (ctx, args) => {
|
|
285
|
+
await ctx.db.delete(args.id);
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
`);
|
|
289
|
+
|
|
290
|
+
// Create Convex API (normally auto-generated, but we create manually for the demo)
|
|
291
|
+
// This creates function references that Convex's useQuery/useMutation understand
|
|
292
|
+
vfs.writeFileSync('/convex/_generated/api.ts', `// Convex API - manually created for browser demo
|
|
293
|
+
// In a real project, this is auto-generated by 'npx convex dev'
|
|
294
|
+
|
|
295
|
+
// Function references for the Convex client
|
|
296
|
+
// These are string identifiers that map to server functions
|
|
297
|
+
export const api = {
|
|
298
|
+
todos: {
|
|
299
|
+
list: "todos:list",
|
|
300
|
+
create: "todos:create",
|
|
301
|
+
toggle: "todos:toggle",
|
|
302
|
+
remove: "todos:remove",
|
|
303
|
+
},
|
|
304
|
+
} as const;
|
|
305
|
+
`);
|
|
306
|
+
|
|
307
|
+
// Create server stubs (needed for schema/function imports to work)
|
|
308
|
+
vfs.writeFileSync('/convex/_generated/server.ts', `// Server stubs for browser demo
|
|
309
|
+
// In a real project, this is auto-generated by Convex
|
|
310
|
+
|
|
311
|
+
export function query<Args, Output>(config: {
|
|
312
|
+
args: Args;
|
|
313
|
+
handler: (ctx: any, args: any) => Promise<Output>;
|
|
314
|
+
}) {
|
|
315
|
+
return config;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function mutation<Args, Output>(config: {
|
|
319
|
+
args: Args;
|
|
320
|
+
handler: (ctx: any, args: any) => Promise<Output>;
|
|
321
|
+
}) {
|
|
322
|
+
return config;
|
|
323
|
+
}
|
|
324
|
+
`);
|
|
325
|
+
|
|
326
|
+
// Create Convex provider using real Convex client from CDN
|
|
327
|
+
vfs.writeFileSync('/lib/convex.tsx', `"use client";
|
|
328
|
+
|
|
329
|
+
import React, { useState, useEffect } from 'react';
|
|
330
|
+
import { ConvexProvider as BaseConvexProvider, ConvexReactClient, useQuery as useConvexQuery, useMutation as useConvexMutation } from 'convex/react';
|
|
331
|
+
|
|
332
|
+
// Re-export the API
|
|
333
|
+
export { api } from '../convex/_generated/api.ts';
|
|
334
|
+
|
|
335
|
+
// Get Convex URL using standard Next.js env var pattern
|
|
336
|
+
// Falls back to window.__CONVEX_URL__ for backwards compatibility
|
|
337
|
+
const getConvexUrl = () => {
|
|
338
|
+
// Standard Next.js pattern: process.env.NEXT_PUBLIC_*
|
|
339
|
+
if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_CONVEX_URL) {
|
|
340
|
+
return process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
341
|
+
}
|
|
342
|
+
// Fallback for backwards compatibility
|
|
343
|
+
if (typeof window !== 'undefined' && (window as any).__CONVEX_URL__) {
|
|
344
|
+
return (window as any).__CONVEX_URL__;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Create client lazily
|
|
350
|
+
let client: ConvexReactClient | null = null;
|
|
351
|
+
|
|
352
|
+
function getClient() {
|
|
353
|
+
const url = getConvexUrl();
|
|
354
|
+
if (!url) return null;
|
|
355
|
+
if (!client || (client as any)._address !== url) {
|
|
356
|
+
client = new ConvexReactClient(url);
|
|
357
|
+
}
|
|
358
|
+
return client;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Wrapper hooks that handle the case when Convex is not connected
|
|
362
|
+
export function useQuery(query: any, ...args: any[]) {
|
|
363
|
+
const url = getConvexUrl();
|
|
364
|
+
// When not connected, return undefined
|
|
365
|
+
if (!url) return undefined;
|
|
366
|
+
return useConvexQuery(query, ...args);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function useMutation(mutation: any) {
|
|
370
|
+
const url = getConvexUrl();
|
|
371
|
+
const convexMutation = url ? useConvexMutation(mutation) : null;
|
|
372
|
+
|
|
373
|
+
return async (args: any) => {
|
|
374
|
+
if (!convexMutation) {
|
|
375
|
+
console.warn('Convex not connected - mutation ignored');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
return convexMutation(args);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function ConvexProvider({ children }: { children: React.ReactNode }) {
|
|
383
|
+
const [convexUrl, setConvexUrl] = useState(getConvexUrl());
|
|
384
|
+
|
|
385
|
+
// Check for URL changes (after deploy)
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
const checkUrl = () => {
|
|
388
|
+
const url = getConvexUrl();
|
|
389
|
+
if (url !== convexUrl) {
|
|
390
|
+
setConvexUrl(url);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Check periodically for URL changes
|
|
395
|
+
const interval = setInterval(checkUrl, 1000);
|
|
396
|
+
return () => clearInterval(interval);
|
|
397
|
+
}, [convexUrl]);
|
|
398
|
+
|
|
399
|
+
const convexClient = getClient();
|
|
400
|
+
|
|
401
|
+
if (!convexClient) {
|
|
402
|
+
// Show a message when Convex is not configured
|
|
403
|
+
return (
|
|
404
|
+
<div className="min-h-screen bg-background font-sans antialiased">
|
|
405
|
+
<div className="flex flex-col items-center justify-center min-h-screen p-8 text-center">
|
|
406
|
+
<div className="max-w-md space-y-4">
|
|
407
|
+
<h2 className="text-2xl font-bold">Connect to Convex</h2>
|
|
408
|
+
<p className="text-muted-foreground">
|
|
409
|
+
Enter your Convex deploy key in the console panel and click "Deploy Schema" to connect.
|
|
410
|
+
</p>
|
|
411
|
+
<div className="p-4 bg-muted rounded-lg text-left text-sm">
|
|
412
|
+
<p className="font-medium mb-2">Files ready in /convex/:</p>
|
|
413
|
+
<ul className="space-y-1 text-muted-foreground">
|
|
414
|
+
<li>schema.ts - Database schema (todos table)</li>
|
|
415
|
+
<li>todos.ts - Query and mutation functions</li>
|
|
416
|
+
</ul>
|
|
417
|
+
</div>
|
|
418
|
+
<p className="text-xs text-muted-foreground">
|
|
419
|
+
Get a deploy key from your Convex dashboard at convex.dev
|
|
420
|
+
</p>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<BaseConvexProvider client={convexClient}>
|
|
429
|
+
{children}
|
|
430
|
+
</BaseConvexProvider>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
`);
|
|
434
|
+
|
|
435
|
+
// Create Button component (shadcn/ui style)
|
|
436
|
+
vfs.writeFileSync('/components/ui/button.tsx', `import React from 'react';
|
|
437
|
+
import { cn } from '../../lib/utils.ts';
|
|
438
|
+
|
|
439
|
+
const buttonVariants = {
|
|
440
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
441
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
442
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
443
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
444
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
445
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const buttonSizes = {
|
|
449
|
+
default: "h-10 px-4 py-2",
|
|
450
|
+
sm: "h-9 rounded-md px-3",
|
|
451
|
+
lg: "h-11 rounded-md px-8",
|
|
452
|
+
icon: "h-10 w-10",
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
456
|
+
variant?: keyof typeof buttonVariants;
|
|
457
|
+
size?: keyof typeof buttonSizes;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function Button({
|
|
461
|
+
className,
|
|
462
|
+
variant = "default",
|
|
463
|
+
size = "default",
|
|
464
|
+
...props
|
|
465
|
+
}: ButtonProps) {
|
|
466
|
+
return (
|
|
467
|
+
<button
|
|
468
|
+
className={cn(
|
|
469
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
470
|
+
buttonVariants[variant],
|
|
471
|
+
buttonSizes[size],
|
|
472
|
+
className
|
|
473
|
+
)}
|
|
474
|
+
{...props}
|
|
475
|
+
/>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
`);
|
|
479
|
+
|
|
480
|
+
// Create Card component
|
|
481
|
+
vfs.writeFileSync('/components/ui/card.tsx', `import React from 'react';
|
|
482
|
+
import { cn } from '../../lib/utils.ts';
|
|
483
|
+
|
|
484
|
+
export function Card({
|
|
485
|
+
className,
|
|
486
|
+
...props
|
|
487
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
488
|
+
return (
|
|
489
|
+
<div
|
|
490
|
+
className={cn(
|
|
491
|
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
492
|
+
className
|
|
493
|
+
)}
|
|
494
|
+
{...props}
|
|
495
|
+
/>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function CardHeader({
|
|
500
|
+
className,
|
|
501
|
+
...props
|
|
502
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
503
|
+
return (
|
|
504
|
+
<div
|
|
505
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
506
|
+
{...props}
|
|
507
|
+
/>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function CardTitle({
|
|
512
|
+
className,
|
|
513
|
+
...props
|
|
514
|
+
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
515
|
+
return (
|
|
516
|
+
<h3
|
|
517
|
+
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
|
518
|
+
{...props}
|
|
519
|
+
/>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function CardDescription({
|
|
524
|
+
className,
|
|
525
|
+
...props
|
|
526
|
+
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
527
|
+
return (
|
|
528
|
+
<p
|
|
529
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
530
|
+
{...props}
|
|
531
|
+
/>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function CardContent({
|
|
536
|
+
className,
|
|
537
|
+
...props
|
|
538
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
539
|
+
return <div className={cn("p-6 pt-0", className)} {...props} />;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function CardFooter({
|
|
543
|
+
className,
|
|
544
|
+
...props
|
|
545
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
546
|
+
return (
|
|
547
|
+
<div
|
|
548
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
549
|
+
{...props}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
`);
|
|
554
|
+
|
|
555
|
+
// Create Input component
|
|
556
|
+
vfs.writeFileSync('/components/ui/input.tsx', `import React from 'react';
|
|
557
|
+
import { cn } from '../../lib/utils.ts';
|
|
558
|
+
|
|
559
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
560
|
+
|
|
561
|
+
export function Input({ className, type, ...props }: InputProps) {
|
|
562
|
+
return (
|
|
563
|
+
<input
|
|
564
|
+
type={type}
|
|
565
|
+
className={cn(
|
|
566
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
567
|
+
className
|
|
568
|
+
)}
|
|
569
|
+
{...props}
|
|
570
|
+
/>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
`);
|
|
574
|
+
|
|
575
|
+
// Create Badge component
|
|
576
|
+
vfs.writeFileSync('/components/ui/badge.tsx', `import React from 'react';
|
|
577
|
+
import { cn } from '../../lib/utils.ts';
|
|
578
|
+
|
|
579
|
+
const badgeVariants = {
|
|
580
|
+
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
581
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
582
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
583
|
+
outline: "text-foreground",
|
|
584
|
+
success: "border-transparent bg-green-500 text-white",
|
|
585
|
+
warning: "border-transparent bg-yellow-500 text-white",
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
589
|
+
variant?: keyof typeof badgeVariants;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
|
593
|
+
return (
|
|
594
|
+
<div
|
|
595
|
+
className={cn(
|
|
596
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
597
|
+
badgeVariants[variant],
|
|
598
|
+
className
|
|
599
|
+
)}
|
|
600
|
+
{...props}
|
|
601
|
+
/>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
`);
|
|
605
|
+
|
|
606
|
+
// Create Checkbox component
|
|
607
|
+
vfs.writeFileSync('/components/ui/checkbox.tsx', `import React from 'react';
|
|
608
|
+
import { cn } from '../../lib/utils.ts';
|
|
609
|
+
|
|
610
|
+
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
611
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function Checkbox({ className, checked, onCheckedChange, ...props }: CheckboxProps) {
|
|
615
|
+
return (
|
|
616
|
+
<input
|
|
617
|
+
type="checkbox"
|
|
618
|
+
checked={checked}
|
|
619
|
+
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
|
620
|
+
className={cn(
|
|
621
|
+
"h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
622
|
+
className
|
|
623
|
+
)}
|
|
624
|
+
{...props}
|
|
625
|
+
/>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
`);
|
|
629
|
+
|
|
630
|
+
// Create Select component (simplified)
|
|
631
|
+
vfs.writeFileSync('/components/ui/select.tsx', `import React from 'react';
|
|
632
|
+
import { cn } from '../../lib/utils.ts';
|
|
633
|
+
|
|
634
|
+
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
|
|
635
|
+
|
|
636
|
+
export function Select({ className, children, ...props }: SelectProps) {
|
|
637
|
+
return (
|
|
638
|
+
<select
|
|
639
|
+
className={cn(
|
|
640
|
+
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
641
|
+
className
|
|
642
|
+
)}
|
|
643
|
+
{...props}
|
|
644
|
+
>
|
|
645
|
+
{children}
|
|
646
|
+
</select>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
`);
|
|
650
|
+
|
|
651
|
+
// Create TaskList component (uses real Convex API)
|
|
652
|
+
vfs.writeFileSync('/components/task-list.tsx', `"use client";
|
|
653
|
+
|
|
654
|
+
import React from 'react';
|
|
655
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card.tsx';
|
|
656
|
+
import { Button } from './ui/button.tsx';
|
|
657
|
+
import { Input } from './ui/input.tsx';
|
|
658
|
+
import { Badge } from './ui/badge.tsx';
|
|
659
|
+
import { Checkbox } from './ui/checkbox.tsx';
|
|
660
|
+
import { Select } from './ui/select.tsx';
|
|
661
|
+
import { useQuery, useMutation, api } from '../lib/convex.tsx';
|
|
662
|
+
import { cn } from '../lib/utils.ts';
|
|
663
|
+
|
|
664
|
+
type Todo = {
|
|
665
|
+
_id: string;
|
|
666
|
+
_creationTime: number;
|
|
667
|
+
title: string;
|
|
668
|
+
completed: boolean;
|
|
669
|
+
priority: "low" | "medium" | "high";
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const priorityColors = {
|
|
673
|
+
low: "success" as const,
|
|
674
|
+
medium: "warning" as const,
|
|
675
|
+
high: "destructive" as const,
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
function TaskItem({
|
|
679
|
+
task,
|
|
680
|
+
onToggle,
|
|
681
|
+
onDelete
|
|
682
|
+
}: {
|
|
683
|
+
task: Todo;
|
|
684
|
+
onToggle: () => void;
|
|
685
|
+
onDelete: () => void;
|
|
686
|
+
}) {
|
|
687
|
+
return (
|
|
688
|
+
<div className={cn(
|
|
689
|
+
"flex items-center gap-4 p-4 border rounded-lg transition-all",
|
|
690
|
+
task.completed && "opacity-50 bg-muted"
|
|
691
|
+
)}>
|
|
692
|
+
<Checkbox
|
|
693
|
+
checked={task.completed}
|
|
694
|
+
onCheckedChange={onToggle}
|
|
695
|
+
/>
|
|
696
|
+
<div className="flex-1 min-w-0">
|
|
697
|
+
<p className={cn(
|
|
698
|
+
"font-medium truncate",
|
|
699
|
+
task.completed && "line-through text-muted-foreground"
|
|
700
|
+
)}>
|
|
701
|
+
{task.title}
|
|
702
|
+
</p>
|
|
703
|
+
<p className="text-xs text-muted-foreground">
|
|
704
|
+
Created {new Date(task._creationTime).toLocaleDateString()}
|
|
705
|
+
</p>
|
|
706
|
+
</div>
|
|
707
|
+
<Badge variant={priorityColors[task.priority]}>
|
|
708
|
+
{task.priority}
|
|
709
|
+
</Badge>
|
|
710
|
+
<Button
|
|
711
|
+
variant="ghost"
|
|
712
|
+
size="sm"
|
|
713
|
+
onClick={onDelete}
|
|
714
|
+
className="text-destructive hover:text-destructive"
|
|
715
|
+
>
|
|
716
|
+
Delete
|
|
717
|
+
</Button>
|
|
718
|
+
</div>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function TaskList() {
|
|
723
|
+
const todos = useQuery(api.todos.list) as Todo[] | undefined;
|
|
724
|
+
const createTodo = useMutation(api.todos.create);
|
|
725
|
+
const toggleTodo = useMutation(api.todos.toggle);
|
|
726
|
+
const removeTodo = useMutation(api.todos.remove);
|
|
727
|
+
|
|
728
|
+
const [newTitle, setNewTitle] = React.useState("");
|
|
729
|
+
const [priority, setPriority] = React.useState<Todo["priority"]>("medium");
|
|
730
|
+
|
|
731
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
732
|
+
e.preventDefault();
|
|
733
|
+
if (!newTitle.trim()) return;
|
|
734
|
+
|
|
735
|
+
await createTodo({ title: newTitle.trim(), priority });
|
|
736
|
+
setNewTitle("");
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const completedCount = todos?.filter(t => t.completed).length ?? 0;
|
|
740
|
+
const totalCount = todos?.length ?? 0;
|
|
741
|
+
|
|
742
|
+
return (
|
|
743
|
+
<Card className="w-full max-w-2xl mx-auto">
|
|
744
|
+
<CardHeader>
|
|
745
|
+
<CardTitle className="flex items-center gap-2">
|
|
746
|
+
Task Manager
|
|
747
|
+
<Badge variant="secondary">
|
|
748
|
+
{completedCount}/{totalCount} done
|
|
749
|
+
</Badge>
|
|
750
|
+
</CardTitle>
|
|
751
|
+
<CardDescription>
|
|
752
|
+
Real-time sync powered by Convex - running from the browser!
|
|
753
|
+
</CardDescription>
|
|
754
|
+
</CardHeader>
|
|
755
|
+
<CardContent className="space-y-4">
|
|
756
|
+
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
757
|
+
<Input
|
|
758
|
+
placeholder="Add a new task..."
|
|
759
|
+
value={newTitle}
|
|
760
|
+
onChange={(e) => setNewTitle(e.target.value)}
|
|
761
|
+
className="flex-1"
|
|
762
|
+
/>
|
|
763
|
+
<Select
|
|
764
|
+
value={priority}
|
|
765
|
+
onChange={(e) => setPriority(e.target.value as Todo["priority"])}
|
|
766
|
+
className="w-32"
|
|
767
|
+
>
|
|
768
|
+
<option value="low">Low</option>
|
|
769
|
+
<option value="medium">Medium</option>
|
|
770
|
+
<option value="high">High</option>
|
|
771
|
+
</Select>
|
|
772
|
+
<Button type="submit">Add Task</Button>
|
|
773
|
+
</form>
|
|
774
|
+
|
|
775
|
+
<div className="space-y-2">
|
|
776
|
+
{todos === undefined ? (
|
|
777
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
778
|
+
Loading tasks...
|
|
779
|
+
</div>
|
|
780
|
+
) : todos.length === 0 ? (
|
|
781
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
782
|
+
No tasks yet. Add one above!
|
|
783
|
+
</div>
|
|
784
|
+
) : (
|
|
785
|
+
todos.map((task) => (
|
|
786
|
+
<TaskItem
|
|
787
|
+
key={task._id}
|
|
788
|
+
task={task}
|
|
789
|
+
onToggle={() => toggleTodo({ id: task._id })}
|
|
790
|
+
onDelete={() => removeTodo({ id: task._id })}
|
|
791
|
+
/>
|
|
792
|
+
))
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
</CardContent>
|
|
796
|
+
</Card>
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
`);
|
|
800
|
+
|
|
801
|
+
// Create root layout (App Router)
|
|
802
|
+
// Note: In browser environment, we don't use <html>/<head>/<body> tags
|
|
803
|
+
// since we're rendering inside an existing HTML document's #__next div
|
|
804
|
+
vfs.writeFileSync('/app/layout.tsx', `import React from 'react';
|
|
805
|
+
import './globals.css';
|
|
806
|
+
import { ConvexProvider } from '../lib/convex.tsx';
|
|
807
|
+
|
|
808
|
+
export const metadata = {
|
|
809
|
+
title: 'Convex App Demo',
|
|
810
|
+
description: 'A realistic Next.js + Convex app running in the browser',
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
export default function RootLayout({
|
|
814
|
+
children,
|
|
815
|
+
}: {
|
|
816
|
+
children: React.ReactNode;
|
|
817
|
+
}) {
|
|
818
|
+
return (
|
|
819
|
+
<ConvexProvider>
|
|
820
|
+
<div className="min-h-screen bg-background font-sans antialiased">
|
|
821
|
+
<div className="relative flex min-h-screen flex-col">
|
|
822
|
+
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
823
|
+
<div className="container flex h-14 items-center">
|
|
824
|
+
<div className="mr-4 flex">
|
|
825
|
+
<a href="/" className="mr-6 flex items-center space-x-2">
|
|
826
|
+
<span className="font-bold text-xl">TaskApp</span>
|
|
827
|
+
</a>
|
|
828
|
+
<nav className="flex items-center space-x-6 text-sm font-medium">
|
|
829
|
+
<a href="/" className="transition-colors hover:text-foreground/80 text-foreground">
|
|
830
|
+
Home
|
|
831
|
+
</a>
|
|
832
|
+
<a href="/tasks" className="transition-colors hover:text-foreground/80 text-muted-foreground">
|
|
833
|
+
Tasks
|
|
834
|
+
</a>
|
|
835
|
+
<a href="/about" className="transition-colors hover:text-foreground/80 text-muted-foreground">
|
|
836
|
+
About
|
|
837
|
+
</a>
|
|
838
|
+
</nav>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
</header>
|
|
842
|
+
<main className="flex-1">
|
|
843
|
+
{children}
|
|
844
|
+
</main>
|
|
845
|
+
<footer className="border-t py-6 md:py-0">
|
|
846
|
+
<div className="container flex flex-col items-center justify-between gap-4 md:h-14 md:flex-row">
|
|
847
|
+
<p className="text-center text-sm leading-loose text-muted-foreground">
|
|
848
|
+
Running in browser with virtual Node.js
|
|
849
|
+
</p>
|
|
850
|
+
</div>
|
|
851
|
+
</footer>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</ConvexProvider>
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
`);
|
|
858
|
+
|
|
859
|
+
// Create home page (App Router) - Shows TaskList directly
|
|
860
|
+
vfs.writeFileSync('/app/page.tsx', `"use client";
|
|
861
|
+
|
|
862
|
+
import React from 'react';
|
|
863
|
+
import { TaskList } from '../components/task-list.tsx';
|
|
864
|
+
|
|
865
|
+
export default function HomePage() {
|
|
866
|
+
return (
|
|
867
|
+
<div className="container py-10">
|
|
868
|
+
<div className="mb-8 text-center">
|
|
869
|
+
<h1 className="text-3xl font-bold tracking-tight">Task Manager</h1>
|
|
870
|
+
<p className="text-muted-foreground mt-2">
|
|
871
|
+
Real-time sync powered by Convex - running in the browser!
|
|
872
|
+
</p>
|
|
873
|
+
</div>
|
|
874
|
+
<TaskList />
|
|
875
|
+
</div>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
`);
|
|
879
|
+
|
|
880
|
+
// Create original home page content as a separate page (for reference)
|
|
881
|
+
vfs.writeFileSync('/app/features/page.tsx', `import React from 'react';
|
|
882
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card.tsx';
|
|
883
|
+
import { Button } from '../../components/ui/button.tsx';
|
|
884
|
+
import { Badge } from '../../components/ui/badge.tsx';
|
|
885
|
+
|
|
886
|
+
export default function FeaturesPage() {
|
|
887
|
+
return (
|
|
888
|
+
<div className="container py-10">
|
|
889
|
+
{/* Feature Cards */}
|
|
890
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
891
|
+
<Card>
|
|
892
|
+
<CardHeader>
|
|
893
|
+
<CardTitle className="flex items-center gap-2">
|
|
894
|
+
⚡ React 18
|
|
895
|
+
</CardTitle>
|
|
896
|
+
<CardDescription>
|
|
897
|
+
Latest React with Concurrent features
|
|
898
|
+
</CardDescription>
|
|
899
|
+
</CardHeader>
|
|
900
|
+
<CardContent>
|
|
901
|
+
<p className="text-sm text-muted-foreground">
|
|
902
|
+
Using React 18 with automatic batching, Suspense,
|
|
903
|
+
and concurrent rendering for optimal performance.
|
|
904
|
+
</p>
|
|
905
|
+
</CardContent>
|
|
906
|
+
</Card>
|
|
907
|
+
|
|
908
|
+
<Card>
|
|
909
|
+
<CardHeader>
|
|
910
|
+
<CardTitle className="flex items-center gap-2">
|
|
911
|
+
🎨 shadcn/ui
|
|
912
|
+
</CardTitle>
|
|
913
|
+
<CardDescription>
|
|
914
|
+
Beautiful, accessible components
|
|
915
|
+
</CardDescription>
|
|
916
|
+
</CardHeader>
|
|
917
|
+
<CardContent>
|
|
918
|
+
<p className="text-sm text-muted-foreground">
|
|
919
|
+
Beautifully designed components built with Radix UI
|
|
920
|
+
primitives and Tailwind CSS.
|
|
921
|
+
</p>
|
|
922
|
+
</CardContent>
|
|
923
|
+
</Card>
|
|
924
|
+
|
|
925
|
+
<Card>
|
|
926
|
+
<CardHeader>
|
|
927
|
+
<CardTitle className="flex items-center gap-2">
|
|
928
|
+
🔄 Convex (Mock)
|
|
929
|
+
</CardTitle>
|
|
930
|
+
<CardDescription>
|
|
931
|
+
Real-time data sync simulation
|
|
932
|
+
</CardDescription>
|
|
933
|
+
</CardHeader>
|
|
934
|
+
<CardContent>
|
|
935
|
+
<p className="text-sm text-muted-foreground">
|
|
936
|
+
Demonstrates the Convex pattern with useQuery and
|
|
937
|
+
useMutation hooks using mock data.
|
|
938
|
+
</p>
|
|
939
|
+
</CardContent>
|
|
940
|
+
</Card>
|
|
941
|
+
|
|
942
|
+
<Card>
|
|
943
|
+
<CardHeader>
|
|
944
|
+
<CardTitle className="flex items-center gap-2">
|
|
945
|
+
🎯 TypeScript
|
|
946
|
+
</CardTitle>
|
|
947
|
+
<CardDescription>
|
|
948
|
+
Full type safety
|
|
949
|
+
</CardDescription>
|
|
950
|
+
</CardHeader>
|
|
951
|
+
<CardContent>
|
|
952
|
+
<p className="text-sm text-muted-foreground">
|
|
953
|
+
Written in TypeScript with strict mode enabled
|
|
954
|
+
for maximum type safety and developer experience.
|
|
955
|
+
</p>
|
|
956
|
+
</CardContent>
|
|
957
|
+
</Card>
|
|
958
|
+
|
|
959
|
+
<Card>
|
|
960
|
+
<CardHeader>
|
|
961
|
+
<CardTitle className="flex items-center gap-2">
|
|
962
|
+
📱 Responsive
|
|
963
|
+
</CardTitle>
|
|
964
|
+
<CardDescription>
|
|
965
|
+
Mobile-first design
|
|
966
|
+
</CardDescription>
|
|
967
|
+
</CardHeader>
|
|
968
|
+
<CardContent>
|
|
969
|
+
<p className="text-sm text-muted-foreground">
|
|
970
|
+
Fully responsive design that works great on any device,
|
|
971
|
+
from mobile phones to desktop monitors.
|
|
972
|
+
</p>
|
|
973
|
+
</CardContent>
|
|
974
|
+
</Card>
|
|
975
|
+
|
|
976
|
+
<Card>
|
|
977
|
+
<CardHeader>
|
|
978
|
+
<CardTitle className="flex items-center gap-2">
|
|
979
|
+
🌐 Browser Runtime
|
|
980
|
+
</CardTitle>
|
|
981
|
+
<CardDescription>
|
|
982
|
+
No server required
|
|
983
|
+
</CardDescription>
|
|
984
|
+
</CardHeader>
|
|
985
|
+
<CardContent>
|
|
986
|
+
<p className="text-sm text-muted-foreground">
|
|
987
|
+
Running entirely in the browser using virtual Node.js
|
|
988
|
+
shims and Service Workers.
|
|
989
|
+
</p>
|
|
990
|
+
</CardContent>
|
|
991
|
+
</Card>
|
|
992
|
+
</div>
|
|
993
|
+
</div>
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
`);
|
|
997
|
+
|
|
998
|
+
// Create features directory
|
|
999
|
+
vfs.mkdirSync('/app/features', { recursive: true });
|
|
1000
|
+
|
|
1001
|
+
// Create tasks page (App Router)
|
|
1002
|
+
vfs.writeFileSync('/app/tasks/page.tsx', `"use client";
|
|
1003
|
+
|
|
1004
|
+
import React from 'react';
|
|
1005
|
+
import { TaskList } from '../../components/task-list.tsx';
|
|
1006
|
+
|
|
1007
|
+
export default function TasksPage() {
|
|
1008
|
+
return (
|
|
1009
|
+
<div className="container py-10">
|
|
1010
|
+
<div className="mb-8 text-center">
|
|
1011
|
+
<h1 className="text-3xl font-bold tracking-tight">Task Manager</h1>
|
|
1012
|
+
<p className="text-muted-foreground mt-2">
|
|
1013
|
+
Add, complete, and manage your tasks
|
|
1014
|
+
</p>
|
|
1015
|
+
</div>
|
|
1016
|
+
<TaskList />
|
|
1017
|
+
</div>
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
`);
|
|
1021
|
+
|
|
1022
|
+
// Create about page (App Router)
|
|
1023
|
+
vfs.writeFileSync('/app/about/page.tsx', `import React from 'react';
|
|
1024
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card.tsx';
|
|
1025
|
+
import { Badge } from '../../components/ui/badge.tsx';
|
|
1026
|
+
|
|
1027
|
+
export default function AboutPage() {
|
|
1028
|
+
return (
|
|
1029
|
+
<div className="container py-10 max-w-3xl">
|
|
1030
|
+
<div className="mb-8">
|
|
1031
|
+
<Badge variant="outline" className="mb-4">About</Badge>
|
|
1032
|
+
<h1 className="text-3xl font-bold tracking-tight">How It Works</h1>
|
|
1033
|
+
<p className="text-muted-foreground mt-2">
|
|
1034
|
+
This demo showcases running a complex Next.js application entirely in the browser.
|
|
1035
|
+
</p>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<div className="space-y-6">
|
|
1039
|
+
<Card>
|
|
1040
|
+
<CardHeader>
|
|
1041
|
+
<CardTitle>Virtual File System</CardTitle>
|
|
1042
|
+
<CardDescription>In-memory file system simulation</CardDescription>
|
|
1043
|
+
</CardHeader>
|
|
1044
|
+
<CardContent className="prose prose-sm">
|
|
1045
|
+
<p>
|
|
1046
|
+
All project files exist in a virtual file system (VFS) in memory.
|
|
1047
|
+
This includes React components, configuration files, and even
|
|
1048
|
+
npm package contents.
|
|
1049
|
+
</p>
|
|
1050
|
+
</CardContent>
|
|
1051
|
+
</Card>
|
|
1052
|
+
|
|
1053
|
+
<Card>
|
|
1054
|
+
<CardHeader>
|
|
1055
|
+
<CardTitle>Node.js Shims</CardTitle>
|
|
1056
|
+
<CardDescription>Browser-compatible Node.js APIs</CardDescription>
|
|
1057
|
+
</CardHeader>
|
|
1058
|
+
<CardContent className="prose prose-sm">
|
|
1059
|
+
<p>
|
|
1060
|
+
Core Node.js modules like <code>fs</code>, <code>path</code>, <code>crypto</code>,
|
|
1061
|
+
<code>stream</code>, and <code>http</code> are shimmed to work in the browser
|
|
1062
|
+
using Web APIs.
|
|
1063
|
+
</p>
|
|
1064
|
+
</CardContent>
|
|
1065
|
+
</Card>
|
|
1066
|
+
|
|
1067
|
+
<Card>
|
|
1068
|
+
<CardHeader>
|
|
1069
|
+
<CardTitle>esbuild-wasm</CardTitle>
|
|
1070
|
+
<CardDescription>Fast JSX/TypeScript compilation</CardDescription>
|
|
1071
|
+
</CardHeader>
|
|
1072
|
+
<CardContent className="prose prose-sm">
|
|
1073
|
+
<p>
|
|
1074
|
+
JSX and TypeScript files are transformed to JavaScript in real-time
|
|
1075
|
+
using esbuild-wasm, which runs WebAssembly in the browser.
|
|
1076
|
+
</p>
|
|
1077
|
+
</CardContent>
|
|
1078
|
+
</Card>
|
|
1079
|
+
|
|
1080
|
+
<Card>
|
|
1081
|
+
<CardHeader>
|
|
1082
|
+
<CardTitle>Service Worker</CardTitle>
|
|
1083
|
+
<CardDescription>Request interception and routing</CardDescription>
|
|
1084
|
+
</CardHeader>
|
|
1085
|
+
<CardContent className="prose prose-sm">
|
|
1086
|
+
<p>
|
|
1087
|
+
A Service Worker intercepts HTTP requests and routes them to the
|
|
1088
|
+
virtual dev server, enabling file-based routing without a real backend.
|
|
1089
|
+
</p>
|
|
1090
|
+
</CardContent>
|
|
1091
|
+
</Card>
|
|
1092
|
+
|
|
1093
|
+
<Card>
|
|
1094
|
+
<CardHeader>
|
|
1095
|
+
<CardTitle>Convex Mock</CardTitle>
|
|
1096
|
+
<CardDescription>Simulated real-time database</CardDescription>
|
|
1097
|
+
</CardHeader>
|
|
1098
|
+
<CardContent className="prose prose-sm">
|
|
1099
|
+
<p>
|
|
1100
|
+
The Convex client is mocked to demonstrate the pattern of using
|
|
1101
|
+
<code>useQuery</code> and <code>useMutation</code> hooks. In production,
|
|
1102
|
+
this would connect to a real Convex backend.
|
|
1103
|
+
</p>
|
|
1104
|
+
</CardContent>
|
|
1105
|
+
</Card>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
`);
|
|
1111
|
+
|
|
1112
|
+
// Create API route
|
|
1113
|
+
vfs.writeFileSync('/pages/api/health.js', `export default function handler(req, res) {
|
|
1114
|
+
res.status(200).json({
|
|
1115
|
+
status: 'ok',
|
|
1116
|
+
timestamp: new Date().toISOString(),
|
|
1117
|
+
runtime: 'browser-node-shim'
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
`);
|
|
1121
|
+
|
|
1122
|
+
// Create public files
|
|
1123
|
+
vfs.writeFileSync('/public/favicon.ico', 'favicon placeholder');
|
|
1124
|
+
vfs.writeFileSync('/public/robots.txt', 'User-agent: *\nAllow: /');
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Initialize the Convex App demo
|
|
1129
|
+
*/
|
|
1130
|
+
export async function initConvexAppDemo(
|
|
1131
|
+
outputElement: HTMLElement,
|
|
1132
|
+
options: {
|
|
1133
|
+
installPackages?: boolean;
|
|
1134
|
+
} = {}
|
|
1135
|
+
): Promise<{ vfs: VirtualFS; runtime: Runtime }> {
|
|
1136
|
+
const log = (message: string) => {
|
|
1137
|
+
const line = document.createElement('div');
|
|
1138
|
+
line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
1139
|
+
outputElement.appendChild(line);
|
|
1140
|
+
outputElement.scrollTop = outputElement.scrollHeight;
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
log('Creating virtual file system...');
|
|
1144
|
+
const vfs = new VirtualFS();
|
|
1145
|
+
|
|
1146
|
+
log('Creating Convex App project structure...');
|
|
1147
|
+
createConvexAppProject(vfs);
|
|
1148
|
+
|
|
1149
|
+
// Optionally install npm packages
|
|
1150
|
+
if (options.installPackages) {
|
|
1151
|
+
log('Installing npm packages (this may take a while)...');
|
|
1152
|
+
const npm = new PackageManager(vfs);
|
|
1153
|
+
|
|
1154
|
+
for (const pkg of DEMO_PACKAGES) {
|
|
1155
|
+
try {
|
|
1156
|
+
log(`Installing ${pkg}...`);
|
|
1157
|
+
await npm.install(pkg, {
|
|
1158
|
+
onProgress: (msg) => log(` ${msg}`),
|
|
1159
|
+
});
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
log(`Warning: Failed to install ${pkg}: ${error}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
log('Initializing runtime...');
|
|
1167
|
+
const runtime = new Runtime(vfs, {
|
|
1168
|
+
cwd: '/',
|
|
1169
|
+
env: {
|
|
1170
|
+
NODE_ENV: 'development',
|
|
1171
|
+
},
|
|
1172
|
+
onConsole: (method, args) => {
|
|
1173
|
+
const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
|
|
1174
|
+
log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
log('Setting up file watcher...');
|
|
1179
|
+
vfs.watch('/app', { recursive: true }, (eventType, filename) => {
|
|
1180
|
+
log(`File ${eventType}: ${filename}`);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
log('Convex App demo initialized!');
|
|
1184
|
+
log('');
|
|
1185
|
+
log('Project structure:');
|
|
1186
|
+
listFiles(vfs, '/', log, ' ');
|
|
1187
|
+
|
|
1188
|
+
return { vfs, runtime };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Start the dev server for Convex App demo
|
|
1193
|
+
*/
|
|
1194
|
+
export async function startConvexAppDevServer(
|
|
1195
|
+
vfs: VirtualFS,
|
|
1196
|
+
options: {
|
|
1197
|
+
port?: number;
|
|
1198
|
+
log?: (message: string) => void;
|
|
1199
|
+
} = {}
|
|
1200
|
+
): Promise<{
|
|
1201
|
+
server: NextDevServer;
|
|
1202
|
+
url: string;
|
|
1203
|
+
stop: () => void;
|
|
1204
|
+
}> {
|
|
1205
|
+
const port = options.port || 3002;
|
|
1206
|
+
const log = options.log || console.log;
|
|
1207
|
+
|
|
1208
|
+
log('Starting Convex App dev server...');
|
|
1209
|
+
|
|
1210
|
+
// Create NextDevServer with App Router preference
|
|
1211
|
+
const server = new NextDevServer(vfs, {
|
|
1212
|
+
port,
|
|
1213
|
+
root: '/',
|
|
1214
|
+
preferAppRouter: true,
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Get the server bridge
|
|
1218
|
+
const bridge = getServerBridge();
|
|
1219
|
+
|
|
1220
|
+
// Initialize Service Worker
|
|
1221
|
+
try {
|
|
1222
|
+
log('Initializing Service Worker...');
|
|
1223
|
+
await bridge.initServiceWorker();
|
|
1224
|
+
log('Service Worker ready');
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
log(`Warning: Service Worker failed to initialize: ${error}`);
|
|
1227
|
+
log('Falling back to direct request handling...');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Register event handlers
|
|
1231
|
+
bridge.on('server-ready', (p: unknown, u: unknown) => {
|
|
1232
|
+
log(`Server ready at ${u}`);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// Wire up the NextDevServer to handle requests through the bridge
|
|
1236
|
+
const httpServer = createHttpServerWrapper(server);
|
|
1237
|
+
bridge.registerServer(httpServer, port);
|
|
1238
|
+
|
|
1239
|
+
// Start watching for file changes
|
|
1240
|
+
server.start();
|
|
1241
|
+
log('File watcher started');
|
|
1242
|
+
|
|
1243
|
+
// Set up HMR event forwarding
|
|
1244
|
+
server.on('hmr-update', (update: unknown) => {
|
|
1245
|
+
log(`HMR update: ${JSON.stringify(update)}`);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const url = bridge.getServerUrl(port);
|
|
1249
|
+
log(`Convex App dev server running at: ${url}/`);
|
|
1250
|
+
|
|
1251
|
+
return {
|
|
1252
|
+
server,
|
|
1253
|
+
url: url + '/',
|
|
1254
|
+
stop: () => {
|
|
1255
|
+
server.stop();
|
|
1256
|
+
bridge.unregisterServer(port);
|
|
1257
|
+
},
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Create an http.Server-compatible wrapper
|
|
1263
|
+
*/
|
|
1264
|
+
function createHttpServerWrapper(devServer: NextDevServer) {
|
|
1265
|
+
return {
|
|
1266
|
+
listening: true,
|
|
1267
|
+
address: () => ({ port: devServer.getPort(), address: '0.0.0.0', family: 'IPv4' }),
|
|
1268
|
+
async handleRequest(
|
|
1269
|
+
method: string,
|
|
1270
|
+
url: string,
|
|
1271
|
+
headers: Record<string, string>,
|
|
1272
|
+
body?: string | Buffer
|
|
1273
|
+
) {
|
|
1274
|
+
const bodyBuffer = body
|
|
1275
|
+
? typeof body === 'string'
|
|
1276
|
+
? Buffer.from(body)
|
|
1277
|
+
: body
|
|
1278
|
+
: undefined;
|
|
1279
|
+
return devServer.handleRequest(method, url, headers, bodyBuffer);
|
|
1280
|
+
},
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function listFiles(
|
|
1285
|
+
vfs: VirtualFS,
|
|
1286
|
+
path: string,
|
|
1287
|
+
log: (msg: string) => void,
|
|
1288
|
+
indent: string
|
|
1289
|
+
): void {
|
|
1290
|
+
try {
|
|
1291
|
+
const entries = vfs.readdirSync(path);
|
|
1292
|
+
for (const entry of entries) {
|
|
1293
|
+
if (entry === 'node_modules') {
|
|
1294
|
+
log(`${indent}${entry}/ (skipped)`);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const fullPath = path === '/' ? `/${entry}` : `${path}/${entry}`;
|
|
1298
|
+
try {
|
|
1299
|
+
const stat = vfs.statSync(fullPath);
|
|
1300
|
+
if (stat.isDirectory()) {
|
|
1301
|
+
log(`${indent}${entry}/`);
|
|
1302
|
+
listFiles(vfs, fullPath, log, indent + ' ');
|
|
1303
|
+
} else {
|
|
1304
|
+
log(`${indent}${entry}`);
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
log(`${indent}${entry}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
// Directory doesn't exist or can't be read
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Export for use in HTML demos
|
|
1316
|
+
export { PACKAGE_JSON, DEMO_PACKAGES };
|