sandboxedjs 0.2.10 → 0.2.12
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 +96 -63
- package/assets/logo.png +0 -0
- package/bin/sandboxedjs-egress.mjs +25 -10
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/service-worker.js +3 -2
- package/docs/agent/COMMANDS.md +85 -0
- package/docs/agent/DECISION-TREE.md +84 -0
- package/docs/agent/INVARIANTS.md +40 -0
- package/docs/agent/LAUNCH-PROMPT.md +37 -0
- package/docs/agent/LOOP.md +84 -0
- package/docs/agent/README.md +77 -0
- package/docs/agent/ROADMAP.md +37 -0
- package/docs/agent/STATE.md +93 -0
- package/docs/agent/tasks/00-verify-inherited-work.md +36 -0
- package/docs/agent/tasks/01-authoritative-metadata.md +34 -0
- package/docs/agent/tasks/02-native-dependencies.md +35 -0
- package/docs/agent/tasks/03-reproducible-inputs.md +27 -0
- package/docs/agent/tasks/04-build-frontends.md +27 -0
- package/docs/agent/tasks/05-registry-integration.md +27 -0
- package/docs/agent/tasks/06-package-cohorts.md +42 -0
- package/docs/agent/tasks/07-build-on-miss-boundary.md +31 -0
- package/docs/browser-runtime-architecture.md +142 -0
- package/docs/compatibility-implementation-plan.md +98 -0
- package/docs/developer-tool-packs.md +134 -0
- package/docs/frontend-automation.md +49 -0
- package/docs/fullstack-deployment.md +163 -0
- package/docs/handoff.md +275 -0
- package/docs/original-x64.md +39 -0
- package/docs/platform-hardening.md +49 -0
- package/docs/python/abi.md +97 -0
- package/docs/python/architecture.md +94 -0
- package/docs/python/baseline-inventory.md +54 -0
- package/docs/python/build-on-miss.md +198 -0
- package/docs/python/compatibility.md +206 -0
- package/docs/python/cross-build.md +354 -0
- package/docs/python/extensions.md +282 -0
- package/docs/python/release-gates.md +46 -0
- package/docs/python/virtual-sockets-plan.md +331 -0
- package/docs/runtime-lifecycle-fixes.md +39 -0
- package/docs/server-previews.md +268 -0
- package/docs/virtual-browser.md +120 -0
- package/package.json +5 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Render a site running inside SandboxedJs
|
|
2
|
+
|
|
3
|
+
For deployment recipes and the FastAPI + static frontend case, start with the
|
|
4
|
+
[full-stack deployment guide](fullstack-deployment.md).
|
|
5
|
+
|
|
6
|
+
A SandboxedJs server listens on a **virtual port**, not an operating-system socket. A log such as `http://localhost:5173` identifies the server inside the container. Pasting that URL into the host browser does not expose it.
|
|
7
|
+
|
|
8
|
+
Choose the bridge for the environment running the container:
|
|
9
|
+
|
|
10
|
+
| Goal | API | Result |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Render a whole site in a browser-only coding sandbox | `createPreview(box)` | Service-worker URL for an iframe; no Node backend required |
|
|
13
|
+
| Reach a container running in Node | `box.expose(port)` | Real loopback HTTP URL |
|
|
14
|
+
| Inspect an API or obtain HTML/assets without rendering | `box.request(port, { path })` | Status, headers, text and exact bytes |
|
|
15
|
+
| Display one self-contained HTML response | `renderInto(box, element, { port })` | Opaque-origin iframe; not a complete site proxy |
|
|
16
|
+
|
|
17
|
+
Use an iframe for a document with its own scripts, styles and navigation. Inserting its HTML into your IDE's DOM does not create a working site environment.
|
|
18
|
+
|
|
19
|
+
## Browser-only: a complete small example
|
|
20
|
+
|
|
21
|
+
Run this TypeScript in your host application's browser bundle. Supply an element with a visible height, for example `<div id="preview" style="height:500px"></div>`. Configure hosting as described below first.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createContainer, createPreview } from 'sandboxedjs';
|
|
25
|
+
|
|
26
|
+
export async function mountSandboxSite(element: HTMLElement) {
|
|
27
|
+
const box = await createContainer({
|
|
28
|
+
cwd: '/workspace',
|
|
29
|
+
isolation: 'worker',
|
|
30
|
+
files: {
|
|
31
|
+
'/workspace/server.cjs': `
|
|
32
|
+
require('http').createServer((req, res) => {
|
|
33
|
+
if (req.url === '/app.js') {
|
|
34
|
+
res.setHeader('Content-Type', 'text/javascript');
|
|
35
|
+
res.end('document.querySelector("button").onclick = () => document.querySelector("button").textContent = "It works!";');
|
|
36
|
+
} else {
|
|
37
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
38
|
+
res.end('<!doctype html><html><body><h1>Inside SandboxedJs</h1><button>Click me</button><script src="/app.js"></script></body></html>');
|
|
39
|
+
}
|
|
40
|
+
}).listen(3000, () => console.log('Listening on virtual port 3000'));
|
|
41
|
+
`,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const server = box.spawn('node server.cjs', { cwd: '/workspace' });
|
|
45
|
+
let preview: Awaited<ReturnType<typeof createPreview>> = null;
|
|
46
|
+
const frame = document.createElement('iframe');
|
|
47
|
+
frame.title = 'Sandbox site';
|
|
48
|
+
frame.style.cssText = 'width:100%;height:100%;border:0';
|
|
49
|
+
|
|
50
|
+
const dispose = async () => {
|
|
51
|
+
frame.remove();
|
|
52
|
+
server.kill();
|
|
53
|
+
await server.wait();
|
|
54
|
+
try { await preview?.dispose(); }
|
|
55
|
+
finally { box.dispose(); }
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (!(await box.waitForPort(3000, { timeoutMs: 30_000 }))) {
|
|
60
|
+
throw new Error('The sandbox server did not open port 3000. Inspect its stdout/stderr.');
|
|
61
|
+
}
|
|
62
|
+
preview = await createPreview(box);
|
|
63
|
+
if (!preview) {
|
|
64
|
+
throw new Error('Preview requires a secure context and service-worker support.');
|
|
65
|
+
}
|
|
66
|
+
frame.src = preview.urlFor(3000);
|
|
67
|
+
element.replaceChildren(frame);
|
|
68
|
+
return {
|
|
69
|
+
box,
|
|
70
|
+
server,
|
|
71
|
+
reload: () => { frame.src = preview!.urlFor(3000); },
|
|
72
|
+
dispose,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await dispose();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Mount after the host DOM exists:
|
|
81
|
+
// const site = await mountSandboxSite(document.querySelector('#preview')!);
|
|
82
|
+
// Later: site.reload();
|
|
83
|
+
// On application/component teardown: await site.dispose();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This example intentionally renders trusted code on the host origin. The absolute `/app.js` request is routed back into the virtual server. No rewriting of the site's HTML or JavaScript is necessary.
|
|
87
|
+
|
|
88
|
+
### What the browser bridge does
|
|
89
|
+
|
|
90
|
+
1. `createPreview()` registers the shipped service worker and connects a message channel to the container owner page.
|
|
91
|
+
2. `urlFor(3000)` returns a URL under that worker's scope, such as `/assets/__sbx__/3000/`. Treat the returned URL as opaque; do not hardcode the asset directory.
|
|
92
|
+
3. An iframe navigation claims a virtual port. The worker associates the resulting browser client with that port.
|
|
93
|
+
4. Requests from that client, including absolute asset and API paths, go through `box.request()` and return as browser responses.
|
|
94
|
+
5. A previewed page's calls to another **loopback** address — `http://localhost:8000/api`, `http://127.0.0.1:8000/api` — are answered by that port in the same container, not by the reader's machine. A project split into a frontend and a backend can keep the address its code is written against. Requests to any other host (`https://api.example.com`, a LAN address) still go to the network, and a page that is not previewing anything is never redirected.
|
|
95
|
+
|
|
96
|
+
Responses are delivered whole rather than streamed, so a page reading an
|
|
97
|
+
incremental body — a server-sent-event endpoint, a streamed completion — sees
|
|
98
|
+
it arrive in one piece when the response ends rather than progressively. A
|
|
99
|
+
request is therefore outstanding for as long as the whole answer takes, which
|
|
100
|
+
is what `timeoutMs` bounds:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const preview = await createPreview(box, { timeoutMs: 10 * 60_000 });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The default is five minutes. When it is exceeded the page gets a 504 whose body
|
|
107
|
+
names the path, the port and this setting.
|
|
108
|
+
|
|
109
|
+
The container owner page must remain alive. The URL is not a published website, a remote tunnel, or a standalone share link. A virtual port appearing in the terminal does not mean the host machine is listening on that port.
|
|
110
|
+
|
|
111
|
+
## Use a real project or a terminal in an IDE
|
|
112
|
+
|
|
113
|
+
Create the container with `network: { allowOutbound: true }` when it needs npm downloads. Load project files into its filesystem, then use the normal commands:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const install = await box.exec('npm install', { cwd: '/workspace/my-app' });
|
|
117
|
+
if (install.exitCode !== 0) throw new Error(install.output);
|
|
118
|
+
|
|
119
|
+
// Keep this process handle. Do not await completion of a long-running server.
|
|
120
|
+
const dev = box.spawn('npm run dev', { cwd: '/workspace/my-app' });
|
|
121
|
+
if (!(await box.waitForPort(5173, { timeoutMs: 60_000 }))) {
|
|
122
|
+
dev.kill();
|
|
123
|
+
throw new Error('Dev server did not start; show its logs.');
|
|
124
|
+
}
|
|
125
|
+
const preview = await createPreview(box);
|
|
126
|
+
if (!preview) throw new Error('Browser preview unavailable');
|
|
127
|
+
iframe.src = preview.urlFor(5173);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For an interactive terminal, use `Terminal` with `box.session()` and let the user run `npm create vite`, answer the prompts, and choose installation/startup. Worker support is required for the synchronous child-process calls used by scaffolding tools. The preview can attach to that same container; do not create a second container just to render its site.
|
|
131
|
+
|
|
132
|
+
For an IDE's port selector, poll `box.net.listening()` and use each entry's `port`. Do not assume every framework uses 5173. For programmatically spawned commands, consume `stdout` and `stderr` using their asynchronous `read()` methods; `Terminal` already forwards terminal output to its configured writer.
|
|
133
|
+
|
|
134
|
+
Keep the lifetimes separate:
|
|
135
|
+
|
|
136
|
+
- Hide/close the pane: remove or hide the iframe; keep the server and container alive.
|
|
137
|
+
- Reload the page preview: assign `frame.src = preview.urlFor(port)` again.
|
|
138
|
+
- Stop the server: kill the owned process, or send Ctrl+C through the active terminal; wait for its port to disappear and clear the frame.
|
|
139
|
+
- Replace a project: remove its iframe and await the old preview's disposal before attaching the new container.
|
|
140
|
+
- Destroy the IDE session: stop owned processes, dispose the preview, and dispose the container.
|
|
141
|
+
|
|
142
|
+
The companion CLI implements an icon toggle, port selection, reload/close, a left-side divider on desktop, and a bottom divider on mobile. The divider supports pointer dragging, arrow keys and double-click reset. These are UI concerns; the core bridge only provides URLs.
|
|
143
|
+
|
|
144
|
+
## Hosting the browser application
|
|
145
|
+
|
|
146
|
+
The IDE host page—not the Vite server inside the sandbox—needs these response headers for shared memory and threaded WASM:
|
|
147
|
+
|
|
148
|
+
```http
|
|
149
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
150
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Use HTTPS in deployment, or localhost/loopback for local development. Service workers must be enabled. Check the actual page's `crossOriginIsolated` value; a meta tag cannot enable it.
|
|
154
|
+
|
|
155
|
+
For a Vite-based host application:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { defineConfig } from 'vite';
|
|
159
|
+
const headers = {
|
|
160
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
161
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
162
|
+
};
|
|
163
|
+
export default defineConfig({
|
|
164
|
+
optimizeDeps: { exclude: ['@rolldown/binding-wasm32-wasi'] },
|
|
165
|
+
server: { headers },
|
|
166
|
+
preview: { headers },
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Cloudflare Pages
|
|
171
|
+
|
|
172
|
+
Cloudflare Pages can host the outer application as static assets. In a Vite host application, create `public/_headers`, build with
|
|
173
|
+
`npm run build`, set the Pages output directory to `dist`, and verify that Vite copied
|
|
174
|
+
the file to `dist/_headers`:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
/*
|
|
178
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
179
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
180
|
+
Cross-Origin-Resource-Policy: same-origin
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Pages treats `_headers` as header configuration. It is not a file the browser needs to download.
|
|
184
|
+
After deployment, verify `crossOriginIsolated` in the deployed page and confirm that the service
|
|
185
|
+
worker script is a JavaScript response under the same HTTPS origin. A Pages static deployment
|
|
186
|
+
hosts the IDE and its service worker; it does not host the virtual sandbox port separately.
|
|
187
|
+
The site running inside SandboxedJs is fetched through the service-worker/message bridge in the
|
|
188
|
+
browser visitor's tab.
|
|
189
|
+
|
|
190
|
+
Excluding the binding preserves the URL of its WASI worker. For a built application, the package also includes a local static host:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
npm run build
|
|
194
|
+
npx sandboxedjs-serve ./dist 4173
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This starts the **outer host application** with the required headers. It does not replace `npm run dev` inside the container and does not move guest execution to Node. For production, configure your actual host/CDN with HTTPS, these headers, correct JavaScript/WASM MIME types and real asset URLs. Cross-origin assets need compatible CORS/CORP policies.
|
|
198
|
+
|
|
199
|
+
The preview service worker must be emitted as a same-origin HTTP(S) file, not an inlined `data:` URL. The default package URL includes Vite's `?no-inline` hint. Other bundlers must preserve/copy the worker asset. If you relocate it, use `createPreview(box, { scriptUrl: '/preview/service-worker.js' })`. A custom `scope` must be permitted by the worker script location or the host's `Service-Worker-Allowed` header.
|
|
200
|
+
|
|
201
|
+
## Node-hosted containers
|
|
202
|
+
|
|
203
|
+
`box.expose()` opens a real Node HTTP listener. It is not the browser-only API.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { createContainer } from 'sandboxedjs';
|
|
207
|
+
|
|
208
|
+
const box = await createContainer({
|
|
209
|
+
files: {
|
|
210
|
+
'/app/server.cjs': "require('http').createServer((q,r)=>r.end('Hello from the sandbox')).listen(3000)",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
const server = box.spawn('node server.cjs', { cwd: '/app' });
|
|
214
|
+
if (!(await box.waitForPort(3000, { timeoutMs: 30_000 }))) {
|
|
215
|
+
server.kill();
|
|
216
|
+
box.dispose();
|
|
217
|
+
throw new Error('Server startup failed');
|
|
218
|
+
}
|
|
219
|
+
const bridge = await box.expose(3000); // random available loopback port
|
|
220
|
+
console.log(bridge.url); // open this in a browser or iframe
|
|
221
|
+
// For a fixed host port: box.expose(3000, { hostPort: 8080 })
|
|
222
|
+
|
|
223
|
+
// When finished:
|
|
224
|
+
// await bridge.close();
|
|
225
|
+
// server.kill();
|
|
226
|
+
// await server.wait();
|
|
227
|
+
// box.dispose();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Loopback is reachable only from the host machine. Remote access needs an explicitly designed backend/proxy and authentication. An HTTPS frontend cannot freely embed an HTTP backend; plan TLS and framing policies for deployments.
|
|
231
|
+
|
|
232
|
+
This bridge does not make all packages host-independent. In particular, the current Vite 8/Rolldown WASI integration uses the browser binding's mirrored filesystem; the Node binding uses the real host filesystem. See the README's known limits before choosing a Node-hosted toolchain. A plain HTTP server is supported on both hosts.
|
|
233
|
+
|
|
234
|
+
## A response without a full site
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const response = await box.request(3000, { path: '/api/items' });
|
|
238
|
+
console.log(response.status, response.headers, response.body);
|
|
239
|
+
const exactBytes = response.bytes;
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
For self-contained HTML, `renderInto()` uses an iframe with `sandbox="allow-scripts"` and an opaque origin:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { renderInto } from 'sandboxedjs';
|
|
246
|
+
await renderInto(box, document.querySelector('#preview')!, { port: 3000 });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
This renders one response. It does not route the document's relative/absolute scripts, styles, images or API calls into the container. It is unsuitable as a replacement for `createPreview()` when displaying a normal Vite application. The sandbox attribute also is not a network-access policy.
|
|
250
|
+
|
|
251
|
+
## Boundaries to plan for
|
|
252
|
+
|
|
253
|
+
- **Trust:** `createPreview()` serves guest scripts on the host origin. They can access parent-page state, cookies and storage. It is not an isolation boundary for untrusted code. Running the preview/runtime on a separate origin requires additional architecture; there is no `previewOrigin` switch that implements this for you.
|
|
254
|
+
- **HMR:** the injected WebSocket shim tunnels supported guest connections through the owner page. It is enabled by default; disabling injection or blocking it with CSP removes this support. External WebSockets and framework-specific behavior need separate testing.
|
|
255
|
+
- **Multiple sessions:** the current service worker has one connected container per registration. Reusing the same scope across containers/tabs can replace that connection, and disposing one registration affects its users. Isolate registrations or use one active session; scopes alone do not provide a security boundary. Concurrent browser Rolldown projects also need distinct absolute working directories because the binding shares a WASI filesystem per page.
|
|
256
|
+
- **Lifetime:** the worker asks live owner pages to reconnect after restart. Its in-memory client bindings can still be lost; explicit preview-prefixed URLs remain routable. This does not persist the container after the owner page closes or provide multi-tenant routing.
|
|
257
|
+
- **HTTP coverage:** the bridge buffers responses; it is not a general TCP socket, streaming transport or complete reverse proxy. Absolute external URLs, redirects, cookies and application-specific framing policies may need additional handling. Check the behavior your framework actually needs.
|
|
258
|
+
|
|
259
|
+
## Troubleshooting a blank or refused iframe
|
|
260
|
+
|
|
261
|
+
1. Check the current page/build. An older bundle or stale iframe may still contain a previously fixed error. Save/export needed work before refreshing the host page; tab-memory sessions are not persisted.
|
|
262
|
+
2. Confirm the virtual server is listed by `box.net.listening()` and `await box.request(port, { path: '/' })` returns the expected HTML.
|
|
263
|
+
3. Confirm the iframe uses `preview.urlFor(port)`, not the guest's printed `localhost` URL.
|
|
264
|
+
4. Inspect the service-worker registration, script URL, MIME type, scope and browser console. A successful registration is not evidence that every later navigation was routed.
|
|
265
|
+
5. Inspect framing/isolation errors separately from connection errors: CSP, `X-Frame-Options` and COEP can replace a frame with a refusal page even while the virtual server is running.
|
|
266
|
+
6. If HTML/assets work but only a WebSocket fails, that is the current HMR limitation, not proof that the HTTP server failed.
|
|
267
|
+
|
|
268
|
+
A browser-native refusal page is not a diagnosis by itself. Compare the virtual HTTP response, bridge state and actual browser error before changing framework commands.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Virtual browser
|
|
2
|
+
|
|
3
|
+
Anything that drives Chromium through the DevTools protocol — Playwright from
|
|
4
|
+
Node.js or Python, Puppeteer — gets the SandboxedJs virtual browser instead of
|
|
5
|
+
a Chromium binary. No browser is downloaded and nothing is patched in the
|
|
6
|
+
driver: the protocol is the seam.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
// inside a container, after `npm install playwright-core`
|
|
10
|
+
import { chromium } from 'playwright-core';
|
|
11
|
+
|
|
12
|
+
const browser = await chromium.launch();
|
|
13
|
+
const page = await browser.newPage();
|
|
14
|
+
await page.goto('https://example.test/');
|
|
15
|
+
console.log(await page.evaluate(() => 6 * 7)); // 42
|
|
16
|
+
await browser.close();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# after `pip install playwright`
|
|
21
|
+
import asyncio
|
|
22
|
+
from playwright.async_api import async_playwright
|
|
23
|
+
|
|
24
|
+
async def main():
|
|
25
|
+
async with async_playwright() as p:
|
|
26
|
+
browser = await p.chromium.launch()
|
|
27
|
+
page = await browser.new_page()
|
|
28
|
+
print(await page.evaluate("() => 6 * 7"))
|
|
29
|
+
await browser.close()
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How a launch is routed
|
|
35
|
+
|
|
36
|
+
1. **Installing Playwright writes the browser it will look for.** When
|
|
37
|
+
`playwright-core` is installed with npm, or `playwright` with pip, the
|
|
38
|
+
executables named in its `browsers.json` are written under
|
|
39
|
+
`~/.cache/ms-playwright` as stubs for the `chrome` built-in. Only Chromium
|
|
40
|
+
(and its headless shell) is provided; Firefox and WebKit stay missing, so
|
|
41
|
+
asking for them fails with Playwright's own message.
|
|
42
|
+
2. **Playwright spawns it with `--remote-debugging-pipe`.** Its `stdio` is
|
|
43
|
+
`['ignore', 'pipe', 'pipe', 'pipe', 'pipe']`: commands arrive on fd 3 and
|
|
44
|
+
replies leave on fd 4, each JSON message terminated by a NUL byte.
|
|
45
|
+
`child_process` supports pipes above fd 2 for this, in-realm and through
|
|
46
|
+
the worker.
|
|
47
|
+
3. **`chrome` hands every message to `CdpServer`** (`src/browser/`), which owns
|
|
48
|
+
targets, sessions, frames and execution contexts.
|
|
49
|
+
|
|
50
|
+
`chrome`, `chromium`, `chromium-browser`, `google-chrome` and
|
|
51
|
+
`chrome-headless-shell` are all the same built-in.
|
|
52
|
+
|
|
53
|
+
## What a page is today
|
|
54
|
+
|
|
55
|
+
A page has a real, isolated JavaScript realm — `node:vm` in Node.js, a detached
|
|
56
|
+
iframe in a browser — and **no DOM yet**. That covers `launch`, contexts,
|
|
57
|
+
`newPage`, `goto` (the navigation commits and fires its lifecycle events; the
|
|
58
|
+
URL is not fetched), `url`, `evaluate` / `evaluateHandle` with arguments,
|
|
59
|
+
promises and thrown errors, and `close`.
|
|
60
|
+
|
|
61
|
+
Anything that needs a document — locators, `click`, `fill`, `content`,
|
|
62
|
+
`screenshot`, network interception — does not work yet. Unimplemented protocol
|
|
63
|
+
methods are refused with Chromium's own `'Method' wasn't found` error rather
|
|
64
|
+
than answered with an empty success, so a driver fails where the gap is.
|
|
65
|
+
`*.enable`, `*.disable` and `set*` configuration calls are accepted, because a
|
|
66
|
+
page without the feature can honestly take them.
|
|
67
|
+
|
|
68
|
+
The DOM, CSS layout and painting engine plugs in behind `CdpServer`; that is
|
|
69
|
+
the next stage.
|
|
70
|
+
|
|
71
|
+
## Python specifics
|
|
72
|
+
|
|
73
|
+
`playwright` publishes only platform wheels, because each bundles Node.js. The
|
|
74
|
+
resolver accepts its Linux x86-64 wheel by name (`src/python/substitutions.ts`)
|
|
75
|
+
and the install replaces `driver/node` with a script that runs the container's
|
|
76
|
+
`node`.
|
|
77
|
+
|
|
78
|
+
`greenlet` cannot work in WebAssembly — it switches native stacks. A built-in
|
|
79
|
+
stand-in is installed instead: it imports and can be subclassed, and switching
|
|
80
|
+
raises `greenlet.error`. Playwright only switches greenlets in its synchronous
|
|
81
|
+
API, so **`playwright.async_api` works and `playwright.sync_api` fails** with
|
|
82
|
+
that error, which names the asyncio API.
|
|
83
|
+
|
|
84
|
+
Playwright's Python driver is the container's Node.js speaking a
|
|
85
|
+
length-prefixed binary protocol over stdin and stdout, which exercised several
|
|
86
|
+
general gaps, now fixed:
|
|
87
|
+
|
|
88
|
+
- **Binary stdio.** A `Buffer` written to stdout, or read from a live pipe,
|
|
89
|
+
used to be decoded as UTF-8 on the way through. Pods still emit text on
|
|
90
|
+
`output`; bytes as written go on `raw-output`, which `node` forwards into
|
|
91
|
+
pipes.
|
|
92
|
+
- **Live stdin.** A pipe inherited from Python was reported as non-interactive,
|
|
93
|
+
so `node` read it to end-of-file before starting the script — and a driver
|
|
94
|
+
whose stdin never closes never started.
|
|
95
|
+
- **Pipe backpressure.** A kernel pipe holds 64 KiB and accepts a prefix of a
|
|
96
|
+
larger write; the child-output wrapper ignored the count and dropped the
|
|
97
|
+
rest. Output is now queued and written as the reader makes room.
|
|
98
|
+
- **Built-in interop.** A partially supported built-in answered `__esModule`,
|
|
99
|
+
so bundlers' `__toESM` lost its `default` (`net`, read by Playwright at load).
|
|
100
|
+
- **ESM detection.** `new.target` in CommonJS was mistaken for `import.meta`.
|
|
101
|
+
|
|
102
|
+
And two in the Python runtime itself:
|
|
103
|
+
|
|
104
|
+
- **asyncio subprocesses.** Without `pidfd_open`, CPython waits for children
|
|
105
|
+
on a helper thread, whose blocking host call fails with EIO. A polling child
|
|
106
|
+
watcher (installed through the runtime's `sitecustomize`) reaps with
|
|
107
|
+
`WNOHANG` on the loop's own thread instead.
|
|
108
|
+
- **Kernel pipes in selectors.** A pipe's descriptor number inside the
|
|
109
|
+
interpreter differs from the kernel's; `poll` now translates it (sockets
|
|
110
|
+
already use kernel numbers and are tracked so they are never translated),
|
|
111
|
+
a stream's readiness is asked of the kernel with a zero timeout, and
|
|
112
|
+
`O_NONBLOCK` reads return `EAGAIN` instead of waiting.
|
|
113
|
+
|
|
114
|
+
## Not supported
|
|
115
|
+
|
|
116
|
+
- Headed Chromium, `--remote-debugging-port`, and connecting over WebSocket
|
|
117
|
+
(`connectOverCDP`): only the pipe transport is served.
|
|
118
|
+
- Firefox, WebKit.
|
|
119
|
+
- The Python synchronous API.
|
|
120
|
+
- Anything needing a DOM, layout or pixels (for now).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandboxedjs",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "A Linux-like container that runs entirely inside Node.js — POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
"!dist/**/*.map",
|
|
54
54
|
"bin",
|
|
55
55
|
"README.md",
|
|
56
|
-
"LICENSE"
|
|
56
|
+
"LICENSE",
|
|
57
|
+
"assets/logo.png",
|
|
58
|
+
"docs"
|
|
57
59
|
],
|
|
58
60
|
"scripts": {
|
|
59
61
|
"build": "tsup && node python-runtime/scripts/copy_runtime.mjs",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"test:watch": "vitest",
|
|
64
66
|
"check": "npm run typecheck && npm run build && npm test",
|
|
65
67
|
"prepublishOnly": "npm run typecheck && npm run build && npm run test:release",
|
|
66
|
-
"test:release": "vitest run test/python-runtime/extension-abi.test.ts test/python-runtime/resolver.test.ts test/no-static-node-builtins.test.ts"
|
|
68
|
+
"test:release": "vitest run test/python-runtime/extension-abi.test.ts test/python-runtime/resolver.test.ts test/no-static-node-builtins.test.ts test/preview-worker-build.test.ts test/egress-cli.test.ts"
|
|
67
69
|
},
|
|
68
70
|
"keywords": [
|
|
69
71
|
"sandbox",
|