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

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 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

// 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 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:

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 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) 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

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:

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 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

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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →