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/README.md
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
# almostnode
|
|
2
|
+
|
|
3
|
+
**Node.js in your browser. Just like that.**
|
|
4
|
+
|
|
5
|
+
A lightweight, browser-native Node.js runtime environment. Run Node.js code, install npm packages, and develop with Vite or Next.js - all without a server.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
> **Warning:** This project is experimental and may contain bugs. Use with caution in production environments.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Virtual File System** - Full in-memory filesystem with Node.js-compatible API
|
|
18
|
+
- **Node.js API Shims** - 40+ shimmed modules (`fs`, `path`, `http`, `events`, and more)
|
|
19
|
+
- **npm Package Installation** - Install and run real npm packages in the browser
|
|
20
|
+
- **Dev Servers** - Built-in Vite and Next.js development servers
|
|
21
|
+
- **Hot Module Replacement** - React Refresh support for instant updates
|
|
22
|
+
- **TypeScript Support** - First-class TypeScript/TSX transformation via esbuild-wasm
|
|
23
|
+
- **Service Worker Architecture** - Intercepts requests for seamless dev experience
|
|
24
|
+
- **Optional Web Worker Support** - Offload code execution to a Web Worker for improved UI responsiveness
|
|
25
|
+
- **Secure by Default** - Cross-origin sandbox support for running untrusted code safely
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- **Node.js 20+** - Required for development and building
|
|
32
|
+
- **Modern browser** - Chrome, Firefox, Safari, or Edge with ES2020+ support
|
|
33
|
+
|
|
34
|
+
> **Note:** almostnode runs in the browser and emulates Node.js 20 APIs. The Node.js requirement is only for development tooling (Vite, Vitest, TypeScript).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install almostnode
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createContainer } from 'almostnode';
|
|
50
|
+
|
|
51
|
+
// Create a Node.js container in the browser
|
|
52
|
+
const container = createContainer();
|
|
53
|
+
|
|
54
|
+
// Execute JavaScript code directly
|
|
55
|
+
const result = container.execute(`
|
|
56
|
+
const path = require('path');
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
|
|
59
|
+
// Use Node.js APIs in the browser!
|
|
60
|
+
fs.writeFileSync('/hello.txt', 'Hello from the browser!');
|
|
61
|
+
module.exports = fs.readFileSync('/hello.txt', 'utf8');
|
|
62
|
+
`);
|
|
63
|
+
|
|
64
|
+
console.log(result.exports); // "Hello from the browser!"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Working with Virtual File System
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { createContainer } from 'almostnode';
|
|
71
|
+
|
|
72
|
+
const container = createContainer();
|
|
73
|
+
const { vfs } = container;
|
|
74
|
+
|
|
75
|
+
// Pre-populate the virtual filesystem
|
|
76
|
+
vfs.writeFileSync('/src/index.js', `
|
|
77
|
+
const data = require('./data.json');
|
|
78
|
+
console.log('Users:', data.users.length);
|
|
79
|
+
module.exports = data;
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
vfs.writeFileSync('/src/data.json', JSON.stringify({
|
|
83
|
+
users: [{ name: 'Alice' }, { name: 'Bob' }]
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
// Run from the virtual filesystem
|
|
87
|
+
const result = container.runFile('/src/index.js');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### With npm Packages
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { createContainer } from 'almostnode';
|
|
94
|
+
|
|
95
|
+
const container = createContainer();
|
|
96
|
+
|
|
97
|
+
// Install a package
|
|
98
|
+
await container.npm.install('lodash');
|
|
99
|
+
|
|
100
|
+
// Use it in your code
|
|
101
|
+
container.execute(`
|
|
102
|
+
const _ = require('lodash');
|
|
103
|
+
console.log(_.capitalize('hello world'));
|
|
104
|
+
`);
|
|
105
|
+
// Output: Hello world
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### With Next.js Dev Server
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { VirtualFS, NextDevServer, getServerBridge } from 'almostnode';
|
|
112
|
+
|
|
113
|
+
const vfs = new VirtualFS();
|
|
114
|
+
|
|
115
|
+
// Create a Next.js page
|
|
116
|
+
vfs.mkdirSync('/pages', { recursive: true });
|
|
117
|
+
vfs.writeFileSync('/pages/index.jsx', `
|
|
118
|
+
import { useState } from 'react';
|
|
119
|
+
|
|
120
|
+
export default function Home() {
|
|
121
|
+
const [count, setCount] = useState(0);
|
|
122
|
+
return (
|
|
123
|
+
<div>
|
|
124
|
+
<h1>Count: {count}</h1>
|
|
125
|
+
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
// Start the dev server
|
|
132
|
+
const server = new NextDevServer(vfs, { port: 3000 });
|
|
133
|
+
const bridge = getServerBridge();
|
|
134
|
+
await bridge.initServiceWorker();
|
|
135
|
+
bridge.registerServer(server, 3000);
|
|
136
|
+
|
|
137
|
+
// Access at: /__virtual__/3000/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Comparison with WebContainers
|
|
143
|
+
|
|
144
|
+
| Feature | almostnode | WebContainers |
|
|
145
|
+
|---------|-----------|---------------|
|
|
146
|
+
| **Bundle Size** | ~50KB | ~2MB |
|
|
147
|
+
| **Startup Time** | Instant | 2-5 seconds |
|
|
148
|
+
| **Execution Model** | Main thread or Web Worker (configurable) | Web Worker isolates |
|
|
149
|
+
| **Shell** | `just-bash` (POSIX subset) | Full Linux kernel |
|
|
150
|
+
| **Native Modules** | Stubs only | Full support |
|
|
151
|
+
| **Networking** | Virtual ports | Real TCP/IP |
|
|
152
|
+
| **Use Case** | Lightweight playgrounds, demos | Full development environments |
|
|
153
|
+
|
|
154
|
+
### When to use almostnode
|
|
155
|
+
|
|
156
|
+
- Building code playgrounds or tutorials
|
|
157
|
+
- Creating interactive documentation
|
|
158
|
+
- Prototyping without server setup
|
|
159
|
+
- Educational tools
|
|
160
|
+
- Lightweight sandboxed execution
|
|
161
|
+
|
|
162
|
+
### Example: Code Playground
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { createContainer } from 'almostnode';
|
|
166
|
+
|
|
167
|
+
function createPlayground() {
|
|
168
|
+
const container = createContainer();
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
run: (code: string) => {
|
|
172
|
+
try {
|
|
173
|
+
const result = container.execute(code);
|
|
174
|
+
return { success: true, result: result.exports };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return { success: false, error: error.message };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
reset: () => container.runtime.clearCache(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Usage
|
|
184
|
+
const playground = createPlayground();
|
|
185
|
+
const output = playground.run(`
|
|
186
|
+
const crypto = require('crypto');
|
|
187
|
+
module.exports = crypto.randomUUID();
|
|
188
|
+
`);
|
|
189
|
+
console.log(output); // { success: true, result: "550e8400-e29b-..." }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### When to use WebContainers
|
|
193
|
+
|
|
194
|
+
- Full-fidelity Node.js development
|
|
195
|
+
- Running native modules
|
|
196
|
+
- Complex build pipelines
|
|
197
|
+
- Production-like environments
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## API Reference
|
|
202
|
+
|
|
203
|
+
### `createContainer(options?)`
|
|
204
|
+
|
|
205
|
+
Creates a new container with all components initialized.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
interface ContainerOptions {
|
|
209
|
+
cwd?: string; // Working directory (default: '/')
|
|
210
|
+
env?: Record<string, string>; // Environment variables
|
|
211
|
+
onConsole?: (method: string, args: any[]) => void; // Console hook
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const container = createContainer({
|
|
215
|
+
cwd: '/app',
|
|
216
|
+
env: { NODE_ENV: 'development' },
|
|
217
|
+
onConsole: (method, args) => console.log(`[${method}]`, ...args),
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
- `container.vfs` - VirtualFS instance
|
|
223
|
+
- `container.runtime` - Runtime instance
|
|
224
|
+
- `container.npm` - PackageManager instance
|
|
225
|
+
- `container.serverBridge` - ServerBridge instance
|
|
226
|
+
|
|
227
|
+
### VirtualFS
|
|
228
|
+
|
|
229
|
+
Node.js-compatible filesystem API.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Synchronous operations
|
|
233
|
+
vfs.writeFileSync(path, content);
|
|
234
|
+
vfs.readFileSync(path, encoding?);
|
|
235
|
+
vfs.mkdirSync(path, { recursive: true });
|
|
236
|
+
vfs.readdirSync(path);
|
|
237
|
+
vfs.statSync(path);
|
|
238
|
+
vfs.unlinkSync(path);
|
|
239
|
+
vfs.rmdirSync(path);
|
|
240
|
+
vfs.existsSync(path);
|
|
241
|
+
vfs.renameSync(oldPath, newPath);
|
|
242
|
+
|
|
243
|
+
// Async operations
|
|
244
|
+
await vfs.readFile(path, encoding?);
|
|
245
|
+
await vfs.stat(path);
|
|
246
|
+
|
|
247
|
+
// File watching
|
|
248
|
+
vfs.watch(path, { recursive: true }, (event, filename) => {
|
|
249
|
+
console.log(`${event}: ${filename}`);
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Runtime
|
|
254
|
+
|
|
255
|
+
Execute JavaScript/TypeScript code.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Execute code string
|
|
259
|
+
runtime.execute('console.log("Hello")');
|
|
260
|
+
|
|
261
|
+
// Run a file from VirtualFS
|
|
262
|
+
runtime.runFile('/path/to/file.js');
|
|
263
|
+
|
|
264
|
+
// Require a module
|
|
265
|
+
const module = runtime.require('/path/to/module.js');
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### createRuntime (Async Runtime Factory)
|
|
269
|
+
|
|
270
|
+
For advanced use cases, use `createRuntime` to create a runtime with security options:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { createRuntime, VirtualFS } from 'almostnode';
|
|
274
|
+
|
|
275
|
+
const vfs = new VirtualFS();
|
|
276
|
+
|
|
277
|
+
// RECOMMENDED: Cross-origin sandbox (fully isolated)
|
|
278
|
+
const secureRuntime = await createRuntime(vfs, {
|
|
279
|
+
sandbox: 'https://your-sandbox.vercel.app',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// For demos/trusted code: Same-origin with explicit opt-in
|
|
283
|
+
const demoRuntime = await createRuntime(vfs, {
|
|
284
|
+
dangerouslyAllowSameOrigin: true,
|
|
285
|
+
useWorker: true, // Optional: run in Web Worker
|
|
286
|
+
cwd: '/project',
|
|
287
|
+
env: { NODE_ENV: 'development' },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Both modes use the same async API
|
|
291
|
+
const result = await secureRuntime.execute('module.exports = 1 + 1;');
|
|
292
|
+
console.log(result.exports); // 2
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Security Modes
|
|
296
|
+
|
|
297
|
+
| Mode | Option | Security Level | Use Case |
|
|
298
|
+
|------|--------|----------------|----------|
|
|
299
|
+
| **Cross-origin sandbox** | `sandbox: 'https://...'` | Highest | Production, untrusted code |
|
|
300
|
+
| **Same-origin Worker** | `dangerouslyAllowSameOrigin: true, useWorker: true` | Medium | Demos with trusted code |
|
|
301
|
+
| **Same-origin main thread** | `dangerouslyAllowSameOrigin: true` | Lowest | Trusted code only |
|
|
302
|
+
|
|
303
|
+
**Security by default:** `createRuntime()` throws an error if neither `sandbox` nor `dangerouslyAllowSameOrigin` is provided.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Sandbox Setup
|
|
308
|
+
|
|
309
|
+
For running untrusted code securely, deploy a cross-origin sandbox. The key requirement is that the sandbox must be served from a **different origin** (different domain, subdomain, or port).
|
|
310
|
+
|
|
311
|
+
### Quick Setup (Vercel)
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { generateSandboxFiles } from 'almostnode';
|
|
315
|
+
|
|
316
|
+
const files = generateSandboxFiles();
|
|
317
|
+
// Write files['index.html'] and files['vercel.json'] to a directory
|
|
318
|
+
// Deploy: cd sandbox && vercel --prod
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Manual Setup (Any Platform)
|
|
322
|
+
|
|
323
|
+
The sandbox requires two things:
|
|
324
|
+
|
|
325
|
+
#### 1. The sandbox HTML page
|
|
326
|
+
|
|
327
|
+
Create an `index.html` that loads almostnode and handles postMessage:
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<!DOCTYPE html>
|
|
331
|
+
<html>
|
|
332
|
+
<head><meta charset="UTF-8"></head>
|
|
333
|
+
<body>
|
|
334
|
+
<script type="module">
|
|
335
|
+
import { VirtualFS, Runtime } from 'https://unpkg.com/almostnode/dist/index.js';
|
|
336
|
+
|
|
337
|
+
let vfs = null;
|
|
338
|
+
let runtime = null;
|
|
339
|
+
|
|
340
|
+
window.addEventListener('message', async (event) => {
|
|
341
|
+
const { type, id, code, filename, vfsSnapshot, options, path, content } = event.data;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
switch (type) {
|
|
345
|
+
case 'init':
|
|
346
|
+
vfs = VirtualFS.fromSnapshot(vfsSnapshot);
|
|
347
|
+
runtime = new Runtime(vfs, {
|
|
348
|
+
cwd: options?.cwd,
|
|
349
|
+
env: options?.env,
|
|
350
|
+
onConsole: (method, args) => {
|
|
351
|
+
parent.postMessage({ type: 'console', consoleMethod: method, consoleArgs: args }, '*');
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
break;
|
|
355
|
+
case 'execute':
|
|
356
|
+
const result = runtime.execute(code, filename);
|
|
357
|
+
parent.postMessage({ type: 'result', id, result }, '*');
|
|
358
|
+
break;
|
|
359
|
+
case 'runFile':
|
|
360
|
+
const runResult = runtime.runFile(filename);
|
|
361
|
+
parent.postMessage({ type: 'result', id, result: runResult }, '*');
|
|
362
|
+
break;
|
|
363
|
+
case 'syncFile':
|
|
364
|
+
if (content === null) { try { vfs.unlinkSync(path); } catch {} }
|
|
365
|
+
else { vfs.writeFileSync(path, content); }
|
|
366
|
+
break;
|
|
367
|
+
case 'clearCache':
|
|
368
|
+
runtime?.clearCache();
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (id) parent.postMessage({ type: 'error', id, error: error.message }, '*');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
parent.postMessage({ type: 'ready' }, '*');
|
|
377
|
+
</script>
|
|
378
|
+
</body>
|
|
379
|
+
</html>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
#### 2. Required HTTP headers
|
|
383
|
+
|
|
384
|
+
The sandbox server must include these headers:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
Access-Control-Allow-Origin: *
|
|
388
|
+
Cross-Origin-Resource-Policy: cross-origin
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Example configurations:**
|
|
392
|
+
|
|
393
|
+
<details>
|
|
394
|
+
<summary>Nginx</summary>
|
|
395
|
+
|
|
396
|
+
```nginx
|
|
397
|
+
server {
|
|
398
|
+
listen 3002;
|
|
399
|
+
root /path/to/sandbox;
|
|
400
|
+
|
|
401
|
+
location / {
|
|
402
|
+
add_header Access-Control-Allow-Origin *;
|
|
403
|
+
add_header Cross-Origin-Resource-Policy cross-origin;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
</details>
|
|
408
|
+
|
|
409
|
+
<details>
|
|
410
|
+
<summary>Apache (.htaccess)</summary>
|
|
411
|
+
|
|
412
|
+
```apache
|
|
413
|
+
Header set Access-Control-Allow-Origin "*"
|
|
414
|
+
Header set Cross-Origin-Resource-Policy "cross-origin"
|
|
415
|
+
```
|
|
416
|
+
</details>
|
|
417
|
+
|
|
418
|
+
<details>
|
|
419
|
+
<summary>Express.js</summary>
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
app.use((req, res, next) => {
|
|
423
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
424
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
425
|
+
next();
|
|
426
|
+
});
|
|
427
|
+
app.use(express.static('sandbox'));
|
|
428
|
+
app.listen(3002);
|
|
429
|
+
```
|
|
430
|
+
</details>
|
|
431
|
+
|
|
432
|
+
<details>
|
|
433
|
+
<summary>Python (http.server)</summary>
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
437
|
+
|
|
438
|
+
class CORSHandler(SimpleHTTPRequestHandler):
|
|
439
|
+
def end_headers(self):
|
|
440
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
441
|
+
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
|
|
442
|
+
super().end_headers()
|
|
443
|
+
|
|
444
|
+
HTTPServer(('', 3002), CORSHandler).serve_forever()
|
|
445
|
+
```
|
|
446
|
+
</details>
|
|
447
|
+
|
|
448
|
+
### Use in your app
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
const runtime = await createRuntime(vfs, {
|
|
452
|
+
sandbox: 'https://sandbox.yourdomain.com', // Must be different origin!
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Code runs in isolated cross-origin iframe
|
|
456
|
+
const result = await runtime.execute(untrustedCode);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Local Development
|
|
460
|
+
|
|
461
|
+
For local testing, run the sandbox on a different port:
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Terminal 1: Main app on port 5173
|
|
465
|
+
npm run dev
|
|
466
|
+
|
|
467
|
+
# Terminal 2: Sandbox on port 3002
|
|
468
|
+
npm run sandbox
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Then use `sandbox: 'http://localhost:3002/sandbox/'` in your app.
|
|
472
|
+
|
|
473
|
+
### What cross-origin sandbox protects
|
|
474
|
+
|
|
475
|
+
| Threat | Status |
|
|
476
|
+
|--------|--------|
|
|
477
|
+
| Cookies | Blocked (different origin) |
|
|
478
|
+
| localStorage | Blocked (different origin) |
|
|
479
|
+
| IndexedDB | Blocked (different origin) |
|
|
480
|
+
| DOM access | Blocked (cross-origin iframe) |
|
|
481
|
+
|
|
482
|
+
**Note:** Network requests from the sandbox are still possible. Add CSP headers for additional protection.
|
|
483
|
+
|
|
484
|
+
### PackageManager
|
|
485
|
+
|
|
486
|
+
Install npm packages.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// Install a package
|
|
490
|
+
await npm.install('react');
|
|
491
|
+
await npm.install('lodash@4.17.21');
|
|
492
|
+
|
|
493
|
+
// Install multiple packages
|
|
494
|
+
await npm.install(['react', 'react-dom']);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Supported Node.js APIs
|
|
500
|
+
|
|
501
|
+
**967 compatibility tests** verify our Node.js API coverage.
|
|
502
|
+
|
|
503
|
+
### Fully Shimmed Modules
|
|
504
|
+
|
|
505
|
+
| Module | Tests | Coverage | Notes |
|
|
506
|
+
|--------|-------|----------|-------|
|
|
507
|
+
| `path` | 219 | High | POSIX paths (no Windows) |
|
|
508
|
+
| `buffer` | 95 | High | All common operations |
|
|
509
|
+
| `fs` | 76 | High | Sync + promises API |
|
|
510
|
+
| `url` | 67 | High | WHATWG URL + legacy parser |
|
|
511
|
+
| `util` | 77 | High | format, inspect, promisify |
|
|
512
|
+
| `process` | 60 | High | env, cwd, hrtime, EventEmitter |
|
|
513
|
+
| `events` | 50 | High | Full EventEmitter API |
|
|
514
|
+
| `os` | 58 | High | Platform info (simulated) |
|
|
515
|
+
| `crypto` | 57 | High | Hash, HMAC, random, sign/verify |
|
|
516
|
+
| `querystring` | 52 | High | parse, stringify, escape |
|
|
517
|
+
| `stream` | 44 | Medium | Readable, Writable, Transform |
|
|
518
|
+
| `zlib` | 39 | High | gzip, deflate, brotli |
|
|
519
|
+
| `tty` | 40 | High | ReadStream, WriteStream |
|
|
520
|
+
| `perf_hooks` | 33 | High | Performance API |
|
|
521
|
+
|
|
522
|
+
### Stubbed Modules
|
|
523
|
+
|
|
524
|
+
These modules export empty objects or no-op functions:
|
|
525
|
+
- `net`, `tls`, `dns`, `dgram`
|
|
526
|
+
- `cluster`, `worker_threads`
|
|
527
|
+
- `vm`, `v8`, `inspector`
|
|
528
|
+
- `async_hooks`
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Framework Support
|
|
533
|
+
|
|
534
|
+
### Vite
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
import { VirtualFS, ViteDevServer, getServerBridge } from 'almostnode';
|
|
538
|
+
|
|
539
|
+
const vfs = new VirtualFS();
|
|
540
|
+
|
|
541
|
+
// Create a React app
|
|
542
|
+
vfs.writeFileSync('/index.html', `
|
|
543
|
+
<!DOCTYPE html>
|
|
544
|
+
<html>
|
|
545
|
+
<body>
|
|
546
|
+
<div id="root"></div>
|
|
547
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
548
|
+
</body>
|
|
549
|
+
</html>
|
|
550
|
+
`);
|
|
551
|
+
|
|
552
|
+
vfs.mkdirSync('/src', { recursive: true });
|
|
553
|
+
vfs.writeFileSync('/src/main.jsx', `
|
|
554
|
+
import React from 'react';
|
|
555
|
+
import ReactDOM from 'react-dom/client';
|
|
556
|
+
|
|
557
|
+
function App() {
|
|
558
|
+
return <h1>Hello Vite!</h1>;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
562
|
+
`);
|
|
563
|
+
|
|
564
|
+
// Start Vite dev server
|
|
565
|
+
const server = new ViteDevServer(vfs, { port: 5173 });
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Next.js
|
|
569
|
+
|
|
570
|
+
Supports both **Pages Router** and **App Router**:
|
|
571
|
+
|
|
572
|
+
#### Pages Router
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
/pages
|
|
576
|
+
/index.jsx → /
|
|
577
|
+
/about.jsx → /about
|
|
578
|
+
/users/[id].jsx → /users/:id
|
|
579
|
+
/api/hello.js → /api/hello
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
#### App Router
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
/app
|
|
586
|
+
/layout.jsx → Root layout
|
|
587
|
+
/page.jsx → /
|
|
588
|
+
/about/page.jsx → /about
|
|
589
|
+
/users/[id]/page.jsx → /users/:id
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Hot Module Replacement (HMR)
|
|
595
|
+
|
|
596
|
+
almostnode includes built-in Hot Module Replacement support for instant updates during development. When you edit files, changes appear immediately in the preview without a full page reload.
|
|
597
|
+
|
|
598
|
+
### How It Works
|
|
599
|
+
|
|
600
|
+
HMR is automatically enabled when using `NextDevServer` or `ViteDevServer`. The system uses:
|
|
601
|
+
|
|
602
|
+
1. **VirtualFS file watching** - Detects file changes via `vfs.watch()`
|
|
603
|
+
2. **postMessage API** - Communicates updates between the main page and preview iframe
|
|
604
|
+
3. **React Refresh** - Preserves React component state during updates
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// HMR works automatically - just edit files and save
|
|
608
|
+
vfs.writeFileSync('/app/page.tsx', updatedContent);
|
|
609
|
+
// The preview iframe will automatically refresh with the new content
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Setup Requirements
|
|
613
|
+
|
|
614
|
+
For security, the preview iframe should be sandboxed. HMR uses `postMessage` for communication, which works correctly with sandboxed iframes:
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
// Create sandboxed iframe for security
|
|
618
|
+
const iframe = document.createElement('iframe');
|
|
619
|
+
iframe.src = '/__virtual__/3000/';
|
|
620
|
+
// Sandbox restricts the iframe's capabilities - add only what you need
|
|
621
|
+
iframe.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups';
|
|
622
|
+
container.appendChild(iframe);
|
|
623
|
+
|
|
624
|
+
// Register the iframe as HMR target after it loads
|
|
625
|
+
iframe.onload = () => {
|
|
626
|
+
if (iframe.contentWindow) {
|
|
627
|
+
devServer.setHMRTarget(iframe.contentWindow);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Recommended sandbox permissions:**
|
|
633
|
+
- `allow-scripts` - Required for JavaScript execution
|
|
634
|
+
- `allow-same-origin` - Allows the iframe to access cookies, localStorage, and IndexedDB (only add if your app needs these; omit for better isolation)
|
|
635
|
+
- `allow-forms` - If your app uses forms
|
|
636
|
+
- `allow-popups` - If your app opens new windows/tabs
|
|
637
|
+
|
|
638
|
+
> **Note:** The service worker intercepts `/__virtual__/` requests at the origin level, not the iframe level. The `allow-same-origin` attribute does NOT affect service worker functionality. For maximum security isolation, consider using **cross-origin sandbox mode** (see below) which doesn't use `allow-same-origin`.
|
|
639
|
+
|
|
640
|
+
### Manual HMR Triggering
|
|
641
|
+
|
|
642
|
+
If you need to manually trigger HMR updates (e.g., after programmatic file changes):
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
function triggerHMR(path: string, iframe: HTMLIFrameElement): void {
|
|
646
|
+
if (iframe.contentWindow) {
|
|
647
|
+
iframe.contentWindow.postMessage({
|
|
648
|
+
type: 'update',
|
|
649
|
+
path,
|
|
650
|
+
timestamp: Date.now(),
|
|
651
|
+
channel: 'next-hmr', // Use 'vite-hmr' for Vite
|
|
652
|
+
}, '*');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// After writing a file
|
|
657
|
+
vfs.writeFileSync('/app/page.tsx', newContent);
|
|
658
|
+
triggerHMR('/app/page.tsx', iframe);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Supported File Types
|
|
662
|
+
|
|
663
|
+
| File Type | HMR Behavior |
|
|
664
|
+
|-----------|--------------|
|
|
665
|
+
| `.jsx`, `.tsx` | React Refresh (preserves state) |
|
|
666
|
+
| `.js`, `.ts` | Full module reload |
|
|
667
|
+
| `.css` | Style injection (no reload) |
|
|
668
|
+
| `.json` | Full page reload |
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Development
|
|
673
|
+
|
|
674
|
+
### Setup
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
git clone https://github.com/user/almostnode.git
|
|
678
|
+
cd almostnode
|
|
679
|
+
npm install
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Run Tests
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
# Unit tests
|
|
686
|
+
npm test
|
|
687
|
+
|
|
688
|
+
# E2E tests (requires Playwright)
|
|
689
|
+
npm run test:e2e
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Development Server
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
npm run dev
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Open `http://localhost:5173/examples/next-demo.html` to see the Next.js demo.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Contributing
|
|
703
|
+
|
|
704
|
+
Contributions are welcome! Please:
|
|
705
|
+
|
|
706
|
+
1. Fork the repository
|
|
707
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
708
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
709
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
710
|
+
5. Open a Pull Request
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## License
|
|
715
|
+
|
|
716
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## Acknowledgments
|
|
721
|
+
|
|
722
|
+
- [esbuild-wasm](https://github.com/evanw/esbuild) - Lightning-fast JavaScript/TypeScript transformation
|
|
723
|
+
- [just-bash](https://github.com/user/just-bash) - POSIX shell in WebAssembly
|
|
724
|
+
- [React Refresh](https://github.com/facebook/react/tree/main/packages/react-refresh) - Hot module replacement for React
|
|
725
|
+
- [Comlink](https://github.com/GoogleChromeLabs/comlink) - Web Worker communication made simple
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
<p align="center">
|
|
730
|
+
Made with care for the browser
|
|
731
|
+
</p>
|