almostnode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +731 -0
  3. package/dist/__sw__.js +394 -0
  4. package/dist/ai-chatbot-demo-entry.d.ts +6 -0
  5. package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
  6. package/dist/ai-chatbot-demo.d.ts +42 -0
  7. package/dist/ai-chatbot-demo.d.ts.map +1 -0
  8. package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
  9. package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
  10. package/dist/convex-app-demo-entry.d.ts +6 -0
  11. package/dist/convex-app-demo-entry.d.ts.map +1 -0
  12. package/dist/convex-app-demo.d.ts +68 -0
  13. package/dist/convex-app-demo.d.ts.map +1 -0
  14. package/dist/cors-proxy.d.ts +46 -0
  15. package/dist/cors-proxy.d.ts.map +1 -0
  16. package/dist/create-runtime.d.ts +42 -0
  17. package/dist/create-runtime.d.ts.map +1 -0
  18. package/dist/demo.d.ts +6 -0
  19. package/dist/demo.d.ts.map +1 -0
  20. package/dist/dev-server.d.ts +97 -0
  21. package/dist/dev-server.d.ts.map +1 -0
  22. package/dist/frameworks/next-dev-server.d.ts +202 -0
  23. package/dist/frameworks/next-dev-server.d.ts.map +1 -0
  24. package/dist/frameworks/vite-dev-server.d.ts +85 -0
  25. package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
  26. package/dist/index.cjs +14965 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.ts +71 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +14867 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/next-demo.d.ts +49 -0
  33. package/dist/next-demo.d.ts.map +1 -0
  34. package/dist/npm/index.d.ts +71 -0
  35. package/dist/npm/index.d.ts.map +1 -0
  36. package/dist/npm/registry.d.ts +66 -0
  37. package/dist/npm/registry.d.ts.map +1 -0
  38. package/dist/npm/resolver.d.ts +52 -0
  39. package/dist/npm/resolver.d.ts.map +1 -0
  40. package/dist/npm/tarball.d.ts +29 -0
  41. package/dist/npm/tarball.d.ts.map +1 -0
  42. package/dist/runtime-interface.d.ts +90 -0
  43. package/dist/runtime-interface.d.ts.map +1 -0
  44. package/dist/runtime.d.ts +103 -0
  45. package/dist/runtime.d.ts.map +1 -0
  46. package/dist/sandbox-helpers.d.ts +43 -0
  47. package/dist/sandbox-helpers.d.ts.map +1 -0
  48. package/dist/sandbox-runtime.d.ts +65 -0
  49. package/dist/sandbox-runtime.d.ts.map +1 -0
  50. package/dist/server-bridge.d.ts +89 -0
  51. package/dist/server-bridge.d.ts.map +1 -0
  52. package/dist/shims/assert.d.ts +51 -0
  53. package/dist/shims/assert.d.ts.map +1 -0
  54. package/dist/shims/async_hooks.d.ts +37 -0
  55. package/dist/shims/async_hooks.d.ts.map +1 -0
  56. package/dist/shims/buffer.d.ts +20 -0
  57. package/dist/shims/buffer.d.ts.map +1 -0
  58. package/dist/shims/child_process-browser.d.ts +92 -0
  59. package/dist/shims/child_process-browser.d.ts.map +1 -0
  60. package/dist/shims/child_process.d.ts +93 -0
  61. package/dist/shims/child_process.d.ts.map +1 -0
  62. package/dist/shims/chokidar.d.ts +55 -0
  63. package/dist/shims/chokidar.d.ts.map +1 -0
  64. package/dist/shims/cluster.d.ts +52 -0
  65. package/dist/shims/cluster.d.ts.map +1 -0
  66. package/dist/shims/crypto.d.ts +122 -0
  67. package/dist/shims/crypto.d.ts.map +1 -0
  68. package/dist/shims/dgram.d.ts +34 -0
  69. package/dist/shims/dgram.d.ts.map +1 -0
  70. package/dist/shims/diagnostics_channel.d.ts +80 -0
  71. package/dist/shims/diagnostics_channel.d.ts.map +1 -0
  72. package/dist/shims/dns.d.ts +87 -0
  73. package/dist/shims/dns.d.ts.map +1 -0
  74. package/dist/shims/domain.d.ts +25 -0
  75. package/dist/shims/domain.d.ts.map +1 -0
  76. package/dist/shims/esbuild.d.ts +105 -0
  77. package/dist/shims/esbuild.d.ts.map +1 -0
  78. package/dist/shims/events.d.ts +37 -0
  79. package/dist/shims/events.d.ts.map +1 -0
  80. package/dist/shims/fs.d.ts +115 -0
  81. package/dist/shims/fs.d.ts.map +1 -0
  82. package/dist/shims/fsevents.d.ts +67 -0
  83. package/dist/shims/fsevents.d.ts.map +1 -0
  84. package/dist/shims/http.d.ts +217 -0
  85. package/dist/shims/http.d.ts.map +1 -0
  86. package/dist/shims/http2.d.ts +81 -0
  87. package/dist/shims/http2.d.ts.map +1 -0
  88. package/dist/shims/https.d.ts +36 -0
  89. package/dist/shims/https.d.ts.map +1 -0
  90. package/dist/shims/inspector.d.ts +25 -0
  91. package/dist/shims/inspector.d.ts.map +1 -0
  92. package/dist/shims/module.d.ts +22 -0
  93. package/dist/shims/module.d.ts.map +1 -0
  94. package/dist/shims/net.d.ts +100 -0
  95. package/dist/shims/net.d.ts.map +1 -0
  96. package/dist/shims/os.d.ts +159 -0
  97. package/dist/shims/os.d.ts.map +1 -0
  98. package/dist/shims/path.d.ts +72 -0
  99. package/dist/shims/path.d.ts.map +1 -0
  100. package/dist/shims/perf_hooks.d.ts +50 -0
  101. package/dist/shims/perf_hooks.d.ts.map +1 -0
  102. package/dist/shims/process.d.ts +93 -0
  103. package/dist/shims/process.d.ts.map +1 -0
  104. package/dist/shims/querystring.d.ts +23 -0
  105. package/dist/shims/querystring.d.ts.map +1 -0
  106. package/dist/shims/readdirp.d.ts +52 -0
  107. package/dist/shims/readdirp.d.ts.map +1 -0
  108. package/dist/shims/readline.d.ts +62 -0
  109. package/dist/shims/readline.d.ts.map +1 -0
  110. package/dist/shims/rollup.d.ts +34 -0
  111. package/dist/shims/rollup.d.ts.map +1 -0
  112. package/dist/shims/sentry.d.ts +163 -0
  113. package/dist/shims/sentry.d.ts.map +1 -0
  114. package/dist/shims/stream.d.ts +181 -0
  115. package/dist/shims/stream.d.ts.map +1 -0
  116. package/dist/shims/tls.d.ts +53 -0
  117. package/dist/shims/tls.d.ts.map +1 -0
  118. package/dist/shims/tty.d.ts +30 -0
  119. package/dist/shims/tty.d.ts.map +1 -0
  120. package/dist/shims/url.d.ts +64 -0
  121. package/dist/shims/url.d.ts.map +1 -0
  122. package/dist/shims/util.d.ts +106 -0
  123. package/dist/shims/util.d.ts.map +1 -0
  124. package/dist/shims/v8.d.ts +73 -0
  125. package/dist/shims/v8.d.ts.map +1 -0
  126. package/dist/shims/vfs-adapter.d.ts +126 -0
  127. package/dist/shims/vfs-adapter.d.ts.map +1 -0
  128. package/dist/shims/vm.d.ts +45 -0
  129. package/dist/shims/vm.d.ts.map +1 -0
  130. package/dist/shims/worker_threads.d.ts +66 -0
  131. package/dist/shims/worker_threads.d.ts.map +1 -0
  132. package/dist/shims/ws.d.ts +66 -0
  133. package/dist/shims/ws.d.ts.map +1 -0
  134. package/dist/shims/zlib.d.ts +161 -0
  135. package/dist/shims/zlib.d.ts.map +1 -0
  136. package/dist/transform.d.ts +24 -0
  137. package/dist/transform.d.ts.map +1 -0
  138. package/dist/virtual-fs.d.ts +226 -0
  139. package/dist/virtual-fs.d.ts.map +1 -0
  140. package/dist/vite-demo.d.ts +35 -0
  141. package/dist/vite-demo.d.ts.map +1 -0
  142. package/dist/vite-sw.js +132 -0
  143. package/dist/worker/runtime-worker.d.ts +8 -0
  144. package/dist/worker/runtime-worker.d.ts.map +1 -0
  145. package/dist/worker-runtime.d.ts +50 -0
  146. package/dist/worker-runtime.d.ts.map +1 -0
  147. package/package.json +85 -0
  148. package/src/ai-chatbot-demo-entry.ts +244 -0
  149. package/src/ai-chatbot-demo.ts +509 -0
  150. package/src/convex-app-demo-entry.ts +1107 -0
  151. package/src/convex-app-demo.ts +1316 -0
  152. package/src/cors-proxy.ts +81 -0
  153. package/src/create-runtime.ts +147 -0
  154. package/src/demo.ts +304 -0
  155. package/src/dev-server.ts +274 -0
  156. package/src/frameworks/next-dev-server.ts +2224 -0
  157. package/src/frameworks/vite-dev-server.ts +702 -0
  158. package/src/index.ts +101 -0
  159. package/src/next-demo.ts +1784 -0
  160. package/src/npm/index.ts +347 -0
  161. package/src/npm/registry.ts +152 -0
  162. package/src/npm/resolver.ts +385 -0
  163. package/src/npm/tarball.ts +209 -0
  164. package/src/runtime-interface.ts +103 -0
  165. package/src/runtime.ts +1046 -0
  166. package/src/sandbox-helpers.ts +173 -0
  167. package/src/sandbox-runtime.ts +252 -0
  168. package/src/server-bridge.ts +426 -0
  169. package/src/shims/assert.ts +664 -0
  170. package/src/shims/async_hooks.ts +86 -0
  171. package/src/shims/buffer.ts +75 -0
  172. package/src/shims/child_process-browser.ts +217 -0
  173. package/src/shims/child_process.ts +463 -0
  174. package/src/shims/chokidar.ts +313 -0
  175. package/src/shims/cluster.ts +67 -0
  176. package/src/shims/crypto.ts +830 -0
  177. package/src/shims/dgram.ts +47 -0
  178. package/src/shims/diagnostics_channel.ts +196 -0
  179. package/src/shims/dns.ts +172 -0
  180. package/src/shims/domain.ts +58 -0
  181. package/src/shims/esbuild.ts +805 -0
  182. package/src/shims/events.ts +195 -0
  183. package/src/shims/fs.ts +803 -0
  184. package/src/shims/fsevents.ts +63 -0
  185. package/src/shims/http.ts +904 -0
  186. package/src/shims/http2.ts +96 -0
  187. package/src/shims/https.ts +86 -0
  188. package/src/shims/inspector.ts +30 -0
  189. package/src/shims/module.ts +82 -0
  190. package/src/shims/net.ts +359 -0
  191. package/src/shims/os.ts +195 -0
  192. package/src/shims/path.ts +199 -0
  193. package/src/shims/perf_hooks.ts +92 -0
  194. package/src/shims/process.ts +346 -0
  195. package/src/shims/querystring.ts +97 -0
  196. package/src/shims/readdirp.ts +228 -0
  197. package/src/shims/readline.ts +110 -0
  198. package/src/shims/rollup.ts +80 -0
  199. package/src/shims/sentry.ts +133 -0
  200. package/src/shims/stream.ts +1126 -0
  201. package/src/shims/tls.ts +95 -0
  202. package/src/shims/tty.ts +64 -0
  203. package/src/shims/url.ts +171 -0
  204. package/src/shims/util.ts +312 -0
  205. package/src/shims/v8.ts +113 -0
  206. package/src/shims/vfs-adapter.ts +402 -0
  207. package/src/shims/vm.ts +83 -0
  208. package/src/shims/worker_threads.ts +111 -0
  209. package/src/shims/ws.ts +382 -0
  210. package/src/shims/zlib.ts +289 -0
  211. package/src/transform.ts +313 -0
  212. package/src/types/external.d.ts +67 -0
  213. package/src/virtual-fs.ts +903 -0
  214. package/src/vite-demo.ts +577 -0
  215. package/src/worker/runtime-worker.ts +128 -0
  216. package/src/worker-runtime.ts +145 -0
