1
1
// eslint-disable-next-line unicorn/import-style
2
2
import { posix as path } from "node:path" ;
3
3
4
+ import { Directory , Fd , File , Inode , OpenFile , PreopenDirectory , WASI , WASIProcExit } from "@bjorn3/browser_wasi_shim" ;
4
5
import esbuildWasmURL from "@esbuild/wasi-preview1/esbuild.wasm?url" ;
5
6
import * as Comlink from "comlink" ;
6
7
import type * as esbuild from "esbuild" ;
7
8
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" ;
11
9
12
10
import { memoize } from "../helpers" ;
13
11
import { configJsonFilename } from "./constants" ;
@@ -19,11 +17,6 @@ async function runEsbuildWasi(
19
17
files : Map < string , string > ,
20
18
fallbackEntrypoint : string ,
21
19
) : Promise < string > {
22
- const fs = createFileSystem ( [ {
23
- type : "mem" ,
24
- contents : Object . fromEntries ( files ) ,
25
- } ] ) ;
26
-
27
20
let config : esbuild . BuildOptions = {
28
21
bundle : true ,
29
22
packages : "external" ,
@@ -73,50 +66,44 @@ async function runEsbuildWasi(
73
66
let stdout = "" ;
74
67
let stderr = "" ;
75
68
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
+ }
98
73
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
+ ] ;
110
92
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
+ } ) ;
112
99
113
100
let exitCode : number ;
114
101
try {
115
- wasi . start ( instance ) ;
102
+ wasi . start ( instance as any ) ;
116
103
exitCode = 0 ;
117
104
} catch ( e ) {
118
- if ( e instanceof WASIExitError ) {
119
- exitCode = e . code ?? 127 ;
105
+ if ( e instanceof WASIProcExit ) {
106
+ exitCode = e . code ;
120
107
} else {
121
108
return ( e as any ) . toString ( ) ;
122
109
}
@@ -142,24 +129,62 @@ async function runEsbuildWasi(
142
129
// actual listing out of esbuild's CLI.
143
130
//
144
131
// 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 ;
149
136
output += "\n\n" ;
150
137
}
151
138
152
139
return wasiHeader + output . trim ( ) ;
153
140
}
154
141
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 ) } ;
163
188
}
164
189
}
165
190
}
0 commit comments