Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions integrations/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2104,3 +2104,31 @@
function withBOM(text: string): string {
return '\uFEFF' + text
}

test(
'CSS parse errors should include filename and line number',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'broken.css': css`
/* Test file to reproduce the CSS parsing error */
.test {
color: red;
/* margin-bottom: calc(var(--spacing) * 5); */ */
}
`,
},
},
async ({ exec, expect }) => {
await expect(exec('pnpm tailwindcss --input broken.css --output dist/out.css')).rejects.toThrow(

Check failure on line 2130 in integrations/cli/index.test.ts

View workflow job for this annotation

GitHub Actions / Linux / cli

cli/index.test.ts > CSS parse errors should include filename and line number

AssertionError: expected [Function] to throw error matching /Invalid declaration.*at.*broken\.css:…/ but got 'Command failed: pnpm tailwindcss --in…' - Expected: /Invalid declaration.*at.*broken\.css:5:49/ + Received: "Command failed: pnpm tailwindcss --input broken.css --output dist/out.css ≈ tailwindcss v4.1.17 Error: Invalid declaration: `*/` at /tmp/tailwind-integrationsUph6jZ/broken.css:4:50 " ❯ cli/index.test.ts:2130:5 ❯ utils.ts:452:14

Check failure on line 2130 in integrations/cli/index.test.ts

View workflow job for this annotation

GitHub Actions / Linux / cli

cli/index.test.ts > CSS parse errors should include filename and line number

AssertionError: expected [Function] to throw error matching /Invalid declaration.*at.*broken\.css:…/ but got 'Command failed: pnpm tailwindcss --in…' - Expected: /Invalid declaration.*at.*broken\.css:5:49/ + Received: "Command failed: pnpm tailwindcss --input broken.css --output dist/out.css ≈ tailwindcss v4.1.17 Error: Invalid declaration: `*/` at /tmp/tailwind-integrationsiKwogS/broken.css:4:50 " ❯ cli/index.test.ts:2130:5 ❯ utils.ts:452:14

Check failure on line 2130 in integrations/cli/index.test.ts

View workflow job for this annotation

GitHub Actions / Linux / cli

cli/index.test.ts > CSS parse errors should include filename and line number

AssertionError: expected [Function] to throw error matching /Invalid declaration.*at.*broken\.css:…/ but got 'Command failed: pnpm tailwindcss --in…' - Expected: /Invalid declaration.*at.*broken\.css:5:49/ + Received: "Command failed: pnpm tailwindcss --input broken.css --output dist/out.css ≈ tailwindcss v4.1.17 Error: Invalid declaration: `*/` at /tmp/tailwind-integrationsLfjdTP/broken.css:4:50 " ❯ cli/index.test.ts:2130:5 ❯ utils.ts:452:14
/Invalid declaration.*at.*broken\.css:5:49/,
)
},
)
56 changes: 56 additions & 0 deletions packages/tailwindcss/src/css-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,54 @@
`[Error: Invalid declaration: \`bar\`]`,
)
})

it('should include filename and line number in error messages when from option is provided', () => {
expect(() => {
CSS.parse('/* margin-bottom: calc(var(--spacing) * 5); */ */', { from: 'test.css' })
}).toThrowErrorMatchingInlineSnapshot(

Check failure on line 1222 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Windows > errors > should include filename and line number in error messages when from option is provided

Error: snapshot function didn't throw ❯ src/css-parser.test.ts:1222:10

Check failure on line 1222 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Unix > errors > should include filename and line number in error messages when from option is provided

Error: snapshot function didn't throw ❯ src/css-parser.test.ts:1222:10
`[Error: Invalid declaration: \`*/\` at test.css:1:49]`,
)
})

