OpenContainers is a clean-room, browser-native runtime for running Node-style JavaScript projects without a server-side container. It provides a WebContainer-compatible facade, a virtual filesystem, selected Node built-ins, npm package installation, HTTP previews, and WebSocket bridging for browser-based playgrounds and REPLs.
OpenContainers is currently an early implementation. It is useful for browser-based JavaScript and Node experiments, but it is not a full Linux or Node.js VM.
Current constraints include:
- Native Node add-ons are not supported.
- Browser CORS and network restrictions still apply to external fetches.
- Raw external TCP and TLS sockets are not supported.
- Package lifecycle scripts are permission-gated.
- Preview servers are routed through the OpenContainers service worker.
packages/embedcontains the WebContainer-compatibleOpenContainerfacade.packages/runtime-nodecontains the Node-like runtime and built-ins.packages/fscontains the virtual filesystem.packages/npmcontains npm registry and installer support.packages/service-workerandpackages/preview-clienthandle browser previews.
The npm package intentionally ships the README, package.json, and the bundled browser runtime at packages/embed/dist/webcontainer-compatible.js.
That bundle contains:
- The
OpenContainerandWebContainerbrowser facades. - The virtual filesystem, Node-like runtime, shell, npm runner, preview bridge, and service worker generator.
createOpenContainersServiceWorkerScript(), which writes the service worker your app must serve from its own origin.
You do not need the repository source files to build a REPL host. You import the bundled runtime from opencontainers, serve the generated service worker from your public directory, and provide your own editor, terminal, file UI, and preview iframe.
Run the test suite:
npm testInstall OpenContainers from npm:
npm install opencontainersThe package exposes the browser runtime through opencontainers and opencontainers/webcontainer.
Boot a container, mount files, and spawn a process:
import { OpenContainer } from "opencontainers";
const container = await OpenContainer.boot({
projectId: "demo",
previewBasePath: "/opencontainers/preview",
serviceWorkerUrl: "/opencontainers-runtime-sw.js",
});
await container.mount({
"index.js": {
file: {
contents: "console.log('Hello from OpenContainers');",
},
},
});
const process = await container.spawn("node", ["index.js"]);
let output = "";
await process.output.pipeTo(
new WritableStream({
write(chunk) {
output += String(chunk);
},
}),
);
const exitCode = await process.exit;
console.log({ exitCode, output });OpenContainers gives you the runtime pieces needed to build a browser REPL. Your host app provides the editor, console UI, file state, package controls, and styling.
The minimum host app flow is:
- Install
opencontainers. - Serve the OpenContainers service worker from your own origin.
- Boot an
OpenContainer. - Mount the user's files.
- Spawn
node,npm, or another supported command. - Pipe process output into your console UI.
- Listen for
server-readyand point an iframe at the preview URL.
npm and npx are routed through the real npm CLI inside the virtual filesystem, not a hand-written installer shim.
On first use, OpenContainers downloads a pinned npm CLI tarball from the npm registry, verifies and extracts it into:
/home/opencontainers/.opencontainers/npm/npm-11.17.0npm's cache is stored in:
/home/opencontainers/.npmThat means normal flows such as these work from either your app's run button or a terminal UI backed by container.spawn():
npm install chalk
node index.js
npx cowsay OpenContainers
npm run devThe runtime still enforces browser constraints. Native Node add-ons, postinstall scripts without permission, raw external TCP, and browser-blocked network/CORS requests are not made available just because npm is real.
Service workers must be served from the same origin as your app. They cannot be imported directly from node_modules at runtime.
OpenContainers currently exports a service worker generator. Add a small script that writes it into your app's public/static directory:
// scripts/write-opencontainers-sw.mjs
import { mkdirSync, writeFileSync } from "node:fs";
import { createOpenContainersServiceWorkerScript } from "opencontainers";
mkdirSync("public", { recursive: true });
writeFileSync(
"public/opencontainers-runtime-sw.js",
createOpenContainersServiceWorkerScript({
previewBasePath: "/opencontainers/preview",
}),
);For a Vite app, run it before local dev and before production builds:
{
"scripts": {
"predev": "node scripts/write-opencontainers-sw.mjs",
"dev": "vite",
"prebuild": "node scripts/write-opencontainers-sw.mjs",
"build": "vite build"
}
}This creates a browser-fetchable file at:
/opencontainers-runtime-sw.jsThe service worker needs a root-level scope so it can intercept preview requests under /opencontainers/preview/.... OpenContainer.boot() registers it with scope: "/".
Service workers require HTTPS in production. Localhost is allowed for development.
Use the same service worker URL and preview base path when booting:
import { OpenContainer } from "opencontainers";
const container = await OpenContainer.boot({
projectId: "repl",
serviceWorkerUrl: "/opencontainers-runtime-sw.js",
previewBasePath: "/opencontainers/preview",
});
container.on("error", (error) => {
console.error(error);
});On the first page load after installing a service worker, the page may not be controlled yet. If previews do not connect and you receive the "Service Worker is not controlling this page" error, reload the page once and run again.
Mount a WebContainer-style file tree, then spawn a command:
await container.mount({
"index.js": {
file: {
contents: `
const http = require("node:http");
http.createServer((_req, res) => {
res.writeHead(200, { "content-type": "text/html" });
res.end("<h1>Hello from OpenContainers</h1>");
}).listen(3000, () => {
console.log("Server ready on port 3000");
});
`,
},
},
});
const process = await container.spawn("node", ["index.js"]);Pipe process output into your console:
process.output.pipeTo(
new WritableStream({
write(chunk) {
appendToConsole(String(chunk));
},
}),
);
process.exit.then((exitCode) => {
appendToConsole(`Exited with code ${exitCode}`);
});When runtime code starts an HTTP server, OpenContainers emits a preview URL:
container.on("server-ready", (port, url) => {
console.log(`Server ready on port ${port}: ${url}`);
iframe.src = url;
});For the example above, the URL will look like:
https://your-app.example/opencontainers/preview/repl:3000/The browser loads that URL through your same-origin service worker. The service worker forwards the request into the OpenContainers runtime, which dispatches it to the server listening inside the container.
A small host implementation looks like this:
import { OpenContainer } from "opencontainers";
const iframe = document.querySelector("iframe");
const runButton = document.querySelector("button[data-run]");
const editor = createYourEditorSomehow();
const container = await OpenContainer.boot({
projectId: "repl",
serviceWorkerUrl: "/opencontainers-runtime-sw.js",
previewBasePath: "/opencontainers/preview",
});
container.on("server-ready", (_port, url) => {
iframe.src = url;
});
container.on("error", (error) => {
appendToConsole(error.stack || error.message);
});
runButton.addEventListener("click", async () => {
await container.mount({
"index.js": {
file: {
contents: editor.getValue(),
},
},
});
const process = await container.spawn("node", ["index.js"]);
process.output.pipeTo(
new WritableStream({
write(chunk) {
appendToConsole(String(chunk));
},
}),
);
const exitCode = await process.exit;
appendToConsole(`Exited with code ${exitCode}`);
});That is enough for a basic browser REPL with Node-style execution and HTTP previews. From there, add your own editor, package UI, persistence, sharing, examples, and visual design.
OpenContainers detects HTTP servers started inside the runtime and emits preview URLs:
container.on("server-ready", (port, url) => {
console.log(`Server ready on port ${port}: ${url}`);
iframe.src = url;
});Your app must serve and register the OpenContainers service worker. The default runtime options expect:
- Service worker URL:
/opencontainers-runtime-sw.js - Preview route base:
/opencontainers/preview
Follow the service worker setup above when integrating OpenContainers into your own app.
OpenContainers exports a WebContainer alias for compatibility with code that expects a WebContainer-shaped API:
import { WebContainer } from "opencontainers/webcontainer";
const container = await WebContainer.boot();The compatibility layer supports common flows such as mounting files, spawning Node commands, listening for server-ready events, and dispatching preview requests. It does not aim to perfectly emulate every StackBlitz WebContainers behavior.
npm test
npm run test:milestones