Skip to content

Commit 914f608

Browse files
committed
fix #3558: put the stop() api call back
1 parent 2aa166b commit 914f608

File tree

6 files changed

+81
-10
lines changed

6 files changed

+81
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
}
4444
```
4545

46+
* Provide the `stop()` API in node to exit esbuild's child process ([#3558](https://github.com/evanw/esbuild/issues/3558))
47+
48+
You can now call `stop()` in esbuild's node API to exit esbuild's child process to reclaim the resources used. It only makes sense to do this for a long-lived node process when you know you will no longer be making any more esbuild API calls. It is not necessary to call this to allow node to exit, and it's advantageous to not call this in between calls to esbuild's API as sharing a single long-lived esbuild child process is more efficient than re-creating a new esbuild child process for every API call. This API call used to exist but was removed in [version 0.9.0](https://github.com/evanw/esbuild/releases/v0.9.0). This release adds it back due to a user request.
49+
4650
## 0.19.10
4751

4852
* Fix glob imports in TypeScript files ([#3319](https://github.com/evanw/esbuild/issues/3319))

lib/npm/browser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export const analyzeMetafileSync: typeof types.analyzeMetafileSync = () => {
4343
throw new Error(`The "analyzeMetafileSync" API only works in node`)
4444
}
4545

46+
export const stop = () => {
47+
if (stopService) stopService()
48+
}
49+
4650
interface Service {
4751
build: typeof types.build
4852
context: typeof types.context
@@ -52,6 +56,7 @@ interface Service {
5256
}
5357

5458
let initializePromise: Promise<void> | undefined
59+
let stopService: (() => void) | undefined
5560
let longLivedService: Service | undefined
5661

5762
let ensureServiceIsRunning = (): Service => {
@@ -129,6 +134,13 @@ const startRunningService = async (wasmURL: string | URL, wasmModule: WebAssembl
129134
// This will throw if WebAssembly module instantiation fails
130135
await firstMessagePromise
131136

137+
stopService = () => {
138+
worker.terminate()
139+
initializePromise = undefined
140+
stopService = undefined
141+
longLivedService = undefined
142+
}
143+
132144
longLivedService = {
133145
build: (options: types.BuildOptions) =>
134146
new Promise<types.BuildResult>((resolve, reject) =>

lib/npm/node.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ export let analyzeMetafileSync: typeof types.analyzeMetafileSync = (metafile, op
220220
return result!
221221
}
222222

223+
export const stop = () => {
224+
if (stopService) stopService()
225+
if (workerThreadService) workerThreadService.stop()
226+
}
227+
223228
let initializeWasCalled = false
224229

225230
export let initialize: typeof types.initialize = options => {
@@ -243,6 +248,7 @@ interface Service {
243248

244249
let defaultWD = process.cwd()
245250
let longLivedService: Service | undefined
251+
let stopService: (() => void) | undefined
246252

247253
let ensureServiceIsRunning = (): Service => {
248254
if (longLivedService) return longLivedService
@@ -278,6 +284,16 @@ let ensureServiceIsRunning = (): Service => {
278284
stdout.on('data', readFromStdout)
279285
stdout.on('end', afterClose)
280286

287+
stopService = () => {
288+
// Close all resources related to the subprocess.
289+
stdin.destroy()
290+
stdout.destroy()
291+
child.kill()
292+
initializeWasCalled = false
293+
longLivedService = undefined
294+
stopService = undefined
295+
}
296+
281297
let refCount = 0
282298
child.unref()
283299
if (stdin.unref) {
@@ -395,6 +411,7 @@ interface WorkerThreadService {
395411
transformSync(input: string | Uint8Array, options?: types.TransformOptions): types.TransformResult
396412
formatMessagesSync: typeof types.formatMessagesSync
397413
analyzeMetafileSync: typeof types.analyzeMetafileSync
414+
stop(): void
398415
}
399416

400417
let workerThreadService: WorkerThreadService | null = null
@@ -475,7 +492,7 @@ let startWorkerThreadService = (worker_threads: typeof import('worker_threads'))
475492
// Calling unref() on a worker will allow the thread to exit if it's the last
476493
// only active handle in the event system. This means node will still exit
477494
// when there are no more event handlers from the main thread. So there's no
478-
// need to have a "stop()" function.
495+
// need to call the "stop()" function.
479496
worker.unref()
480497

481498
return {
@@ -492,6 +509,10 @@ let startWorkerThreadService = (worker_threads: typeof import('worker_threads'))
492509
analyzeMetafileSync(metafile, options) {
493510
return runCallSync('analyzeMetafile', [metafile, options])
494511
},
512+
stop() {
513+
worker.terminate()
514+
workerThreadService = null
515+
},
495516
}
496517
}
497518

lib/shared/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,3 +661,16 @@ export interface InitializeOptions {
661661
}
662662

663663
export let version: string
664+
665+
// Call this function to terminate esbuild's child process. The child process
666+
// is not terminated and re-created for each API call because it's more
667+
// efficient to keep it around when there are multiple API calls.
668+
//
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.
676+
export declare function stop(): void;

scripts/esbuild.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,7 @@ const buildDenoLib = async (esbuildPath) => {
251251
fs.writeFileSync(path.join(denoDir, 'wasm.js'), modWASM)
252252

253253
// Generate "deno/mod.d.ts"
254-
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8') +
255-
`\n// Unlike node, Deno lacks the necessary APIs to clean up child processes` +
256-
`\n// automatically. You must manually call stop() in Deno when you're done` +
257-
`\n// using esbuild or Deno will continue running forever.` +
258-
`\nexport function stop(): void;` +
259-
`\n`
254+
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
260255
fs.writeFileSync(path.join(denoDir, 'mod.d.ts'), types_ts)
261256
fs.writeFileSync(path.join(denoDir, 'wasm.d.ts'), types_ts)
262257

