diff --git a/packages/vite/src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts b/packages/vite/src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts new file mode 100644 index 00000000000000..e41c31d9f0b9e8 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/cssPreprocessorTeardown.spec.ts @@ -0,0 +1,51 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import type { ChildProcess } from 'node:child_process' +import { describe, expect, test } from 'vitest' +import { createServer } from '../../index' + +const getActiveHandles = (): unknown[] => (process as any)._getActiveHandles() + +const runningSassWorkers = (): ChildProcess[] => + getActiveHandles().filter((h): h is ChildProcess => { + if (!h || (h as object).constructor?.name !== 'ChildProcess') return false + const cp = h as ChildProcess & { spawnfile?: string } + return ( + cp.exitCode == null && + typeof cp.spawnfile === 'string' && + cp.spawnfile.includes('sass') + ) + }) + +describe('css preprocessor worker teardown', () => { + test('awaits sass-embedded worker disposal on server.close()', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vite-sass-teardown-')) + const scssPath = path.join(root, 'a.scss') + fs.writeFileSync(scssPath, '$c: red;\nbody { color: $c; }\n') + + const server = await createServer({ + root, + logLevel: 'silent', + configFile: false, + server: { ws: false }, + }) + await server.listen() + + try { + await server.pluginContainer.transform( + fs.readFileSync(scssPath, 'utf8'), + scssPath, + ) + } catch { + // the optimizer can throw ERR_OUTDATED_OPTIMIZED_DEP post-transform; + // not relevant here — we only need the scss processor to have run. + } + + expect(runningSassWorkers().length).toBeGreaterThan(0) + + await server.close() + + expect(runningSassWorkers().length).toBe(0) + }, 30_000) +}) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 9ab2901f0e85c2..1353831780a45b 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -331,8 +331,8 @@ export function cssPlugin(config: ResolvedConfig): Plugin { ) }, - buildEnd() { - preprocessorWorkerController?.close() + async buildEnd() { + await preprocessorWorkerController?.close() }, load: { @@ -2391,7 +2391,7 @@ type StylePreprocessor = { options: Options, resolvers: CSSAtImportResolvers, ) => StylePreprocessorResults | Promise - close: () => void + close: () => void | Promise } export interface StylePreprocessorResults { @@ -2612,8 +2612,8 @@ const scssProcessor = ( const normalizedErrors = new WeakSet() return { - close() { - worker?.stop() + async close() { + await worker?.stop() }, async process(environment, source, root, options, resolvers) { let sassPackage = loadSassPackage(root, failedSassEmbedded ?? false) @@ -3150,10 +3150,8 @@ const createPreprocessorWorkerController = (maxWorkers: number | undefined) => { return scss.process(environment, source, root, opts, resolvers) } - const close = () => { - less.close() - scss.close() - styl.close() + const close = async () => { + await Promise.all([less.close(), scss.close(), styl.close()]) } return {