@@ -0,0 +1,1784 @@
1
+ /**
2
+ * Next.js Demo - Running Next.js-style app in the browser using our Node.js shims
3
+ */
4
+
5
+ import { VirtualFS } from './virtual-fs';
6
+ import { Runtime } from './runtime';
7
+ import { createRuntime } from './create-runtime';
8
+ import type { IRuntime } from './runtime-interface';
9
+ import { NextDevServer } from './frameworks/next-dev-server';
10
+ import { getServerBridge } from './server-bridge';
11
+ import { Buffer } from './shims/stream';
12
+ import { PackageManager, InstallOptions, InstallResult } from './npm';
13
+
14
+ /**
15
+ * Create a Next.js project structure in the virtual filesystem
16
+ */
17
+ export function createNextProject(vfs: VirtualFS): void {
18
+ // Create package.json
19
+ vfs.writeFileSync(
20
+ '/package.json',
21
+ JSON.stringify(
22
+ {
23
+ name: 'next-browser-demo',
24
+ version: '1.0.0',
25
+ scripts: {
26
+ dev: 'next dev',
27
+ build: 'next build',
28
+ start: 'next start',
29
+ },
30
+ dependencies: {
31
+ next: '^14.0.0',
32
+ react: '^18.2.0',
33
+ 'react-dom': '^18.2.0',
34
+ },
35
+ },
36
+ null,
37
+ 2
38
+ )
39
+ );
40
+
41
+ // Create directories
42
+ vfs.mkdirSync('/pages', { recursive: true });
43
+ vfs.mkdirSync('/pages/api', { recursive: true });
44
+ vfs.mkdirSync('/pages/users', { recursive: true });
45
+ vfs.mkdirSync('/public', { recursive: true });
46
+ vfs.mkdirSync('/styles', { recursive: true });
47
+
48
+ // Create global styles
49
+ vfs.writeFileSync(
50
+ '/styles/globals.css',
51
+ `* {
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ :root {
56
+ --foreground-rgb: 0, 0, 0;
57
+ --background-start-rgb: 214, 219, 220;
58
+ --background-end-rgb: 255, 255, 255;
59
+ }
60
+
61
+ body {
62
+ margin: 0;
63
+ padding: 0;
64
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
65
+ background: linear-gradient(
66
+ to bottom,
67
+ transparent,
68
+ rgb(var(--background-end-rgb))
69
+ ) rgb(var(--background-start-rgb));
70
+ min-height: 100vh;
71
+ }
72
+
73
+ a {
74
+ color: #0070f3;
75
+ text-decoration: none;
76
+ }
77
+
78
+ a:hover {
79
+ text-decoration: underline;
80
+ }
81
+
82
+ .container {
83
+ max-width: 800px;
84
+ margin: 0 auto;
85
+ padding: 2rem;
86
+ }
87
+
88
+ .card {
89
+ background: white;
90
+ border-radius: 12px;
91
+ padding: 1.5rem;
92
+ box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.1);
93
+ margin-bottom: 1rem;
94
+ }
95
+
96
+ .counter-display {
97
+ font-size: 4rem;
98
+ font-weight: bold;
99
+ text-align: center;
100
+ padding: 1rem;
101
+ }
102
+
103
+ .counter-buttons {
104
+ display: flex;
105
+ gap: 0.5rem;
106
+ justify-content: center;
107
+ margin-top: 1rem;
108
+ }
109
+
110
+ button {
111
+ padding: 0.75rem 1.5rem;
112
+ font-size: 1rem;
113
+ border: none;
114
+ border-radius: 8px;
115
+ background: #0070f3;
116
+ color: white;
117
+ cursor: pointer;
118
+ transition: background 0.2s;
119
+ }
120
+
121
+ button:hover {
122
+ background: #005cc5;
123
+ }
124
+
125
+ nav {
126
+ background: white;
127
+ padding: 1rem 2rem;
128
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
129
+ margin-bottom: 2rem;
130
+ }
131
+
132
+ nav ul {
133
+ list-style: none;
134
+ margin: 0;
135
+ padding: 0;
136
+ display: flex;
137
+ gap: 1.5rem;
138
+ }
139
+
140
+ .api-result {
141
+ background: #f5f5f5;
142
+ padding: 1rem;
143
+ border-radius: 8px;
144
+ font-family: monospace;
145
+ margin-top: 1rem;
146
+ }
147
+ `
148
+ );
149
+
150
+ // Create index page
151
+ vfs.writeFileSync(
152
+ '/pages/index.jsx',
153
+ `import React, { useState } from 'react';
154
+ import Link from 'next/link';
155
+
156
+ function Counter() {
157
+ const [count, setCount] = useState(0);
158
+
159
+ return (
160
+ <div className="card">
161
+ <h2>Interactive Counter</h2>
162
+ <div className="counter-display">{count}</div>
163
+ <div className="counter-buttons">
164
+ <button onClick={() => setCount(c => c - 1)}>-</button>
165
+ <button onClick={() => setCount(0)}>Reset</button>
166
+ <button onClick={() => setCount(c => c + 1)}>+</button>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ export default function Home() {
173
+ return (
174
+ <div>
175
+ <nav>
176
+ <ul>
177
+ <li><Link href="/">Home</Link></li>
178
+ <li><Link href="/about">About</Link></li>
179
+ <li><Link href="/users/1">User 1</Link></li>
180
+ <li><Link href="/api-demo">API Demo</Link></li>
181
+ </ul>
182
+ </nav>
183
+
184
+ <div className="container">
185
+ <h1>Welcome to Next.js in Browser!</h1>
186
+ <p>This is a Next.js-style app running entirely in your browser.</p>
187
+
188
+ <Counter />
189
+
190
+ <div className="card">
191
+ <h3>Features</h3>
192
+ <ul>
193
+ <li>File-based routing (/pages directory)</li>
194
+ <li>Dynamic routes (/users/[id])</li>
195
+ <li>API routes (/api/*)</li>
196
+ <li>Hot Module Replacement</li>
197
+ <li>React Refresh (preserves state)</li>
198
+ </ul>
199
+ </div>
200
+
201
+ <div className="card">
202
+ <h3>How it works</h3>
203
+ <p>
204
+ This demo uses a Service Worker to intercept requests and serve files
205
+ from a virtual filesystem. JSX is transformed to JavaScript using esbuild-wasm,
206
+ and React Refresh enables state-preserving HMR.
207
+ </p>
208
+ </div>
209
+
210
+ {/* Tailwind CSS Demo Section */}
211
+ <div className="mt-6 p-6 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl shadow-lg text-white">
212
+ <h3 className="text-xl font-bold mb-2">Tailwind CSS is Ready!</h3>
213
+ <p className="opacity-90 mb-4">
214
+ This section uses Tailwind utility classes. Install a package to see more Tailwind demos.
215
+ </p>
216
+ <div className="flex gap-2">
217
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">p-6</span>
218
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">rounded-xl</span>
219
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">shadow-lg</span>
220
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">gradient</span>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ );
226
+ }
227
+ `
228
+ );
229
+
230
+ // Create about page
231
+ vfs.writeFileSync(
232
+ '/pages/about.jsx',
233
+ `import React from 'react';
234
+ import Link from 'next/link';
235
+ import { useRouter } from 'next/router';
236
+
237
+ export default function About() {
238
+ const router = useRouter();
239
+
240
+ return (
241
+ <div>
242
+ <nav>
243
+ <ul>
244
+ <li><Link href="/">Home</Link></li>
245
+ <li><Link href="/about">About</Link></li>
246
+ <li><Link href="/users/1">User 1</Link></li>
247
+ <li><Link href="/api-demo">API Demo</Link></li>
248
+ </ul>
249
+ </nav>
250
+
251
+ <div className="container">
252
+ <h1>About Page</h1>
253
+
254
+ <div className="card">
255
+ <p>Current path: <code>{router.pathname}</code></p>
256
+ <p>This page demonstrates:</p>
257
+ <ul>
258
+ <li>File-based routing</li>
259
+ <li>next/router hook</li>
260
+ <li>Client-side navigation</li>
261
+ </ul>
262
+ </div>
263
+
264
+ <div className="card">
265
+ <h3>Navigation</h3>
266
+ <p>Try clicking the links above to navigate between pages without full page reloads.</p>
267
+ <button onClick={() => router.push('/')}>
268
+ Go Home (using router.push)
269
+ </button>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ );
274
+ }
275
+ `
276
+ );
277
+
278
+ // Create dynamic user page
279
+ vfs.writeFileSync(
280
+ '/pages/users/[id].jsx',
281
+ `import React, { useState, useEffect } from 'react';
282
+ import Link from 'next/link';
283
+ import { useRouter } from 'next/router';
284
+
285
+ const users = {
286
+ '1': { name: 'Alice Johnson', email: 'alice@example.com', role: 'Developer' },
287
+ '2': { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
288
+ '3': { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
289
+ };
290
+
291
+ export default function UserPage() {
292
+ const router = useRouter();
293
+ const [userId, setUserId] = useState(null);
294
+
295
+ useEffect(() => {
296
+ // Extract user ID from pathname
297
+ const match = window.location.pathname.match(/\\/users\\/([^\\/]+)/);
298
+ if (match) {
299
+ setUserId(match[1]);
300
+ }
301
+ }, [router.pathname]);
302
+
303
+ const user = userId ? users[userId] : null;
304
+
305
+ return (
306
+ <div>
307
+ <nav>
308
+ <ul>
309
+ <li><Link href="/">Home</Link></li>
310
+ <li><Link href="/about">About</Link></li>
311
+ <li><Link href="/users/1">User 1</Link></li>
312
+ <li><Link href="/users/2">User 2</Link></li>
313
+ <li><Link href="/users/3">User 3</Link></li>
314
+ </ul>
315
+ </nav>
316
+
317
+ <div className="container">
318
+ <h1>User Profile</h1>
319
+
320
+ {user ? (
321
+ <div className="card">
322
+ <h2>{user.name}</h2>
323
+ <p><strong>Email:</strong> {user.email}</p>
324
+ <p><strong>Role:</strong> {user.role}</p>
325
+ <p><strong>User ID:</strong> {userId}</p>
326
+ </div>
327
+ ) : (
328
+ <div className="card">
329
+ <p>Loading user... (ID: {userId || 'unknown'})</p>
330
+ </div>
331
+ )}
332
+
333
+ <div className="card">
334
+ <h3>Dynamic Routing</h3>
335
+ <p>This page uses the <code>[id]</code> dynamic segment.</p>
336
+ <p>The route <code>/users/[id].jsx</code> matches any <code>/users/*</code> path.</p>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ );
341
+ }
342
+ `
343
+ );
344
+
345
+ // Create API demo page
346
+ vfs.writeFileSync(
347
+ '/pages/api-demo.jsx',
348
+ `import React, { useState } from 'react';
349
+ import Link from 'next/link';
350
+
351
+ export default function ApiDemo() {
352
+ const [result, setResult] = useState(null);
353
+ const [loading, setLoading] = useState(false);
354
+
355
+ const callApi = async (endpoint) => {
356
+ setLoading(true);
357
+ try {
358
+ // Use relative path (remove leading slash) so it resolves relative to current page
359
+ const relativePath = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
360
+ const response = await fetch(relativePath);
361
+ const data = await response.json();
362
+ setResult({ endpoint, data, status: response.status });
363
+ } catch (error) {
364
+ setResult({ endpoint, error: error.message, status: 'error' });
365
+ }
366
+ setLoading(false);
367
+ };
368
+
369
+ return (
370
+ <div>
371
+ <nav>
372
+ <ul>
373
+ <li><Link href="/">Home</Link></li>
374
+ <li><Link href="/about">About</Link></li>
375
+ <li><Link href="/api-demo">API Demo</Link></li>
376
+ </ul>
377
+ </nav>
378
+
379
+ <div className="container">
380
+ <h1>API Routes Demo</h1>
381
+
382
+ <div className="card">
383
+ <h3>Test API Endpoints</h3>
384
+ <p>Click a button to call an API route:</p>
385
+
386
+ <div className="counter-buttons">
387
+ <button onClick={() => callApi('/api/hello')} disabled={loading}>
388
+ GET /api/hello
389
+ </button>
390
+ <button onClick={() => callApi('/api/users')} disabled={loading}>
391
+ GET /api/users
392
+ </button>
393
+ <button onClick={() => callApi('/api/time')} disabled={loading}>
394
+ GET /api/time
395
+ </button>
396
+ </div>
397
+
398
+ {result && (
399
+ <div className="api-result">
400
+ <strong>Endpoint:</strong> {result.endpoint}<br/>
401
+ <strong>Status:</strong> {result.status}<br/>
402
+ <strong>Response:</strong>
403
+ <pre>{JSON.stringify(result.data || result.error, null, 2)}</pre>
404
+ </div>
405
+ )}
406
+ </div>
407
+
408
+ <div className="card">
409
+ <h3>Node.js https Module Demo</h3>
410
+ <p style={{ marginBottom: '1rem' }}>
411
+ This endpoint uses Node.js <code>https.get()</code> to fetch data server-side.
412
+ Requires CORS proxy to be configured.
413
+ </p>
414
+
415
+ <div className="counter-buttons">
416
+ <button onClick={() => callApi('/api/github-proxy?username=octocat')} disabled={loading}>
417
+ GET /api/github-proxy (Node.js https)
418
+ </button>
419
+ </div>
420
+ </div>
421
+
422
+ <div className="card">
423
+ <h3>About API Routes</h3>
424
+ <p>
425
+ API routes are defined in <code>/pages/api/</code> directory.
426
+ Each file exports a handler function that receives request and response objects.
427
+ </p>
428
+ <p style={{ marginTop: '0.5rem', color: '#666' }}>
429
+ The github-proxy endpoint demonstrates using Node.js <code>https</code> module
430
+ to make outbound HTTP requests, similar to how it works in real Node.js.
431
+ </p>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ );
436
+ }
437
+ `
438
+ );
439
+
440
+ // Create external API demo page
441
+ vfs.writeFileSync(
442
+ '/pages/external-api.jsx',
443
+ `import React, { useState } from 'react';
444
+ import Link from 'next/link';
445
+
446
+ export default function ExternalApiDemo() {
447
+ const [user, setUser] = useState(null);
448
+ const [loading, setLoading] = useState(false);
449
+ const [error, setError] = useState(null);
450
+
451
+ const fetchGitHubUser = async () => {
452
+ setLoading(true);
453
+ setError(null);
454
+ try {
455
+ // Uses proxyFetch from window - will use proxy if configured
456
+ const response = await window.__proxyFetch('https://api.github.com/users/octocat');
457
+ if (!response.ok) {
458
+ throw new Error('Failed to fetch: ' + response.status);
459
+ }
460
+ const data = await response.json();
461
+ setUser(data);
462
+ } catch (err) {
463
+ setError(err.message);
464
+ }
465
+ setLoading(false);
466
+ };
467
+
468
+ return (
469
+ <div>
470
+ <nav>
471
+ <ul>
472
+ <li><Link href="/">Home</Link></li>
473
+ <li><Link href="/about">About</Link></li>
474
+ <li><Link href="/api-demo">API Demo</Link></li>
475
+ <li><Link href="/external-api">External API</Link></li>
476
+ </ul>
477
+ </nav>
478
+
479
+ <div className="container">
480
+ <h1>External API Demo</h1>
481
+
482
+ <div className="p-6 bg-gradient-to-r from-green-400 to-blue-500 rounded-xl text-white mb-6">
483
+ <h2 className="text-xl font-bold mb-2">CORS Proxy Demo</h2>
484
+ <p className="opacity-90">
485
+ This page demonstrates fetching from external APIs.
486
+ Configure a CORS proxy in the editor panel if you encounter CORS errors.
487
+ </p>
488
+ </div>
489
+
490
+ <div className="card">
491
+ <h3>GitHub User API</h3>
492
+ <p style={{ marginBottom: '1rem', color: '#666' }}>
493
+ Fetches user data from the GitHub API. If CORS errors occur,
494
+ set a proxy URL like <code>https://corsproxy.io/?</code> in the settings.
495
+ </p>
496
+
497
+ <button
498
+ onClick={fetchGitHubUser}
499
+ disabled={loading}
500
+ style={{ marginBottom: '1rem' }}
501
+ >
502
+ {loading ? 'Loading...' : 'Fetch GitHub User (octocat)'}
503
+ </button>
504
+
505
+ {error && (
506
+ <div style={{
507
+ background: '#fee2e2',
508
+ color: '#dc2626',
509
+ padding: '1rem',
510
+ borderRadius: '8px',
511
+ marginBottom: '1rem'
512
+ }}>
513
+ <strong>Error:</strong> {error}
514
+ <p style={{ marginTop: '0.5rem', fontSize: '0.9rem' }}>
515
+ Try setting a CORS proxy in the editor panel.
516
+ </p>
517
+ </div>
518
+ )}
519
+
520
+ {user && (
521
+ <div style={{
522
+ background: '#f3f4f6',
523
+ padding: '1.5rem',
524
+ borderRadius: '12px',
525
+ display: 'flex',
526
+ gap: '1rem',
527
+ alignItems: 'flex-start'
528
+ }}>
529
+ <img
530
+ src={user.avatar_url}
531
+ alt={user.login}
532
+ style={{ width: '80px', height: '80px', borderRadius: '50%' }}
533
+ />
534
+ <div>
535
+ <h4 style={{ margin: 0, fontSize: '1.25rem' }}>{user.name || user.login}</h4>
536
+ <p style={{ color: '#666', margin: '0.25rem 0' }}>@{user.login}</p>
537
+ {user.bio && <p style={{ margin: '0.5rem 0' }}>{user.bio}</p>}
538
+ <p style={{ fontSize: '0.875rem', color: '#888' }}>
539
+ Followers: {user.followers} | Public Repos: {user.public_repos}
540
+ </p>
541
+ </div>
542
+ </div>
543
+ )}
544
+ </div>
545
+
546
+ <div className="card">
547
+ <h3>How it works</h3>
548
+ <p>
549
+ External API calls from the browser may be blocked by CORS if the server
550
+ doesn't allow your origin. A CORS proxy forwards your request through a
551
+ server that adds the appropriate headers.
552
+ </p>
553
+ <pre style={{
554
+ background: '#1e1e1e',
555
+ color: '#d4d4d4',
556
+ padding: '1rem',
557
+ borderRadius: '8px',
558
+ overflow: 'auto',
559
+ fontSize: '0.875rem'
560
+ }}>
561
+ {String.raw\`// Without proxy (may fail with CORS error)
562
+ fetch('https://api.github.com/users/octocat')
563
+
564
+ // With proxy configured
565
+ setCorsProxy('https://corsproxy.io/?');
566
+ proxyFetch('https://api.github.com/users/octocat')\`}
567
+ </pre>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ );
572
+ }
573
+ `
574
+ );
575
+
576
+ // Create API routes
577
+ vfs.writeFileSync(
578
+ '/pages/api/hello.js',
579
+ `export default function handler(req, res) {
580
+ res.status(200).json({
581
+ message: 'Hello from Next.js API!',
582
+ timestamp: new Date().toISOString(),
583
+ });
584
+ }
585
+ `
586
+ );
587
+
588
+ vfs.writeFileSync(
589
+ '/pages/api/users.js',
590
+ `export default function handler(req, res) {
591
+ const users = [
592
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com' },
593
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com' },
594
+ { id: 3, name: 'Carol Williams', email: 'carol@example.com' },
595
+ ];
596
+
597
+ res.status(200).json({ users, count: users.length });
598
+ }
599
+ `
600
+ );
601
+
602
+ vfs.writeFileSync(
603
+ '/pages/api/time.js',
604
+ `export default function handler(req, res) {
605
+ const now = new Date();
606
+
607
+ res.status(200).json({
608
+ iso: now.toISOString(),
609
+ local: now.toLocaleString(),
610
+ unix: Math.floor(now.getTime() / 1000),
611
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
612
+ });
613
+ }
614
+ `
615
+ );
616
+
617
+ // Create API route that demonstrates Node.js https module
618
+ vfs.writeFileSync(
619
+ '/pages/api/github-proxy.js',
620
+ `// This API route uses Node.js https module to make server-side requests
621
+ import https from 'https';
622
+
623
+ export default function handler(req, res) {
624
+ const username = req.query.username || 'octocat';
625
+
626
+ // Use Node.js https.get() to fetch from GitHub API
627
+ https.get(\`https://api.github.com/users/\${username}\`, {
628
+ headers: {
629
+ 'User-Agent': 'Node.js-Browser-Runtime',
630
+ 'Accept': 'application/json'
631
+ }
632
+ }, (response) => {
633
+ let data = '';
634
+
635
+ response.on('data', (chunk) => {
636
+ data += chunk;
637
+ });
638
+
639
+ response.on('end', () => {
640
+ try {
641
+ const user = JSON.parse(data);
642
+ res.status(200).json({
643
+ success: true,
644
+ message: 'Fetched using Node.js https module!',
645
+ user: {
646
+ login: user.login,
647
+ name: user.name,
648
+ bio: user.bio,
649
+ avatar_url: user.avatar_url,
650
+ followers: user.followers,
651
+ public_repos: user.public_repos,
652
+ }
653
+ });
654
+ } catch (error) {
655
+ res.status(500).json({
656
+ success: false,
657
+ error: 'Failed to parse response',
658
+ raw: data.substring(0, 200)
659
+ });
660
+ }
661
+ });
662
+ }).on('error', (error) => {
663
+ res.status(500).json({
664
+ success: false,
665
+ error: error.message,
666
+ hint: 'Make sure CORS proxy is configured for https.get() to work'
667
+ });
668
+ });
669
+ }
670
+ `
671
+ );
672
+
673
+ // Create 404 page
674
+ vfs.writeFileSync(
675
+ '/pages/404.jsx',
676
+ `import React from 'react';
677
+ import Link from 'next/link';
678
+
679
+ export default function Custom404() {
680
+ return (
681
+ <div className="container" style={{ textAlign: 'center', paddingTop: '4rem' }}>
682
+ <h1 style={{ fontSize: '4rem', margin: 0 }}>404</h1>
683
+ <p style={{ fontSize: '1.5rem', color: '#666' }}>Page Not Found</p>
684
+ <p>
685
+ <Link href="/">Go back home</Link>
686
+ </p>
687
+ </div>
688
+ );
689
+ }
690
+ `
691
+ );
692
+
693
+ // Create TypeScript example page
694
+ vfs.mkdirSync('/pages/typescript', { recursive: true });
695
+ vfs.writeFileSync(
696
+ '/pages/typescript/index.tsx',
697
+ `import React, { useState, useCallback } from 'react';
698
+ import Link from 'next/link';
699
+
700
+ // TypeScript interfaces
701
+ interface Todo {
702
+ id: number;
703
+ text: string;
704
+ completed: boolean;
705
+ }
706
+
707
+ interface TodoItemProps {
708
+ todo: Todo;
709
+ onToggle: (id: number) => void;
710
+ onDelete: (id: number) => void;
711
+ }
712
+
713
+ // Typed component with props
714
+ function TodoItem({ todo, onToggle, onDelete }: TodoItemProps): JSX.Element {
715
+ return (
716
+ <div style={{
717
+ display: 'flex',
718
+ alignItems: 'center',
719
+ gap: '0.5rem',
720
+ padding: '0.5rem',
721
+ background: todo.completed ? '#e8f5e9' : '#fff',
722
+ borderRadius: '4px',
723
+ marginBottom: '0.5rem',
724
+ }}>
725
+ <input
726
+ type="checkbox"
727
+ checked={todo.completed}
728
+ onChange={() => onToggle(todo.id)}
729
+ />
730
+ <span style={{
731
+ flex: 1,
732
+ textDecoration: todo.completed ? 'line-through' : 'none',
733
+ color: todo.completed ? '#888' : '#000',
734
+ }}>
735
+ {todo.text}
736
+ </span>
737
+ <button
738
+ onClick={() => onDelete(todo.id)}
739
+ style={{ padding: '0.25rem 0.5rem', fontSize: '0.8rem' }}
740
+ >
741
+ Delete
742
+ </button>
743
+ </div>
744
+ );
745
+ }
746
+
747
+ // Main page component with TypeScript
748
+ export default function TypeScriptDemo(): JSX.Element {
749
+ const [todos, setTodos] = useState<Todo[]>([
750
+ { id: 1, text: 'Learn TypeScript', completed: true },
751
+ { id: 2, text: 'Build with Next.js', completed: false },
752
+ { id: 3, text: 'Try HMR with types', completed: false },
753
+ ]);
754
+ const [newTodo, setNewTodo] = useState<string>('');
755
+
756
+ const addTodo = useCallback((): void => {
757
+ if (!newTodo.trim()) return;
758
+ const todo: Todo = {
759
+ id: Date.now(),
760
+ text: newTodo.trim(),
761
+ completed: false,
762
+ };
763
+ setTodos((prev: Todo[]) => [...prev, todo]);
764
+ setNewTodo('');
765
+ }, [newTodo]);
766
+
767
+ const toggleTodo = useCallback((id: number): void => {
768
+ setTodos((prev: Todo[]) =>
769
+ prev.map((t: Todo) => t.id === id ? { ...t, completed: !t.completed } : t)
770
+ );
771
+ }, []);
772
+
773
+ const deleteTodo = useCallback((id: number): void => {
774
+ setTodos((prev: Todo[]) => prev.filter((t: Todo) => t.id !== id));
775
+ }, []);
776
+
777
+ const completedCount: number = todos.filter((t: Todo) => t.completed).length;
778
+
779
+ return (
780
+ <div>
781
+ <nav>
782
+ <ul>
783
+ <li><Link href="/">Home</Link></li>
784
+ <li><Link href="/about">About</Link></li>
785
+ <li><Link href="/typescript">TypeScript Demo</Link></li>
786
+ </ul>
787
+ </nav>
788
+
789
+ <div className="container">
790
+ <h1>TypeScript Demo</h1>
791
+ <p>This page is written in <code>.tsx</code> with full type annotations!</p>
792
+
793
+ <div className="card">
794
+ <h3>Todo List ({completedCount}/{todos.length} completed)</h3>
795
+
796
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
797
+ <input
798
+ type="text"
799
+ value={newTodo}
800
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewTodo(e.target.value)}
801
+ onKeyDown={(e: React.KeyboardEvent) => e.key === 'Enter' && addTodo()}
802
+ placeholder="Add a new todo..."
803
+ style={{ flex: 1, padding: '0.5rem', borderRadius: '4px', border: '1px solid #ccc' }}
804
+ />
805
+ <button onClick={addTodo}>Add</button>
806
+ </div>
807
+
808
+ {todos.map((todo: Todo) => (
809
+ <TodoItem
810
+ key={todo.id}
811
+ todo={todo}
812
+ onToggle={toggleTodo}
813
+ onDelete={deleteTodo}
814
+ />
815
+ ))}
816
+ </div>
817
+
818
+ <div className="card">
819
+ <h3>TypeScript Features Used</h3>
820
+ <ul>
821
+ <li><code>interface Todo</code> - Type definition</li>
822
+ <li><code>useState&lt;Todo[]&gt;</code> - Generic state</li>
823
+ <li><code>JSX.Element</code> - Return type annotation</li>
824
+ <li><code>React.ChangeEvent</code> - Event types</li>
825
+ <li><code>useCallback</code> with typed parameters</li>
826
+ </ul>
827
+ <p style={{ marginTop: '1rem', color: '#666' }}>
828
+ Edit this file and save - HMR will preserve your todo list state!
829
+ </p>
830
+ </div>
831
+ </div>
832
+ </div>
833
+ );
834
+ }
835
+ `
836
+ );
837
+
838
+ // Create public files
839
+ vfs.writeFileSync('/public/favicon.ico', 'favicon placeholder');
840
+ vfs.writeFileSync('/public/robots.txt', 'User-agent: *\nAllow: /');
841
+ }
842
+
843
+ /**
844
+ * Create a Next.js App Router project structure in the virtual filesystem
845
+ */
846
+ export function createNextAppRouterProject(vfs: VirtualFS): void {
847
+ // Create package.json
848
+ vfs.writeFileSync(
849
+ '/package.json',
850
+ JSON.stringify(
851
+ {
852
+ name: 'next-app-router-demo',
853
+ version: '1.0.0',
854
+ scripts: {
855
+ dev: 'next dev',
856
+ build: 'next build',
857
+ start: 'next start',
858
+ },
859
+ dependencies: {
860
+ next: '^14.0.0',
861
+ react: '^18.2.0',
862
+ 'react-dom': '^18.2.0',
863
+ },
864
+ },
865
+ null,
866
+ 2
867
+ )
868
+ );
869
+
870
+ // Create directories
871
+ vfs.mkdirSync('/app', { recursive: true });
872
+ vfs.mkdirSync('/app/about', { recursive: true });
873
+ vfs.mkdirSync('/app/dashboard', { recursive: true });
874
+ vfs.mkdirSync('/app/users', { recursive: true });
875
+ vfs.mkdirSync('/app/users/[id]', { recursive: true });
876
+ vfs.mkdirSync('/public', { recursive: true });
877
+
878
+ // Create global styles
879
+ vfs.writeFileSync(
880
+ '/app/globals.css',
881
+ `* {
882
+ box-sizing: border-box;
883
+ }
884
+
885
+ :root {
886
+ --foreground-rgb: 0, 0, 0;
887
+ --background-start-rgb: 214, 219, 220;
888
+ --background-end-rgb: 255, 255, 255;
889
+ }
890
+
891
+ body {
892
+ margin: 0;
893
+ padding: 0;
894
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
895
+ background: linear-gradient(
896
+ to bottom,
897
+ transparent,
898
+ rgb(var(--background-end-rgb))
899
+ ) rgb(var(--background-start-rgb));
900
+ min-height: 100vh;
901
+ }
902
+
903
+ a {
904
+ color: #0070f3;
905
+ text-decoration: none;
906
+ }
907
+
908
+ a:hover {
909
+ text-decoration: underline;
910
+ }
911
+
912
+ .container {
913
+ max-width: 800px;
914
+ margin: 0 auto;
915
+ padding: 2rem;
916
+ }
917
+
918
+ .card {
919
+ background: white;
920
+ border-radius: 12px;
921
+ padding: 1.5rem;
922
+ box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.1);
923
+ margin-bottom: 1rem;
924
+ }
925
+
926
+ nav {
927
+ background: white;
928
+ padding: 1rem 2rem;
929
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
930
+ }
931
+
932
+ nav ul {
933
+ list-style: none;
934
+ margin: 0;
935
+ padding: 0;
936
+ display: flex;
937
+ gap: 1.5rem;
938
+ }
939
+
940
+ button {
941
+ padding: 0.75rem 1.5rem;
942
+ font-size: 1rem;
943
+ border: none;
944
+ border-radius: 8px;
945
+ background: #0070f3;
946
+ color: white;
947
+ cursor: pointer;
948
+ transition: background 0.2s;
949
+ }
950
+
951
+ button:hover {
952
+ background: #005cc5;
953
+ }
954
+
955
+ .counter {
956
+ text-align: center;
957
+ padding: 2rem;
958
+ }
959
+
960
+ .counter-display {
961
+ font-size: 4rem;
962
+ font-weight: bold;
963
+ }
964
+
965
+ .counter-buttons {
966
+ display: flex;
967
+ gap: 0.5rem;
968
+ justify-content: center;
969
+ margin-top: 1rem;
970
+ }
971
+
972
+ .layout-indicator {
973
+ position: fixed;
974
+ bottom: 1rem;
975
+ right: 1rem;
976
+ background: #333;
977
+ color: white;
978
+ padding: 0.5rem 1rem;
979
+ border-radius: 4px;
980
+ font-size: 0.75rem;
981
+ }
982
+ `
983
+ );
984
+
985
+ // Create root layout
986
+ vfs.writeFileSync(
987
+ '/app/layout.jsx',
988
+ `import React from 'react';
989
+
990
+ export default function RootLayout({ children }) {
991
+ return (
992
+ <html lang="en">
993
+ <head>
994
+ <title>Next.js App Router Demo</title>
995
+ </head>
996
+ <body>
997
+ <nav>
998
+ <ul>
999
+ <li><a href="/">Home</a></li>
1000
+ <li><a href="/about">About</a></li>
1001
+ <li><a href="/dashboard">Dashboard</a></li>
1002
+ <li><a href="/users/1">User 1</a></li>
1003
+ </ul>
1004
+ </nav>
1005
+ <main>
1006
+ {children}
1007
+ </main>
1008
+ <div className="layout-indicator">Root Layout</div>
1009
+ </body>
1010
+ </html>
1011
+ );
1012
+ }
1013
+ `
1014
+ );
1015
+
1016
+ // Create home page
1017
+ vfs.writeFileSync(
1018
+ '/app/page.jsx',
1019
+ `'use client';
1020
+
1021
+ import React, { useState } from 'react';
1022
+ import { usePathname } from 'next/navigation';
1023
+
1024
+ function Counter() {
1025
+ const [count, setCount] = useState(0);
1026
+
1027
+ return (
1028
+ <div className="counter card">
1029
+ <h2>Interactive Counter</h2>
1030
+ <div className="counter-display">{count}</div>
1031
+ <div className="counter-buttons">
1032
+ <button onClick={() => setCount(c => c - 1)}>-</button>
1033
+ <button onClick={() => setCount(0)}>Reset</button>
1034
+ <button onClick={() => setCount(c => c + 1)}>+</button>
1035
+ </div>
1036
+ <p style={{ marginTop: '1rem', color: '#666' }}>
1037
+ Edit this file and save - counter state will be preserved!
1038
+ </p>
1039
+ </div>
1040
+ );
1041
+ }
1042
+
1043
+ export default function HomePage() {
1044
+ const pathname = usePathname();
1045
+
1046
+ return (
1047
+ <div className="container">
1048
+ <h1>Welcome to Next.js App Router!</h1>
1049
+ <p>Current path: <code>{pathname}</code></p>
1050
+
1051
+ <Counter />
1052
+
1053
+ <div className="card">
1054
+ <h3>App Router Features</h3>
1055
+ <ul>
1056
+ <li><strong>Nested Layouts</strong> - See the layout indicator in the corner</li>
1057
+ <li><strong>usePathname()</strong> - App Router navigation hook</li>
1058
+ <li><strong>Client Components</strong> - Interactive components with 'use client'</li>
1059
+ <li><strong>Dynamic Routes</strong> - /users/[id] pattern</li>
1060
+ <li><strong>HMR</strong> - Edit files to see instant updates</li>
1061
+ </ul>
1062
+ </div>
1063
+
1064
+ <div className="card">
1065
+ <h3>How it works</h3>
1066
+ <p>
1067
+ This is a browser-based Next.js-compatible environment using:
1068
+ </p>
1069
+ <ul>
1070
+ <li>Virtual file system for /app directory</li>
1071
+ <li>Service Worker for request interception</li>
1072
+ <li>esbuild-wasm for JSX/TypeScript transformation</li>
1073
+ <li>React Refresh for state-preserving HMR</li>
1074
+ </ul>
1075
+ </div>
1076
+
1077
+ {/* Tailwind CSS Demo Section */}
1078
+ <div className="mt-6 p-6 bg-gradient-to-r from-cyan-500 to-blue-500 rounded-xl shadow-lg text-white">
1079
+ <h3 className="text-xl font-bold mb-2">Tailwind CSS is Ready!</h3>
1080
+ <p className="opacity-90 mb-4">
1081
+ This section uses Tailwind utility classes. Install a package to see more Tailwind demos.
1082
+ </p>
1083
+ <div className="flex gap-2 flex-wrap">
1084
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">p-6</span>
1085
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">rounded-xl</span>
1086
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">shadow-lg</span>
1087
+ <span className="px-3 py-1 bg-white/20 rounded-full text-sm">gradient</span>
1088
+ </div>
1089
+ </div>
1090
+ </div>
1091
+ );
1092
+ }
1093
+ `
1094
+ );
1095
+
1096
+ // Create about page
1097
+ vfs.writeFileSync(
1098
+ '/app/about/page.jsx',
1099
+ `'use client';
1100
+
1101
+ import React from 'react';
1102
+ import { usePathname, useRouter } from 'next/navigation';
1103
+
1104
+ export default function AboutPage() {
1105
+ const pathname = usePathname();
1106
+ const router = useRouter();
1107
+
1108
+ return (
1109
+ <div className="container">
1110
+ <h1>About Page</h1>
1111
+
1112
+ <div className="card">
1113
+ <p>Current path: <code>{pathname}</code></p>
1114
+ <p>
1115
+ This page demonstrates the <code>usePathname</code> and{' '}
1116
+ <code>useRouter</code> hooks from <code>next/navigation</code>.
1117
+ </p>
1118
+
1119
+ <button onClick={() => router.push('/')}>
1120
+ Go Home (router.push)
1121
+ </button>
1122
+ </div>
1123
+
1124
+ <div className="card">
1125
+ <h3>App Router vs Pages Router</h3>
1126
+ <p>
1127
+ The App Router uses <code>next/navigation</code> instead of{' '}
1128
+ <code>next/router</code>. Key differences:
1129
+ </p>
1130
+ <ul>
1131
+ <li><code>useRouter()</code> returns push, replace, refresh, back, forward</li>
1132
+ <li><code>usePathname()</code> returns current path</li>
1133
+ <li><code>useSearchParams()</code> returns URL search params</li>
1134
+ <li>No <code>query</code> object - use <code>useParams()</code> instead</li>
1135
+ </ul>
1136
+ </div>
1137
+ </div>
1138
+ );
1139
+ }
1140
+ `
1141
+ );
1142
+
1143
+ // Create dashboard with nested layout
1144
+ vfs.writeFileSync(
1145
+ '/app/dashboard/layout.jsx',
1146
+ `import React from 'react';
1147
+
1148
+ export default function DashboardLayout({ children }) {
1149
+ return (
1150
+ <div>
1151
+ <div style={{
1152
+ background: '#f0f4f8',
1153
+ padding: '1rem',
1154
+ marginBottom: '1rem',
1155
+ borderRadius: '8px',
1156
+ display: 'flex',
1157
+ gap: '1rem',
1158
+ flexWrap: 'wrap'
1159
+ }}>
1160
+ <a href="/dashboard" style={{ fontWeight: 'bold' }}>Dashboard Home</a>
1161
+ <span>|</span>
1162
+ <a href="/dashboard">Overview</a>
1163
+ <a href="/dashboard">Settings</a>
1164
+ <a href="/dashboard">Analytics</a>
1165
+ </div>
1166
+ {children}
1167
+ <div className="layout-indicator" style={{ bottom: '3rem' }}>
1168
+ Dashboard Layout (nested)
1169
+ </div>
1170
+ </div>
1171
+ );
1172
+ }
1173
+ `
1174
+ );
1175
+
1176
+ vfs.writeFileSync(
1177
+ '/app/dashboard/page.jsx',
1178
+ `'use client';
1179
+
1180
+ import React, { useState } from 'react';
1181
+
1182
+ export default function DashboardPage() {
1183
+ const [activeTab, setActiveTab] = useState('overview');
1184
+
1185
+ return (
1186
+ <div className="container">
1187
+ <h1>Dashboard</h1>
1188
+
1189
+ <div className="card">
1190
+ <p>
1191
+ This page demonstrates <strong>nested layouts</strong>. Notice there
1192
+ are two layout indicators - one from the root layout and one from the
1193
+ dashboard layout.
1194
+ </p>
1195
+ </div>
1196
+
1197
+ <div className="card">
1198
+ <h3>Dashboard Content</h3>
1199
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
1200
+ <button
1201
+ onClick={() => setActiveTab('overview')}
1202
+ style={{ opacity: activeTab === 'overview' ? 1 : 0.6 }}
1203
+ >
1204
+ Overview
1205
+ </button>
1206
+ <button
1207
+ onClick={() => setActiveTab('stats')}
1208
+ style={{ opacity: activeTab === 'stats' ? 1 : 0.6 }}
1209
+ >
1210
+ Stats
1211
+ </button>
1212
+ <button
1213
+ onClick={() => setActiveTab('settings')}
1214
+ style={{ opacity: activeTab === 'settings' ? 1 : 0.6 }}
1215
+ >
1216
+ Settings
1217
+ </button>
1218
+ </div>
1219
+
1220
+ <div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '8px' }}>
1221
+ {activeTab === 'overview' && (
1222
+ <div>
1223
+ <h4>Overview</h4>
1224
+ <p>Welcome to your dashboard. This is the overview tab.</p>
1225
+ </div>
1226
+ )}
1227
+ {activeTab === 'stats' && (
1228
+ <div>
1229
+ <h4>Statistics</h4>
1230
+ <p>Views: 1,234 | Visitors: 567 | Conversions: 89</p>
1231
+ </div>
1232
+ )}
1233
+ {activeTab === 'settings' && (
1234
+ <div>
1235
+ <h4>Settings</h4>
1236
+ <p>Configure your dashboard preferences here.</p>
1237
+ </div>
1238
+ )}
1239
+ </div>
1240
+ </div>
1241
+ </div>
1242
+ );
1243
+ }
1244
+ `
1245
+ );
1246
+
1247
+ // Create dynamic user page
1248
+ vfs.writeFileSync(
1249
+ '/app/users/[id]/page.jsx',
1250
+ `'use client';
1251
+
1252
+ import React, { useState, useEffect } from 'react';
1253
+ import { usePathname, useRouter } from 'next/navigation';
1254
+
1255
+ const users = {
1256
+ '1': { name: 'Alice Johnson', email: 'alice@example.com', role: 'Developer' },
1257
+ '2': { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
1258
+ '3': { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
1259
+ };
1260
+
1261
+ export default function UserPage() {
1262
+ const pathname = usePathname();
1263
+ const router = useRouter();
1264
+ const [userId, setUserId] = useState(null);
1265
+
1266
+ useEffect(() => {
1267
+ // Extract user ID from pathname
1268
+ const match = pathname.match(/\\/users\\/([^\\/]+)/);
1269
+ if (match) {
1270
+ setUserId(match[1]);
1271
+ }
1272
+ }, [pathname]);
1273
+
1274
+ const user = userId ? users[userId] : null;
1275
+
1276
+ return (
1277
+ <div className="container">
1278
+ <h1>User Profile</h1>
1279
+
1280
+ {user ? (
1281
+ <div className="card">
1282
+ <h2>{user.name}</h2>
1283
+ <p><strong>Email:</strong> {user.email}</p>
1284
+ <p><strong>Role:</strong> {user.role}</p>
1285
+ <p><strong>User ID:</strong> {userId}</p>
1286
+ </div>
1287
+ ) : userId ? (
1288
+ <div className="card">
1289
+ <p>User not found: {userId}</p>
1290
+ </div>
1291
+ ) : (
1292
+ <div className="card">
1293
+ <p>Loading...</p>
1294
+ </div>
1295
+ )}
1296
+
1297
+ <div className="card">
1298
+ <h3>Navigate to other users:</h3>
1299
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
1300
+ <button onClick={() => router.push('/users/1')}>User 1</button>
1301
+ <button onClick={() => router.push('/users/2')}>User 2</button>
1302
+ <button onClick={() => router.push('/users/3')}>User 3</button>
1303
+ </div>
1304
+ </div>
1305
+
1306
+ <div className="card">
1307
+ <h3>Dynamic Routes in App Router</h3>
1308
+ <p>
1309
+ This page uses the <code>[id]</code> dynamic segment.
1310
+ The folder structure is: <code>/app/users/[id]/page.jsx</code>
1311
+ </p>
1312
+ <p>
1313
+ In the full Next.js, you'd use <code>useParams()</code> to get the ID,
1314
+ but here we're parsing it from <code>usePathname()</code>.
1315
+ </p>
1316
+ </div>
1317
+ </div>
1318
+ );
1319
+ }
1320
+ `
1321
+ );
1322
+
1323
+ // Create TypeScript example page
1324
+ vfs.mkdirSync('/app/typescript', { recursive: true });
1325
+ vfs.writeFileSync(
1326
+ '/app/typescript/page.tsx',
1327
+ `'use client';
1328
+
1329
+ import React, { useState, useCallback, useMemo } from 'react';
1330
+ import { usePathname, useRouter } from 'next/navigation';
1331
+
1332
+ // TypeScript types
1333
+ type FilterType = 'all' | 'active' | 'completed';
1334
+
1335
+ interface Task {
1336
+ id: number;
1337
+ title: string;
1338
+ priority: 'low' | 'medium' | 'high';
1339
+ done: boolean;
1340
+ createdAt: Date;
1341
+ }
1342
+
1343
+ interface TaskListProps {
1344
+ tasks: Task[];
1345
+ onToggle: (id: number) => void;
1346
+ onDelete: (id: number) => void;
1347
+ }
1348
+
1349
+ // Priority badge component with typed props
1350
+ function PriorityBadge({ priority }: { priority: Task['priority'] }): JSX.Element {
1351
+ const colors: Record<Task['priority'], string> = {
1352
+ low: '#4caf50',
1353
+ medium: '#ff9800',
1354
+ high: '#f44336',
1355
+ };
1356
+
1357
+ return (
1358
+ <span style={{
1359
+ background: colors[priority],
1360
+ color: 'white',
1361
+ padding: '2px 8px',
1362
+ borderRadius: '12px',
1363
+ fontSize: '0.7rem',
1364
+ textTransform: 'uppercase',
1365
+ }}>
1366
+ {priority}
1367
+ </span>
1368
+ );
1369
+ }
1370
+
1371
+ // Task list component
1372
+ function TaskList({ tasks, onToggle, onDelete }: TaskListProps): JSX.Element {
1373
+ if (tasks.length === 0) {
1374
+ return <p style={{ color: '#888', fontStyle: 'italic' }}>No tasks to show</p>;
1375
+ }
1376
+
1377
+ return (
1378
+ <div>
1379
+ {tasks.map((task: Task) => (
1380
+ <div
1381
+ key={task.id}
1382
+ style={{
1383
+ display: 'flex',
1384
+ alignItems: 'center',
1385
+ gap: '0.75rem',
1386
+ padding: '0.75rem',
1387
+ background: task.done ? '#f5f5f5' : 'white',
1388
+ borderRadius: '8px',
1389
+ marginBottom: '0.5rem',
1390
+ border: '1px solid #e0e0e0',
1391
+ }}
1392
+ >
1393
+ <input
1394
+ type="checkbox"
1395
+ checked={task.done}
1396
+ onChange={() => onToggle(task.id)}
1397
+ style={{ width: '18px', height: '18px' }}
1398
+ />
1399
+ <div style={{ flex: 1 }}>
1400
+ <div style={{
1401
+ textDecoration: task.done ? 'line-through' : 'none',
1402
+ color: task.done ? '#888' : '#333',
1403
+ }}>
1404
+ {task.title}
1405
+ </div>
1406
+ <div style={{ fontSize: '0.75rem', color: '#888', marginTop: '2px' }}>
1407
+ Created: {task.createdAt.toLocaleDateString()}
1408
+ </div>
1409
+ </div>
1410
+ <PriorityBadge priority={task.priority} />
1411
+ <button
1412
+ onClick={() => onDelete(task.id)}
1413
+ style={{
1414
+ padding: '4px 12px',
1415
+ background: '#ffebee',
1416
+ border: 'none',
1417
+ borderRadius: '4px',
1418
+ color: '#c62828',
1419
+ cursor: 'pointer',
1420
+ }}
1421
+ >
1422
+ ×
1423
+ </button>
1424
+ </div>
1425
+ ))}
1426
+ </div>
1427
+ );
1428
+ }
1429
+
1430
+ // Main page component
1431
+ export default function TypeScriptAppRouterDemo(): JSX.Element {
1432
+ const pathname = usePathname();
1433
+ const router = useRouter();
1434
+
1435
+ const [tasks, setTasks] = useState<Task[]>([
1436
+ { id: 1, title: 'Learn TypeScript generics', priority: 'high', done: false, createdAt: new Date() },
1437
+ { id: 2, title: 'Build App Router pages', priority: 'medium', done: true, createdAt: new Date() },
1438
+ { id: 3, title: 'Test HMR with types', priority: 'low', done: false, createdAt: new Date() },
1439
+ ]);
1440
+
1441
+ const [newTask, setNewTask] = useState<string>('');
1442
+ const [priority, setPriority] = useState<Task['priority']>('medium');
1443
+ const [filter, setFilter] = useState<FilterType>('all');
1444
+
1445
+ // Typed callbacks
1446
+ const addTask = useCallback((): void => {
1447
+ if (!newTask.trim()) return;
1448
+
1449
+ const task: Task = {
1450
+ id: Date.now(),
1451
+ title: newTask.trim(),
1452
+ priority,
1453
+ done: false,
1454
+ createdAt: new Date(),
1455
+ };
1456
+
1457
+ setTasks((prev: Task[]) => [...prev, task]);
1458
+ setNewTask('');
1459
+ }, [newTask, priority]);
1460
+
1461
+ const toggleTask = useCallback((id: number): void => {
1462
+ setTasks((prev: Task[]) =>
1463
+ prev.map((t: Task) => t.id === id ? { ...t, done: !t.done } : t)
1464
+ );
1465
+ }, []);
1466
+
1467
+ const deleteTask = useCallback((id: number): void => {
1468
+ setTasks((prev: Task[]) => prev.filter((t: Task) => t.id !== id));
1469
+ }, []);
1470
+
1471
+ // Memoized filtered tasks
1472
+ const filteredTasks = useMemo((): Task[] => {
1473
+ switch (filter) {
1474
+ case 'active': return tasks.filter((t: Task) => !t.done);
1475
+ case 'completed': return tasks.filter((t: Task) => t.done);
1476
+ default: return tasks;
1477
+ }
1478
+ }, [tasks, filter]);
1479
+
1480
+ // Stats with explicit types
1481
+ const stats: { total: number; done: number; pending: number } = useMemo(() => ({
1482
+ total: tasks.length,
1483
+ done: tasks.filter((t: Task) => t.done).length,
1484
+ pending: tasks.filter((t: Task) => !t.done).length,
1485
+ }), [tasks]);
1486
+
1487
+ return (
1488
+ <div className="container">
1489
+ <h1>TypeScript + App Router</h1>
1490
+ <p>Path: <code>{pathname}</code> | This is <code>/app/typescript/page.tsx</code></p>
1491
+
1492
+ <div className="card">
1493
+ <h3>Task Manager ({stats.done}/{stats.total} done)</h3>
1494
+
1495
+ {/* Add task form */}
1496
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
1497
+ <input
1498
+ type="text"
1499
+ value={newTask}
1500
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewTask(e.target.value)}
1501
+ onKeyDown={(e: React.KeyboardEvent) => e.key === 'Enter' && addTask()}
1502
+ placeholder="Add a new task..."
1503
+ style={{ flex: 1, padding: '0.5rem', borderRadius: '4px', border: '1px solid #ccc' }}
1504
+ />
1505
+ <select
1506
+ value={priority}
1507
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
1508
+ setPriority(e.target.value as Task['priority'])
1509
+ }
1510
+ style={{ padding: '0.5rem', borderRadius: '4px', border: '1px solid #ccc' }}
1511
+ >
1512
+ <option value="low">Low</option>
1513
+ <option value="medium">Medium</option>
1514
+ <option value="high">High</option>
1515
+ </select>
1516
+ <button onClick={addTask}>Add</button>
1517
+ </div>
1518
+
1519
+ {/* Filter buttons */}
1520
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
1521
+ {(['all', 'active', 'completed'] as FilterType[]).map((f: FilterType) => (
1522
+ <button
1523
+ key={f}
1524
+ onClick={() => setFilter(f)}
1525
+ style={{
1526
+ background: filter === f ? '#0070f3' : '#e0e0e0',
1527
+ color: filter === f ? 'white' : '#333',
1528
+ }}
1529
+ >
1530
+ {f.charAt(0).toUpperCase() + f.slice(1)}
1531
+ {f === 'all' ? \` (\${stats.total})\` : f === 'active' ? \` (\${stats.pending})\` : \` (\${stats.done})\`}
1532
+ </button>
1533
+ ))}
1534
+ </div>
1535
+
1536
+ <TaskList tasks={filteredTasks} onToggle={toggleTask} onDelete={deleteTask} />
1537
+ </div>
1538
+
1539
+ <div className="card">
1540
+ <h3>TypeScript Features Demonstrated</h3>
1541
+ <ul>
1542
+ <li><code>type FilterType = 'all' | 'active' | 'completed'</code> - Union types</li>
1543
+ <li><code>interface Task</code> with typed properties</li>
1544
+ <li><code>Task['priority']</code> - Indexed access types</li>
1545
+ <li><code>Record&lt;Task['priority'], string&gt;</code> - Utility types</li>
1546
+ <li><code>useMemo&lt;Task[]&gt;</code> - Generic hooks</li>
1547
+ <li><code>React.ChangeEvent&lt;HTMLInputElement&gt;</code> - Event types</li>
1548
+ </ul>
1549
+ <button onClick={() => router.push('/')} style={{ marginTop: '1rem' }}>
1550
+ ← Back to Home
1551
+ </button>
1552
+ </div>
1553
+ </div>
1554
+ );
1555
+ }
1556
+ `
1557
+ );
1558
+
1559
+ // Create public files
1560
+ vfs.writeFileSync('/public/favicon.ico', 'favicon placeholder');
1561
+ vfs.writeFileSync('/public/robots.txt', 'User-agent: *\nAllow: /');
1562
+ }
1563
+
1564
+ /**
1565
+ * Initialize the Next.js demo
1566
+ */
1567
+ export async function initNextDemo(
1568
+ outputElement: HTMLElement,
1569
+ options: { useWorker?: boolean } = {}
1570
+ ): Promise<{ vfs: VirtualFS; runtime: IRuntime }> {
1571
+ const { useWorker = false } = options;
1572
+
1573
+ const log = (message: string) => {
1574
+ const line = document.createElement('div');
1575
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
1576
+ outputElement.appendChild(line);
1577
+ outputElement.scrollTop = outputElement.scrollHeight;
1578
+ };
1579
+
1580
+ log('Creating virtual file system...');
1581
+ const vfs = new VirtualFS();
1582
+
1583
+ log('Creating Next.js project structure...');
1584
+ createNextProject(vfs);
1585
+
1586
+ log(`Initializing runtime (${useWorker ? 'Web Worker mode' : 'main thread'})...`);
1587
+ const runtime = await createRuntime(vfs, {
1588
+ dangerouslyAllowSameOrigin: true, // Demo uses trusted code
1589
+ useWorker,
1590
+ cwd: '/',
1591
+ env: {
1592
+ NODE_ENV: 'development',
1593
+ },
1594
+ onConsole: (method, args) => {
1595
+ const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
1596
+ log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
1597
+ },
1598
+ });
1599
+
1600
+ if (useWorker) {
1601
+ log('Runtime is running in a Web Worker for better UI responsiveness');
1602
+ }
1603
+
1604
+ log('Setting up file watcher...');
1605
+ vfs.watch('/pages', { recursive: true }, (eventType, filename) => {
1606
+ log(`File ${eventType}: ${filename}`);
1607
+ });
1608
+
1609
+ log('Next.js demo initialized!');
1610
+ log('');
1611
+ log('Virtual FS contents:');
1612
+ listFiles(vfs, '/', log, ' ');
1613
+
1614
+ return { vfs, runtime };
1615
+ }
1616
+
1617
+ /**
1618
+ * Start the Next.js dev server using Service Worker approach
1619
+ */
1620
+ export async function startNextDevServer(
1621
+ vfs: VirtualFS,
1622
+ options: {
1623
+ port?: number;
1624
+ log?: (message: string) => void;
1625
+ } = {}
1626
+ ): Promise<{
1627
+ server: NextDevServer;
1628
+ url: string;
1629
+ stop: () => void;
1630
+ }> {
1631
+ const port = options.port || 3001;
1632
+ const log = options.log || console.log;
1633
+
1634
+ log('Starting Next.js dev server...');
1635
+
1636
+ // Create NextDevServer
1637
+ const server = new NextDevServer(vfs, { port, root: '/' });
1638
+
1639
+ // Get the server bridge
1640
+ const bridge = getServerBridge();
1641
+
1642
+ // Initialize Service Worker
1643
+ try {
1644
+ log('Initializing Service Worker...');
1645
+ await bridge.initServiceWorker();
1646
+ log('Service Worker ready');
1647
+ } catch (error) {
1648
+ log(`Warning: Service Worker failed to initialize: ${error}`);
1649
+ log('Falling back to direct request handling...');
1650
+ }
1651
+
1652
+ // Register the server with the bridge
1653
+ bridge.on('server-ready', (p: unknown, u: unknown) => {
1654
+ log(`Server ready at ${u}`);
1655
+ });
1656
+
1657
+ // Wire up the NextDevServer to handle requests through the bridge
1658
+ const httpServer = createHttpServerWrapper(server);
1659
+ bridge.registerServer(httpServer, port);
1660
+
1661
+ // Start watching for file changes
1662
+ server.start();
1663
+ log('File watcher started');
1664
+
1665
+ // Set up HMR event forwarding
1666
+ server.on('hmr-update', (update: unknown) => {
1667
+ log(`HMR update: ${JSON.stringify(update)}`);
1668
+ });
1669
+
1670
+ const url = bridge.getServerUrl(port);
1671
+ log(`Next.js dev server running at: ${url}/`);
1672
+
1673
+ return {
1674
+ server,
1675
+ url: url + '/',
1676
+ stop: () => {
1677
+ server.stop();
1678
+ bridge.unregisterServer(port);
1679
+ },
1680
+ };
1681
+ }
1682
+
1683
+ /**
1684
+ * Create an http.Server-compatible wrapper around NextDevServer
1685
+ */
1686
+ function createHttpServerWrapper(devServer: NextDevServer) {
1687
+ return {
1688
+ listening: true,
1689
+ address: () => ({ port: devServer.getPort(), address: '0.0.0.0', family: 'IPv4' }),
1690
+ async handleRequest(
1691
+ method: string,
1692
+ url: string,
1693
+ headers: Record<string, string>,
1694
+ body?: string | Buffer
1695
+ ) {
1696
+ const bodyBuffer = body
1697
+ ? typeof body === 'string'
1698
+ ? Buffer.from(body)
1699
+ : body
1700
+ : undefined;
1701
+ return devServer.handleRequest(method, url, headers, bodyBuffer);
1702
+ },
1703
+ };
1704
+ }
1705
+
1706
+ function listFiles(
1707
+ vfs: VirtualFS,
1708
+ path: string,
1709
+ log: (msg: string) => void,
1710
+ indent: string
1711
+ ): void {
1712
+ try {
1713
+ const entries = vfs.readdirSync(path);
1714
+ for (const entry of entries) {
1715
+ const fullPath = path === '/' ? '/' + entry : path + '/' + entry;
1716
+ const stats = vfs.statSync(fullPath);
1717
+ if (stats.isDirectory()) {
1718
+ log(`${indent}📁 ${entry}/`);
1719
+ listFiles(vfs, fullPath, log, indent + ' ');
1720
+ } else {
1721
+ log(`${indent}📄 ${entry}`);
1722
+ }
1723
+ }
1724
+ } catch (e) {
1725
+ log(`${indent}Error: ${e}`);
1726
+ }
1727
+ }
1728
+
1729
+ /**
1730
+ * Initialize the Next.js App Router demo
1731
+ */
1732
+ export async function initNextAppRouterDemo(
1733
+ outputElement: HTMLElement,
1734
+ options: { useWorker?: boolean } = {}
1735
+ ): Promise<{ vfs: VirtualFS; runtime: IRuntime }> {
1736
+ const { useWorker = false } = options;
1737
+
1738
+ const log = (message: string) => {
1739
+ const line = document.createElement('div');
1740
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
1741
+ outputElement.appendChild(line);
1742
+ outputElement.scrollTop = outputElement.scrollHeight;
1743
+ };
1744
+
1745
+ log('Creating virtual file system...');
1746
+ const vfs = new VirtualFS();
1747
+
1748
+ log('Creating Next.js App Router project structure...');
1749
+ createNextAppRouterProject(vfs);
1750
+
1751
+ log(`Initializing runtime (${useWorker ? 'Web Worker mode' : 'main thread'})...`);
1752
+ const runtime = await createRuntime(vfs, {
1753
+ dangerouslyAllowSameOrigin: true, // Demo uses trusted code
1754
+ useWorker,
1755
+ cwd: '/',
1756
+ env: {
1757
+ NODE_ENV: 'development',
1758
+ },
1759
+ onConsole: (method, args) => {
1760
+ const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
1761
+ log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
1762
+ },
1763
+ });
1764
+
1765
+ if (useWorker) {
1766
+ log('Runtime is running in a Web Worker for better UI responsiveness');
1767
+ }
1768
+
1769
+ log('Setting up file watcher...');
1770
+ vfs.watch('/app', { recursive: true }, (eventType, filename) => {
1771
+ log(`File ${eventType}: ${filename}`);
1772
+ });
1773
+
1774
+ log('Next.js App Router demo initialized!');
1775
+ log('');
1776
+ log('Virtual FS contents:');
1777
+ listFiles(vfs, '/', log, ' ');
1778
+
1779
+ return { vfs, runtime };
1780
+ }
1781
+
1782
+ // Export for use in the demo page
1783
+ export { VirtualFS, Runtime, NextDevServer, PackageManager, createRuntime };
1784
+ export type { InstallOptions, InstallResult, IRuntime };