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
package/src/next-demo.ts
ADDED
|
@@ -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<Todo[]></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<Task['priority'], string></code> - Utility types</li>
|
|
1546
|
+
<li><code>useMemo<Task[]></code> - Generic hooks</li>
|
|
1547
|
+
<li><code>React.ChangeEvent<HTMLInputElement></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 };
|