Skip to content

Commit e4f50bd

Browse files
committed
Use browser_wasi_shim
1 parent f37bd93 commit e4f50bd

File tree

3 files changed

+92
-108
lines changed

3 files changed

+92
-108
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"vendor-esbuild": "tsx ./scripts/vendor-esbuild.ts"
1111
},
1212
"dependencies": {
13+
"@bjorn3/browser_wasi_shim": "^0.4.1",
1314
"@codemirror/lang-javascript": "^6.2.3",
1415
"@emotion/react": "^11.14.0",
1516
"@esbuild/wasi-preview1": "0.25.0",
@@ -26,8 +27,7 @@
2627
"jsonc-parser": "^3.3.1",
2728
"lz-string": "^1.5.0",
2829
"react": "^18.3.1",
29-
"react-dom": "^18.3.1",
30-
"wasi-js": "^1.7.3"
30+
"react-dom": "^18.3.1"
3131
},
3232
"devDependencies": {
3333
"@eslint/js": "^9.20.0",

pnpm-lock.yaml

Lines changed: 8 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/esbuild/wasiWorker.ts

Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
// eslint-disable-next-line unicorn/import-style
22
import { posix as path } from "node:path";
33

4+
import { Directory, Fd, File, Inode, OpenFile, PreopenDirectory, WASI, WASIProcExit } from "@bjorn3/browser_wasi_shim";
45
import esbuildWasmURL from "@esbuild/wasi-preview1/esbuild.wasm?url";
56
import * as Comlink from "comlink";
67
import type * as esbuild from "esbuild";
78
import * as JSONC from "jsonc-parser";
8-
import WASI, { createFileSystem } from "wasi-js";
9-
import browserBindings from "wasi-js/dist/bindings/browser";
10-
import { WASIExitError, WASIFileSystem } from "wasi-js/dist/types";
119

1210
import { memoize } from "../helpers";
1311
import { configJsonFilename } from "./constants";
@@ -19,11 +17,6 @@ async function runEsbuildWasi(
1917
files: Map<string, string>,
2018
fallbackEntrypoint: string,
2119
): Promise<string> {
22-
const fs = createFileSystem([{
23-
type: "mem",
24-
contents: Object.fromEntries(files),
25-
}]);
26-
2720
let config: esbuild.BuildOptions = {
2821
bundle: true,
2922
packages: "external",
@@ -73,50 +66,44 @@ async function runEsbuildWasi(
7366
let stdout = "";
7467
let stderr = "";
7568

76-
let sab: Int32Array | undefined;
77-
const wasi = new WASI({
78-
args,
79-
env: {
80-
PWD: parsed.absWorkingDir ?? "/",
81-
},
82-
// Workaround for bug in wasi-js; browser-hrtime incorrectly returns a number.
83-
bindings: { ...browserBindings, fs, hrtime: (...args) => BigInt(browserBindings.hrtime(...args)) },
84-
preopens: {
85-
"/": "/",
86-
},
87-
sendStdout: (data) => {
88-
stdout += new TextDecoder().decode(data);
89-
},
90-
sendStderr: (data) => {
91-
stderr += new TextDecoder().decode(data);
92-
},
93-
sleep: (ms) => {
94-
sab ??= new Int32Array(new SharedArrayBuffer(4));
95-
Atomics.wait(sab, 0, 0, Math.max(ms, 1));
96-
},
97-
});
69+
class StringOutput extends Fd {
70+
constructor(private output: (data: string) => void) {
71+
super();
72+
}
9873

99-
const module = await getModule();
100-
let imports = wasi.getImports(module);
101-
102-
// Newer Go builds require this function, which is not shimmed
103-
// in wasi-js.
104-
imports = {
105-
wasi_snapshot_preview1: {
106-
...imports.wasi_snapshot_preview1,
107-
sock_accept: () => -1,
108-
},
109-
};
74+
override fd_write(data: Uint8Array): { ret: number; nwritten: number; } {
75+
this.output(new TextDecoder().decode(data));
76+
return { ret: 0, nwritten: data.length };
77+
}
78+
}
79+
80+
const fs = createFileSystem(files);
81+
82+
let fds = [
83+
new OpenFile(new File([])), // stdin
84+
new StringOutput((data) => {
85+
stdout += data;
86+
}),
87+
new StringOutput((data) => {
88+
stderr += data;
89+
}),
90+
fs,
91+
];
11092

111-
const instance = await WebAssembly.instantiate(module, imports);
93+
const wasi = new WASI(args, [`PWD=${parsed.absWorkingDir ?? "/"}`], fds, { debug: false });
94+
95+
const module = await getModule();
96+
const instance = await WebAssembly.instantiate(module, {
97+
"wasi_snapshot_preview1": wasi.wasiImport,
98+
});
11299

113100
let exitCode: number;
114101
try {
115-
wasi.start(instance);
102+
wasi.start(instance as any);
116103
exitCode = 0;
117104
} catch (e) {
118-
if (e instanceof WASIExitError) {
119-
exitCode = e.code ?? 127;
105+
if (e instanceof WASIProcExit) {
106+
exitCode = e.code;
120107
} else {
121108
return (e as any).toString();
122109
}
@@ -142,24 +129,62 @@ async function runEsbuildWasi(
142129
// actual listing out of esbuild's CLI.
143130
//
144131
// TODO: Now that we have a real FS, use --metafile and get fancy?
145-
for (const p of walk(fs, "/")) {
146-
if (files.has(p)) continue;
147-
output += `// @filename: ${p}\n`;
148-
output += fs.readFileSync(p, { encoding: "utf8" });
132+
for (const { name, contents } of walk(fs, "/")) {
133+
if (files.has(name)) continue;
134+
output += `// @filename: ${name}\n`;
135+
output += contents;
149136
output += "\n\n";
150137
}
151138

152139
return wasiHeader + output.trim();
153140
}
154141

155-
function* walk(fs: WASIFileSystem, dir: string): Generator<string> {
156-
for (const p of fs.readdirSync(dir)) {
157-
const entry = path.join(dir, p);
158-
const stat = fs.statSync(entry);
159-
if (stat.isDirectory()) {
160-
yield* walk(fs, entry);
161-
} else if (stat.isFile()) {
162-
yield entry;
142+
type Tree = Map<string, string | Tree>;
143+
144+
function createFileSystem(files: Map<string, string>): PreopenDirectory {
145+
// Convert to Tree
146+
const tree: Tree = new Map();
147+
for (const [name, data] of files) {
148+
const parts = name.slice(1).split("/");
149+
const parents = parts.slice(0, -1);
150+
const base = parts.at(-1)!;
151+
152+
let current = tree;
153+
for (const parent of parents) {
154+
if (!current.has(parent)) {
155+
current.set(parent, new Map());
156+
}
157+
current = current.get(parent) as Tree;
158+
}
159+
current.set(base, data);
160+
}
161+
162+
function build(name: "/", tree: Tree): PreopenDirectory;
163+
function build(name: string, tree: Tree): Directory;
164+
function build(name: string, tree: Tree): PreopenDirectory | Directory {
165+
const contents = new Map<string, Inode>();
166+
for (const [name, data] of tree) {
167+
if (typeof data === "string") {
168+
contents.set(name, new File(new TextEncoder().encode(data)));
169+
} else {
170+
contents.set(name, build(name, data));
171+
}
172+
}
173+
return name === "/" ? new PreopenDirectory(name, contents) : new Directory(contents);
174+
}
175+
176+
return build("/", tree);
177+
}
178+
179+
function* walk(fs: PreopenDirectory | Directory, name: string): Generator<{ name: string; contents: string; }> {
180+
const dir = fs instanceof PreopenDirectory ? fs.dir : fs;
181+
182+
for (const [childName, child] of dir.contents) {
183+
const childPath = path.join(name, childName);
184+
if (child instanceof Directory) {
185+
yield* walk(child, childPath);
186+
} else if (child instanceof File) {
187+
yield { name: childPath, contents: new TextDecoder().decode(child.data) };
163188
}
164189
}
165190
}

0 commit comments

Comments
 (0)