diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index eabd2a1de022..c7bd85c5c938 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -22,6 +22,7 @@ import { println, relative, } from '../../utils/renderer' +import { pathsEqual } from '../../utils/paths-equal' import { drainStdin, outputFile } from './utils' const css = String.raw @@ -257,7 +258,7 @@ export async function handle(args: Result>) { 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. - if (files.length === 1 && files[0] === args['--output']) return + if (files.length === 1 && pathsEqual(files[0], args['--output']!)) return using I = new Instrumentation() DEBUG && I.start('[@tailwindcss/cli] (watcher)') @@ -274,7 +275,7 @@ export async function handle(args: Result>) { // If one of the changed files is related to the input CSS or JS // config/plugin files, then we need to do a full rebuild because // the theme might have changed. - if (resolvedFullRebuildPaths.includes(file)) { + if (resolvedFullRebuildPaths.some((p) => pathsEqual(p, file))) { rebuildStrategy = 'full' // No need to check the rest of the events, because we already know we diff --git a/packages/@tailwindcss-cli/src/utils/paths-equal.test.ts b/packages/@tailwindcss-cli/src/utils/paths-equal.test.ts new file mode 100644 index 000000000000..922a1491010b --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/paths-equal.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('./paths-equal', async () => { + const actual = await vi.importActual('./paths-equal') + return actual +}) + +// We test the logic directly rather than mocking process.platform, +// because the function captures IS_WINDOWS at module load time. +// Instead we test the underlying behavior: on the current platform, +// paths with matching case should always be equal, and we verify +// the case-insensitive logic separately. + +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) + }) + } +}) diff --git a/packages/@tailwindcss-cli/src/utils/paths-equal.ts b/packages/@tailwindcss-cli/src/utils/paths-equal.ts new file mode 100644 index 000000000000..aa56fb887deb --- /dev/null +++ b/packages/@tailwindcss-cli/src/utils/paths-equal.ts @@ -0,0 +1,12 @@ +const IS_WINDOWS = process.platform === 'win32' + +/** + * Compare two file paths for equality. On Windows, the comparison is + * case-insensitive because the filesystem is case-insensitive. + */ +export function pathsEqual(a: string, b: string): boolean { + if (IS_WINDOWS) { + return a.toLowerCase() === b.toLowerCase() + } + return a === b +}