it('should include filename and line number for multi-line CSS errors', () => {
const multiLineCss = `/* Test file */
.test {
color: red;
/* margin-bottom: calc(var(--spacing) * 5); */ */
}`
expect(() => {
CSS.parse(multiLineCss, { from: 'styles.css' })
}).toThrowErrorMatchingInlineSnapshot(

Check failure on line 1235 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Windows > errors > should include filename and line number for multi-line CSS errors

Error: Snapshot `Line endings: Windows > errors > should include filename and line number for multi-line CSS errors 1` mismatched Expected: "[Error: Invalid declaration: `*/` at styles.css:4:49]" Received: "[Error: Invalid declaration: `*/` at styles.css:4:50]" ❯ src/css-parser.test.ts:1235:10

Check failure on line 1235 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Unix > errors > should include filename and line number for multi-line CSS errors

Error: Snapshot `Line endings: Unix > errors > should include filename and line number for multi-line CSS errors 1` mismatched Expected: "[Error: Invalid declaration: `*/` at styles.css:4:49]" Received: "[Error: Invalid declaration: `*/` at styles.css:4:50]" ❯ src/css-parser.test.ts:1235:10
`[Error: Invalid declaration: \`*/\` at styles.css:4:49]`,
)
})

it('should include filename and line number for missing opening brace errors', () => {
const cssWithMissingBrace = `.foo {
color: red;
}
.bar
color: blue;
}`
expect(() => {
CSS.parse(cssWithMissingBrace, { from: 'broken.css' })
}).toThrowErrorMatchingInlineSnapshot(
`[Error: Missing opening { at broken.css:7:1]`,
)
})

it('should include filename and line number for unterminated string errors', () => {
const cssWithUnterminatedString = `.foo {
content: "Hello world!
font-weight: bold;
}`
expect(() => {
CSS.parse(cssWithUnterminatedString, { from: 'string-error.css' })
}).toThrowErrorMatchingInlineSnapshot(

Check failure on line 1262 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Windows > errors > should include filename and line number for unterminated string errors

Error: Snapshot `Line endings: Windows > errors > should include filename and line number for unterminated string errors 1` mismatched Expected: "[Error: Unterminated string: "Hello world! at string-error.css:2:12]" Received: "[Error: Unterminated string: "Hello world!" at string-error.css:2:12]" ❯ src/css-parser.test.ts:1262:10

Check failure on line 1262 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Unix > errors > should include filename and line number for unterminated string errors

Error: Snapshot `Line endings: Unix > errors > should include filename and line number for unterminated string errors 1` mismatched Expected: "[Error: Unterminated string: "Hello world! at string-error.css:2:12]" Received: "[Error: Unterminated string: "Hello world!" at string-error.css:2:12]" ❯ src/css-parser.test.ts:1262:10
`[Error: Unterminated string: "Hello world! at string-error.css:2:12]`,
)
})
})

it('ignores BOM at the beginning of a file', () => {
Expand All @@ -1227,4 +1275,12 @@
},
])
})

it('should not include filename when from option is not provided', () => {
expect(() => {
CSS.parse('/* margin-bottom: calc(var(--spacing) * 5); */ */')
}).toThrowErrorMatchingInlineSnapshot(

Check failure on line 1282 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Windows > should not include filename when from option is not provided

Error: InlineSnapshot cannot be used inside of test.each or describe.each ❯ src/css-parser.test.ts:1282:8

Check failure on line 1282 in packages/tailwindcss/src/css-parser.test.ts

View workflow job for this annotation

GitHub Actions / Linux

src/css-parser.test.ts > Line endings: Unix > should not include filename when from option is not provided

Error: InlineSnapshot cannot be used inside of test.each or describe.each ❯ src/css-parser.test.ts:1282:8
`[Error: Invalid declaration: \`*/\`]`,
)
})
})
49 changes: 37 additions & 12 deletions packages/tailwindcss/src/css-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@ export interface ParseOptions {
from?: string
}

function getLineAndColumn(input: string, position: number): { line: number; column: number } {
let line = 1
let column = 1

for (let i = 0; i < position && i < input.length; i++) {
if (input.charCodeAt(i) === LINE_BREAK) {
line++
column = 1
} else {
column++
}
}

return { line, column }
}

function formatError(message: string, source: Source | null, position: number): string {
if (!source) {
return message
}

const { line, column } = getLineAndColumn(source.code, position)
return `${message} at ${source.file}:${line}:${column}`
}

