Skip to content

Commit 116f63e

Browse files
authored
Work around issues with Deno 1.31+ (#3685)
1 parent cc74e60 commit 116f63e

File tree

4 files changed

+35
-39
lines changed

4 files changed

+35
-39
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
})(Foo || {});
3636
```
3737

38+
* Work around issues with Deno 1.31+ ([#3682](https://github.com/evanw/esbuild/issues/3682))
39+
40+
Version 0.20.0 of esbuild changed how the esbuild child process is run in esbuild's API for Deno. Previously it used `Deno.run` but that API is being removed in favor of `Deno.Command`. As part of this change, esbuild is now calling the new `unref` function on esbuild's long-lived child process, which is supposed to allow Deno to exit when your code has finished running even though the child process is still around (previously you had to explicitly call esbuild's `stop()` function to terminate the child process for Deno to be able to exit).
41+
42+
However, this introduced a problem for Deno's testing API which now fails some tests that use esbuild with `error: Promise resolution is still pending but the event loop has already resolved`. It's unclear to me why this is happening. The call to `unref` was recommended by someone on the Deno core team, and calling Node's equivalent `unref` API has been working fine for esbuild in Node for a long time. It could be that I'm using it incorrectly, or that there's some reference counting and/or garbage collection bug in Deno's internals, or that Deno's `unref` just works differently than Node's `unref`. In any case, it's not good for Deno tests that use esbuild to be failing.
43+
44+
In this release, I am removing the call to `unref` to fix this issue. This means that you will now have to call esbuild's `stop()` function to allow Deno to exit, just like you did before esbuild version 0.20.0 when this regression was introduced.
45+
46+
Note: This regression wasn't caught earlier because Deno doesn't seem to fail tests that have outstanding `setTimeout` calls, which esbuild's test harness was using to enforce a maximum test runtime. Adding a `setTimeout` was allowing esbuild's Deno tests to succeed. So this regression doesn't necessarily apply to all people using tests in Deno.
47+
3848
## 0.20.1
3949

4050
* Fix a bug with the CSS nesting transform ([#3648](https://github.com/evanw/esbuild/issues/3648))

lib/deno/mod.ts

+5-19
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,6 @@ type SpawnFn = (cmd: string, options: {
192192
read(): Promise<Uint8Array | null>
193193
close(): Promise<void> | void
194194
status(): Promise<{ code: number }>
195-
unref(): void
196-
ref(): void
197195
}
198196

199197
// Deno ≥1.40
@@ -235,8 +233,6 @@ const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => {
235233
await child.status
236234
},
237235
status: () => child.status,
238-
unref: () => child.unref(),
239-
ref: () => child.ref(),
240236
}
241237
}
242238

@@ -277,8 +273,6 @@ const spawnOld: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => {
277273
child.close()
278274
},
279275
status: () => child.status(),
280-
unref: () => { },
281-
ref: () => { },
282276
}
283277
}
284278

@@ -334,20 +328,12 @@ const ensureServiceIsRunning = (): Promise<Service> => {
334328
})
335329
readMoreStdout()
336330

337-
let refCount = 0
338-
child.unref() // Allow Deno to exit when esbuild is running
339-
340-
const refs: common.Refs = {
341-
ref() { if (++refCount === 1) child.ref(); },
342-
unref() { if (--refCount === 0) child.unref(); },
343-
}
344-
345331
return {
346332
build: (options: types.BuildOptions) =>
347333
new Promise<types.BuildResult>((resolve, reject) => {
348334
service.buildOrContext({
349335
callName: 'build',
350-
refs,
336+
refs: null,
351337
options,
352338
isTTY,
353339
defaultWD,
@@ -359,7 +345,7 @@ const ensureServiceIsRunning = (): Promise<Service> => {
359345
new Promise<types.BuildContext>((resolve, reject) =>
360346
service.buildOrContext({
361347
callName: 'context',
362-
refs,
348+
refs: null,
363349
options,
364350
isTTY,
365351
defaultWD,
@@ -370,7 +356,7 @@ const ensureServiceIsRunning = (): Promise<Service> => {
370356
new Promise<types.TransformResult>((resolve, reject) =>
371357
service.transform({
372358
callName: 'transform',
373-
refs,
359+
refs: null,
374360
input,
375361
options: options || {},
376362
isTTY,
@@ -403,7 +389,7 @@ const ensureServiceIsRunning = (): Promise<Service> => {
403389
new Promise((resolve, reject) =>
404390
service.formatMessages({
405391
callName: 'formatMessages',
406-
refs,
392+
refs: null,
407393
messages,
408394
options,
409395
callback: (err, res) => err ? reject(err) : resolve(res!),
@@ -413,7 +399,7 @@ const ensureServiceIsRunning = (): Promise<Service> => {
413399
new Promise((resolve, reject) =>
414400
service.analyzeMetafile({
415401
callName: 'analyzeMetafile',
416-
refs,
402+
refs: null,
417403
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
418404
options,
419405
callback: (err, res) => err ? reject(err) : resolve(res!),

lib/shared/types.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -664,13 +664,15 @@ export let version: string
664664

665665
// Call this function to terminate esbuild's child process. The child process
666666
// is not terminated and re-created after each API call because it's more
667-
// efficient to keep it around when there are multiple API calls. This child
668-
// process normally exits automatically when the parent process exits, so you
669-
// usually don't need to call this function.
667+
// efficient to keep it around when there are multiple API calls.
670668
//
671-
// One reason you might want to call this is if you know you will not make any
672-
// more esbuild API calls and you want to clean up resources (since the esbuild
673-
// child process takes up some memory even when idle).
669+
// In node this happens automatically before the parent node process exits. So
670+
// you only need to call this if you know you will not make any more esbuild
671+
// API calls and you want to clean up resources.
672+
//
673+
// Unlike node, Deno lacks the necessary APIs to clean up child processes
674+
// automatically. You must manually call stop() in Deno when you're done
675+
// using esbuild or Deno will continue running forever.
674676
//
675677
// Another reason you might want to call this is if you are using esbuild from
676678
// within a Deno test. Deno fails tests that create a child process without

scripts/deno-tests.js

+12-14
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@ try {
1515
Deno.mkdirSync(rootTestDir, { recursive: true })
1616

1717
function test(name, backends, fn) {
18-
const singleTest = (name, fn) => Deno.test({
19-
name,
20-
fn: () => new Promise((resolve, reject) => {
21-
const minutes = 5
22-
const timeout = setTimeout(() => reject(new Error(`Timeout for "${name}" after ${minutes} minutes`)), minutes * 60 * 1000)
23-
const cancel = () => clearTimeout(timeout)
24-
const promise = fn()
25-
promise.then(cancel, cancel)
26-
promise.then(resolve, reject)
27-
}),
28-
})
18+
// Note: Do not try to add a timeout for the tests below. It turns out that
19+
// calling "setTimeout" from within "Deno.test" changes how Deno waits for
20+
// promises in a way that masks problems that would otherwise occur. We want
21+
// test coverage for the way that people will likely be using these API calls.
22+
//
23+
// Specifically tests that Deno would otherwise have failed with "error:
24+
// Promise resolution is still pending but the event loop has already
25+
// resolved" were being allowed to pass instead. See this issue for more
26+
// information: https://github.com/evanw/esbuild/issues/3682
2927

3028
for (const backend of backends) {
3129
switch (backend) {
3230
case 'native':
33-
singleTest(name + '-native', async () => {
31+
Deno.test(name + '-native', async () => {
3432
let testDir = path.join(rootTestDir, name + '-native')
3533
await Deno.mkdir(testDir, { recursive: true })
3634
try {
@@ -43,7 +41,7 @@ function test(name, backends, fn) {
4341
break
4442

4543
case 'wasm-main':
46-
singleTest(name + '-wasm-main', async () => {
44+
Deno.test(name + '-wasm-main', async () => {
4745
let testDir = path.join(rootTestDir, name + '-wasm-main')
4846
await esbuildWASM.initialize({ wasmModule, worker: false })
4947
await Deno.mkdir(testDir, { recursive: true })
@@ -57,7 +55,7 @@ function test(name, backends, fn) {
5755
break
5856

5957
case 'wasm-worker':
60-
singleTest(name + '-wasm-worker', async () => {
58+
Deno.test(name + '-wasm-worker', async () => {
6159
let testDir = path.join(rootTestDir, name + '-wasm-worker')
6260
await esbuildWASM.initialize({ wasmModule, worker: true })
6361
await Deno.mkdir(testDir, { recursive: true })

0 commit comments

Comments
 (0)