scripts/js-api-tests.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7038,7 +7038,7 @@ let functionScopeCases = [
70387038
}
70397039
}
70407040

7041-
let syncTests = {
7041+
let apiSyncTests = {
70427042
async defaultExport({ esbuild }) {
70437043
assert.strictEqual(typeof esbuild.version, 'string')
70447044
assert.strictEqual(esbuild.version, esbuild.default.version)
@@ -7356,6 +7356,26 @@ let childProcessTests = {
73567356
},
73577357
}
73587358

7359+
let syncTests = {
7360+
async startStop({ esbuild }) {
7361+
for (let i = 0; i < 3; i++) {
7362+
let result1 = await esbuild.transform('1+2')
7363+
assert.strictEqual(result1.code, '1 + 2;\n')
7364+
7365+
let result2 = esbuild.transformSync('2+3')
7366+
assert.strictEqual(result2.code, '2 + 3;\n')
7367+
7368+
let result3 = await esbuild.build({ stdin: { contents: '1+2' }, write: false })
7369+
assert.strictEqual(result3.outputFiles[0].text, '1 + 2;\n')
7370+
7371+
let result4 = esbuild.buildSync({ stdin: { contents: '2+3' }, write: false })
7372+
assert.strictEqual(result4.outputFiles[0].text, '2 + 3;\n')
7373+
7374+
esbuild.stop()
7375+
}
7376+
},
7377+
}
7378+
73597379
async function assertSourceMap(jsSourceMap, source) {
73607380
jsSourceMap = JSON.parse(jsSourceMap)
73617381
assert.deepStrictEqual(jsSourceMap.version, 3)
@@ -7399,11 +7419,11 @@ async function main() {
73997419
...Object.entries(transformTests),
74007420
...Object.entries(formatTests),
74017421
...Object.entries(analyzeTests),
7402-
...Object.entries(syncTests),
7422+
...Object.entries(apiSyncTests),
74037423
...Object.entries(childProcessTests),
74047424
]
74057425

7406-
const allTestsPassed = (await Promise.all(tests.map(([name, fn]) => {
7426+
let allTestsPassed = (await Promise.all(tests.map(([name, fn]) => {
74077427
const promise = runTest(name, fn)
74087428

74097429
// Time out each individual test after 3 minutes. This exists to help debug test hangs in CI.
@@ -7415,6 +7435,12 @@ async function main() {
74157435
return promise.finally(() => clearTimeout(timeout))
74167436
}))).every(success => success)
74177437

7438+
for (let [name, fn] of Object.entries(syncTests)) {
7439+
if (!await runTest(name, fn)) {
7440+
allTestsPassed = false
7441+
}
7442+
}
7443+
74187444
if (!allTestsPassed) {
74197445
console.error(`❌ js api tests failed`)
74207446
process.exit(1)

0 commit comments

Comments
 (0)