Skip to content

Commit 44d70d3

Browse files
authored
add wasmModule option to initialize JS API (#2155)
1 parent e86abce commit 44d70d3

File tree

7 files changed

+107
-45
lines changed

7 files changed

+107
-45
lines changed

CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,28 @@
101101
};
102102
```
103103

104+
* Add the `wasmModule` option to the `initialize` JS API ([#1093](https://github.com/evanw/esbuild/issues/1093))
105+
106+
The `initialize` JS API must be called when using esbuild in the browser to provide the WebAssembly module for esbuild to use. Previously the only way to do that was using the `wasmURL` API option like this:
107+
108+
```js
109+
await esbuild.initialize({
110+
wasmURL: '/node_modules/esbuild-wasm/esbuild.wasm',
111+
})
112+
console.log(await esbuild.transform('1+2'))
113+
```
114+
115+
With this release, you can now also initialize esbuild using a `WebAssembly.Module` instance using the `wasmModule` API option instead. The example above is equivalent to the following code:
116+
117+
```js
118+
await esbuild.initialize({
119+
wasmModule: await WebAssembly.compileStreaming(fetch('/node_modules/esbuild-wasm/esbuild.wasm'))
120+
})
121+
console.log(await esbuild.transform('1+2'))
122+
```
123+
124+
This could be useful for environments where you want more control over how the WebAssembly download happens or where downloading the WebAssembly module is not possible.
125+
104126
## 0.14.31
105127

106128
* Add support for parsing "optional variance annotations" from TypeScript 4.7 ([#2102](https://github.com/evanw/esbuild/pull/2102))

lib/npm/browser.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as ourselves from "./browser"
44

55
declare const ESBUILD_VERSION: string;
66
declare let WEB_WORKER_SOURCE_CODE: string
7-
declare let WEB_WORKER_FUNCTION: (postMessage: (data: Uint8Array) => void) => (event: { data: Uint8Array | ArrayBuffer }) => void
7+
declare let WEB_WORKER_FUNCTION: (postMessage: (data: Uint8Array) => void) => (event: { data: Uint8Array | ArrayBuffer | WebAssembly.Module }) => void
88

99
export let version = ESBUILD_VERSION;
1010

@@ -59,25 +59,31 @@ let ensureServiceIsRunning = (): Service => {
5959
export const initialize: typeof types.initialize = options => {
6060
options = common.validateInitializeOptions(options || {});
6161
let wasmURL = options.wasmURL;
62+
let wasmModule = options.wasmModule;
6263
let useWorker = options.worker !== false;
63-
if (!wasmURL) throw new Error('Must provide the "wasmURL" option');
64-
wasmURL += '';
64+
if (!wasmURL && !wasmModule) throw new Error('Must provide either the "wasmURL" option or the "wasmModule" option');
6565
if (initializePromise) throw new Error('Cannot call "initialize" more than once');
66-
initializePromise = startRunningService(wasmURL, useWorker);
66+
initializePromise = startRunningService(wasmURL || '', wasmModule, useWorker);
6767
initializePromise.catch(() => {
6868
// Let the caller try again if this fails
6969
initializePromise = void 0;
7070
});
7171
return initializePromise;
7272
}
7373

74-
const startRunningService = async (wasmURL: string, useWorker: boolean): Promise<void> => {
75-
let res = await fetch(wasmURL);
76-
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
77-
let wasm = await res.arrayBuffer();
74+
const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<void> => {
75+
let wasm: ArrayBuffer | WebAssembly.Module;
76+
if (wasmModule) {
77+
wasm = wasmModule;
78+
} else {
79+
let res = await fetch(wasmURL);
80+
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
81+
wasm = await res.arrayBuffer();
82+
}
83+
7884
let worker: {
7985
onmessage: ((event: any) => void) | null
80-
postMessage: (data: Uint8Array | ArrayBuffer) => void
86+
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
8187
terminate: () => void
8288
}
8389

@@ -90,7 +96,7 @@ const startRunningService = async (wasmURL: string, useWorker: boolean): Promise
9096
let onmessage = WEB_WORKER_FUNCTION((data: Uint8Array) => worker.onmessage!({ data }))
9197
worker = {
9298
onmessage: null,
93-
postMessage: data => onmessage({ data }),
99+
postMessage: data => setTimeout(() => onmessage({ data })),
94100
terminate() {
95101
},
96102
}

lib/npm/worker.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// This file is part of the web worker source code
22

3+
interface Go {
4+
argv: string[]
5+
importObject: WebAssembly.Imports
6+
run(instance: WebAssembly.Instance): void
7+
}
8+
39
declare const ESBUILD_VERSION: string;
410
declare function postMessage(message: any): void;
511

6-
onmessage = ({ data: wasm }) => {
12+
onmessage = ({ data: wasm }: { data: ArrayBuffer | WebAssembly.Module }) => {
713
let decoder = new TextDecoder()
814
let fs = (globalThis as any).fs
915

@@ -57,9 +63,14 @@ onmessage = ({ data: wasm }) => {
5763
callback(null, count)
5864
}
5965

60-
let go = new (globalThis as any).Go()
66+
let go: Go = new (globalThis as any).Go()
6167
go.argv = ['', `--service=${ESBUILD_VERSION}`]
6268

63-
WebAssembly.instantiate(wasm, go.importObject)
64-
.then(({ instance }) => go.run(instance))
69+
if (wasm instanceof WebAssembly.Module) {
70+
WebAssembly.instantiate(wasm, go.importObject)
71+
.then(instance => go.run(instance))
72+
} else {
73+
WebAssembly.instantiate(wasm, go.importObject)
74+
.then(({ instance }) => go.run(instance))
75+
}
6576
}

lib/shared/common.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ let mustBeArray = <T>(value: T[] | undefined): string | null =>
3535
let mustBeObject = (value: Object | undefined): string | null =>
3636
typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'an object';
3737

38+
let mustBeWebAssemblyModule = (value: WebAssembly.Module | undefined): string | null =>
39+
value instanceof WebAssembly.Module ? null : 'a WebAssembly.Module';
40+
3841
let mustBeArrayOrRecord = <T extends string>(value: T[] | Record<T, T> | undefined): string | null =>
3942
typeof value === 'object' && value !== null ? null : 'an array or an object';
4043

@@ -75,10 +78,12 @@ function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string):
7578
export function validateInitializeOptions(options: types.InitializeOptions): types.InitializeOptions {
7679
let keys: OptionKeys = Object.create(null);
7780
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeString);
81+
let wasmModule = getFlag(options, keys, 'wasmModule', mustBeWebAssemblyModule);
7882
let worker = getFlag(options, keys, 'worker', mustBeBoolean);
79-
checkForInvalidFlags(options, keys, 'in startService() call');
83+
checkForInvalidFlags(options, keys, 'in initialize() call');
8084
return {
8185
wasmURL,
86+
wasmModule,
8287
worker,
8388
};
8489
}

lib/shared/types.ts

+10
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,16 @@ export interface InitializeOptions {
571571
*/
572572
wasmURL?: string
573573

574+
/**
575+
* The result of calling "new WebAssembly.Module(buffer)" where "buffer"
576+
* is a typed array or ArrayBuffer containing the binary code of the
577+
* "esbuild.wasm" file.
578+
*
579+
* You can use this as an alternative to "wasmURL" for environments where it's
580+
* not possible to download the WebAssembly module.
581+
*/
582+
wasmModule?: WebAssembly.Module
583+
574584
/**
575585
* By default esbuild runs the WebAssembly-based browser API in a web worker
576586
* to avoid blocking the UI thread. This can be disabled by setting "worker"

scripts/browser/browser-tests.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -186,37 +186,45 @@ let pages = {};
186186

187187
for (let format of ['iife', 'esm']) {
188188
for (let min of [false, true]) {
189-
for (let async of [false, true]) {
190-
let code = `
191-
window.testStart = function() {
192-
esbuild.initialize({
193-
wasmURL: '/esbuild.wasm',
194-
worker: ${async},
195-
}).then(() => {
196-
return (${runAllTests})({ esbuild })
197-
}).then(() => {
198-
testDone()
199-
}).catch(e => {
200-
testFail('' + (e && e.stack || e))
201-
testDone()
202-
})
203-
}
204-
`;
205-
let page;
206-
if (format === 'esm') {
207-
page = `
208-
<script type="module">
209-
import * as esbuild from '/esm/browser${min ? '.min' : ''}.js'
210-
${code}
211-
</script>
212-
`;
213-
} else {
214-
page = `
215-
<script src="/lib/browser${min ? '.min' : ''}.js"></script>
216-
<script>${code}</script>
189+
for (let worker of [false, true]) {
190+
for (let module of [false, true]) {
191+
let code = `
192+
window.testStart = function() {
193+
let promise = ${module}
194+
? esbuild.initialize({
195+
wasmURL: '/esbuild.wasm',
196+
worker: ${worker},
197+
})
198+
: WebAssembly.compileStreaming(fetch('/esbuild.wasm')).then(module => esbuild.initialize({
199+
wasmModule: module,
200+
worker: ${worker},
201+
}))
202+
promise.then(() => {
203+
return (${runAllTests})({ esbuild })
204+
}).then(() => {
205+
testDone()
206+
}).catch(e => {
207+
testFail('' + (e && e.stack || e))
208+
testDone()
209+
})
210+
}
217211
`;
212+
let page;
213+
if (format === 'esm') {
214+
page = `
215+
<script type="module">
216+
import * as esbuild from '/esm/browser${min ? '.min' : ''}.js'
217+
${code}
218+
</script>
219+
`;
220+
} else {
221+
page = `
222+
<script src="/lib/browser${min ? '.min' : ''}.js"></script>
223+
<script>${code}</script>
224+
`;
225+
}
226+
pages[format + (min ? 'Min' : '') + (worker ? 'Worker' : '') + (module ? 'Module' : '')] = page;
218227
}
219-
pages[format + (min ? 'Min' : '') + (async ? 'Async' : '')] = page;
220228
}
221229
}
222230
}

scripts/esbuild.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ exports.buildWasmLib = async (esbuildPath) => {
144144
for (let k of Object.getOwnPropertyNames(o))
145145
if (!(k in globalThis))
146146
Object.defineProperty(globalThis, k, { get: () => self[k] });
147-
${wasm_exec_js}
147+
${wasm_exec_js.replace(/\bfs\./g, 'globalThis.fs.')}
148148
${fs.readFileSync(path.join(repoDir, 'lib', 'npm', 'worker.ts'), 'utf8')}
149149
return m => onmessage(m)
150150
`;

0 commit comments

Comments
 (0)