
# WebContainer

`webcontainer` contributes a shell to the [terminal](/extensions/terminal), and behind it is
[WebContainer](https://webcontainers.io): node itself, running in the page, with npm and a real
filesystem. It needs `terminal` alongside it to have somewhere to appear.

::warning
WebContainer needs the page cross-origin isolated. Serve the document with:

```http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
```

Without both headers, `webcontainer` registers no shell at all: no profile in "New Terminal", no
`WebContainer: Open Server` in the palette. It costs nothing to mount unconditionally — see
[The two headers](#the-two-headers) below.
::

```ts
import { Workbench, FileSystem } from "codelet/workbench";
import { terminal } from "codelet/extensions/terminal";
import { webcontainer } from "codelet/extensions/webcontainer";

const workbench = new Workbench({
  parent: document.getElementById("app")!,
  fs: new FileSystem({ "/index.js": "console.log('hi')" }),
  extensions: [terminal(), webcontainer()],
});
```

The SDK is fetched from a CDN by the first terminal tab that asks for it, and by no page that
never opens one. Several tabs are several shells on one machine: WebContainer boots once per
page, so the second tab shares the first one's filesystem, its `node_modules` and its running
processes.

It sits beside [just-bash](/extensions/just-bash) rather than instead of it — "New Terminal"
offers both where both are mounted. just-bash is an interpreter over the tree and starts
instantly; this is a machine you can `npm install` on.

## The two headers

WebContainer needs a `SharedArrayBuffer`, and a page only gets one cross-origin isolated —
`Cross-Origin-Opener-Policy: same-origin` plus `Cross-Origin-Embedder-Policy: credentialless` (or
`require-corp`).

`credentialless` is the one to start with: under `require-corp` every cross-origin subresource
has to send its own `Cross-Origin-Resource-Policy`, which most CDNs do not — including the ones
codelet fetches xterm and language servers from. Safari only understands `require-corp`, so a
page that has to work there needs every cross-origin asset to cooperate with that instead.

Missing headers are silent by design: the extension registers no shell, so you get the workbench
you had rather than a menu entry that could only ever fail, and the reason is one line in the
Logs panel. That is what makes it safe to mount `webcontainer()` unconditionally, including on
pages that will never be isolated.

## Options

| Option        | Type                                           | Default                    | Description                                                                                 |
| ------------- | ---------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
| `workspace`   | `boolean`                                      | `true`                     | Whether the workbench's files are mirrored onto the machine. `false` is an empty container. |
| `exclude`     | `readonly string[]`                            | `["node_modules", ".git"]` | Directory names never mirrored, matched at any depth.                                       |
| `command`     | `string`                                       | `"jsh"`                    | What a tab spawns.                                                                          |
| `args`        | `readonly string[]`                            | —                          | Its arguments.                                                                              |
| `env`         | `Record<string, string>`                       | —                          | Environment variables for every tab.                                                        |
| `name`        | `string`                                       | `"WebContainer"`           | What the profile is called in the menu and on the tab.                                      |
| `cdn`         | `string`                                       | `"https://esm.sh"`         | Where `@webcontainer/api` is fetched from.                                                  |
| `coep`        | `"require-corp" \| "credentialless" \| "none"` | the SDK's own              | Which COEP the page is served with, so the SDK can reach its runtime.                       |
| `workdirName` | `string`                                       | the SDK's own              | Names the working directory: `/home/<workdirName>`.                                         |

## The filesystem

The workbench's tree is handed over when the container boots, and the two are kept in step
after that: what you edit is written into the container, and what a command writes is written
back into the tree. So `npm init -y` puts a `package.json` in the explorer, and a file you save
in the editor is the file the dev server rebuilds.

Both directions are debounced, and `node_modules` and `.git` are never mirrored back — an
install is a hundred thousand files, and the workbench's tree is memory. Add `dist` or anything
else a build fills to `exclude`.

Two things to know. The container's paths are its own: a file the explorer calls `/src/app.js`
is `~/project/src/app.js` in the shell. And an empty directory does not reach the container
until something is written into it.

## Servers

Start a dev server and a tab opens for it on its own — no click needed. The address is not the
`localhost:3000` the command printed: WebContainer serves it from an origin of its own, and that
tab shows the real one. A restart on the same port reloads that tab in place rather than opening
a second, and without stealing focus back from whatever the reader is editing.

Closed the tab? Run `WebContainer: Open Server` from the palette to bring it back — a quick pick
if more than one server is running, otherwise straight to the one there is.

That tab is a real cross-origin frame, not a preview panel: codelet's webviews are sandboxed
without same-origin access, which would leave a dev server running in an opaque origin with its
storage and its hot-reload socket refused. Its bar has a button to open the same address in a
genuine browser tab too, for a login or a popup that refuses to run inside a frame at all.

:read-more{to="/extensions/terminal"}
