Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add wasmModule option to initialize JS API #2155

Merged
merged 4 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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