Skip to content

fix(css): await sass/less/styl worker disposal on teardown (fix #22274)#22275

Merged
bluwy merged 2 commits into
vitejs:mainfrom
jaknas:fix/await-sass-worker-close-on-teardown
May 12, 2026
Merged

fix(css): await sass/less/styl worker disposal on teardown (fix #22274)#22275
bluwy merged 2 commits into
vitejs:mainfrom
jaknas:fix/await-sass-worker-close-on-teardown

Conversation

@jaknas
Copy link
Copy Markdown
Contributor

@jaknas jaknas commented Apr 20, 2026

Description

cssPlugin.buildEnd() and scssProcessor.close() were synchronous but dropped an async worker?.stop() promise. On the scss path, that promise awaits compiler.dispose(), which sends the IPC shutdown to the sass-embedded Dart subprocess — so server.close() could resolve while the Dart worker was still running, leaving its ChildProcess handle alive on the event loop.

In long-lived processes the event loop eventually reaps the workers. In processes that expect to exit after server.close() — scripts, programmatic use, Vitest global teardown — the orphaned ChildProcess keeps Node alive.

See #22274 for the minimal standalone reproduction and exitCode before/after. Different code path from #18224 (file watchers) but the same user-visible symptom.

Changes

  • cssPlugin.buildEnd: sync → async, awaits preprocessorWorkerController?.close()
  • scssProcessor.close: sync → async, awaits worker?.stop() — the only path where stop() is actually async, since makeScssWorker returns a hand-rolled object whose stop() awaits compiler.dispose()
  • createPreprocessorWorkerController fan-out close: sync → async, disposes the three processors in parallel via Promise.all
  • StylePreprocessor<Options>.close type: () => void() => void | Promise<void> (matches the convention used elsewhere in the repo)

lessProcessor.close and stylProcessor.close are unchanged — they go through real artichokie WorkerWithFallback whose stop() is synchronous, so awaiting would be a no-op.

On the teardown path, server.close() now waits for scss's compiler.dispose() to actually complete before resolving.

Tests

New regression test: packages/vite/src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts. Starts a dev server, triggers scss transform, asserts no running sass ChildProcess after await server.close(). Fails on main without this change (expected 1 to be +0).

pnpm --filter vite exec vitest run src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts

Fixes #22274

AI Disclosure

Used claude-opus-4-7-thinking-xhigh to file the PR.

@jaknas jaknas marked this pull request as ready for review April 20, 2026 08:54
Comment thread packages/vite/src/node/plugins/css.ts
@jaknas jaknas force-pushed the fix/await-sass-worker-close-on-teardown branch from dd58617 to 038b819 Compare April 24, 2026 11:23
Comment thread packages/vite/src/node/plugins/css.ts Outdated
Comment thread packages/vite/src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts Outdated
@jaknas jaknas force-pushed the fix/await-sass-worker-close-on-teardown branch from 269ef69 to ceb36e3 Compare April 27, 2026 08:21
jaknas added 2 commits May 4, 2026 09:21
The cssPlugin buildEnd hook and the scss/less/styl processor close
methods were synchronous but invoked the asynchronous worker.stop()
without awaiting it. server.close() could therefore resolve before
the preprocessor workers had actually shut down, leaving their
ChildProcess handles alive on the event loop.

Make close async end-to-end, dispose the three processors in
parallel via Promise.all in createPreprocessorWorkerController, and
await the controller from buildEnd.
Address review feedback: only the scss processor's close() actually needs
to be async (it awaits compiler.dispose()); less and styl use real
artichokie WorkerWithFallback whose stop() is sync.

- Widen StylePreprocessor.close to () => void | Promise<void>
  matching the pattern used elsewhere in the repo
- Revert lessProcessor.close and stylProcessor.close to sync
- scssProcessor.close stays async (load-bearing)
- preprocessorWorkerController.close still awaits Promise.all,
  which transparently handles the mixed void / Promise<void> returns

Also drop unnecessary `port: 0` from the regression test.
@jaknas jaknas force-pushed the fix/await-sass-worker-close-on-teardown branch from ceb36e3 to f745d9e Compare May 4, 2026 07:21
@bluwy bluwy merged commit b7edcb7 into vitejs:main May 12, 2026
15 checks passed
@jaknas jaknas deleted the fix/await-sass-worker-close-on-teardown branch May 12, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: sass-embedded ChildProcess outlives server.close() - cssPlugin teardown drops worker.stop() promise

2 participants