Skip to content

Commit

Permalink
enable using esbuild with deno and wasm (#2359)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw authored Jun 30, 2022
1 parent 4e631f5 commit 79a3512
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 67 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## Unreleased

* Enable using esbuild in Deno via WebAssembly ([#2323](https://github.com/evanw/esbuild/issues/2323))

The native implementation of esbuild is much faster than the WebAssembly version, but some people don't want to give Deno the `--allow-run` permission necessary to run esbuild and are ok waiting longer for their builds to finish when using the WebAssembly backend. With this release, you can now use esbuild via WebAssembly in Deno. To do this you will need to import from `wasm.js` instead of `mod.js`:

```js
import * as esbuild from 'https://deno.land/x/[email protected]/wasm.js'
const ts = 'let test: boolean = true'
const result = await esbuild.transform(ts, { loader: 'ts' })
console.log('result:', result)
```

Make sure you run Deno with `--allow-net` so esbuild can download the WebAssembly module. Using esbuild like this starts up a worker thread that runs esbuild in parallel (unless you call `esbuild.initialize({ worker: false })` to tell esbuild to run on the main thread). If you want to, you can call `esbuild.stop()` to terminate the worker if you won't be using esbuild anymore and you want to reclaim the memory.

Note that Deno appears to have a bug where background WebAssembly optimization can prevent the process from exiting for many seconds. If you are trying to use Deno and WebAssembly to run esbuild quickly, you may need to manually call `Deno.exit(0)` after your code has finished running.

* Add support for font file MIME types ([#2337](https://github.com/evanw/esbuild/issues/2337))

This release adds support for font file MIME types to esbuild, which means they are now recognized by the built-in local web server and they are now used when a font file is loaded using the `dataurl` loader. The full set of newly-added file extension MIME type mappings is as follows:
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ platform-neutral: esbuild
node scripts/esbuild.js npm/esbuild/package.json --version
node scripts/esbuild.js ./esbuild --neutral

platform-deno: esbuild
platform-deno: platform-wasm
node scripts/esbuild.js ./esbuild --deno

publish-all: check-go-version
Expand Down Expand Up @@ -489,7 +489,8 @@ publish-deno:
test -d deno/.git || (rm -fr deno && git clone [email protected]:esbuild/deno-esbuild.git deno)
cd deno && git fetch && git checkout main && git reset --hard origin/main
@$(MAKE) --no-print-directory platform-deno
cd deno && git commit -am "publish $(ESBUILD_VERSION) to deno"
cd deno && git add mod.js mod.d.ts wasm.js wasm.d.ts esbuild.wasm
cd deno && git commit -m "publish $(ESBUILD_VERSION) to deno"
cd deno && git tag "v$(ESBUILD_VERSION)"
cd deno && git push origin main "v$(ESBUILD_VERSION)"

Expand Down
2 changes: 1 addition & 1 deletion lib/deno/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ let ensureServiceIsRunning = (): Promise<Service> => {
startWriteFromQueueWorker()
},
isSync: false,
isBrowser: false,
isWriteUnavailable: false,
esbuild: ourselves,
})

Expand Down
172 changes: 172 additions & 0 deletions lib/deno/wasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as types from "../shared/types"
import * as common from "../shared/common"
import * as ourselves from "./wasm"

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 | WebAssembly.Module }) => void

export let version = ESBUILD_VERSION

export let build: typeof types.build = (options: types.BuildOptions): Promise<any> =>
ensureServiceIsRunning().then(service =>
service.build(options))

export const serve: typeof types.serve = () => {
throw new Error(`The "serve" API does not work in Deno via WebAssembly`)
}

export const transform: typeof types.transform = (input, options) =>
ensureServiceIsRunning().then(service =>
service.transform(input, options))

export const formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().then(service =>
service.formatMessages(messages, options))

export const analyzeMetafile: typeof types.analyzeMetafile = (metafile, options) =>
ensureServiceIsRunning().then(service =>
service.analyzeMetafile(metafile, options))

export const buildSync: typeof types.buildSync = () => {
throw new Error(`The "buildSync" API does not work in Deno`)
}

export const transformSync: typeof types.transformSync = () => {
throw new Error(`The "transformSync" API does not work in Deno`)
}

export const formatMessagesSync: typeof types.formatMessagesSync = () => {
throw new Error(`The "formatMessagesSync" API does not work in Deno`)
}

export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => {
throw new Error(`The "analyzeMetafileSync" API does not work in Deno`)
}

export const stop = () => {
if (stopService) stopService()
}

interface Service {
build: typeof types.build
transform: typeof types.transform
formatMessages: typeof types.formatMessages
analyzeMetafile: typeof types.analyzeMetafile
}

let initializePromise: Promise<Service> | undefined;
let stopService: (() => void) | undefined

let ensureServiceIsRunning = (): Promise<Service> => {
return initializePromise || startRunningService('', undefined, true)
}

export const initialize: typeof types.initialize = async (options) => {
options = common.validateInitializeOptions(options || {})
let wasmURL = options.wasmURL;
let wasmModule = options.wasmModule;
let useWorker = options.worker !== false;
if (initializePromise) throw new Error('Cannot call "initialize" more than once');
initializePromise = startRunningService(wasmURL || '', wasmModule, useWorker);
initializePromise.catch(() => {
// Let the caller try again if this fails
initializePromise = void 0;
});
await initializePromise;
}

const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<Service> => {
let wasm: WebAssembly.Module;
if (wasmModule) {
wasm = wasmModule;
} else {
if (!wasmURL) wasmURL = new URL('esbuild.wasm', import.meta.url).href
wasm = await WebAssembly.compileStreaming(fetch(wasmURL))
}

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

if (useWorker) {
// Run esbuild off the main thread
let blob = new Blob([`onmessage=${WEB_WORKER_SOURCE_CODE}(postMessage)`], { type: 'text/javascript' })
worker = new Worker(URL.createObjectURL(blob), { type: 'module' })
} else {
// Run esbuild on the main thread
let onmessage = WEB_WORKER_FUNCTION((data: Uint8Array) => worker.onmessage!({ data }))
worker = {
onmessage: null,
postMessage: data => setTimeout(() => onmessage({ data })),
terminate() {
},
}
}

worker.postMessage(wasm)
worker.onmessage = ({ data }) => readFromStdout(data)

let { readFromStdout, service } = common.createChannel({
writeToStdin(bytes) {
worker.postMessage(bytes)
},
isSync: false,
isWriteUnavailable: true,
esbuild: ourselves,
})

stopService = () => {
worker.terminate()
initializePromise = undefined
stopService = undefined
}

return {
build: (options: types.BuildOptions): Promise<any> =>
new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrServe({
callName: 'build',
refs: null,
serveOptions: null,
options,
isTTY: false,
defaultWD: '/',
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
})),
transform: (input, options) =>
new Promise((resolve, reject) =>
service.transform({
callName: 'transform',
refs: null,
input,
options: options || {},
isTTY: false,
fs: {
readFile(_, callback) { callback(new Error('Internal error'), null); },
writeFile(_, callback) { callback(null); },
},
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
formatMessages: (messages, options) =>
new Promise((resolve, reject) =>
service.formatMessages({
callName: 'formatMessages',
refs: null,
messages,
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
analyzeMetafile: (metafile, options) =>
new Promise((resolve, reject) =>
service.analyzeMetafile({
callName: 'analyzeMetafile',
refs: null,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
}
}
2 changes: 1 addition & 1 deletion lib/npm/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
worker.postMessage(bytes)
},
isSync: false,
isBrowser: true,
isWriteUnavailable: true,
esbuild: ourselves,
})

Expand Down
4 changes: 2 additions & 2 deletions lib/npm/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ let ensureServiceIsRunning = (): Service => {
},
readFileSync: fs.readFileSync,
isSync: false,
isBrowser: false,
isWriteUnavailable: false,
esbuild: ourselves,
});

Expand Down Expand Up @@ -365,7 +365,7 @@ let runServiceSync = (callback: (service: common.StreamService) => void): void =
stdin = bytes;
},
isSync: true,
isBrowser: false,
isWriteUnavailable: false,
esbuild: ourselves,
});
callback(service);
Expand Down
6 changes: 3 additions & 3 deletions lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ export interface StreamIn {
writeToStdin: (data: Uint8Array) => void;
readFileSync?: (path: string, encoding: 'utf8') => string;
isSync: boolean;
isBrowser: boolean;
isWriteUnavailable: boolean;
esbuild: types.PluginBuild['esbuild'];
}

Expand Down Expand Up @@ -1158,7 +1158,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
if (callerRefs) callerRefs.unref()
},
}
let writeDefault = !streamIn.isBrowser;
let writeDefault = !streamIn.isWriteUnavailable;
let {
entries,
flags,
Expand Down Expand Up @@ -1287,7 +1287,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
});
};

if (write && streamIn.isBrowser) throw new Error(`Cannot enable "write" in the browser`);
if (write && streamIn.isWriteUnavailable) throw new Error(`The "write" option is unavailable in this environment`);
if (incremental && streamIn.isSync) throw new Error(`Cannot use "incremental" with a synchronous build`);
if (watch && streamIn.isSync) throw new Error(`Cannot use "watch" with a synchronous build`);
sendRequest<protocol.BuildRequest, protocol.BuildResponse>(refs, request, (error, response) => {
Expand Down
File renamed without changes.
Loading

0 comments on commit 79a3512

Please sign in to comment.