From 6d42ff0fe71328ed1eb60469e52f219c827fbebb Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 25 Jan 2024 23:10:53 -0500 Subject: [PATCH] work around api deprecations in deno 1.40.x --- .github/workflows/ci.yml | 42 ++++++++++-- CHANGELOG.md | 8 +++ lib/deno/mod.ts | 143 +++++++++++++++++++++++++++++---------- 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, 160 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..4258a5a55c3 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 cause tests that call `esbuild.stop()` to fail 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 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..45f99688f40 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,139 @@ 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 () => { + child.kill() + await child.status // Without this, Deno can fail tests with some weird error about "op_spawn_wait" + }, + 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 +407,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