Skip to content

Commit 79a3512

Browse files
authored
enable using esbuild with deno and wasm (#2359)
1 parent 4e631f5 commit 79a3512

File tree

10 files changed

+343
-67
lines changed

10 files changed

+343
-67
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
## Unreleased
44

5+
* Enable using esbuild in Deno via WebAssembly ([#2323](https://github.com/evanw/esbuild/issues/2323))
6+
7+
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`:
8+
9+
```js
10+
import * as esbuild from 'https://deno.land/x/[email protected]/wasm.js'
11+
const ts = 'let test: boolean = true'
12+
const result = await esbuild.transform(ts, { loader: 'ts' })
13+
console.log('result:', result)
14+
```
15+
16+
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.
17+
18+
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.
19+
520
* Add support for font file MIME types ([#2337](https://github.com/evanw/esbuild/issues/2337))
621

722
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:

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ platform-neutral: esbuild
350350
node scripts/esbuild.js npm/esbuild/package.json --version
351351
node scripts/esbuild.js ./esbuild --neutral
352352

353-
platform-deno: esbuild
353+
platform-deno: platform-wasm
354354
node scripts/esbuild.js ./esbuild --deno
355355

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

lib/deno/mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ let ensureServiceIsRunning = (): Promise<Service> => {
223223
startWriteFromQueueWorker()
224224
},
225225
isSync: false,
226-
isBrowser: false,
226+
isWriteUnavailable: false,
227227
esbuild: ourselves,
228228
})
229229

lib/deno/wasm.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import * as types from "../shared/types"
2+
import * as common from "../shared/common"
3+
import * as ourselves from "./wasm"
4+
5+
declare const ESBUILD_VERSION: string;
6+
declare let WEB_WORKER_SOURCE_CODE: string
7+
declare let WEB_WORKER_FUNCTION: (postMessage: (data: Uint8Array) => void) => (event: { data: Uint8Array | ArrayBuffer | WebAssembly.Module }) => void
8+
9+
export let version = ESBUILD_VERSION
10+
11+
export let build: typeof types.build = (options: types.BuildOptions): Promise<any> =>
12+
ensureServiceIsRunning().then(service =>
13+
service.build(options))
14+
15+
export const serve: typeof types.serve = () => {
16+
throw new Error(`The "serve" API does not work in Deno via WebAssembly`)
17+
}
18+
19+
export const transform: typeof types.transform = (input, options) =>
20+
ensureServiceIsRunning().then(service =>
21+
service.transform(input, options))
22+
23+
export const formatMessages: typeof types.formatMessages = (messages, options) =>
24+
ensureServiceIsRunning().then(service =>
25+
service.formatMessages(messages, options))
26+
27+
export const analyzeMetafile: typeof types.analyzeMetafile = (metafile, options) =>
28+
ensureServiceIsRunning().then(service =>
29+
service.analyzeMetafile(metafile, options))
30+
31+
export const buildSync: typeof types.buildSync = () => {
32+
throw new Error(`The "buildSync" API does not work in Deno`)
33+
}
34+
35+
export const transformSync: typeof types.transformSync = () => {
36+
throw new Error(`The "transformSync" API does not work in Deno`)
37+
}
38+
39+
export const formatMessagesSync: typeof types.formatMessagesSync = () => {
40+
throw new Error(`The "formatMessagesSync" API does not work in Deno`)
41+
}
42+
43+
export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => {
44+
throw new Error(`The "analyzeMetafileSync" API does not work in Deno`)
45+
}
46+
47+
export const stop = () => {
48+
if (stopService) stopService()
49+
}
50+
51+
interface Service {
52+
build: typeof types.build
53+
transform: typeof types.transform
54+
formatMessages: typeof types.formatMessages
55+
analyzeMetafile: typeof types.analyzeMetafile
56+
}
57+
58+
let initializePromise: Promise<Service> | undefined;
59+
let stopService: (() => void) | undefined
60+
61+
let ensureServiceIsRunning = (): Promise<Service> => {
62+
return initializePromise || startRunningService('', undefined, true)
63+
}
64+
65+
export const initialize: typeof types.initialize = async (options) => {
66+
options = common.validateInitializeOptions(options || {})
67+
let wasmURL = options.wasmURL;
68+
let wasmModule = options.wasmModule;
69+
let useWorker = options.worker !== false;
70+
if (initializePromise) throw new Error('Cannot call "initialize" more than once');
71+
initializePromise = startRunningService(wasmURL || '', wasmModule, useWorker);
72+
initializePromise.catch(() => {
73+
// Let the caller try again if this fails
74+
initializePromise = void 0;
75+
});
76+
await initializePromise;
77+
}
78+
79+
const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<Service> => {
80+
let wasm: WebAssembly.Module;
81+
if (wasmModule) {
82+
wasm = wasmModule;
83+
} else {
84+
if (!wasmURL) wasmURL = new URL('esbuild.wasm', import.meta.url).href
85+
wasm = await WebAssembly.compileStreaming(fetch(wasmURL))
86+
}
87+
88+
let worker: {
89+
onmessage: ((event: any) => void) | null
90+
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
91+
terminate: () => void
92+
}
93+
94+
if (useWorker) {
95+
// Run esbuild off the main thread
96+
let blob = new Blob([`onmessage=${WEB_WORKER_SOURCE_CODE}(postMessage)`], { type: 'text/javascript' })
97+
worker = new Worker(URL.createObjectURL(blob), { type: 'module' })
98+
} else {
99+
// Run esbuild on the main thread
100+
let onmessage = WEB_WORKER_FUNCTION((data: Uint8Array) => worker.onmessage!({ data }))
101+
worker = {
102+
onmessage: null,
103+
postMessage: data => setTimeout(() => onmessage({ data })),
104+
terminate() {
105+
},
106+
}
107+
}
108+
109+
worker.postMessage(wasm)
110+
worker.onmessage = ({ data }) => readFromStdout(data)
111+
112+
let { readFromStdout, service } = common.createChannel({
113+
writeToStdin(bytes) {
114+
worker.postMessage(bytes)
115+
},
116+
isSync: false,
117+
isWriteUnavailable: true,
118+
esbuild: ourselves,
119+
})
120+
121+
stopService = () => {
122+
worker.terminate()
123+
initializePromise = undefined
124+
stopService = undefined
125+
}
126+
127+
return {
128+
build: (options: types.BuildOptions): Promise<any> =>
129+
new Promise<types.BuildResult>((resolve, reject) =>
130+
service.buildOrServe({
131+
callName: 'build',
132+
refs: null,
133+
serveOptions: null,
134+
options,
135+
isTTY: false,
136+
defaultWD: '/',
137+
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
138+
})),
139+
transform: (input, options) =>
140+
new Promise((resolve, reject) =>
141+
service.transform({
142+
callName: 'transform',
143+
refs: null,
144+
input,
145+
options: options || {},
146+
isTTY: false,
147+
fs: {
148+
readFile(_, callback) { callback(new Error('Internal error'), null); },
149+
writeFile(_, callback) { callback(null); },
150+
},
151+
callback: (err, res) => err ? reject(err) : resolve(res!),
152+
})),
153+
formatMessages: (messages, options) =>
154+
new Promise((resolve, reject) =>
155+
service.formatMessages({
156+
callName: 'formatMessages',
157+
refs: null,
158+
messages,
159+
options,
160+
callback: (err, res) => err ? reject(err) : resolve(res!),
161+
})),
162+
analyzeMetafile: (metafile, options) =>
163+
new Promise((resolve, reject) =>
164+
service.analyzeMetafile({
165+
callName: 'analyzeMetafile',
166+
refs: null,
167+
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
168+
options,
169+
callback: (err, res) => err ? reject(err) : resolve(res!),
170+
})),
171+
}
172+
}

lib/npm/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
110110
worker.postMessage(bytes)
111111
},
112112
isSync: false,
113-
isBrowser: true,
113+
isWriteUnavailable: true,
114114
esbuild: ourselves,
115115
})
116116

lib/npm/node.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ let ensureServiceIsRunning = (): Service => {
263263
},
264264
readFileSync: fs.readFileSync,
265265
isSync: false,
266-
isBrowser: false,
266+
isWriteUnavailable: false,
267267
esbuild: ourselves,
268268
});
269269

@@ -365,7 +365,7 @@ let runServiceSync = (callback: (service: common.StreamService) => void): void =
365365
stdin = bytes;
366366
},
367367
isSync: true,
368-
isBrowser: false,
368+
isWriteUnavailable: false,
369369
esbuild: ourselves,
370370
});
371371
callback(service);

lib/shared/common.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export interface StreamIn {
424424
writeToStdin: (data: Uint8Array) => void;
425425
readFileSync?: (path: string, encoding: 'utf8') => string;
426426
isSync: boolean;
427-
isBrowser: boolean;
427+
isWriteUnavailable: boolean;
428428
esbuild: types.PluginBuild['esbuild'];
429429
}
430430

@@ -1158,7 +1158,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
11581158
if (callerRefs) callerRefs.unref()
11591159
},
11601160
}
1161-
let writeDefault = !streamIn.isBrowser;
1161+
let writeDefault = !streamIn.isWriteUnavailable;
11621162
let {
11631163
entries,
11641164
flags,
@@ -1287,7 +1287,7 @@ export function createChannel(streamIn: StreamIn): StreamOut {
12871287
});
12881288
};
12891289

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

0 commit comments

Comments
 (0)