almostnode 0.1.0 → 0.2.1
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/README.md +184 -2
- package/dist/index.cjs +70 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +52 -9
- package/dist/index.mjs.map +1 -1
- package/dist/next-plugin.cjs +56 -0
- package/dist/next-plugin.cjs.map +1 -0
- package/dist/next-plugin.d.ts +62 -0
- package/dist/next-plugin.d.ts.map +1 -0
- package/dist/next-plugin.mjs +30 -0
- package/dist/next-plugin.mjs.map +1 -0
- package/dist/sandbox-helpers.d.ts +29 -6
- package/dist/sandbox-helpers.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +10 -1
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/vite-plugin.cjs +56 -0
- package/dist/vite-plugin.cjs.map +1 -0
- package/dist/vite-plugin.d.ts +36 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.mjs +30 -0
- package/dist/vite-plugin.mjs.map +1 -0
- package/package.json +24 -4
- package/src/frameworks/next-dev-server.ts +2 -2
- package/src/index.ts +1 -0
- package/src/next-plugin.ts +96 -0
- package/src/sandbox-helpers.ts +101 -9
- package/src/server-bridge.ts +14 -2
- package/src/vite-plugin.ts +76 -0
package/README.md
CHANGED
|
@@ -64,6 +64,26 @@ const result = container.execute(`
|
|
|
64
64
|
console.log(result.exports); // "Hello from the browser!"
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
> **⚠️ Security Warning:** The example above runs code on the main thread with full access to your page. **Do not use `createContainer()` or `container.execute()` with untrusted code.** For untrusted code, use `createRuntime()` with a cross-origin sandbox - see [Sandbox Setup](#sandbox-setup).
|
|
68
|
+
|
|
69
|
+
### Running Untrusted Code Securely
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createRuntime, VirtualFS } from 'almostnode';
|
|
73
|
+
|
|
74
|
+
const vfs = new VirtualFS();
|
|
75
|
+
|
|
76
|
+
// Create a secure runtime with cross-origin isolation
|
|
77
|
+
const runtime = await createRuntime(vfs, {
|
|
78
|
+
sandbox: 'https://your-sandbox.vercel.app', // Deploy with generateSandboxFiles()
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Now it's safe to run untrusted code
|
|
82
|
+
const result = await runtime.execute(untrustedCode);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
See [Sandbox Setup](#sandbox-setup) for deployment instructions.
|
|
86
|
+
|
|
67
87
|
### Working with Virtual File System
|
|
68
88
|
|
|
69
89
|
```typescript
|
|
@@ -139,6 +159,156 @@ bridge.registerServer(server, 3000);
|
|
|
139
159
|
|
|
140
160
|
---
|
|
141
161
|
|
|
162
|
+
## Service Worker Setup
|
|
163
|
+
|
|
164
|
+
almostnode uses a Service Worker to intercept HTTP requests and route them to virtual dev servers (e.g., `ViteDevServer`, `NextDevServer`).
|
|
165
|
+
|
|
166
|
+
> **Note:** The service worker is only needed if you're using dev servers with URL access (e.g., `/__virtual__/3000/`). If you're only executing code with `runtime.execute()`, you don't need the service worker.
|
|
167
|
+
|
|
168
|
+
### Which Setup Do I Need?
|
|
169
|
+
|
|
170
|
+
| Use Case | Setup Required |
|
|
171
|
+
|----------|----------------|
|
|
172
|
+
| Cross-origin sandbox (recommended for untrusted code) | `generateSandboxFiles()` - includes everything |
|
|
173
|
+
| Same-origin with Vite | `almostnodePlugin` from `almostnode/vite` |
|
|
174
|
+
| Same-origin with Next.js | `getServiceWorkerContent` from `almostnode/next` |
|
|
175
|
+
| Same-origin with other frameworks | Manual copy to public directory |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### Option 1: Cross-Origin Sandbox (Recommended)
|
|
180
|
+
|
|
181
|
+
When using `createRuntime()` with a cross-origin `sandbox` URL, the service worker must be deployed **with the sandbox**, not your main app.
|
|
182
|
+
|
|
183
|
+
The `generateSandboxFiles()` helper generates all required files:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { generateSandboxFiles } from 'almostnode';
|
|
187
|
+
import fs from 'fs';
|
|
188
|
+
|
|
189
|
+
const files = generateSandboxFiles();
|
|
190
|
+
|
|
191
|
+
// Creates: index.html, vercel.json, __sw__.js
|
|
192
|
+
fs.mkdirSync('sandbox', { recursive: true });
|
|
193
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
194
|
+
fs.writeFileSync(`sandbox/${filename}`, content);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Deploy to a different origin:
|
|
198
|
+
// cd sandbox && vercel --prod
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Generated files:**
|
|
202
|
+
| File | Purpose |
|
|
203
|
+
|------|---------|
|
|
204
|
+
| `index.html` | Sandbox page that loads almostnode and registers the service worker |
|
|
205
|
+
| `vercel.json` | CORS headers for cross-origin iframe embedding |
|
|
206
|
+
| `__sw__.js` | Service worker for intercepting dev server requests |
|
|
207
|
+
|
|
208
|
+
See [Sandbox Setup](#sandbox-setup) for full deployment instructions.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### Option 2: Same-Origin with Vite
|
|
213
|
+
|
|
214
|
+
For trusted code using `dangerouslyAllowSameOrigin: true`:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// vite.config.ts
|
|
218
|
+
import { defineConfig } from 'vite';
|
|
219
|
+
import { almostnodePlugin } from 'almostnode/vite';
|
|
220
|
+
|
|
221
|
+
export default defineConfig({
|
|
222
|
+
plugins: [almostnodePlugin()]
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The plugin serves `/__sw__.js` automatically during development.
|
|
227
|
+
|
|
228
|
+
**Custom path:**
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// vite.config.ts
|
|
232
|
+
almostnodePlugin({ swPath: '/custom/__sw__.js' })
|
|
233
|
+
|
|
234
|
+
// Then in your app:
|
|
235
|
+
await bridge.initServiceWorker({ swUrl: '/custom/__sw__.js' });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Option 3: Same-Origin with Next.js
|
|
241
|
+
|
|
242
|
+
For trusted code using `dangerouslyAllowSameOrigin: true`:
|
|
243
|
+
|
|
244
|
+
**App Router:**
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// app/__sw__.js/route.ts
|
|
248
|
+
import { getServiceWorkerContent } from 'almostnode/next';
|
|
249
|
+
|
|
250
|
+
export async function GET() {
|
|
251
|
+
return new Response(getServiceWorkerContent(), {
|
|
252
|
+
headers: {
|
|
253
|
+
'Content-Type': 'application/javascript',
|
|
254
|
+
'Cache-Control': 'no-cache',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Pages Router:**
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// pages/api/__sw__.ts
|
|
264
|
+
import { getServiceWorkerContent } from 'almostnode/next';
|
|
265
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
266
|
+
|
|
267
|
+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
268
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
269
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
270
|
+
res.send(getServiceWorkerContent());
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Initialize with the correct path:**
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// App Router (file-based route)
|
|
278
|
+
await bridge.initServiceWorker({ swUrl: '/__sw__.js' });
|
|
279
|
+
|
|
280
|
+
// Pages Router (API route)
|
|
281
|
+
await bridge.initServiceWorker({ swUrl: '/api/__sw__' });
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Available exports from `almostnode/next`:**
|
|
285
|
+
|
|
286
|
+
| Export | Description |
|
|
287
|
+
|--------|-------------|
|
|
288
|
+
| `getServiceWorkerContent()` | Returns the service worker file content as a string |
|
|
289
|
+
| `getServiceWorkerPath()` | Returns the absolute path to the service worker file |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Option 4: Manual Setup (Other Frameworks)
|
|
294
|
+
|
|
295
|
+
Copy the service worker to your public directory:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
cp node_modules/almostnode/dist/__sw__.js ./public/
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Or programmatically:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { getServiceWorkerPath } from 'almostnode/next';
|
|
305
|
+
import fs from 'fs';
|
|
306
|
+
|
|
307
|
+
fs.copyFileSync(getServiceWorkerPath(), './public/__sw__.js');
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
142
312
|
## Comparison with WebContainers
|
|
143
313
|
|
|
144
314
|
| Feature | almostnode | WebContainers |
|
|
@@ -312,12 +482,24 @@ For running untrusted code securely, deploy a cross-origin sandbox. The key requ
|
|
|
312
482
|
|
|
313
483
|
```typescript
|
|
314
484
|
import { generateSandboxFiles } from 'almostnode';
|
|
485
|
+
import fs from 'fs';
|
|
315
486
|
|
|
316
487
|
const files = generateSandboxFiles();
|
|
317
|
-
//
|
|
488
|
+
// Generates: index.html, vercel.json, __sw__.js
|
|
489
|
+
|
|
490
|
+
fs.mkdirSync('sandbox', { recursive: true });
|
|
491
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
492
|
+
fs.writeFileSync(`sandbox/${filename}`, content);
|
|
493
|
+
}
|
|
494
|
+
|
|
318
495
|
// Deploy: cd sandbox && vercel --prod
|
|
319
496
|
```
|
|
320
497
|
|
|
498
|
+
The generated files include:
|
|
499
|
+
- `index.html` - Sandbox page with service worker registration
|
|
500
|
+
- `vercel.json` - CORS headers for cross-origin iframe embedding
|
|
501
|
+
- `__sw__.js` - Service worker for dev server URL access
|
|
502
|
+
|
|
321
503
|
### Manual Setup (Any Platform)
|
|
322
504
|
|
|
323
505
|
The sandbox requires two things:
|
|
@@ -674,7 +856,7 @@ triggerHMR('/app/page.tsx', iframe);
|
|
|
674
856
|
### Setup
|
|
675
857
|
|
|
676
858
|
```bash
|
|
677
|
-
git clone https://github.com/
|
|
859
|
+
git clone https://github.com/Macaly/almostnode.git
|
|
678
860
|
cd almostnode
|
|
679
861
|
npm install
|
|
680
862
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -6,8 +6,31 @@ const pako = require('pako');
|
|
|
6
6
|
const justBash = require('just-bash');
|
|
7
7
|
const resolve_exports = require('resolve.exports');
|
|
8
8
|
const comlink = require('comlink');
|
|
9
|
+
const fs$1 = require('fs');
|
|
10
|
+
const path$1 = require('path');
|
|
11
|
+
const url$2 = require('url');
|
|
9
12
|
|
|
10
13
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
|
+
function _interopNamespaceDefault(e) {
|
|
15
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
|
|
16
|
+
if (e) {
|
|
17
|
+
for (const k in e) {
|
|
18
|
+
if (k !== 'default') {
|
|
19
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
20
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get: () => e[k]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
n.default = e;
|
|
28
|
+
return Object.freeze(n);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs$1);
|
|
32
|
+
const path__namespace = /*#__PURE__*/_interopNamespaceDefault(path$1);
|
|
33
|
+
|
|
11
34
|
function createNodeError(code, syscall, path, message) {
|
|
12
35
|
const errno = {
|
|
13
36
|
ENOENT: -2,
|
|
@@ -9909,7 +9932,34 @@ async function createRuntime(vfs, options = {}) {
|
|
|
9909
9932
|
return new AsyncRuntimeWrapper(vfs, runtimeOptions);
|
|
9910
9933
|
}
|
|
9911
9934
|
|
|
9912
|
-
|
|
9935
|
+
const __dirname$1 = path__namespace.dirname(url$2.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
9936
|
+
function getServiceWorkerContent() {
|
|
9937
|
+
try {
|
|
9938
|
+
let swPath = path__namespace.join(__dirname$1, "__sw__.js");
|
|
9939
|
+
if (fs__namespace.existsSync(swPath)) {
|
|
9940
|
+
return fs__namespace.readFileSync(swPath, "utf-8");
|
|
9941
|
+
}
|
|
9942
|
+
swPath = path__namespace.join(__dirname$1, "../dist/__sw__.js");
|
|
9943
|
+
if (fs__namespace.existsSync(swPath)) {
|
|
9944
|
+
return fs__namespace.readFileSync(swPath, "utf-8");
|
|
9945
|
+
}
|
|
9946
|
+
return null;
|
|
9947
|
+
} catch {
|
|
9948
|
+
return null;
|
|
9949
|
+
}
|
|
9950
|
+
}
|
|
9951
|
+
function getSandboxHtml(options = {}) {
|
|
9952
|
+
const opts = typeof options === "string" ? { almostnodeUrl: options } : options;
|
|
9953
|
+
const almostnodeUrl = opts.almostnodeUrl ?? "https://unpkg.com/almostnode/dist/index.js";
|
|
9954
|
+
const includeServiceWorker = opts.includeServiceWorker ?? true;
|
|
9955
|
+
const serviceWorkerScript = includeServiceWorker ? `
|
|
9956
|
+
// Register service worker for dev server support
|
|
9957
|
+
if ('serviceWorker' in navigator) {
|
|
9958
|
+
navigator.serviceWorker.register('/__sw__.js', { scope: '/' })
|
|
9959
|
+
.then(reg => console.log('[Sandbox] Service worker registered'))
|
|
9960
|
+
.catch(err => console.warn('[Sandbox] Service worker registration failed:', err));
|
|
9961
|
+
}
|
|
9962
|
+
` : "";
|
|
9913
9963
|
return `<!DOCTYPE html>
|
|
9914
9964
|
<html>
|
|
9915
9965
|
<head>
|
|
@@ -9918,7 +9968,8 @@ function getSandboxHtml(justNodeUrl = "https://unpkg.com/almostnode/dist/index.j
|
|
|
9918
9968
|
</head>
|
|
9919
9969
|
<body>
|
|
9920
9970
|
<script type="module">
|
|
9921
|
-
import { VirtualFS, Runtime } from '${
|
|
9971
|
+
import { VirtualFS, Runtime } from '${almostnodeUrl}';
|
|
9972
|
+
${serviceWorkerScript}
|
|
9922
9973
|
|
|
9923
9974
|
let vfs = null;
|
|
9924
9975
|
let runtime = null;
|
|
@@ -10014,11 +10065,18 @@ function getSandboxVercelConfig() {
|
|
|
10014
10065
|
]
|
|
10015
10066
|
};
|
|
10016
10067
|
}
|
|
10017
|
-
function generateSandboxFiles(
|
|
10018
|
-
|
|
10019
|
-
|
|
10068
|
+
function generateSandboxFiles(options = {}) {
|
|
10069
|
+
const opts = typeof options === "string" ? { almostnodeUrl: options } : options;
|
|
10070
|
+
const includeServiceWorker = opts.includeServiceWorker ?? true;
|
|
10071
|
+
const swContent = includeServiceWorker ? getServiceWorkerContent() : null;
|
|
10072
|
+
const files = {
|
|
10073
|
+
"index.html": getSandboxHtml(opts),
|
|
10020
10074
|
"vercel.json": JSON.stringify(getSandboxVercelConfig(), null, 2)
|
|
10021
10075
|
};
|
|
10076
|
+
if (swContent) {
|
|
10077
|
+
files["__sw__.js"] = swContent;
|
|
10078
|
+
}
|
|
10079
|
+
return files;
|
|
10022
10080
|
}
|
|
10023
10081
|
const SANDBOX_SETUP_INSTRUCTIONS = `
|
|
10024
10082
|
# Setting up a almostnode Sandbox on Vercel
|
|
@@ -11006,12 +11064,15 @@ class ServerBridge extends EventEmitter {
|
|
|
11006
11064
|
}
|
|
11007
11065
|
/**
|
|
11008
11066
|
* Initialize Service Worker communication
|
|
11067
|
+
* @param options - Configuration options for the service worker
|
|
11068
|
+
* @param options.swUrl - Custom URL path to the service worker file (default: '/__sw__.js')
|
|
11009
11069
|
*/
|
|
11010
|
-
async initServiceWorker() {
|
|
11070
|
+
async initServiceWorker(options) {
|
|
11011
11071
|
if (!("serviceWorker" in navigator)) {
|
|
11012
11072
|
throw new Error("Service Workers not supported");
|
|
11013
11073
|
}
|
|
11014
|
-
const
|
|
11074
|
+
const swUrl = options?.swUrl ?? "/__sw__.js";
|
|
11075
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
11015
11076
|
scope: "/"
|
|
11016
11077
|
});
|
|
11017
11078
|
const sw = registration.active || registration.waiting || registration.installing;
|
|
@@ -13421,8 +13482,8 @@ class NextDevServer extends DevServer {
|
|
|
13421
13482
|
React.useEffect(() => {
|
|
13422
13483
|
const handlePopState = () => {
|
|
13423
13484
|
setCurrentPath(window.location.pathname);
|
|
13424
|
-
//
|
|
13425
|
-
window.location.reload();
|
|
13485
|
+
// Defer reload outside React's update cycle
|
|
13486
|
+
setTimeout(() => window.location.reload(), 0);
|
|
13426
13487
|
};
|
|
13427
13488
|
|
|
13428
13489
|
window.addEventListener('popstate', handlePopState);
|