fix(cli): use case-insensitive path comparison on Windows in watch mode#20342
fix(cli): use case-insensitive path comparison on Windows in watch mode#20342deepshekhardas wants to merge 1 commit into
Conversation
Confidence Score: 4/5Safe to merge — the watch-mode fix is narrow and well-contained, and the existing infinite-loop guard and full-rebuild logic are preserved correctly. The two call sites in build/index.ts are straightforward and correctly address the case-sensitivity problem. The main gap is that the Windows code path in the test file is never exercised on Linux/macOS CI due to a top-level platform guard, meaning the fix ships without automated verification of the Windows-specific branch. The helper also does not normalize path separators, which could leave a narrower follow-on edge case on Windows. paths-equal.test.ts — the Windows-specific test is skipped on non-Windows CI runners. paths-equal.ts — path separator normalization is absent. Reviews (1): Last reviewed commit: "fix(cli): use case-insensitive path comp..." | Re-trigger Greptile |
| vi.mock('./paths-equal', async () => { | ||
| const actual = await vi.importActual<typeof import('./paths-equal')>('./paths-equal') | ||
| return actual | ||
| }) | ||
|
|
||
| describe('pathsEqual', () => { | ||
| it('returns true for identical paths', async () => { | ||
| const { pathsEqual } = await import('./paths-equal') | ||
| expect(pathsEqual('/foo/bar/baz.css', '/foo/bar/baz.css')).toBe(true) | ||
| }) | ||
|
|
||
| it('returns false for completely different paths', async () => { | ||
| const { pathsEqual } = await import('./paths-equal') | ||
| expect(pathsEqual('/foo/bar.css', '/foo/baz.css')).toBe(false) | ||
| }) | ||
|
|
||
| if (process.platform === 'win32') { | ||
| it('returns true for paths differing only in case on Windows', async () => { | ||
| const { pathsEqual } = await import('./paths-equal') | ||
| expect(pathsEqual('C:\\src\\Input.css', 'C:\\src\\input.css')).toBe(true) | ||
| }) | ||
| } else { | ||
| it('returns false for paths differing in case on non-Windows', async () => { | ||
| const { pathsEqual } = await import('./paths-equal') | ||
| expect(pathsEqual('/src/Input.css', '/src/input.css')).toBe(false) | ||
| }) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Windows tests never run in non-Windows CI
The vi.mock block (lines 3–6) re-exports the actual module unchanged — it's a no-op. More importantly, the if (process.platform === 'win32') branch at line 19 is evaluated at module-load time, so the case-insensitive Windows test case is simply skipped on every Linux/macOS CI runner. The core behavior this PR fixes (Windows case-insensitive comparison) is therefore never exercised in CI.
To test both branches on any platform, mock process.platform with vi.stubGlobal or spy on it so tests can force the Windows code path without needing a Windows runner.
| if (IS_WINDOWS) { | ||
| return a.toLowerCase() === b.toLowerCase() |
There was a problem hiding this comment.
Path separator mismatch not handled
toLowerCase() fixes the case problem but leaves a path-separator edge case unaddressed: on Windows, file watchers (e.g. Chokidar) sometimes emit paths with forward slashes (C:/src/input.css) while CLI arguments may use backslashes (C:\src\input.css). These represent the same path but won't be equal after toLowerCase() alone. Adding path.normalize() (or replacing \ with /) before comparing would close this gap and make the helper fully robust on Windows.
WalkthroughAdds a 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e8d038a4-75fa-4d96-aa16-775e8e91f008
📒 Files selected for processing (3)
packages/@tailwindcss-cli/src/commands/build/index.tspackages/@tailwindcss-cli/src/utils/paths-equal.test.tspackages/@tailwindcss-cli/src/utils/paths-equal.ts
| // 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. | ||
| if (files.length === 1 && files[0] === args['--output']) return | ||
| if (files.length === 1 && args['--output'] && pathsEqual(files[0], args['--output'])) return |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use pathsEqual for other path comparisons to prevent Windows case-mismatch bugs.
While pathsEqual correctly fixes the watcher loop for the output file here, there are two other unchanged strict path comparisons in this file that will still fail on Windows when case differs:
-
Polling watcher (line 614):
let files = scanner.scannedFiles.filter( (file) => file !== args['--output'] && file !== args['--map'], )
If case differs, the output or map file will not be filtered out, potentially causing infinite loops in polling mode.
-
Input/Output collision check (line 157):
if (args['--input'] === args['--output'] && args['--input'] !== '-') {
If the user provides the same file with different casing, this check will fail to catch the collision, leading to the output overwriting the input.
Consider updating these locations to use pathsEqual to fully resolve the issue across the CLI.
🛠️ Proposed fixes for the unchanged lines
For line 157:
// Check if the input and output file paths are identical, otherwise return an
// error to the user.
if (
args['--input'] &&
args['--output'] &&
args['--input'] !== '-' &&
pathsEqual(args['--input'], args['--output'])
) {
eprintln(For line 614:
let files = scanner.scannedFiles.filter(
(file) =>
!(args['--output'] && pathsEqual(file, args['--output'])) &&
!(typeof args['--map'] === 'string' && pathsEqual(file, args['--map'])),
)
Cherry-pick of PR #19774 with rebased conflict resolution.
Fixes #16784
On Windows (case-insensitive filesystem), the CLI --watch\ mode doesn't rebuild when the input file path casing differs from what exists on disk.
Adds a \pathsEqual()\ helper that uses case-insensitive comparison on Windows and applies it to output file identity check and full rebuild path matching.