Skip to content

Commit

Permalink
add wasmModule option to initialize JS API (#2155)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 6, 2022
1 parent e86abce commit 44d70d3
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 45 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@
};
```

* Add the `wasmModule` option to the `initialize` JS API ([#1093](https://github.com/evanw/esbuild/issues/1093))

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:

```js
await esbuild.initialize({
wasmURL: '/node_modules/esbuild-wasm/esbuild.wasm',
})
console.log(await esbuild.transform('1+2'))
```

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:

```js
await esbuild.initialize({
wasmModule: await WebAssembly.compileStreaming(fetch('/node_modules/esbuild-wasm/esbuild.wasm'))
})
console.log(await esbuild.transform('1+2'))
```

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.

## 0.14.31

* Add support for parsing "optional variance annotations" from TypeScript 4.7 ([#2102](https://github.com/evanw/esbuild/pull/2102))
Expand Down
26 changes: 16 additions & 10 deletions lib/npm/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as ourselves from "./browser"

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

export let version = ESBUILD_VERSION;

Expand Down Expand Up @@ -59,25 +59,31 @@ let ensureServiceIsRunning = (): Service => {
export const initialize: typeof types.initialize = options => {
options = common.validateInitializeOptions(options || {});
let wasmURL = options.wasmURL;
let wasmModule = options.wasmModule;
let useWorker = options.worker !== false;
if (!wasmURL) throw new Error('Must provide the "wasmURL" option');
wasmURL += '';
if (!wasmURL && !wasmModule) throw new Error('Must provide either the "wasmURL" option or the "wasmModule" option');
if (initializePromise) throw new Error('Cannot call "initialize" more than once');
initializePromise = startRunningService(wasmURL, useWorker);
initializePromise = startRunningService(wasmURL || '', wasmModule, useWorker);
initializePromise.catch(() => {
// Let the caller try again if this fails
initializePromise = void 0;
});
return initializePromise;
}

const startRunningService = async (wasmURL: string, useWorker: boolean): Promise<void> => {
let res = await fetch(wasmURL);
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
let wasm = await res.arrayBuffer();
const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<void> => {
let wasm: ArrayBuffer | WebAssembly.Module;
if (wasmModule) {
wasm = wasmModule;
} else {
let res = await fetch(wasmURL);
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
wasm = await res.arrayBuffer();
}

let worker: {
onmessage: ((event: any) => void) | null
postMessage: (data: Uint8Array | ArrayBuffer) => void
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
terminate: () => void
}

Expand All @@ -90,7 +96,7 @@ const startRunningService = async (wasmURL: string, useWorker: boolean): Promise
let onmessage = WEB_WORKER_FUNCTION((data: Uint8Array) => worker.onmessage!({ data }))
worker = {
onmessage: null,
postMessage: data => onmessage({ data }),
postMessage: data => setTimeout(() => onmessage({ data })),
terminate() {
},
}
Expand Down
19 changes: 15 additions & 4 deletions lib/npm/worker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// This file is part of the web worker source code

interface Go {
argv: string[]
importObject: WebAssembly.Imports
run(instance: WebAssembly.Instance): void
}

declare const ESBUILD_VERSION: string;
declare function postMessage(message: any): void;

onmessage = ({ data: wasm }) => {
onmessage = ({ data: wasm }: { data: ArrayBuffer | WebAssembly.Module }) => {
let decoder = new TextDecoder()
let fs = (globalThis as any).fs

Expand Down Expand Up @@ -57,9 +63,14 @@ onmessage = ({ data: wasm }) => {
callback(null, count)
}

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

WebAssembly.instantiate(wasm, go.importObject)
.then(({ instance }) => go.run(instance))
if (wasm instanceof WebAssembly.Module) {
WebAssembly.instantiate(wasm, go.importObject)
.then(instance => go.run(instance))
} else {
WebAssembly.instantiate(wasm, go.importObject)
.then(({ instance }) => go.run(instance))
}
}
7 changes: 6 additions & 1 deletion lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ let mustBeArray = <T>(value: T[] | undefined): string | null =>
let mustBeObject = (value: Object | undefined): string | null =>
typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'an object';

let mustBeWebAssemblyModule = (value: WebAssembly.Module | undefined): string | null =>
value instanceof WebAssembly.Module ? null : 'a WebAssembly.Module';

let mustBeArrayOrRecord = <T extends string>(value: T[] | Record<T, T> | undefined): string | null =>
typeof value === 'object' && value !== null ? null : 'an array or an object';

Expand Down Expand Up @@ -75,10 +78,12 @@ function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string):
export function validateInitializeOptions(options: types.InitializeOptions): types.InitializeOptions {
let keys: OptionKeys = Object.create(null);
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeString);
let wasmModule = getFlag(options, keys, 'wasmModule', mustBeWebAssemblyModule);
let worker = getFlag(options, keys, 'worker', mustBeBoolean);
checkForInvalidFlags(options, keys, 'in startService() call');
checkForInvalidFlags(options, keys, 'in initialize() call');
return {
wasmURL,
wasmModule,
worker,
};
}
Expand Down
10 changes: 10 additions & 0 deletions lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,16 @@ export interface InitializeOptions {
*/
wasmURL?: string

/**
* The result of calling "new WebAssembly.Module(buffer)" where "buffer"
* is a typed array or ArrayBuffer containing the binary code of the
* "esbuild.wasm" file.
*
* You can use this as an alternative to "wasmURL" for environments where it's
* not possible to download the WebAssembly module.
*/
wasmModule?: WebAssembly.Module

/**
* By default esbuild runs the WebAssembly-based browser API in a web worker
* to avoid blocking the UI thread. This can be disabled by setting "worker"
Expand Down
66 changes: 37 additions & 29 deletions scripts/browser/browser-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,37 +186,45 @@ let pages = {};

for (let format of ['iife', 'esm']) {
for (let min of [false, true]) {
for (let async of [false, true]) {
let code = `
window.testStart = function() {
esbuild.initialize({
wasmURL: '/esbuild.wasm',
worker: ${async},
}).then(() => {
return (${runAllTests})({ esbuild })
}).then(() => {
testDone()
}).catch(e => {
testFail('' + (e && e.stack || e))
testDone()
})
}
`;
let page;
if (format === 'esm') {
page = `
<script type="module">
import * as esbuild from '/esm/browser${min ? '.min' : ''}.js'
${code}
</script>
`;
} else {
page = `
<script src="/lib/browser${min ? '.min' : ''}.js"></script>
<script>${code}</script>
for (let worker of [false, true]) {
for (let module of [false, true]) {
let code = `
window.testStart = function() {
let promise = ${module}
? esbuild.initialize({
wasmURL: '/esbuild.wasm',
worker: ${worker},
})
: WebAssembly.compileStreaming(fetch('/esbuild.wasm')).then(module => esbuild.initialize({
wasmModule: module,
worker: ${worker},
}))
promise.then(() => {
return (${runAllTests})({ esbuild })
}).then(() => {
testDone()
}).catch(e => {
testFail('' + (e && e.stack || e))
testDone()
})
}
`;
let page;
if (format === 'esm') {
page = `
<script type="module">
import * as esbuild from '/esm/browser${min ? '.min' : ''}.js'
${code}
</script>
`;
} else {
page = `
<script src="/lib/browser${min ? '.min' : ''}.js"></script>
<script>${code}</script>
`;
}
pages[format + (min ? 'Min' : '') + (worker ? 'Worker' : '') + (module ? 'Module' : '')] = page;
}
pages[format + (min ? 'Min' : '') + (async ? 'Async' : '')] = page;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ exports.buildWasmLib = async (esbuildPath) => {
for (let k of Object.getOwnPropertyNames(o))
if (!(k in globalThis))
Object.defineProperty(globalThis, k, { get: () => self[k] });
${wasm_exec_js}
${wasm_exec_js.replace(/\bfs\./g, 'globalThis.fs.')}
${fs.readFileSync(path.join(repoDir, 'lib', 'npm', 'worker.ts'), 'utf8')}
return m => onmessage(m)
`;
Expand Down

0 comments on commit 44d70d3

Please sign in to comment.