A browser-based explorer for WebAssembly components.
You can run and debug a Wasm component in the browser without its host.
Imports are mocked by your input, so you can see how the guest reacts to what its imports return, reproduce a bug, or
test a subcomponent before composing it with wac.
Live demo: https://chenyan2002.github.io/component-ui/
- UI widget from WIT types. Each WIT type maps to a UI widget, so you can enter valid arguments structurally in the browser.
- Mock imports interactively. Any import that hasn't been implemented asks for its return value in the console when the guest calls it, and the guest waits for your answer. This includes methods of imported/host resources, which can also be passed to exports as mock arguments.
- Resource table. Tracks each resource handle: who created it, whether it is live, moved or dropped, and which handle index it reuses. Live instances can be picked as arguments, and dropped from the table.
- Deterministic replay. The call logs can be saved locally and replayed in the command line.
Component UI requires JavaScript Promise Integration (JSPI), which lets the guest suspend while you answer/mock an import.
The same engine runs without the UI, e.g. for an agent to debug a component. Calls are written
as the console shows them, with WAVE
arguments. It needs Node 23 or later; --help lists its options.
npm run build:cli
node dist-cli/component-ui.mjs inspect public/resource.wasm
node dist-cli/component-ui.mjs call public/resource.wasm 'new exports/x(5)' 'exports.test-imports()' --save run.txtskills/component-ui teaches an agent, e.g. Claude Code, to debug
components with the CLI. To use it in any project, put the CLI on the PATH, and the skill where
the agent finds skills, e.g. for Claude Code:
npm run build:cli && npm link
ln -s "$PWD/skills/component-ui" ~/.claude/skills/component-ui- WIT → type AST.
crates/bindgenis a Rust component built onwit-parserandwit-component. It decodes the component's WIT and emits JavaScript that builds a type description (seesrc/wit.ts) of its imports and exports. It also reads and writes values in WAVE, with proxy-component's fork ofwasm-wave. - Transpile. The component itself is transpiled in the browser by jco, with JSPI enabled for imports and exports.
- Instantiate. WASI imports come from the preview2 shim, unless it is turned off; other
imports come from your JavaScript, with console prompts and mocks standing in for whatever is
missing (
src/dialog.ts). - Render. The UI is built with Lit and Web Awesome, and is essentially an interpreter over WIT types.
npm install
npm run dev # start the Vite dev server
npm run typecheck # type-check the app and the tests
npm run build # type-check and build into dist/
npm run preview # serve the production buildEnd-to-end tests use Playwright against the production build:
npx playwright install chromium
npm testThe core also runs headless in Node, without the UI. tests/node checks that, and
needs a Node with JSPI.
npm run test:nodesrc/obj is checked in, so this is only needed after changing crates/bindgen. It requires
Rust with the wasm32-wasip2 target:
rustup target add wasm32-wasip2
npm run build:bindgen- Currently only supports p2 component. Deterministically replaying p3 component is future work.
- A real host object's method that receives a mock argument isn't routed to a prompt, so it may still fail in the host code.
- Calls are shown with their versioned interface, e.g.
wasi:http/types/fields@0.2.3.from-list(…), but the version isn't always the one the guest imported:- Semver-compatible versions of an interface (e.g.,
wasi:io/streams@0.2.0and@0.2.3) are merged into the newest one, so calls through either show as@0.2.3. - jco merges incompatible versions (e.g., 0.2.0 and 0.3.0) into one unversioned import, so a call can't be told apart by version: it is shown with, and a prompt asks for, the types of the first version that has the function, which may be the wrong form for the other.
- Semver-compatible versions of an interface (e.g.,
jcobugs on JSPI- Async imports hang on failure; the loader patches jco's output to rethrow, which may need updating when jco changes. (waiting for release)
- Exported resource constructors and static functions don't emit the correct code (waiting for release)
- Calling synchronous imports inside resource destructor and start function traps (waiting for review)
- The CLI's exit code doesn't reflect a guest calling
wasi:cli/exit; the call shows the error the shim throws. Exiting with the guest's status is future work. - The WIT shown, in the UI and by the CLI, is the one embedded in the component, which keeps the types but usually not the doc comments of the original WIT. Answering such imports well needs the original WIT files or the host's documentation; the skill tells agents to ask for them.
- The log files don't record the calls of imports implemented in JavaScript, nor WASI (when the shim is enabled).
- While a call is waiting on an import, other calls are disabled, since the component instance can't be re-entered.
- jco doesn't expose the handle index of a host-implemented resource, so the resource table shows
host instances as
hostand numbers them in creation order (e.g.#y-1) rather than by handle. If the host and the component both implement a resource of the same name, the host's instances are prefixed, e.g.#host-y-1, so ids stay unique.
This work is inspired by Candid UI and proxy-component, with significant help from Claude.