# How the Worker-JavaScript Backend Handles Durable Relative Imports and Structured I/O

> Discover how the Worker-JavaScript backend manages durable relative imports and structured I/O by resolving imports against a virtual file system and capturing output streams.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-14

---

**The Worker-JavaScript backend in cloudflare/computer resolves relative imports against a Workspace's virtual file system and captures stdout, stderr, and exit codes through a framed I/O stream.**

The `worker-javascript` backend (`@cloudflare/computer/backends/worker-javascript`) provides a full ECMAScript module execution environment inside a Cloudflare Dynamic Worker. It powers durable execution of Workspace scripts with two critical capabilities: resolving relative imports without filesystem access and streaming structured I/O events back to the host.

---

## Durable Relative Imports in Worker-JavaScript

When a script executed by the worker-javascript backend calls `import "./foo.js"`, the backend cannot rely on standard filesystem resolution. Instead, it uses the `WorkspaceRuntimeCapability` to resolve imports against the Workspace's virtual file system.

### How `buildModuleGraph` Resolves Imports

The core resolution logic lives in [`module-graph.ts`](https://github.com/cloudflare/computer/blob/main/module-graph.ts) at lines 70-98. The function walks the AST of the entry module and handles each import specifier:

- **Relative paths** (`specifier.startsWith(".")`) trigger a three-step process:
  1. `resolveRelative` computes the absolute path while preserving directory hierarchy
  2. `capability.readFile` loads the source, with size checked against `maxSourceBytes`
  3. Recursive AST walking builds a complete module graph for the Dynamic Worker loader

- **Trusted modules** (`ws:*` and `node:*`) are injected automatically and excluded from relative-import resolution

This pre-validation ensures import errors surface as deterministic failures before the Worker starts, not mid-execution.

### Example: Relative Import Resolution

```typescript
// workspace entry (exec source)
import "./utils.js";

export default async function (input: any) {
  const { greet } = await import("./utils.js");
  console.log(greet("World"));
  return { message: "done" };
}

```

When executed via `workspace.runtime.exec(..., {backend:"worker-javascript"})`, `buildModuleGraph` resolves [`./utils.js`](https://github.com/cloudflare/computer/blob/main/./utils.js) relative to the script's working directory, reads its contents through the Workspace capability, and adds it to the loader's module map.

### Trusted Module Injection

The backend automatically injects stubs for Workspace capabilities. These bypass the relative-import walker:

```typescript
import { git } from "ws:git";

export default async function () {
  const branches = await git.listBranches();
  console.log("branches:", branches);
}

```

The stub for `ws:git` is injected at [`module-graph.ts`](https://github.com/cloudflare/computer/blob/main/module-graph.ts) lines 37-44, enabling host-side Git functionality without additional configuration.

---

## Structured I/O: Capturing stdout, stderr, and Exit Codes

The worker-javascript backend replaces standard stream APIs with instrumented wrappers that frame every I/O event for reliable host-side consumption.

### Runtime I/O Instrumentation

Inside the Worker, `runtimeWorkerModule` (lines 73-85 in [`worker-javascript.ts`](https://github.com/cloudflare/computer/blob/main/worker-javascript.ts)) performs three key steps:

- Creates a fake `process` object with `stdout.write` and `stderr.write` stubs
- Overrides `console.log` and `console.error` to route through the same recording path
- All writes are **base-64 encoded**, limited by `maxStdioBytes`, and enqueued to an `IdentityTransformStream`

### Framed Output Stream and Host Decoding

The framed stream connects to the host through this pipeline:

1. **Worker side**: `host.attachOutput(output.readable)` streams framed JSON lines
2. **Host side**: `#pumpFrames` (lines 89-110) decodes each frame via `decodeRuntimeFrames`
3. **Event translation**: Frames become `WorkspaceRuntimeEvent` objects (`stdout`, `stderr`, `exit`)
4. **Persistence**: Events are stored via `#persistEvent` and published to subscribers via `#publishEvent`

When user code finishes, the runtime posts an `exit` frame containing the exit code and optional JSON-serializable result. The host finalizes the execution record through `#finalizeOnce`.

### Structured I/O Example

```typescript
export default async function () {
  console.log("hello stdout");          // → stdout event
  console.error("oops stderr");         // → stderr event
  return { success: true, value: 42 }; // → exit event with result
}

```

The host receives these events:

| Event | Payload | Source Property |
|:---|:---|:---|
| `stdout` | Base-64 encoded bytes | `event.name === "stdout"` |
| `stderr` | Base-64 encoded bytes | `event.name === "stderr"` |
| `exit` | `{code: 0, result: {...}}` | `event.name === "exit"` |

Access events via `workspace.runtime.getExec({id, after: 0})` or consume the real-time `ReadableStream` from `exec()`.

---

## Configuration and Limits

The backend enforces resource boundaries through constructor options:

```typescript
new WorkerJavaScriptBackend({
  loader,
  maxStdioBytes: 64 * 1024,      // 64 KiB combined stdout+stderr
  maxSourceBytes: 2 * 1024 * 1024, // 2 MiB total source
});

```

Exceeding `maxStdioBytes` triggers truncation with `...[stdio truncated]` appended (see `runtimeWorkerModule` lines 106-127).

---

## Execution Lifecycle

The complete flow from connection to finalization:

1. **Connection**: `WorkerJavaScriptBackend.connect` returns a `JavaScriptBackendHandle` owning SQLite tables for durable logs
2. **Execution request**: `exec(input)` creates an execution ID, validates limits, and builds the module graph
3. **Module graph construction**: Relative imports resolved, trusted modules injected
4. **Dynamic Worker launch**: `startJavaScriptExecution` passes the module map and [`workspace-runtime-runner.js`](https://github.com/cloudflare/computer/blob/main/workspace-runtime-runner.js) entry to a new Worker
5. **Runtime setup**: `runtimeWorkerModule` instruments I/O and creates the framed stream
6. **Host streaming**: `#pumpFrames` decodes, persists, and publishes events
7. **Finalization**: `exit` frame triggers `#finalizeOnce` to persist exit code and result

---

## Summary

- **Durable relative imports** are resolved via `WorkspaceRuntimeCapability` in `buildModuleGraph`, with trusted `ws:*` modules automatically injected
- **Structured I/O** replaces native stream APIs with framed, base-64 encoded events streamed through `IdentityTransformStream`
- **Pre-validation** of the complete module graph prevents mid-execution import failures
- **Configurable limits** on source size and stdio protect host resources
- **Full execution lifecycle** from `connect()` through `exec()` to final event persistence is implemented in [`worker-javascript.ts`](https://github.com/cloudflare/computer/blob/main/worker-javascript.ts)

---

## Frequently Asked Questions

### How does worker-javascript resolve imports without filesystem access?

The backend uses `WorkspaceRuntimeCapability.readFile` and `resolveConfined` to load modules from the Workspace's virtual file system. The `buildModuleGraph` function in [`module-graph.ts`](https://github.com/cloudflare/computer/blob/main/module-graph.ts) (lines 70-98) walks import specifiers and resolves relative paths against the Workspace directory structure, building a complete module map before the Dynamic Worker starts.

### What happens if a script exceeds the stdio size limit?

When output exceeds `maxStdioBytes`, the runtime truncates further writes and appends `...[stdio truncated]` to the stream. This occurs in `runtimeWorkerModule` at lines 106-127. The limit applies to the combined total of stdout and stderr to prevent unbounded memory growth in the Worker.

### Can I stream execution output in real time?

Yes. The `exec()` method returns a `ReadableStream` of `WorkspaceRuntimeEvent` objects that emits `stdout`, `stderr`, and finally `exit` events as they occur. Alternatively, poll `workspace.runtime.getExec({id, after: sequenceNumber})` to retrieve events durably from SQLite storage.