From 41a724fe9fa9818fc1d5d292625b216d927a9bc9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 9 Sep 2025 16:35:09 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + .../@tailwindcss-cli/src/commands/build/index.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7a1fcbeb31..b05c7f1cbaf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handle `'` syntax in ClojureScript when extracting classes ([#18888](https://github.com/tailwindlabs/tailwindcss/pull/18888)) - Handle `@variant` inside `@custom-variant` ([#18885](https://github.com/tailwindlabs/tailwindcss/pull/18885)) - Merge suggestions when using `@utility` ([#18900](https://github.com/tailwindlabs/tailwindcss/pull/18900)) +- Ensure that file system watchers created when using the CLI are always cleaned up ([#18905](https://github.com/tailwindlabs/tailwindcss/pull/18905)) ## [4.1.13] - 2025-09-03 diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 206bf27fbca2..794b54ae3be6 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -239,9 +239,9 @@ export async function handle(args: Result>) { // Watch for changes if (args['--watch']) { - let cleanupWatchers = await createWatchers( - watchDirectories(scanner), - async function handle(files) { + let cleanupWatchers: (() => Promise)[] = [] + cleanupWatchers.push( + await createWatchers(watchDirectories(scanner), async function handle(files) { try { // If the only change happened to the output file, then we don't want to // trigger a rebuild because that will result in an infinite loop. @@ -304,15 +304,15 @@ export async function handle(args: Result>) { // Setup new watchers DEBUG && I.start('Setup new watchers') - let newCleanupWatchers = await createWatchers(watchDirectories(scanner), handle) + let newCleanupFunction = await createWatchers(watchDirectories(scanner), handle) DEBUG && I.end('Setup new watchers') // Clear old watchers DEBUG && I.start('Cleanup old watchers') - await cleanupWatchers() + await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup())) DEBUG && I.end('Cleanup old watchers') - cleanupWatchers = newCleanupWatchers + cleanupWatchers.push(newCleanupFunction) // Re-compile the CSS DEBUG && I.start('Build CSS') @@ -362,14 +362,14 @@ export async function handle(args: Result>) { eprintln(err.toString()) } } - }, + }), ) // Abort the watcher if `stdin` is closed to avoid zombie processes. You can // disable this behavior with `--watch=always`. if (args['--watch'] !== 'always') { process.stdin.on('end', () => { - cleanupWatchers().then( + Promise.all(cleanupWatchers.map((fn) => fn())).then( () => process.exit(0), () => process.exit(1), )