Skip to Content
Wasmer JSTutorialsCreating an Interactive Terminal with XTerm.js

Creating an Interactive Terminal with XTerm.js

Welcome to this hands-on guide where we will integrate @wasmer/sdk with xterm.js to create a functional Bash terminal in the browser. This powerful combination leverages the capabilities of WebAssembly and WASIX, enabling you to run Bash and core Unix utilities interactively in a web environment.

The Wasmer.sh  website is a real-world example of how the JavaScript SDK can be used to provide a real terminal in the browser.

Create the Project

First, let’s set up our project environment. Create a new directory for your project and initialize it using npm.

npm init -y

Add Dependencies

Once initialized, install @wasmer/sdk, xterm, and xterm-addon-fit by running:

npm install @wasmer/sdk xterm xterm-addon-fit

These packages are crucial; @wasmer/sdk is our WebAssembly runtime, while xterm and its add-on are used to create the terminal interface in the browser.

Install Vite

Next, we’ll use vite for bundling our application. It’s a fast, modern bundler and minifier.

npm install vite --save-dev

Package Scripts

Let’s also set up a couple of scripts to assist development.

package.json
"scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }

These scripts provide quick commands to build your application (npm run build) and start a development server with live reloading (npm run dev).

Create the UI

In your project root, create an index.html file. This file will host our web terminal. It’s a simple HTML document with a div element where the terminal interface will appear:

index.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wasmer Shell</title> <script type="module" defer src="index.ts"></script> </head> <body> <div id="terminal"></div> </body> </html>

Add Styling

Then, import xterm’s CSS in your TypeScript file for styling the terminal. This import is necessary for the terminal’s visual appearance and functionality.

index.ts
import "xterm/css/xterm.css";

Implement the TypeScript Logic

Now, we move to the core logic of our application in TypeScript. Start with importing necessary modules:

index.ts
import type { Instance } from "@wasmer/sdk"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit";

Initialize the SDK

Next, let’s create a main() function and initialize @wasmer/sdk.

index.ts
async function main() { const { Wasmer, init, initializeLogger } = await import("@wasmer/sdk"); await init(); /* ... */ }

You have may noticed that we’re using a dynamic import (await import("@wasmer/sdk")) to pull in @wasmer/sdk rather than a “normal” import { ... } from "@wasmer/sdk".

This is a workaround for a bad interaction between bundlers, xterm, and the @wasmer/sdk threadpool. See ReferenceError: `document is not defined“ for more details.

Call init() to load and set up the WebAssembly environment required by @wasmer/sdk. It’s crucial to do this before using any other functionality of the SDK.

Configure the Terminal

Inside main(), set up your terminal configuration with xterm.js. Here we create a new terminal instance and apply the fit add-on for a responsive layout:

index.ts
async function main() { /* ... */ const term = new Terminal({ cursorBlink: true, convertEol: true }); const fit = new FitAddon(); term.loadAddon(fit); term.open(document.getElementById("terminal")!); fit.fit(); /* ... */ }

Integrate Bash with the Terminal

To integrate Bash, load the sharrattj/bash package using Wasmer.fromRegistry(). This package contains a WASIX-compiled version of Bash and its utilities. Upon loading, connect stdin, stdout, and stderr of the Bash instance to the xterm instance:

index.ts
async function main() { /* ... */ const pkg = await Wasmer.fromRegistry("sharrattj/bash"); term.writeln("Starting..."); const instance = await pkg.entrypoint!.run(); connectStreams(instance, term); }

The connectStreams() function routes data between the Bash instance and the terminal. It ensures that user inputs and program outputs are correctly handled in the terminal:

index.ts
const encoder = new TextEncoder(); function connectStreams(instance: Instance, term: Terminal) { const stdin = instance.stdin?.getWriter(); term.onData(data => stdin?.write(encoder.encode(data))); instance.stdout.pipeTo(new WritableStream({ write: chunk => term.write(chunk) })); instance.stderr.pipeTo(new WritableStream({ write: chunk => term.write(chunk) })); }

Take it for a Test Drive

Running npm run dev right now will show a terminal with a blinking cursor that doesn’t seem to do anything. If you open up the dev tools, you’ll see a message along the lines of this:

Library.mjs:11 Uncaught (in promise) Error: Unable to find "sharrattj/bash" in the registry at A2.wbg.__wbg_new_ab87fd305ed9004b (Library.mjs:11:46367) at 013772d2:0x2c846c at 013772d2:0x3a271a at 013772d2:0x143722 at 013772d2:0x3266e4 at 013772d2:0x3f1911 at 013772d2:0x3eac6c at cA (Library.mjs:11:25455) at C2 (Library.mjs:11:25290)

This is a pretty unhelpful error message, but we can make troubleshooting a lot easier by enabling logging just after the await init().

index.ts
async function main() { await init(); initializeLogger("debug"); /* ... */ }

Hitting save and reloading the page now gives us some more useful information.

DEBUG from_registry{specifier="sharrattj/bash"}: wasmer_js::runtime: Initializing the global runtime DEBUG from_registry{specifier="sharrattj/bash"}: wasmer_js::tasks::scheduler: Spinning up the scheduler thread_id=0 DEBUG from_registry{specifier="sharrattj/bash"}:from_registry:query{package=sharrattj/bash}:query_graphql: wasmer_wasix::runtime::resolver::wapm_source: Querying the GraphQL API request.url=https://registry.wasmer.io/graphql request.method=POST DEBUG from_registry{specifier="sharrattj/bash"}:from_registry:query{package=sharrattj/bash}:query_graphql: wasmer_js::tasks::scheduler: Sending message current_thread=0 scheduler_thread=0 msg=SpawnAsync(_) WARN from_registry{specifier="sharrattj/bash"}: wasmer_js::tasks::scheduler: An error occurred while handling a message error=Failed to execute 'postMessage' on 'Worker': SharedArrayBuffer transfer requires self.crossOriginIsolated. DEBUG from_registry{specifier="sharrattj/bash"}:from_registry:query{package=sharrattj/bash}:query_graphql: wasmer_wasix::runtime::resolver::wapm_source: close DEBUG from_registry{specifier="sharrattj/bash"}:from_registry:query{package=sharrattj/bash}: wasmer_wasix::runtime::resolver::wapm_source: close DEBUG from_registry{specifier="sharrattj/bash"}:from_registry: wasmer_wasix::bin_factory::binary_package: close

I’ve formatted the log output for readability, but it looks like we’ve run into SharedArrayBuffer and Cross-Origin Isolation issues!

Configure your Dev Server

The fix is to make sure Vite’s dev server sends the correct COOP and COEP headers through vite.config.js.

vite.config.js
import { defineConfig } from "vite"; export default defineConfig({ server: { headers: { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }, }, });

Test the Application

Now, it’s time to actually see your Bash terminal in action:

  1. Build the Application: Run npm run dev to bundle your TypeScript code.
  2. Open the Application: Open http://localhost:5173/  in your browser to see the terminal interface.

You should see a functional Bash terminal running in your browser, capable of executing basic *nix commands.

You might want to remove that initializeLogger() call at this point to avoid filling your console with spam.

Conclusion

Congratulations! You’ve successfully integrated a WebAssembly-powered Bash terminal in the browser using @wasmer/sdk and xterm.js. This setup demonstrates the incredible capabilities of WebAssembly in bringing complex server-side applications like Bash to the web client.

Feel free to explore and extend this application further. Perhaps you can integrate more utilities or enhance the UI/UX of the terminal. The possibilities are endless, and the power of WebAssembly makes it all possible in the browser.

Resources

wasmerio/wasmer-js
Last updated on