From 294952f1702fb49be2d6795ee5bd7a6ac396ae78 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 25 Feb 2025 16:07:16 +0100 Subject: [PATCH] Handle BOM (#16800) Resolves #15662 Resolves #15467 ## Test plan Added integration tests for upgrade tooling (which already worked surprisingly?) and CLI. --- CHANGELOG.md | 1 + integrations/cli/index.test.ts | 76 +++++++++++++++++++++ integrations/upgrade/index.test.ts | 68 ++++++++++++++++++ integrations/utils.ts | 10 ++- packages/tailwindcss/src/css-parser.test.ts | 11 +++ packages/tailwindcss/src/css-parser.ts | 1 + 6 files changed, 164 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 092460791ed3..4b9687e148d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `@reference "…"` does not emit CSS variables ([#16774](https://github.com/tailwindlabs/tailwindcss/pull/16774)) - Fix an issue where `@reference "…"` would sometimes omit keyframe animations ([#16774](https://github.com/tailwindlabs/tailwindcss/pull/16774)) - Ensure `z-*!` utilities are property marked as `!important` ([#16795](https://github.com/tailwindlabs/tailwindcss/pull/16795)) +- Read UTF-8 CSS files that start with a byte-order mark (BOM) ([#16796](https://github.com/tailwindlabs/tailwindcss/pull/16796)) ## [4.0.8] - 2025-02-21 diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index bc8fb433ddf2..bb9624c20710 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -1314,3 +1314,79 @@ test( ) }, ) + +test( + 'can read files with UTF-8 files with BOM', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'index.css': withBOM(css` + @reference 'tailwindcss/theme.css'; + @import 'tailwindcss/utilities'; + `), + 'index.html': withBOM(html` +
+ `), + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm tailwindcss --input index.css --output dist/out.css') + + expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(` + " + --- ./dist/out.css --- + .underline { + text-decoration-line: underline; + } + " + `) + }, +) + +test( + 'fails when reading files with UTF-16 files with BOM', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + }, + }, + async ({ fs, exec, expect }) => { + await fs.write( + 'index.css', + withBOM(css` + @reference 'tailwindcss/theme.css'; + @import 'tailwindcss/utilities'; + `), + 'utf16le', + ) + await fs.write( + 'index.html', + withBOM(html` +
+ `), + 'utf16le', + ) + + await expect(exec('pnpm tailwindcss --input index.css --output dist/out.css')).rejects.toThrow( + /Invalid declaration:/, + ) + }, +) + +function withBOM(text: string): string { + return '\uFEFF' + text +} diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 1ba696cb3413..1da138f2c97c 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -2745,3 +2745,71 @@ test( `) }, ) + +test( + `can read files with BOM`, + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "^3", + "@tailwindcss/upgrade": "workspace:^" + }, + "devDependencies": { + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'tailwind.config.js': js` + /** @type {import('tailwindcss').Config} */ + module.exports = { + content: ['./src/**/*.{html,js}'], + } + `, + 'src/index.html': withBOM(html` +
+ `), + 'src/input.css': withBOM(css` + @tailwind base; + @tailwind components; + @tailwind utilities; + `), + }, + }, + async ({ exec, fs, expect }) => { + await exec('npx @tailwindcss/upgrade') + + expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(` + " + --- ./src/index.html --- + 
+ + --- ./src/input.css --- + @import 'tailwindcss'; + + /* + The default border color has changed to \`currentColor\` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + @layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } + } + " + `) + }, +) + +function withBOM(text: string): string { + return '\uFEFF' + text +} diff --git a/integrations/utils.ts b/integrations/utils.ts index c8fa03359044..4558edd035d8 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -42,7 +42,7 @@ interface TestContext { exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise spawn(command: string, options?: ChildProcessOptions): Promise fs: { - write(filePath: string, content: string): Promise + write(filePath: string, content: string, encoding?: BufferEncoding): Promise create(filePaths: string[]): Promise read(filePath: string): Promise glob(pattern: string): Promise<[string, string][]> @@ -268,7 +268,11 @@ export function test( } }, fs: { - async write(filename: string, content: string | Uint8Array): Promise { + async write( + filename: string, + content: string | Uint8Array, + encoding: BufferEncoding = 'utf8', + ): Promise { let full = path.join(root, filename) let dir = path.dirname(full) await fs.mkdir(dir, { recursive: true }) @@ -286,7 +290,7 @@ export function test( content = content.replace(/\n/g, '\r\n') } - await fs.writeFile(full, content, 'utf-8') + await fs.writeFile(full, content, encoding) }, async create(filenames: string[]): Promise { diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts index 25d5b3a454ca..379b4b942793 100644 --- a/packages/tailwindcss/src/css-parser.test.ts +++ b/packages/tailwindcss/src/css-parser.test.ts @@ -1154,4 +1154,15 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => { ) }) }) + + it('ignores BOM at the beginning of a file', () => { + expect(parse("\uFEFF@reference 'tailwindcss';")).toEqual([ + { + kind: 'at-rule', + name: '@reference', + nodes: [], + params: "'tailwindcss'", + }, + ]) + }) }) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index d80bb3e79807..df10fa850035 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -31,6 +31,7 @@ const AT_SIGN = 0x40 const EXCLAMATION_MARK = 0x21 export function parse(input: string) { + if (input[0] === '\uFEFF') input = input.slice(1) input = input.replaceAll('\r\n', '\n') let ast: AstNode[] = []