Skip to content

Instantly share code, notes, and snippets.

@bvisness
Created September 11, 2025 16:16
Show Gist options
  • Select an option

  • Save bvisness/66e4b65af09a40b6c3bd4bd316d2d7e4 to your computer and use it in GitHub Desktop.

Select an option

Save bvisness/66e4b65af09a40b6c3bd4bd316d2d7e4 to your computer and use it in GitHub Desktop.
An esbuild plugin to get WASI 0.1 modules running in the browser.
import { createWriteStream } from "node:fs";
import { access, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { isAbsolute, join, relative } from "node:path";
import { pipeline } from "node:stream/promises";
import * as esbuild from "esbuild";
import { componentNew, transpile } from "@bytecodealliance/jco";
const WASI_ADAPTER_URL = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.2/wasi_snapshot_preview1.command.wasm";
const WASI_ADAPTER_PATH = join(tmpdir(), "wasi_snapshot_preview1.command.wasm");
// To use, import a WASM module with { type = "wasi-0.1" } like so:
//
// import { loader, instantiate } from "mycomponent.wasm" with { type: "wasi-0.1", instantiation: "async" }
//
// Then instantiate using @bytecodealliance/preview2-shim.
export function wasi01({ fetchPrefix = "" } = {}) {
return {
name: "wasi-0.1",
setup(build) {
const fileContents = new Map();
// WASM files imported with { type: "wasi-0.1" } will be componentized by
// wasm-tools and transpiled by jco. This step resolves the wasm file to a
// stub JS module that will be generated by this plugin.
build.onResolve({ filter: /\.wasm$/ }, args => {
if (args.with.type === "wasi-0.1") {
if (args.resolveDir === "" && !isAbsolute(path)) {
throw new Error(`Could not resolve an absolute path for ${args.path}`);
}
const absPath = isAbsolute(args.path) ? args.path : join(args.resolveDir, args.path);
return {
path: relative("", absPath),
namespace: "wasi-0.1-module-stub",
pluginData: {
instantiation: args.with.instantiation,
resolveDir: args.resolveDir,
},
};
}
});
// When importing files from within our module stub, we must load their
// contents from fileContents, so resolve them to a different namespace.
build.onResolve({ filter: /.*/ }, args => {
if (args.namespace === "wasi-0.1-module-stub") {
if (fileContents.has(args.path)) {
return {
path: args.path,
namespace: "wasi-0.1-module-contents",
pluginData: {
resolveDir: args.resolveDir,
},
};
}
}
});
// Generates the stub JS module for the WASI module.
build.onLoad({ filter: /.*/, namespace: "wasi-0.1-module-stub" }, async args => {
// Wrap the WASI module in a component, and then transpile it
await downloadIfMissing(WASI_ADAPTER_URL, WASI_ADAPTER_PATH);
const [moduleBytes, adapterBytes] = await Promise.all([
readFile(args.path),
readFile(WASI_ADAPTER_PATH),
]);
const componentBytes = await componentNew(moduleBytes, [["wasi_snapshot_preview1", adapterBytes]]);
const { files } = await transpile(componentBytes, {
name: args.path,
minify: false, // esbuild can handle minifying at the end
instantiation: args.pluginData.instantiation,
nodejsCompat: false,
});
// Save the transpiled contents for later
for (const [filename, contents] of Object.entries(files)) {
if (filename.match(/(\.wasm|\.js)$/)) {
fileContents.set(filename, contents);
}
}
const mainJSPath = `${args.path}.js`;
if (!fileContents.has(mainJSPath)) {
throw new Error(`Didn't see expected file path ${mainJSPath} in jco output`);
}
const wasmFilenames = Array.from(fileContents.keys())
.filter(filename => filename.endsWith(".wasm"));
const stubContents = `
${Array.from(wasmFilenames.entries())
.map(([i, filename]) => `import mod${i} from "${filename}"`)
.join("\n")}
const moduleMap = new Map();
${Array.from(wasmFilenames.entries())
.map(([i, filename]) => `moduleMap.set("${filename}", mod${i});`)
.join("\n")}
export function loader(path) {
return WebAssembly.compileStreaming(fetch("${fetchPrefix}" + moduleMap.get(path)));
}
export * from "${mainJSPath}";
`;
return {
contents: stubContents,
resolveDir: args.pluginData.resolveDir,
};
});
// Load files imported by the stub module
build.onLoad({ filter: /\.js$/, namespace: "wasi-0.1-module-contents" }, async args => {
return {
contents: fileContents.get(args.path),
loader: "js",
resolveDir: args.pluginData.resolveDir,
};
});
build.onLoad({ filter: /\.wasm$/, namespace: "wasi-0.1-module-contents" }, async args => {
return {
contents: fileContents.get(args.path),
loader: "file",
};
});
},
};
};
async function downloadIfMissing(url, filename) {
try {
await access(filename);
return;
} catch { }
console.log(`Downloading ${url}...`);
const res = await fetch(url);
if (!res.ok || !res.body) {
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
}
const fileStream = createWriteStream(filename);
await pipeline(res.body, fileStream);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment