From abc4670a9af2b507d98f2031e015b02c8f928f0c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 15 Jun 2026 17:20:31 +0200 Subject: [PATCH] Prevent crash when using `@tailwindcss/cli` using `--watch` on Windows (#20242) This PR fixes an issue on Windows where the `@tailwindcss/cli` with the `--watch` flag crashes when using a `@source` with a base path that doesn't exist on disk. This happens when setting up the `@parce/watcher` for directories that don't exist. This PR essentially filters out these directories that don't exist on disk to prevent the crash. It might be that if you add the folder _later_, while the watcher is already watching, that you have to restart the `@tailwindcss/cli` (or save the `index.css` (the file that contains the `@source` directives), this also recreates watchers from scratch). If this issue causes problems for `@tailwindcss/postcss` and `@tailwindcss/vite` in the future as well, then we can move this logic back to Oxide. We do maintain the incoming `@source` files as best as possible without resolving to absolute paths. Back when we did resolve them, if that process error'd we just never returned the glob. The root cause is still referencing folders that don't exist. Fixes: #20231 ## Test plan 1. Added a failing test, that did fail on Windows 2. Once fixed, all tests should pass [ci-all] --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 ++- integrations/cli/index.test.ts | 29 +++++++++++++++++++ .../src/commands/build/index.ts | 22 +++++++++++--- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9edab04ed..b6e8e5b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Ensure `@tailwindcss/cli` in `--watch` mode doesn't crash on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242)) ## [4.3.1] - 2026-06-12 diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index 75869ee32..4be12d9a1 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -375,6 +375,35 @@ describe.each([ }, ) + test( + "watch mode with unknown @source paths shouldn't crash on Windows", + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss'; + @source "unknown-folder/**/*"; + `, + }, + }, + async ({ fs, spawn }) => { + let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`) + await process.onStderr((m) => m.includes('Done in')) + + await fs.expectFileToContain('dist/out.css', [candidate`underline`]) + }, + ) + test( 'production build (stdin)', { diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 913657633..b8b11a6e4 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -272,7 +272,7 @@ export async function handle(args: Result>) { if (args['--watch']) { let cleanupWatchers: (() => Promise)[] = [] cleanupWatchers.push( - await createWatchers(watchDirectories(scanner), async function handle(files) { + await createWatchers(await 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. @@ -347,7 +347,7 @@ export async function handle(args: Result>) { // Setup new watchers DEBUG && I.start('Setup new watchers') - let newCleanupFunction = await createWatchers(watchDirectories(scanner), handle) + let newCleanupFunction = await createWatchers(await watchDirectories(scanner), handle) DEBUG && I.end('Setup new watchers') // Clear old watchers @@ -586,8 +586,22 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { } } -function watchDirectories(scanner: Scanner) { - return [...new Set(scanner.normalizedSources.flatMap((globEntry) => globEntry.base))] +async function watchDirectories(scanner: Scanner) { + let directories = ( + await Promise.all( + scanner.normalizedSources.map(async (globEntry) => { + let resolvedPath = path.resolve(globEntry.base) + let realPath = await fs.realpath(resolvedPath).catch(() => resolvedPath) + + return fs + .stat(realPath) + .then((stat) => (stat.isDirectory() ? [realPath] : [])) + .catch(() => []) + }), + ) + ).flat(1) + + return Array.from(new Set(directories)) } function dim(str: string) {