almostnode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +731 -0
- package/dist/__sw__.js +394 -0
- package/dist/ai-chatbot-demo-entry.d.ts +6 -0
- package/dist/ai-chatbot-demo-entry.d.ts.map +1 -0
- package/dist/ai-chatbot-demo.d.ts +42 -0
- package/dist/ai-chatbot-demo.d.ts.map +1 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js +60543 -0
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +1 -0
- package/dist/convex-app-demo-entry.d.ts +6 -0
- package/dist/convex-app-demo-entry.d.ts.map +1 -0
- package/dist/convex-app-demo.d.ts +68 -0
- package/dist/convex-app-demo.d.ts.map +1 -0
- package/dist/cors-proxy.d.ts +46 -0
- package/dist/cors-proxy.d.ts.map +1 -0
- package/dist/create-runtime.d.ts +42 -0
- package/dist/create-runtime.d.ts.map +1 -0
- package/dist/demo.d.ts +6 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/dev-server.d.ts +97 -0
- package/dist/dev-server.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +202 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +85 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -0
- package/dist/index.cjs +14965 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +14867 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next-demo.d.ts +49 -0
- package/dist/next-demo.d.ts.map +1 -0
- package/dist/npm/index.d.ts +71 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/registry.d.ts +66 -0
- package/dist/npm/registry.d.ts.map +1 -0
- package/dist/npm/resolver.d.ts +52 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/tarball.d.ts +29 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/runtime-interface.d.ts +90 -0
- package/dist/runtime-interface.d.ts.map +1 -0
- package/dist/runtime.d.ts +103 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/sandbox-helpers.d.ts +43 -0
- package/dist/sandbox-helpers.d.ts.map +1 -0
- package/dist/sandbox-runtime.d.ts +65 -0
- package/dist/sandbox-runtime.d.ts.map +1 -0
- package/dist/server-bridge.d.ts +89 -0
- package/dist/server-bridge.d.ts.map +1 -0
- package/dist/shims/assert.d.ts +51 -0
- package/dist/shims/assert.d.ts.map +1 -0
- package/dist/shims/async_hooks.d.ts +37 -0
- package/dist/shims/async_hooks.d.ts.map +1 -0
- package/dist/shims/buffer.d.ts +20 -0
- package/dist/shims/buffer.d.ts.map +1 -0
- package/dist/shims/child_process-browser.d.ts +92 -0
- package/dist/shims/child_process-browser.d.ts.map +1 -0
- package/dist/shims/child_process.d.ts +93 -0
- package/dist/shims/child_process.d.ts.map +1 -0
- package/dist/shims/chokidar.d.ts +55 -0
- package/dist/shims/chokidar.d.ts.map +1 -0
- package/dist/shims/cluster.d.ts +52 -0
- package/dist/shims/cluster.d.ts.map +1 -0
- package/dist/shims/crypto.d.ts +122 -0
- package/dist/shims/crypto.d.ts.map +1 -0
- package/dist/shims/dgram.d.ts +34 -0
- package/dist/shims/dgram.d.ts.map +1 -0
- package/dist/shims/diagnostics_channel.d.ts +80 -0
- package/dist/shims/diagnostics_channel.d.ts.map +1 -0
- package/dist/shims/dns.d.ts +87 -0
- package/dist/shims/dns.d.ts.map +1 -0
- package/dist/shims/domain.d.ts +25 -0
- package/dist/shims/domain.d.ts.map +1 -0
- package/dist/shims/esbuild.d.ts +105 -0
- package/dist/shims/esbuild.d.ts.map +1 -0
- package/dist/shims/events.d.ts +37 -0
- package/dist/shims/events.d.ts.map +1 -0
- package/dist/shims/fs.d.ts +115 -0
- package/dist/shims/fs.d.ts.map +1 -0
- package/dist/shims/fsevents.d.ts +67 -0
- package/dist/shims/fsevents.d.ts.map +1 -0
- package/dist/shims/http.d.ts +217 -0
- package/dist/shims/http.d.ts.map +1 -0
- package/dist/shims/http2.d.ts +81 -0
- package/dist/shims/http2.d.ts.map +1 -0
- package/dist/shims/https.d.ts +36 -0
- package/dist/shims/https.d.ts.map +1 -0
- package/dist/shims/inspector.d.ts +25 -0
- package/dist/shims/inspector.d.ts.map +1 -0
- package/dist/shims/module.d.ts +22 -0
- package/dist/shims/module.d.ts.map +1 -0
- package/dist/shims/net.d.ts +100 -0
- package/dist/shims/net.d.ts.map +1 -0
- package/dist/shims/os.d.ts +159 -0
- package/dist/shims/os.d.ts.map +1 -0
- package/dist/shims/path.d.ts +72 -0
- package/dist/shims/path.d.ts.map +1 -0
- package/dist/shims/perf_hooks.d.ts +50 -0
- package/dist/shims/perf_hooks.d.ts.map +1 -0
- package/dist/shims/process.d.ts +93 -0
- package/dist/shims/process.d.ts.map +1 -0
- package/dist/shims/querystring.d.ts +23 -0
- package/dist/shims/querystring.d.ts.map +1 -0
- package/dist/shims/readdirp.d.ts +52 -0
- package/dist/shims/readdirp.d.ts.map +1 -0
- package/dist/shims/readline.d.ts +62 -0
- package/dist/shims/readline.d.ts.map +1 -0
- package/dist/shims/rollup.d.ts +34 -0
- package/dist/shims/rollup.d.ts.map +1 -0
- package/dist/shims/sentry.d.ts +163 -0
- package/dist/shims/sentry.d.ts.map +1 -0
- package/dist/shims/stream.d.ts +181 -0
- package/dist/shims/stream.d.ts.map +1 -0
- package/dist/shims/tls.d.ts +53 -0
- package/dist/shims/tls.d.ts.map +1 -0
- package/dist/shims/tty.d.ts +30 -0
- package/dist/shims/tty.d.ts.map +1 -0
- package/dist/shims/url.d.ts +64 -0
- package/dist/shims/url.d.ts.map +1 -0
- package/dist/shims/util.d.ts +106 -0
- package/dist/shims/util.d.ts.map +1 -0
- package/dist/shims/v8.d.ts +73 -0
- package/dist/shims/v8.d.ts.map +1 -0
- package/dist/shims/vfs-adapter.d.ts +126 -0
- package/dist/shims/vfs-adapter.d.ts.map +1 -0
- package/dist/shims/vm.d.ts +45 -0
- package/dist/shims/vm.d.ts.map +1 -0
- package/dist/shims/worker_threads.d.ts +66 -0
- package/dist/shims/worker_threads.d.ts.map +1 -0
- package/dist/shims/ws.d.ts +66 -0
- package/dist/shims/ws.d.ts.map +1 -0
- package/dist/shims/zlib.d.ts +161 -0
- package/dist/shims/zlib.d.ts.map +1 -0
- package/dist/transform.d.ts +24 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +226 -0
- package/dist/virtual-fs.d.ts.map +1 -0
- package/dist/vite-demo.d.ts +35 -0
- package/dist/vite-demo.d.ts.map +1 -0
- package/dist/vite-sw.js +132 -0
- package/dist/worker/runtime-worker.d.ts +8 -0
- package/dist/worker/runtime-worker.d.ts.map +1 -0
- package/dist/worker-runtime.d.ts +50 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/ai-chatbot-demo-entry.ts +244 -0
- package/src/ai-chatbot-demo.ts +509 -0
- package/src/convex-app-demo-entry.ts +1107 -0
- package/src/convex-app-demo.ts +1316 -0
- package/src/cors-proxy.ts +81 -0
- package/src/create-runtime.ts +147 -0
- package/src/demo.ts +304 -0
- package/src/dev-server.ts +274 -0
- package/src/frameworks/next-dev-server.ts +2224 -0
- package/src/frameworks/vite-dev-server.ts +702 -0
- package/src/index.ts +101 -0
- package/src/next-demo.ts +1784 -0
- package/src/npm/index.ts +347 -0
- package/src/npm/registry.ts +152 -0
- package/src/npm/resolver.ts +385 -0
- package/src/npm/tarball.ts +209 -0
- package/src/runtime-interface.ts +103 -0
- package/src/runtime.ts +1046 -0
- package/src/sandbox-helpers.ts +173 -0
- package/src/sandbox-runtime.ts +252 -0
- package/src/server-bridge.ts +426 -0
- package/src/shims/assert.ts +664 -0
- package/src/shims/async_hooks.ts +86 -0
- package/src/shims/buffer.ts +75 -0
- package/src/shims/child_process-browser.ts +217 -0
- package/src/shims/child_process.ts +463 -0
- package/src/shims/chokidar.ts +313 -0
- package/src/shims/cluster.ts +67 -0
- package/src/shims/crypto.ts +830 -0
- package/src/shims/dgram.ts +47 -0
- package/src/shims/diagnostics_channel.ts +196 -0
- package/src/shims/dns.ts +172 -0
- package/src/shims/domain.ts +58 -0
- package/src/shims/esbuild.ts +805 -0
- package/src/shims/events.ts +195 -0
- package/src/shims/fs.ts +803 -0
- package/src/shims/fsevents.ts +63 -0
- package/src/shims/http.ts +904 -0
- package/src/shims/http2.ts +96 -0
- package/src/shims/https.ts +86 -0
- package/src/shims/inspector.ts +30 -0
- package/src/shims/module.ts +82 -0
- package/src/shims/net.ts +359 -0
- package/src/shims/os.ts +195 -0
- package/src/shims/path.ts +199 -0
- package/src/shims/perf_hooks.ts +92 -0
- package/src/shims/process.ts +346 -0
- package/src/shims/querystring.ts +97 -0
- package/src/shims/readdirp.ts +228 -0
- package/src/shims/readline.ts +110 -0
- package/src/shims/rollup.ts +80 -0
- package/src/shims/sentry.ts +133 -0
- package/src/shims/stream.ts +1126 -0
- package/src/shims/tls.ts +95 -0
- package/src/shims/tty.ts +64 -0
- package/src/shims/url.ts +171 -0
- package/src/shims/util.ts +312 -0
- package/src/shims/v8.ts +113 -0
- package/src/shims/vfs-adapter.ts +402 -0
- package/src/shims/vm.ts +83 -0
- package/src/shims/worker_threads.ts +111 -0
- package/src/shims/ws.ts +382 -0
- package/src/shims/zlib.ts +289 -0
- package/src/transform.ts +313 -0
- package/src/types/external.d.ts +67 -0
- package/src/virtual-fs.ts +903 -0
- package/src/vite-demo.ts +577 -0
- package/src/worker/runtime-worker.ts +128 -0
- package/src/worker-runtime.ts +145 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Proxy Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides optional CORS proxy support for fetching external APIs
|
|
5
|
+
* that don't allow browser origins.
|
|
6
|
+
*
|
|
7
|
+
* No default proxy is configured for security reasons.
|
|
8
|
+
* Users must explicitly set a proxy URL if needed.
|
|
9
|
+
*
|
|
10
|
+
* Example usage:
|
|
11
|
+
* setCorsProxy('https://corsproxy.io/?');
|
|
12
|
+
* const response = await proxyFetch('https://api.example.com/data');
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// No default proxy - must be explicitly set
|
|
16
|
+
let proxyUrl: string | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Set the CORS proxy URL
|
|
20
|
+
* @param url - Proxy URL (e.g., 'https://corsproxy.io/?')
|
|
21
|
+
* The target URL will be appended as an encoded parameter.
|
|
22
|
+
* Set to null to disable proxy and use direct fetch.
|
|
23
|
+
*/
|
|
24
|
+
export function setCorsProxy(url: string | null): void {
|
|
25
|
+
proxyUrl = url;
|
|
26
|
+
if (url) {
|
|
27
|
+
console.log(`[cors-proxy] Proxy configured: ${url}`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log('[cors-proxy] Proxy disabled, using direct fetch');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the current CORS proxy URL
|
|
35
|
+
* @returns The configured proxy URL, or null if not set
|
|
36
|
+
*/
|
|
37
|
+
export function getCorsProxy(): string | null {
|
|
38
|
+
return proxyUrl;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a proxy is configured
|
|
43
|
+
*/
|
|
44
|
+
export function hasProxy(): boolean {
|
|
45
|
+
return proxyUrl !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch with optional CORS proxy
|
|
50
|
+
*
|
|
51
|
+
* If a proxy is configured, the request goes through the proxy.
|
|
52
|
+
* Otherwise, a direct fetch is performed (may hit CORS issues).
|
|
53
|
+
*
|
|
54
|
+
* @param url - The target URL to fetch
|
|
55
|
+
* @param options - Standard fetch options
|
|
56
|
+
* @returns Promise<Response>
|
|
57
|
+
*/
|
|
58
|
+
export async function proxyFetch(
|
|
59
|
+
url: string,
|
|
60
|
+
options?: RequestInit
|
|
61
|
+
): Promise<Response> {
|
|
62
|
+
if (proxyUrl) {
|
|
63
|
+
// Route through proxy
|
|
64
|
+
const proxiedUrl = proxyUrl + encodeURIComponent(url);
|
|
65
|
+
return fetch(proxiedUrl, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// No proxy configured - direct fetch
|
|
69
|
+
return fetch(url, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a proxied URL without fetching
|
|
74
|
+
* Useful for displaying the URL or using with other fetch mechanisms
|
|
75
|
+
*/
|
|
76
|
+
export function buildProxyUrl(url: string): string {
|
|
77
|
+
if (proxyUrl) {
|
|
78
|
+
return proxyUrl + encodeURIComponent(url);
|
|
79
|
+
}
|
|
80
|
+
return url;
|
|
81
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Factory - Create sandboxed, worker, or main-thread runtime
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: By default, createRuntime requires either:
|
|
5
|
+
* 1. A sandbox URL for cross-origin isolation (recommended)
|
|
6
|
+
* 2. Explicit opt-in via dangerouslyAllowSameOrigin for trusted code
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* // Secure: Cross-origin sandbox (recommended for untrusted code)
|
|
10
|
+
* const runtime = await createRuntime(vfs, {
|
|
11
|
+
* sandbox: 'https://myapp-sandbox.vercel.app'
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Same-origin with Worker (for demos/trusted code)
|
|
15
|
+
* const runtime = await createRuntime(vfs, {
|
|
16
|
+
* dangerouslyAllowSameOrigin: true,
|
|
17
|
+
* useWorker: true
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Same-origin main thread (least secure, trusted code only)
|
|
21
|
+
* const runtime = await createRuntime(vfs, {
|
|
22
|
+
* dangerouslyAllowSameOrigin: true
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Runtime } from './runtime';
|
|
27
|
+
import { WorkerRuntime } from './worker-runtime';
|
|
28
|
+
import { SandboxRuntime } from './sandbox-runtime';
|
|
29
|
+
import type { VirtualFS } from './virtual-fs';
|
|
30
|
+
import type { IRuntime, IExecuteResult, CreateRuntimeOptions, IRuntimeOptions } from './runtime-interface';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if Web Workers are available in the current environment
|
|
34
|
+
*/
|
|
35
|
+
function isWorkerAvailable(): boolean {
|
|
36
|
+
return typeof Worker !== 'undefined';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wrapper that makes the synchronous Runtime conform to the async IRuntime interface
|
|
41
|
+
*/
|
|
42
|
+
class AsyncRuntimeWrapper implements IRuntime {
|
|
43
|
+
private runtime: Runtime;
|
|
44
|
+
|
|
45
|
+
constructor(vfs: VirtualFS, options: IRuntimeOptions = {}) {
|
|
46
|
+
this.runtime = new Runtime(vfs, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async execute(code: string, filename?: string): Promise<IExecuteResult> {
|
|
50
|
+
return Promise.resolve(this.runtime.execute(code, filename));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async runFile(filename: string): Promise<IExecuteResult> {
|
|
54
|
+
return Promise.resolve(this.runtime.runFile(filename));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clearCache(): void {
|
|
58
|
+
this.runtime.clearCache();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getVFS(): VirtualFS {
|
|
62
|
+
return this.runtime.getVFS();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the underlying sync Runtime for direct access to sync methods
|
|
67
|
+
*/
|
|
68
|
+
getSyncRuntime(): Runtime {
|
|
69
|
+
return this.runtime;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a runtime instance based on configuration
|
|
75
|
+
*
|
|
76
|
+
* SECURITY: Requires either sandbox URL or explicit dangerouslyAllowSameOrigin.
|
|
77
|
+
*
|
|
78
|
+
* @param vfs - Virtual file system instance
|
|
79
|
+
* @param options - Runtime options including sandbox/security settings
|
|
80
|
+
* @returns Promise resolving to IRuntime instance
|
|
81
|
+
* @throws Error if neither sandbox nor dangerouslyAllowSameOrigin is specified
|
|
82
|
+
*/
|
|
83
|
+
export async function createRuntime(
|
|
84
|
+
vfs: VirtualFS,
|
|
85
|
+
options: CreateRuntimeOptions = {}
|
|
86
|
+
): Promise<IRuntime> {
|
|
87
|
+
const { sandbox, dangerouslyAllowSameOrigin, useWorker = false, ...runtimeOptions } = options;
|
|
88
|
+
|
|
89
|
+
// SECURE: Cross-origin sandbox mode
|
|
90
|
+
if (sandbox) {
|
|
91
|
+
console.log('[createRuntime] Creating SandboxRuntime (cross-origin isolated)');
|
|
92
|
+
const sandboxRuntime = new SandboxRuntime(sandbox, vfs, runtimeOptions);
|
|
93
|
+
// Wait for sandbox to be ready by executing a simple command
|
|
94
|
+
await sandboxRuntime.execute('/* sandbox ready check */', '/__sandbox_init__.js');
|
|
95
|
+
return sandboxRuntime;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// SECURITY CHECK: Same-origin execution requires explicit opt-in
|
|
99
|
+
if (!dangerouslyAllowSameOrigin) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'almostnode: For security, you must either:\n' +
|
|
102
|
+
' 1. Use sandbox mode: { sandbox: "https://your-sandbox.vercel.app" }\n' +
|
|
103
|
+
' 2. Explicitly opt-in to same-origin: { dangerouslyAllowSameOrigin: true }\n' +
|
|
104
|
+
'\n' +
|
|
105
|
+
'Same-origin execution allows code to access cookies, localStorage, and IndexedDB.\n' +
|
|
106
|
+
'Only use dangerouslyAllowSameOrigin for trusted code or demos.\n' +
|
|
107
|
+
'\n' +
|
|
108
|
+
'For sandbox setup instructions, see: https://github.com/anthropics/almostnode#sandbox-setup'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Same-origin modes (requires explicit opt-in)
|
|
113
|
+
let shouldUseWorker = false;
|
|
114
|
+
|
|
115
|
+
if (useWorker === true) {
|
|
116
|
+
shouldUseWorker = isWorkerAvailable();
|
|
117
|
+
if (!shouldUseWorker) {
|
|
118
|
+
console.warn('[createRuntime] Worker requested but not available, falling back to main thread');
|
|
119
|
+
}
|
|
120
|
+
} else if (useWorker === 'auto') {
|
|
121
|
+
shouldUseWorker = isWorkerAvailable();
|
|
122
|
+
console.log(`[createRuntime] Auto mode: using ${shouldUseWorker ? 'worker' : 'main thread'}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (shouldUseWorker) {
|
|
126
|
+
console.log('[createRuntime] Creating WorkerRuntime (same-origin, thread-isolated)');
|
|
127
|
+
const workerRuntime = new WorkerRuntime(vfs, runtimeOptions);
|
|
128
|
+
// Wait for worker to be ready by executing a simple command
|
|
129
|
+
await workerRuntime.execute('/* worker ready check */', '/__worker_init__.js');
|
|
130
|
+
return workerRuntime;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('[createRuntime] Creating main-thread Runtime (same-origin, least secure)');
|
|
134
|
+
return new AsyncRuntimeWrapper(vfs, runtimeOptions);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Re-export types and classes for convenience
|
|
138
|
+
export { Runtime } from './runtime';
|
|
139
|
+
export { WorkerRuntime } from './worker-runtime';
|
|
140
|
+
export { SandboxRuntime } from './sandbox-runtime';
|
|
141
|
+
export type {
|
|
142
|
+
IRuntime,
|
|
143
|
+
IExecuteResult,
|
|
144
|
+
IRuntimeOptions,
|
|
145
|
+
CreateRuntimeOptions,
|
|
146
|
+
VFSSnapshot,
|
|
147
|
+
} from './runtime-interface';
|
package/src/demo.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mini WebContainer Demo
|
|
3
|
+
* Demonstrates running an HTTP server in the browser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VirtualFS } from './virtual-fs';
|
|
7
|
+
import { Runtime } from './runtime';
|
|
8
|
+
import { ServerBridge, getServerBridge, resetServerBridge } from './server-bridge';
|
|
9
|
+
import { PackageManager } from './npm';
|
|
10
|
+
|
|
11
|
+
// DOM elements
|
|
12
|
+
const editorEl = document.getElementById('editor') as HTMLTextAreaElement;
|
|
13
|
+
const previewEl = document.getElementById('preview') as HTMLIFrameElement;
|
|
14
|
+
const terminalEl = document.getElementById('terminal') as HTMLDivElement;
|
|
15
|
+
const statusEl = document.getElementById('status') as HTMLSpanElement;
|
|
16
|
+
const runBtn = document.getElementById('runBtn') as HTMLButtonElement;
|
|
17
|
+
|
|
18
|
+
// State
|
|
19
|
+
let vfs: VirtualFS;
|
|
20
|
+
let runtime: Runtime;
|
|
21
|
+
let serverBridge: ServerBridge;
|
|
22
|
+
let npm: PackageManager;
|
|
23
|
+
let currentServerPort: number | null = null;
|
|
24
|
+
|
|
25
|
+
// Log to terminal
|
|
26
|
+
function log(message: string, type: 'info' | 'error' | 'success' = 'info') {
|
|
27
|
+
const colors = {
|
|
28
|
+
info: '#888',
|
|
29
|
+
error: '#e74c3c',
|
|
30
|
+
success: '#27ae60',
|
|
31
|
+
};
|
|
32
|
+
const time = new Date().toLocaleTimeString();
|
|
33
|
+
terminalEl.innerHTML += `<span style="color: ${colors[type]}">[${time}] ${escapeHtml(message)}</span>\n`;
|
|
34
|
+
terminalEl.scrollTop = terminalEl.scrollHeight;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function escapeHtml(text: string): string {
|
|
38
|
+
return text
|
|
39
|
+
.replace(/&/g, '&')
|
|
40
|
+
.replace(/</g, '<')
|
|
41
|
+
.replace(/>/g, '>');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Update status indicator
|
|
45
|
+
function setStatus(text: string, className: 'loading' | 'ready' | 'error') {
|
|
46
|
+
statusEl.textContent = text;
|
|
47
|
+
statusEl.className = 'status ' + className;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize the container
|
|
51
|
+
function initContainer() {
|
|
52
|
+
vfs = new VirtualFS();
|
|
53
|
+
runtime = new Runtime(vfs, {
|
|
54
|
+
onConsole: (method, args) => {
|
|
55
|
+
const message = args.map(arg =>
|
|
56
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
57
|
+
).join(' ');
|
|
58
|
+
log(message, method === 'error' ? 'error' : 'info');
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
npm = new PackageManager(vfs);
|
|
63
|
+
|
|
64
|
+
// Reset the server bridge to get fresh callbacks
|
|
65
|
+
resetServerBridge();
|
|
66
|
+
|
|
67
|
+
serverBridge = getServerBridge({
|
|
68
|
+
baseUrl: window.location.origin,
|
|
69
|
+
onServerReady: (port, url) => {
|
|
70
|
+
currentServerPort = port;
|
|
71
|
+
log(`Server ready on port ${port}`, 'success');
|
|
72
|
+
setStatus('Running', 'ready');
|
|
73
|
+
|
|
74
|
+
// Load the preview
|
|
75
|
+
loadPreview('/');
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
log('Container initialized', 'success');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Load content from virtual server into preview
|
|
83
|
+
async function loadPreview(path: string) {
|
|
84
|
+
if (!currentServerPort) {
|
|
85
|
+
log('No server running', 'error');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await serverBridge.handleRequest(
|
|
91
|
+
currentServerPort,
|
|
92
|
+
'GET',
|
|
93
|
+
path,
|
|
94
|
+
{
|
|
95
|
+
host: 'localhost',
|
|
96
|
+
'user-agent': 'MiniWebContainer/1.0',
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const contentType = response.headers['content-type'] || 'text/plain';
|
|
101
|
+
const body = response.body.toString();
|
|
102
|
+
|
|
103
|
+
if (contentType.includes('text/html')) {
|
|
104
|
+
// Inject script to intercept link clicks
|
|
105
|
+
const htmlWithInterceptor = injectLinkInterceptor(body);
|
|
106
|
+
const blob = new Blob([htmlWithInterceptor], { type: 'text/html' });
|
|
107
|
+
previewEl.src = URL.createObjectURL(blob);
|
|
108
|
+
} else if (contentType.includes('application/json')) {
|
|
109
|
+
// Display JSON nicely
|
|
110
|
+
const html = `
|
|
111
|
+
<!DOCTYPE html>
|
|
112
|
+
<html>
|
|
113
|
+
<head>
|
|
114
|
+
<style>
|
|
115
|
+
body { font-family: monospace; padding: 1rem; background: #1a1a2e; color: #0f0; }
|
|
116
|
+
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<pre>${escapeHtml(JSON.stringify(JSON.parse(body), null, 2))}</pre>
|
|
121
|
+
</body>
|
|
122
|
+
</html>
|
|
123
|
+
`;
|
|
124
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
125
|
+
previewEl.src = URL.createObjectURL(blob);
|
|
126
|
+
} else {
|
|
127
|
+
// Display as plain text
|
|
128
|
+
const html = `
|
|
129
|
+
<!DOCTYPE html>
|
|
130
|
+
<html>
|
|
131
|
+
<head>
|
|
132
|
+
<style>
|
|
133
|
+
body { font-family: monospace; padding: 1rem; background: #1a1a2e; color: #eee; }
|
|
134
|
+
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<pre>${escapeHtml(body)}</pre>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|
|
141
|
+
`;
|
|
142
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
143
|
+
previewEl.src = URL.createObjectURL(blob);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
log(`Error loading preview: ${error}`, 'error');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Inject script to intercept link clicks in the preview
|
|
151
|
+
function injectLinkInterceptor(html: string): string {
|
|
152
|
+
const script = `
|
|
153
|
+
<script>
|
|
154
|
+
document.addEventListener('click', function(e) {
|
|
155
|
+
const link = e.target.closest('a');
|
|
156
|
+
if (link) {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
// Get the href attribute directly (not the resolved href property)
|
|
159
|
+
const href = link.getAttribute('href');
|
|
160
|
+
if (href) {
|
|
161
|
+
// Handle relative URLs directly
|
|
162
|
+
if (href.startsWith('/')) {
|
|
163
|
+
window.parent.postMessage({ type: 'navigate', path: href }, '*');
|
|
164
|
+
} else if (href.startsWith('http')) {
|
|
165
|
+
// Absolute URL - extract pathname
|
|
166
|
+
try {
|
|
167
|
+
const url = new URL(href);
|
|
168
|
+
window.parent.postMessage({ type: 'navigate', path: url.pathname + url.search }, '*');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error('Invalid URL:', href);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Relative path without leading slash
|
|
174
|
+
window.parent.postMessage({ type: 'navigate', path: '/' + href }, '*');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
// Insert before </body> or at the end
|
|
183
|
+
if (html.includes('</body>')) {
|
|
184
|
+
return html.replace('</body>', script + '</body>');
|
|
185
|
+
}
|
|
186
|
+
return html + script;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if code uses Express
|
|
190
|
+
function usesExpress(code: string): boolean {
|
|
191
|
+
return code.includes("require('express')") || code.includes('require("express")');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if express is installed in VFS
|
|
195
|
+
function isExpressInstalled(): boolean {
|
|
196
|
+
return vfs.existsSync('/node_modules/express/package.json');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Install dependencies
|
|
200
|
+
async function installDependencies() {
|
|
201
|
+
setStatus('Installing...', 'loading');
|
|
202
|
+
runBtn.disabled = true;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
log('Installing express (this may take a moment)...', 'info');
|
|
206
|
+
|
|
207
|
+
await npm.install('express', {
|
|
208
|
+
onProgress: (msg) => log(msg, 'info'),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
log('Express installed successfully!', 'success');
|
|
212
|
+
} catch (error) {
|
|
213
|
+
log(`Install failed: ${error}`, 'error');
|
|
214
|
+
setStatus('Install Failed', 'error');
|
|
215
|
+
throw error;
|
|
216
|
+
} finally {
|
|
217
|
+
runBtn.disabled = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Reset runtime for a fresh execution (but keep VFS with installed packages)
|
|
222
|
+
function resetRuntime() {
|
|
223
|
+
runtime = new Runtime(vfs, {
|
|
224
|
+
onConsole: (method, args) => {
|
|
225
|
+
const message = args.map(arg =>
|
|
226
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
227
|
+
).join(' ');
|
|
228
|
+
log(message, method === 'error' ? 'error' : 'info');
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Reset the server bridge to get fresh callbacks
|
|
233
|
+
resetServerBridge();
|
|
234
|
+
|
|
235
|
+
serverBridge = getServerBridge({
|
|
236
|
+
baseUrl: window.location.origin,
|
|
237
|
+
onServerReady: (port, url) => {
|
|
238
|
+
currentServerPort = port;
|
|
239
|
+
log(`Server ready on port ${port}`, 'success');
|
|
240
|
+
setStatus('Running', 'ready');
|
|
241
|
+
|
|
242
|
+
// Load the preview
|
|
243
|
+
loadPreview('/');
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Run the server code
|
|
249
|
+
async function runServer() {
|
|
250
|
+
const code = editorEl.value;
|
|
251
|
+
|
|
252
|
+
// Reset state
|
|
253
|
+
currentServerPort = null;
|
|
254
|
+
setStatus('Starting...', 'loading');
|
|
255
|
+
runBtn.disabled = true;
|
|
256
|
+
|
|
257
|
+
// Clear terminal
|
|
258
|
+
terminalEl.innerHTML = '';
|
|
259
|
+
log('Running script...');
|
|
260
|
+
|
|
261
|
+
// Reset runtime (keeps VFS with installed packages)
|
|
262
|
+
resetRuntime();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Check if Express is needed and not installed
|
|
266
|
+
if (usesExpress(code) && !isExpressInstalled()) {
|
|
267
|
+
await installDependencies();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Execute the code
|
|
271
|
+
log('Executing code...', 'info');
|
|
272
|
+
runtime.execute(code, '/server.js');
|
|
273
|
+
log('Code executed successfully', 'success');
|
|
274
|
+
|
|
275
|
+
// If no server started after a short delay, mark as completed
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
if (!currentServerPort) {
|
|
278
|
+
setStatus('Completed', 'ready');
|
|
279
|
+
}
|
|
280
|
+
}, 500);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
log(`Error: ${error}`, 'error');
|
|
283
|
+
setStatus('Error', 'error');
|
|
284
|
+
} finally {
|
|
285
|
+
runBtn.disabled = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle messages from preview iframe
|
|
290
|
+
window.addEventListener('message', (event) => {
|
|
291
|
+
if (event.data?.type === 'navigate') {
|
|
292
|
+
log(`Navigating to ${event.data.path}`);
|
|
293
|
+
loadPreview(event.data.path);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Initialize on load
|
|
298
|
+
runBtn.addEventListener('click', runServer);
|
|
299
|
+
|
|
300
|
+
// Initialize container
|
|
301
|
+
initContainer();
|
|
302
|
+
setStatus('Ready', 'ready');
|
|
303
|
+
log('Click "Run Server" to start the HTTP server');
|
|
304
|
+
log('Express will be auto-installed if you use require("express")');
|