Skip to content

Commit 41a724f

Browse files
Fix CLI watcher cleanup race
Fixes a race condition where two parallel invocations of file system change events could cause some cleanup functions to get swallowed by changing cleanupWatchers to an array to retain multiple cleanup functions. Co-authored-by: Gary Rennie <gazler@gmail.com>
1 parent 2f1cbbf commit 41a724f

File tree

2 files changed

+9
-8
lines changed

2 files changed

+9
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888))
1313
- Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885))
1414
- Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900))
15+
- Ensure that file system watchers created when using the CLI are always cleaned up ([#18905](https://github.com/tailwindlabs/tailwindcss/pull/18905))
1516

1617
## [4.1.13] - 2025-09-03
1718

packages/@tailwindcss-cli/src/commands/build/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
239239

240240
// Watch for changes
241241
if (args['--watch']) {
242-
let cleanupWatchers = await createWatchers(
243-
watchDirectories(scanner),
244-
async function handle(files) {
242+
let cleanupWatchers: (() => Promise<void>)[] = []
243+
cleanupWatchers.push(
244+
await createWatchers(watchDirectories(scanner), async function handle(files) {
245245
try {
246246
// If the only change happened to the output file, then we don't want to
247247
// trigger a rebuild because that will result in an infinite loop.
@@ -304,15 +304,15 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
304304

305305
// Setup new watchers
306306
DEBUG && I.start('Setup new watchers')
307-
let newCleanupWatchers = await createWatchers(watchDirectories(scanner), handle)
307+
let newCleanupFunction = await createWatchers(watchDirectories(scanner), handle)
308308
DEBUG && I.end('Setup new watchers')
309309

310310
// Clear old watchers
311311
DEBUG && I.start('Cleanup old watchers')
312-
await cleanupWatchers()
312+
await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup()))
313313
DEBUG && I.end('Cleanup old watchers')
314314

315-
cleanupWatchers = newCleanupWatchers
315+
cleanupWatchers.push(newCleanupFunction)
316316

317317
// Re-compile the CSS
318318
DEBUG && I.start('Build CSS')
@@ -362,14 +362,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
362362
eprintln(err.toString())
363363
}
364364
}
365-
},
365+
}),
366366
)
367367

368368
// Abort the watcher if `stdin` is closed to avoid zombie processes. You can
369369
// disable this behavior with `--watch=always`.
370370
if (args['--watch'] !== 'always') {
371371
process.stdin.on('end', () => {
372-
cleanupWatchers().then(
372+
Promise.all(cleanupWatchers.map((fn) => fn())).then(
373373
() => process.exit(0),
374374
() => process.exit(1),
375375
)

0 commit comments

Comments
 (0)