Skip to content

Commit

Permalink
Handle BOM (#16800)
Browse files Browse the repository at this point in the history
Resolves #15662 
Resolves #15467

## Test plan

Added integration tests for upgrade tooling (which already worked
surprisingly?) and CLI.
  • Loading branch information
philipp-spiess authored Feb 25, 2025
1 parent 662c686 commit 294952f
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
76 changes: 76 additions & 0 deletions integrations/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<div class="underline"></div>
`),
},
},
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`
<div class="underline"></div>
`),
'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
}
68 changes: 68 additions & 0 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<div class="ring"></div>
`),
'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 ---
<div class="ring-3"></div>
--- ./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
}
10 changes: 7 additions & 3 deletions integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ interface TestContext {
exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise<string>
spawn(command: string, options?: ChildProcessOptions): Promise<SpawnedProcess>
fs: {
write(filePath: string, content: string): Promise<void>
write(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>
create(filePaths: string[]): Promise<void>
read(filePath: string): Promise<string>
glob(pattern: string): Promise<[string, string][]>
Expand Down Expand Up @@ -268,7 +268,11 @@ export function test(
}
},
fs: {
async write(filename: string, content: string | Uint8Array): Promise<void> {
async write(
filename: string,
content: string | Uint8Array,
encoding: BufferEncoding = 'utf8',
): Promise<void> {
let full = path.join(root, filename)
let dir = path.dirname(full)
await fs.mkdir(dir, { recursive: true })
Expand All @@ -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<void> {
Expand Down
11 changes: 11 additions & 0 deletions packages/tailwindcss/src/css-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
},
])
})
})
1 change: 1 addition & 0 deletions packages/tailwindcss/src/css-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down

0 comments on commit 294952f

Please sign in to comment.