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 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
- // Write files['index.html'] and files['vercel.json'] to a directory
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/user/almostnode.git
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
- function getSandboxHtml(justNodeUrl = "https://unpkg.com/almostnode/dist/index.js") {
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 '${justNodeUrl}';
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(justNodeUrl) {
10018
- return {
10019
- "index.html": getSandboxHtml(justNodeUrl),
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 registration = await navigator.serviceWorker.register("/__sw__.js", {
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
- // Re-render the page component
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);