From 3b499bfc9eb64159337fad02c9fe25440cc20626 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 25 Jan 2024 23:10:53 -0500 Subject: [PATCH 1/7] work around api deprecations in deno 1.40.x --- .github/workflows/ci.yml | 42 +++++++++-- CHANGELOG.md | 8 +++ lib/deno/mod.ts | 146 ++++++++++++++++++++++++++++++--------- lib/deno/wasm.ts | 1 + lib/npm/browser.ts | 1 + lib/npm/node.ts | 1 + lib/shared/types.ts | 2 +- scripts/deno-tests.js | 6 +- 8 files changed, 163 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 633f48ebf72..529012e932e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,12 +89,10 @@ jobs: with: node-version: 16 - # The version of Deno is pinned because version 1.25.1 was causing test - # flakes due to random segfaults. - - name: Setup Deno 1.24.0 + - name: Setup Deno 1.40.0 uses: denoland/setup-deno@main with: - deno-version: v1.24.0 + deno-version: v1.40.0 - name: Check out code into the Go module directory uses: actions/checkout@v3 @@ -199,8 +197,8 @@ jobs: make test-yarnpnp - esbuild-old-versions: - name: esbuild CI (old versions) + esbuild-old-go-version: + name: esbuild CI (old Go version) runs-on: ubuntu-latest steps: @@ -221,3 +219,35 @@ jobs: - name: make test-old-ts run: make test-old-ts + + esbuild-old-deno-version: + name: esbuild CI (old Deno version) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version: 1.20.12 + id: go + + # Make sure esbuild works with old versions of Deno. Note: It's important + # to test a version before 1.31.0, which introduced the "Deno.Command" API. + - name: Setup Deno 1.24.0 + uses: denoland/setup-deno@main + with: + deno-version: v1.24.0 + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Deno Tests (non-Windows) + if: matrix.os != 'windows-latest' + run: make test-deno + + - name: Deno Tests (Windows) + if: matrix.os == 'windows-latest' + run: make test-deno-windows diff --git a/CHANGELOG.md b/CHANGELOG.md index 926418dd088..7ff71c1fea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +* Work around API deprecations in Deno 1.40.x ([#3609](https://github.com/evanw/esbuild/issues/3609)) + + Deno 1.40.0 introduced run-time warnings about certain APIs that esbuild uses. With this release, esbuild will work around these run-time warnings by using other APIs if they are present, and falling back to the original APIs otherwise. This should avoid the warnings without breaking compatibility with older versions of Deno. + + Unfortunately, doing this introduces a breaking change. The other child process APIs seem to lack a way to synchronously terminate esbuild's child process, which causes Deno to now fail tests with a confusing error about a thing called `op_spawn_wait` in Deno's internals. To work around this, esbuild's `stop()` function has been changed to return a promise, and you now have to change `esbuild.stop()` to `await esbuild.stop()` in all of your Deno tests. + ## 0.19.12 * The "preserve" JSX mode now preserves JSX text verbatim ([#3605](https://github.com/evanw/esbuild/issues/3605)) diff --git a/lib/deno/mod.ts b/lib/deno/mod.ts index 7e24f389a74..64410d7b7a9 100644 --- a/lib/deno/mod.ts +++ b/lib/deno/mod.ts @@ -43,8 +43,8 @@ export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => { throw new Error(`The "analyzeMetafileSync" API does not work in Deno`) } -export const stop = () => { - if (stopService) stopService() +export const stop = async () => { + if (stopService) await stopService() } let initializeWasCalled = false @@ -178,63 +178,142 @@ interface Service { let defaultWD = Deno.cwd() let longLivedService: Promise | undefined -let stopService: (() => void) | undefined +let stopService: (() => Promise) | undefined + +// Declare a common subprocess API for the two implementations below +type SpawnFn = (cmd: string, options: { + args: string[] + stdin: 'piped' | 'inherit' + stdout: 'piped' | 'inherit' + stderr: 'inherit' +}) => { + stdin: { + write(bytes: Uint8Array): void + close(): void + } + stdout: { + read(): Promise + close(): void + } + close(): Promise | void + status(): Promise<{ code: number }> +} + +// Deno ≥1.40 +const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { + const child = new Deno.Command(cmd, { + args, + cwd: defaultWD, + stdin, + stdout, + stderr, + }).spawn() + const writer = child.stdin.getWriter() + const reader = child.stdout.getReader() + return { + stdin: { + write: bytes => writer.write(bytes), + close: () => writer.close(), + }, + stdout: { + read: () => reader.read().then(x => x.value || null), + close: () => reader.cancel(), + }, + close: async () => { + // Note: This can throw with EPERM on Windows (happens in GitHub Actions) + child.kill() + + // Without this, Deno will fail tests with some weird error about "op_spawn_wait" + await child.status + }, + status: () => child.status, + } +} + +// Deno <1.40 +const spawnOld: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { + const child = Deno.run({ + cmd: [cmd].concat(args), + cwd: defaultWD, + stdin, + stdout, + stderr, + }) + const stdoutBuffer = new Uint8Array(4 * 1024 * 1024) + let writeQueue: Uint8Array[] = [] + let isQueueLocked = false + + // We need to keep calling "write()" until it actually writes the data + const startWriteFromQueueWorker = () => { + if (isQueueLocked || writeQueue.length === 0) return + isQueueLocked = true + child.stdin!.write(writeQueue[0]).then(bytesWritten => { + isQueueLocked = false + if (bytesWritten === writeQueue[0].length) writeQueue.shift() + else writeQueue[0] = writeQueue[0].subarray(bytesWritten) + startWriteFromQueueWorker() + }) + } + + return { + stdin: { + write: bytes => { + writeQueue.push(bytes) + startWriteFromQueueWorker() + }, + close: () => child.stdin!.close(), + }, + stdout: { + read: () => child.stdout!.read(stdoutBuffer).then(n => n === null ? null : stdoutBuffer.subarray(0, n)), + close: () => child.stdout!.close(), + }, + close: () => child.close(), + status: () => child.status(), + } +} -let ensureServiceIsRunning = (): Promise => { +// This is a shim for "Deno.run" for newer versions of Deno +const spawn: SpawnFn = Deno.Command ? spawnNew : spawnOld + +const ensureServiceIsRunning = (): Promise => { if (!longLivedService) { longLivedService = (async (): Promise => { const binPath = await install() - const isTTY = Deno.isatty(Deno.stderr.rid) + const isTTY = Deno.stderr.isTerminal + ? Deno.stderr.isTerminal() // Deno ≥1.40 + : Deno.isatty(Deno.stderr.rid) // Deno <1.40 - const child = Deno.run({ - cmd: [binPath, `--service=${version}`], - cwd: defaultWD, + const child = spawn(binPath, { + args: [`--service=${version}`], stdin: 'piped', stdout: 'piped', stderr: 'inherit', }) - stopService = () => { + stopService = async () => { // Close all resources related to the subprocess. child.stdin.close() child.stdout.close() - child.close() + await child.close() initializeWasCalled = false longLivedService = undefined stopService = undefined } - let writeQueue: Uint8Array[] = [] - let isQueueLocked = false - - // We need to keep calling "write()" until it actually writes the data - const startWriteFromQueueWorker = () => { - if (isQueueLocked || writeQueue.length === 0) return - isQueueLocked = true - child.stdin.write(writeQueue[0]).then(bytesWritten => { - isQueueLocked = false - if (bytesWritten === writeQueue[0].length) writeQueue.shift() - else writeQueue[0] = writeQueue[0].subarray(bytesWritten) - startWriteFromQueueWorker() - }) - } - const { readFromStdout, afterClose, service } = common.createChannel({ writeToStdin(bytes) { - writeQueue.push(bytes) - startWriteFromQueueWorker() + child.stdin.write(bytes) }, isSync: false, hasFS: true, esbuild: ourselves, }) - const stdoutBuffer = new Uint8Array(4 * 1024 * 1024) - const readMoreStdout = () => child.stdout.read(stdoutBuffer).then(n => { - if (n === null) { + const readMoreStdout = () => child.stdout.read().then(buffer => { + if (buffer === null) { afterClose(null) } else { - readFromStdout(stdoutBuffer.subarray(0, n)) + readFromStdout(buffer) readMoreStdout() } }).catch(e => { @@ -331,9 +410,8 @@ let ensureServiceIsRunning = (): Promise => { // If we're called as the main script, forward the CLI to the underlying executable if (import.meta.main) { - Deno.run({ - cmd: [await install()].concat(Deno.args), - cwd: defaultWD, + spawn(await install(), { + args: Deno.args, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', diff --git a/lib/deno/wasm.ts b/lib/deno/wasm.ts index 0bad9421a3b..808d2d37ca6 100644 --- a/lib/deno/wasm.ts +++ b/lib/deno/wasm.ts @@ -50,6 +50,7 @@ export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => { export const stop = () => { if (stopService) stopService() + return Promise.resolve() } interface Service { diff --git a/lib/npm/browser.ts b/lib/npm/browser.ts index 9885119a2cd..325687c8dd3 100644 --- a/lib/npm/browser.ts +++ b/lib/npm/browser.ts @@ -45,6 +45,7 @@ export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => { export const stop = () => { if (stopService) stopService() + return Promise.resolve() } interface Service { diff --git a/lib/npm/node.ts b/lib/npm/node.ts index 56a6ddb7076..663937f88b5 100644 --- a/lib/npm/node.ts +++ b/lib/npm/node.ts @@ -223,6 +223,7 @@ export let analyzeMetafileSync: typeof types.analyzeMetafileSync = (metafile, op export const stop = () => { if (stopService) stopService() if (workerThreadService) workerThreadService.stop() + return Promise.resolve() } let initializeWasCalled = false diff --git a/lib/shared/types.ts b/lib/shared/types.ts index b398f845bc4..6221e475459 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -673,4 +673,4 @@ export let version: string // Unlike node, Deno lacks the necessary APIs to clean up child processes // automatically. You must manually call stop() in Deno when you're done // using esbuild or Deno will continue running forever. -export declare function stop(): void; +export declare function stop(): Promise diff --git a/scripts/deno-tests.js b/scripts/deno-tests.js index e0748e7ba1d..c971b337470 100644 --- a/scripts/deno-tests.js +++ b/scripts/deno-tests.js @@ -37,7 +37,7 @@ function test(name, backends, fn) { await fn({ esbuild: esbuildNative, testDir }) await Deno.remove(testDir, { recursive: true }).catch(() => null) } finally { - esbuildNative.stop() + await esbuildNative.stop() } }) break @@ -51,7 +51,7 @@ function test(name, backends, fn) { await fn({ esbuild: esbuildWASM, testDir }) await Deno.remove(testDir, { recursive: true }).catch(() => null) } finally { - esbuildWASM.stop() + await esbuildWASM.stop() } }) break @@ -65,7 +65,7 @@ function test(name, backends, fn) { await fn({ esbuild: esbuildWASM, testDir }) await Deno.remove(testDir, { recursive: true }).catch(() => null) } finally { - esbuildWASM.stop() + await esbuildWASM.stop() } }) break From 5e4663fb90f227d00bac395d87ce4b7bf7384cd4 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Jan 2024 00:13:22 -0500 Subject: [PATCH 2/7] transform esbuild internals for older deno --- scripts/esbuild.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/esbuild.js b/scripts/esbuild.js index 4fb7f9b928f..9ebad8945d7 100644 --- a/scripts/esbuild.js +++ b/scripts/esbuild.js @@ -8,6 +8,7 @@ const denoDir = path.join(repoDir, 'deno') const npmDir = path.join(repoDir, 'npm', 'esbuild') const version = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim() const nodeTarget = 'node10'; // See: https://nodejs.org/en/about/releases/ +const denoTarget = 'deno1'; // See: https://nodejs.org/en/about/releases/ const umdBrowserTarget = 'es2015'; // Transpiles "async" const esmBrowserTarget = 'es2017'; // Preserves "async" @@ -227,7 +228,7 @@ const buildDenoLib = async (esbuildPath) => { path.join(repoDir, 'lib', 'deno', 'mod.ts'), '--bundle', '--outfile=' + path.join(denoDir, 'mod.js'), - '--target=esnext', + '--target=' + denoTarget, '--define:ESBUILD_VERSION=' + JSON.stringify(version), '--platform=neutral', '--log-level=warning', @@ -237,11 +238,11 @@ const buildDenoLib = async (esbuildPath) => { // Generate "deno/wasm.js" const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim() let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'misc', 'wasm', 'wasm_exec.js'), 'utf8') - const wasmWorkerCode = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify: true, target: 'esnext' }) + const wasmWorkerCode = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify: true, target: denoTarget }) const modWASM = childProcess.execFileSync(esbuildPath, [ path.join(repoDir, 'lib', 'deno', 'wasm.ts'), '--bundle', - '--target=esnext', + '--target=' + denoTarget, '--define:ESBUILD_VERSION=' + JSON.stringify(version), '--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCode), '--platform=neutral', From 31393ac0a1d978860ce37f5970d38d72854ceba6 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Jan 2024 01:40:50 -0500 Subject: [PATCH 3/7] `await` more things --- lib/deno/mod.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/deno/mod.ts b/lib/deno/mod.ts index 64410d7b7a9..233d807e63e 100644 --- a/lib/deno/mod.ts +++ b/lib/deno/mod.ts @@ -189,11 +189,11 @@ type SpawnFn = (cmd: string, options: { }) => { stdin: { write(bytes: Uint8Array): void - close(): void + close(): Promise | void } stdout: { read(): Promise - close(): void + close(): Promise | void } close(): Promise | void status(): Promise<{ code: number }> @@ -292,8 +292,8 @@ const ensureServiceIsRunning = (): Promise => { stopService = async () => { // Close all resources related to the subprocess. - child.stdin.close() - child.stdout.close() + await child.stdin.close() + await child.stdout.close() await child.close() initializeWasCalled = false longLivedService = undefined From b4cfc1209b7195c2dd2aaee4db40756c22804d3e Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Jan 2024 23:05:54 -0500 Subject: [PATCH 4/7] try to kill the process without closing stdin --- lib/deno/mod.ts | 50 ++++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/lib/deno/mod.ts b/lib/deno/mod.ts index 233d807e63e..46c0e4246ea 100644 --- a/lib/deno/mod.ts +++ b/lib/deno/mod.ts @@ -187,14 +187,8 @@ type SpawnFn = (cmd: string, options: { stdout: 'piped' | 'inherit' stderr: 'inherit' }) => { - stdin: { - write(bytes: Uint8Array): void - close(): Promise | void - } - stdout: { - read(): Promise - close(): Promise | void - } + write(bytes: Uint8Array): void + read(): Promise close(): Promise | void status(): Promise<{ code: number }> } @@ -211,19 +205,15 @@ const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { const writer = child.stdin.getWriter() const reader = child.stdout.getReader() return { - stdin: { - write: bytes => writer.write(bytes), - close: () => writer.close(), - }, - stdout: { - read: () => reader.read().then(x => x.value || null), - close: () => reader.cancel(), - }, + write: bytes => writer.write(bytes), + read: () => reader.read().then(x => x.value || null), close: async () => { - // Note: This can throw with EPERM on Windows (happens in GitHub Actions) child.kill() - // Without this, Deno will fail tests with some weird error about "op_spawn_wait" + // Wait for the process to exit. The new "kill()" API doesn't flag the + // process as having exited because processes can technically ignore the + // kill signal. Without this, Deno will fail tests that use esbuild with + // an error because the test spawned a process but didn't wait for it. await child.status }, status: () => child.status, @@ -256,18 +246,16 @@ const spawnOld: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { } return { - stdin: { - write: bytes => { - writeQueue.push(bytes) - startWriteFromQueueWorker() - }, - close: () => child.stdin!.close(), + write: bytes => { + writeQueue.push(bytes) + startWriteFromQueueWorker() }, - stdout: { - read: () => child.stdout!.read(stdoutBuffer).then(n => n === null ? null : stdoutBuffer.subarray(0, n)), - close: () => child.stdout!.close(), + read: () => child.stdout!.read(stdoutBuffer).then(n => n === null ? null : stdoutBuffer.subarray(0, n)), + close: () => { + child.stdin!.close() + child.stdout!.close() + child.close() }, - close: () => child.close(), status: () => child.status(), } } @@ -292,8 +280,6 @@ const ensureServiceIsRunning = (): Promise => { stopService = async () => { // Close all resources related to the subprocess. - await child.stdin.close() - await child.stdout.close() await child.close() initializeWasCalled = false longLivedService = undefined @@ -302,14 +288,14 @@ const ensureServiceIsRunning = (): Promise => { const { readFromStdout, afterClose, service } = common.createChannel({ writeToStdin(bytes) { - child.stdin.write(bytes) + child.write(bytes) }, isSync: false, hasFS: true, esbuild: ourselves, }) - const readMoreStdout = () => child.stdout.read().then(buffer => { + const readMoreStdout = () => child.read().then(buffer => { if (buffer === null) { afterClose(null) } else { From 665c5fcdc77822e7d0b7fb3a02970da4b9b4ad99 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Jan 2024 23:25:10 -0500 Subject: [PATCH 5/7] try to close stdin without killing the process --- lib/deno/mod.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/deno/mod.ts b/lib/deno/mod.ts index 46c0e4246ea..8891b18e494 100644 --- a/lib/deno/mod.ts +++ b/lib/deno/mod.ts @@ -208,7 +208,22 @@ const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { write: bytes => writer.write(bytes), read: () => reader.read().then(x => x.value || null), close: async () => { - child.kill() + // We can't call "kill()" because it doesn't seem to work. Tests will + // still fail with "A child process was opened during the test, but not + // closed during the test" even though we kill the child process. + // + // And we can't call both "writer.close()" and "kill()" because then + // there's a race as the child process exits when stdin is closed, and + // "kill()" fails when the child process has already been killed. + // + // So instead we just call "writer.close()" and then hope that this + // causes the child process to exit. It won't work if the stdin consumer + // thread in the child process is hung or busy, but that may be the best + // we can do. + // + // See this for more info: https://github.com/evanw/esbuild/pull/3611 + await writer.close() + await reader.cancel() // Wait for the process to exit. The new "kill()" API doesn't flag the // process as having exited because processes can technically ignore the From 11ec95f3654bfb6b9bc5d02a36169a769ab9c94b Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Fri, 26 Jan 2024 23:51:42 -0500 Subject: [PATCH 6/7] try using the new `unref` API --- lib/deno/mod.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/deno/mod.ts b/lib/deno/mod.ts index 8891b18e494..371b8d80705 100644 --- a/lib/deno/mod.ts +++ b/lib/deno/mod.ts @@ -191,6 +191,8 @@ type SpawnFn = (cmd: string, options: { read(): Promise close(): Promise | void status(): Promise<{ code: number }> + unref(): void + ref(): void } // Deno ≥1.40 @@ -232,6 +234,8 @@ const spawnNew: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { await child.status }, status: () => child.status, + unref: () => child.unref(), + ref: () => child.ref(), } } @@ -272,6 +276,8 @@ const spawnOld: SpawnFn = (cmd, { args, stdin, stdout, stderr }) => { child.close() }, status: () => child.status(), + unref: () => { }, + ref: () => { }, } } @@ -327,12 +333,20 @@ const ensureServiceIsRunning = (): Promise => { }) readMoreStdout() + let refCount = 0 + child.unref() // Allow Deno to exit when esbuild is running + + const refs: common.Refs = { + ref() { if (++refCount === 1) child.ref(); }, + unref() { if (--refCount === 0) child.unref(); }, + } + return { build: (options: types.BuildOptions) => new Promise((resolve, reject) => { service.buildOrContext({ callName: 'build', - refs: null, + refs, options, isTTY, defaultWD, @@ -344,7 +358,7 @@ const ensureServiceIsRunning = (): Promise => { new Promise((resolve, reject) => service.buildOrContext({ callName: 'context', - refs: null, + refs, options, isTTY, defaultWD, @@ -355,7 +369,7 @@ const ensureServiceIsRunning = (): Promise => { new Promise((resolve, reject) => service.transform({ callName: 'transform', - refs: null, + refs, input, options: options || {}, isTTY, @@ -388,7 +402,7 @@ const ensureServiceIsRunning = (): Promise => { new Promise((resolve, reject) => service.formatMessages({ callName: 'formatMessages', - refs: null, + refs, messages, options, callback: (err, res) => err ? reject(err) : resolve(res!), @@ -398,7 +412,7 @@ const ensureServiceIsRunning = (): Promise => { new Promise((resolve, reject) => service.analyzeMetafile({ callName: 'analyzeMetafile', - refs: null, + refs, metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile), options, callback: (err, res) => err ? reject(err) : resolve(res!), From dd139a7c5d0839070167c36c11aff617af282799 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sat, 27 Jan 2024 00:02:21 -0500 Subject: [PATCH 7/7] update the comment on `stop()` --- lib/shared/types.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/shared/types.ts b/lib/shared/types.ts index 6221e475459..a31292cdf56 100644 --- a/lib/shared/types.ts +++ b/lib/shared/types.ts @@ -663,14 +663,17 @@ export interface InitializeOptions { export let version: string // Call this function to terminate esbuild's child process. The child process -// is not terminated and re-created for each API call because it's more -// efficient to keep it around when there are multiple API calls. +// is not terminated and re-created after each API call because it's more +// efficient to keep it around when there are multiple API calls. This child +// process normally exits automatically when the parent process exits, so you +// usually don't need to call this function. // -// In node this happens automatically before the parent node process exits. So -// you only need to call this if you know you will not make any more esbuild -// API calls and you want to clean up resources. +// One reason you might want to call this is if you know you will not make any +// more esbuild API calls and you want to clean up resources (since the esbuild +// child process takes up some memory even when idle). // -// Unlike node, Deno lacks the necessary APIs to clean up child processes -// automatically. You must manually call stop() in Deno when you're done -// using esbuild or Deno will continue running forever. +// Another reason you might want to call this is if you are using esbuild from +// within a Deno test. Deno fails tests that create a child process without +// killing it before the test ends, so you have to call this function (and +// await the returned promise) in every Deno test that uses esbuild. export declare function stop(): Promise