Skip to content

Commit 294952f

Browse files
Handle BOM (#16800)
Resolves #15662 Resolves #15467 ## Test plan Added integration tests for upgrade tooling (which already worked surprisingly?) and CLI.
1 parent 662c686 commit 294952f

File tree

6 files changed

+164
-3
lines changed

6 files changed

+164
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Ensure `@reference "…"` does not emit CSS variables ([#16774](https://github.com/tailwindlabs/tailwindcss/pull/16774))
2121
- Fix an issue where `@reference "…"` would sometimes omit keyframe animations ([#16774](https://github.com/tailwindlabs/tailwindcss/pull/16774))
2222
- Ensure `z-*!` utilities are property marked as `!important` ([#16795](https://github.com/tailwindlabs/tailwindcss/pull/16795))
23+
- Read UTF-8 CSS files that start with a byte-order mark (BOM) ([#16796](https://github.com/tailwindlabs/tailwindcss/pull/16796))
2324

2425
## [4.0.8] - 2025-02-21
2526

integrations/cli/index.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,3 +1314,79 @@ test(
13141314
)
13151315
},
13161316
)
1317+
1318+
test(
1319+
'can read files with UTF-8 files with BOM',
1320+
{
1321+
fs: {
1322+
'package.json': json`
1323+
{
1324+
"dependencies": {
1325+
"tailwindcss": "workspace:^",
1326+
"@tailwindcss/cli": "workspace:^"
1327+
}
1328+
}
1329+
`,
1330+
'index.css': withBOM(css`
1331+
@reference 'tailwindcss/theme.css';
1332+
@import 'tailwindcss/utilities';
1333+
`),
1334+
'index.html': withBOM(html`
1335+
<div class="underline"></div>
1336+
`),
1337+
},
1338+
},
1339+
async ({ fs, exec, expect }) => {
1340+
await exec('pnpm tailwindcss --input index.css --output dist/out.css')
1341+
1342+
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
1343+
"
1344+
--- ./dist/out.css ---
1345+
.underline {
1346+
text-decoration-line: underline;
1347+
}
1348+
"
1349+
`)
1350+
},
1351+
)
1352+
1353+
test(
1354+
'fails when reading files with UTF-16 files with BOM',
1355+
{
1356+
fs: {
1357+
'package.json': json`
1358+
{
1359+
"dependencies": {
1360+
"tailwindcss": "workspace:^",
1361+
"@tailwindcss/cli": "workspace:^"
1362+
}
1363+
}
1364+
`,
1365+
},
1366+
},
1367+
async ({ fs, exec, expect }) => {
1368+
await fs.write(
1369+
'index.css',
1370+
withBOM(css`
1371+
@reference 'tailwindcss/theme.css';
1372+
@import 'tailwindcss/utilities';
1373+
`),
1374+
'utf16le',
1375+
)
1376+
await fs.write(
1377+
'index.html',
1378+
withBOM(html`
1379+
<div class="underline"></div>
1380+
`),
1381+
'utf16le',
1382+
)
1383+
1384+
await expect(exec('pnpm tailwindcss --input index.css --output dist/out.css')).rejects.toThrow(
1385+
/Invalid declaration:/,
1386+
)
1387+
},
1388+
)
1389+
1390+
function withBOM(text: string): string {
1391+
return '\uFEFF' + text
1392+
}

integrations/upgrade/index.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,3 +2745,71 @@ test(
27452745
`)
27462746
},
27472747
)
2748+
2749+
test(
2750+
`can read files with BOM`,
2751+
{
2752+
fs: {
2753+
'package.json': json`
2754+
{
2755+
"dependencies": {
2756+
"tailwindcss": "^3",
2757+
"@tailwindcss/upgrade": "workspace:^"
2758+
},
2759+
"devDependencies": {
2760+
"@tailwindcss/cli": "workspace:^"
2761+
}
2762+
}
2763+
`,
2764+
'tailwind.config.js': js`
2765+
/** @type {import('tailwindcss').Config} */
2766+
module.exports = {
2767+
content: ['./src/**/*.{html,js}'],
2768+
}
2769+
`,
2770+
'src/index.html': withBOM(html`
2771+
<div class="ring"></div>
2772+
`),
2773+
'src/input.css': withBOM(css`
2774+
@tailwind base;
2775+
@tailwind components;
2776+
@tailwind utilities;
2777+
`),
2778+
},
2779+
},
2780+
async ({ exec, fs, expect }) => {
2781+
await exec('npx @tailwindcss/upgrade')
2782+
2783+
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
2784+
"
2785+
--- ./src/index.html ---
2786+
<div class="ring-3"></div>
2787+
2788+
--- ./src/input.css ---
2789+
@import 'tailwindcss';
2790+
2791+
/*
2792+
The default border color has changed to \`currentColor\` in Tailwind CSS v4,
2793+
so we've added these compatibility styles to make sure everything still
2794+
looks the same as it did with Tailwind CSS v3.
2795+
2796+
If we ever want to remove these styles, we need to add an explicit border
2797+
color utility to any element that depends on these defaults.
2798+
*/
2799+
@layer base {
2800+
*,
2801+
::after,
2802+
::before,
2803+
::backdrop,
2804+
::file-selector-button {
2805+
border-color: var(--color-gray-200, currentColor);
2806+
}
2807+
}
2808+
"
2809+
`)
2810+
},
2811+
)
2812+
2813+
function withBOM(text: string): string {
2814+
return '\uFEFF' + text
2815+
}

integrations/utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ interface TestContext {
4242
exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise<string>
4343
spawn(command: string, options?: ChildProcessOptions): Promise<SpawnedProcess>
4444
fs: {
45-
write(filePath: string, content: string): Promise<void>
45+
write(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>
4646
create(filePaths: string[]): Promise<void>
4747
read(filePath: string): Promise<string>
4848
glob(pattern: string): Promise<[string, string][]>
@@ -268,7 +268,11 @@ export function test(
268268
}
269269
},
270270
fs: {
271-
async write(filename: string, content: string | Uint8Array): Promise<void> {
271+
async write(
272+
filename: string,
273+
content: string | Uint8Array,
274+
encoding: BufferEncoding = 'utf8',
275+
): Promise<void> {
272276
let full = path.join(root, filename)
273277
let dir = path.dirname(full)
274278
await fs.mkdir(dir, { recursive: true })
@@ -286,7 +290,7 @@ export function test(
286290
content = content.replace(/\n/g, '\r\n')
287291
}
288292

289-
await fs.writeFile(full, content, 'utf-8')
293+
await fs.writeFile(full, content, encoding)
290294
},
291295

292296
async create(filenames: string[]): Promise<void> {

packages/tailwindcss/src/css-parser.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,4 +1154,15 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
11541154
)
11551155
})
11561156
})
1157+
1158+
it('ignores BOM at the beginning of a file', () => {
1159+
expect(parse("\uFEFF@reference 'tailwindcss';")).toEqual([
1160+
{
1161+
kind: 'at-rule',
1162+
name: '@reference',
1163+
nodes: [],
1164+
params: "'tailwindcss'",
1165+
},
1166+
])
1167+
})
11571168
})

packages/tailwindcss/src/css-parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const AT_SIGN = 0x40
3131
const EXCLAMATION_MARK = 0x21
3232

3333
export function parse(input: string) {
34+
if (input[0] === '\uFEFF') input = input.slice(1)
3435
input = input.replaceAll('\r\n', '\n')
3536

3637
let ast: AstNode[] = []

0 commit comments

Comments
 (0)