export function parse(input: string, opts?: ParseOptions) {
let source: Source | null = opts?.from ? { file: opts.from, code: input } : null

Expand Down Expand Up @@ -138,7 +163,7 @@ export function parse(input: string, opts?: ParseOptions) {

// Start of a string.
else if (currentChar === SINGLE_QUOTE || currentChar === DOUBLE_QUOTE) {
let end = parseString(input, i, currentChar)
let end = parseString(input, i, currentChar, source)

// Adjust `buffer` to include the string.
buffer += input.slice(i, end + 1)
Expand Down Expand Up @@ -192,7 +217,7 @@ export function parse(input: string, opts?: ParseOptions) {

// Start of a string.
else if (peekChar === SINGLE_QUOTE || peekChar === DOUBLE_QUOTE) {
j = parseString(input, j, peekChar)
j = parseString(input, j, peekChar, source)
}

// Start of a comment.
Expand Down Expand Up @@ -269,7 +294,7 @@ export function parse(input: string, opts?: ParseOptions) {
}

let declaration = parseDeclaration(buffer, colonIdx)
if (!declaration) throw new Error(`Invalid custom property, expected a value`)
if (!declaration) throw new Error(formatError(`Invalid custom property, expected a value`, source, start))

if (source) {
declaration.src = [source, start, i]
Expand Down Expand Up @@ -334,7 +359,7 @@ export function parse(input: string, opts?: ParseOptions) {
let declaration = parseDeclaration(buffer)
if (!declaration) {
if (buffer.length === 0) continue
throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
throw new Error(formatError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart))
}

if (source) {
Expand Down Expand Up @@ -391,7 +416,7 @@ export function parse(input: string, opts?: ParseOptions) {
closingBracketStack[closingBracketStack.length - 1] !== ')'
) {
if (closingBracketStack === '') {
throw new Error('Missing opening {')
throw new Error(formatError('Missing opening {', source, i))
}

closingBracketStack = closingBracketStack.slice(0, -1)
Expand Down Expand Up @@ -453,7 +478,7 @@ export function parse(input: string, opts?: ParseOptions) {
// Attach the declaration to the parent.
if (parent) {
let node = parseDeclaration(buffer, colonIdx)
if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
if (!node) throw new Error(formatError(`Invalid declaration: \`${buffer.trim()}\``, source, bufferStart))

if (source) {
node.src = [source, bufferStart, i]
Expand Down Expand Up @@ -492,7 +517,7 @@ export function parse(input: string, opts?: ParseOptions) {
// `)`
else if (currentChar === CLOSE_PAREN) {
if (closingBracketStack[closingBracketStack.length - 1] !== ')') {
throw new Error('Missing opening (')
throw new Error(formatError('Missing opening (', source, i))
}

closingBracketStack = closingBracketStack.slice(0, -1)
Expand Down Expand Up @@ -534,10 +559,10 @@ export function parse(input: string, opts?: ParseOptions) {
// have a leftover `parent`, then it means that we have an unterminated block.
if (closingBracketStack.length > 0 && parent) {
if (parent.kind === 'rule') {
throw new Error(`Missing closing } at ${parent.selector}`)
throw new Error(formatError(`Missing closing } at ${parent.selector}`, source, input.length))
}
if (parent.kind === 'at-rule') {
throw new Error(`Missing closing } at ${parent.name} ${parent.params}`)
throw new Error(formatError(`Missing closing } at ${parent.name} ${parent.params}`, source, input.length))
}
}

Expand Down Expand Up @@ -594,7 +619,7 @@ function parseDeclaration(
)
}

function parseString(input: string, startIdx: number, quoteChar: number): number {
function parseString(input: string, startIdx: number, quoteChar: number, source: Source | null = null): number {
let peekChar: number

// We need to ensure that the closing quote is the same as the opening
Expand Down Expand Up @@ -637,7 +662,7 @@ function parseString(input: string, startIdx: number, quoteChar: number): number
(input.charCodeAt(i + 1) === CARRIAGE_RETURN && input.charCodeAt(i + 2) === LINE_BREAK))
) {
throw new Error(
`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`,
formatError(`Unterminated string: ${input.slice(startIdx, i + 1) + String.fromCharCode(quoteChar)}`, source, startIdx)
)
}

Expand All @@ -656,7 +681,7 @@ function parseString(input: string, startIdx: number, quoteChar: number): number
(peekChar === CARRIAGE_RETURN && input.charCodeAt(i + 1) === LINE_BREAK)
) {
throw new Error(
`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`,
formatError(`Unterminated string: ${input.slice(startIdx, i) + String.fromCharCode(quoteChar)}`, source, startIdx)
)
}
}
Expand Down
Loading