Skip to content

Commit 5b8136e

Browse files
Re-throw errors from PostCSS nodes (#18373)
Fixes #18370 --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent c2aab49 commit 5b8136e

File tree

6 files changed

+87
-35
lines changed

6 files changed

+87
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Do not migrate `variant = 'outline'` during upgrades ([#18922](https://github.com/tailwindlabs/tailwindcss/pull/18922))
2424
- Show Lightning CSS warnings (if any) when optimizing/minifying ([#18918](https://github.com/tailwindlabs/tailwindcss/pull/18918))
2525
- Use `default` export condition for `@tailwindcss/vite` ([#18948](https://github.com/tailwindlabs/tailwindcss/pull/18948))
26+
- Re-throw errors from PostCSS nodes ([#18373](https://github.com/tailwindlabs/tailwindcss/pull/18373))
2627

2728
## [4.1.13] - 2025-09-03
2829

integrations/postcss/index.test.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path'
2-
import { candidate, css, html, js, json, retryAssertion, test, ts, yaml } from '../utils'
2+
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
33

44
test(
55
'production build (string)',
@@ -662,35 +662,72 @@ test(
662662
`,
663663
'src/index.css': css` @import './tailwind.css'; `,
664664
'src/tailwind.css': css`
665-
@reference 'tailwindcss/does-not-exist';
665+
@reference 'tailwindcss/theme';
666666
@import 'tailwindcss/utilities';
667667
`,
668668
},
669669
},
670670
async ({ fs, expect, spawn }) => {
671+
// 1. Start the watcher
672+
//
673+
// It must have valid CSS for the initial build
671674
let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
672675

676+
await process.onStderr((message) => message.includes('Waiting for file changes...'))
677+
678+
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
679+
"
680+
--- dist/out.css ---
681+
.underline {
682+
text-decoration-line: underline;
683+
}
684+
"
685+
`)
686+
687+
// 2. Cause an error
688+
await fs.write(
689+
'src/tailwind.css',
690+
css`
691+
@reference 'tailwindcss/does-not-exist';
692+
@import 'tailwindcss/utilities';
693+
`,
694+
)
695+
696+
// 2.5 Write to a content file
697+
await fs.write('src/index.html', html`
698+
<div class="flex underline"></div>
699+
`)
700+
673701
await process.onStderr((message) =>
674702
message.includes('does-not-exist is not exported from package'),
675703
)
676704

677-
await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))
678-
679-
await process.onStderr((message) => message.includes('Waiting for file changes...'))
705+
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
706+
"
707+
--- dist/out.css ---
708+
.underline {
709+
text-decoration-line: underline;
710+
}
711+
"
712+
`)
680713

681-
// Fix the CSS file
714+
// 3. Fix the CSS file
682715
await fs.write(
683716
'src/tailwind.css',
684717
css`
685718
@reference 'tailwindcss/theme';
686719
@import 'tailwindcss/utilities';
687720
`,
688721
)
689-
await process.onStderr((message) => message.includes('Finished'))
722+
723+
await process.onStderr((message) => message.includes('Waiting for file changes...'))
690724

691725
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
692726
"
693727
--- dist/out.css ---
728+
.flex {
729+
display: flex;
730+
}
694731
.underline {
695732
text-decoration-line: underline;
696733
}
@@ -705,11 +742,22 @@ test(
705742
@import 'tailwindcss/utilities';
706743
`,
707744
)
745+
708746
await process.onStderr((message) =>
709747
message.includes('does-not-exist is not exported from package'),
710748
)
711749

712-
await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))
750+
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
751+
"
752+
--- dist/out.css ---
753+
.flex {
754+
display: flex;
755+
}
756+
.underline {
757+
text-decoration-line: underline;
758+
}
759+
"
760+
`)
713761
},
714762
)
715763

packages/@tailwindcss-postcss/src/ast.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ it('should convert a Tailwind CSS AST into a PostCSS AST', () => {
8484
`
8585

8686
let ast = parse(input)
87-
let transformedAst = cssAstToPostCssAst(ast)
87+
let transformedAst = cssAstToPostCssAst(postcss, ast)
8888

8989
expect(transformedAst.toString()).toMatchInlineSnapshot(`
9090
"@charset "UTF-8";

packages/@tailwindcss-postcss/src/ast.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
import postcss, {
2-
Input,
3-
type ChildNode as PostCssChildNode,
4-
type Container as PostCssContainerNode,
5-
type Root as PostCssRoot,
6-
type Source as PostcssSource,
7-
} from 'postcss'
1+
import type * as postcss from 'postcss'
82
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
93
import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table'
104
import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source'
115
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
126

137
const EXCLAMATION_MARK = 0x21
148

15-
export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot {
16-
let inputMap = new DefaultMap<Source, Input>((src) => {
17-
return new Input(src.code, {
9+
export function cssAstToPostCssAst(
10+
postcss: postcss.Postcss,
11+
ast: AstNode[],
12+
source?: postcss.Source,
13+
): postcss.Root {
14+
let inputMap = new DefaultMap<Source, postcss.Input>((src) => {
15+
return new postcss.Input(src.code, {
1816
map: source?.input.map,
1917
from: src.file ?? undefined,
2018
})
@@ -25,7 +23,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
2523
let root = postcss.root()
2624
root.source = source
2725

28-
function toSource(loc: SourceLocation | undefined): PostcssSource | undefined {
26+
function toSource(loc: SourceLocation | undefined): postcss.Source | undefined {
2927
// Use the fallback if this node has no location info in the AST
3028
if (!loc) return
3129
if (!loc[0]) return
@@ -49,7 +47,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
4947
}
5048
}
5149

52-
function updateSource(astNode: PostCssChildNode, loc: SourceLocation | undefined) {
50+
function updateSource(astNode: postcss.ChildNode, loc: SourceLocation | undefined) {
5351
let source = toSource(loc)
5452

5553
// The `source` property on PostCSS nodes must be defined if present because
@@ -63,7 +61,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
6361
}
6462
}
6563

66-
function transform(node: AstNode, parent: PostCssContainerNode) {
64+
function transform(node: AstNode, parent: postcss.Container) {
6765
// Declaration
6866
if (node.kind === 'declaration') {
6967
let astNode = postcss.decl({
@@ -125,13 +123,13 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
125123
return root
126124
}
127125

128-
export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
129-
let inputMap = new DefaultMap<Input, Source>((input) => ({
126+
export function postCssAstToCssAst(root: postcss.Root): AstNode[] {
127+
let inputMap = new DefaultMap<postcss.Input, Source>((input) => ({
130128
file: input.file ?? input.id ?? null,
131129
code: input.css,
132130
}))
133131

134-
function toSource(node: PostCssChildNode): SourceLocation | undefined {
132+
function toSource(node: postcss.ChildNode): SourceLocation | undefined {
135133
let source = node.source
136134
if (!source) return
137135

@@ -144,7 +142,7 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
144142
}
145143

146144
function transform(
147-
node: PostCssChildNode,
145+
node: postcss.ChildNode,
148146
parent: Extract<AstNode, { nodes: AstNode[] }>['nodes'],
149147
) {
150148
// Declaration

packages/@tailwindcss-postcss/src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ describe('concurrent builds', () => {
391391
let ast = postcss.parse(input)
392392
for (let runner of (plugin as any).plugins) {
393393
if (runner.Once) {
394-
await runner.Once(ast, { result: { opts: { from }, messages: [] } })
394+
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
395395
}
396396
}
397397
return ast.toString()

packages/@tailwindcss-postcss/src/index.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
1111
import { Scanner } from '@tailwindcss/oxide'
1212
import fs from 'node:fs'
1313
import path, { relative } from 'node:path'
14-
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
14+
import type { AcceptedPlugin, PluginCreator, Postcss, Root } from 'postcss'
1515
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
1616
import { cssAstToPostCssAst, postCssAstToCssAst } from './ast'
1717
import fixRelativePathsPlugin from './postcss-fix-relative-paths'
@@ -23,13 +23,13 @@ interface CacheEntry {
2323
compiler: null | ReturnType<typeof compileAst>
2424
scanner: null | Scanner
2525
tailwindCssAst: AstNode[]
26-
cachedPostCssAst: postcss.Root
27-
optimizedPostCssAst: postcss.Root
26+
cachedPostCssAst: Root
27+
optimizedPostCssAst: Root
2828
fullRebuildPaths: string[]
2929
}
3030
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
3131

32-
function getContextFromCache(inputFile: string, opts: PluginOptions): CacheEntry {
32+
function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOptions): CacheEntry {
3333
let key = `${inputFile}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
3434
if (cache.has(key)) return cache.get(key)!
3535
let entry = {
@@ -83,7 +83,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
8383

8484
{
8585
postcssPlugin: 'tailwindcss',
86-
async Once(root, { result }) {
86+
async Once(root, { result, postcss }) {
8787
using I = new Instrumentation()
8888

8989
let inputFile = result.opts.from ?? ''
@@ -114,7 +114,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
114114
DEBUG && I.end('Quick bail check')
115115
}
116116

117-
let context = getContextFromCache(inputFile, opts)
117+
let context = getContextFromCache(postcss, inputFile, opts)
118118
let inputBasePath = path.dirname(path.resolve(inputFile))
119119

120120
// Whether this is the first build or not, if it is, then we can
@@ -310,7 +310,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
310310
} else {
311311
// Convert our AST to a PostCSS AST
312312
DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
313-
context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source)
313+
context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source)
314314
DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
315315
}
316316
}
@@ -349,7 +349,12 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
349349
// We found that throwing the error will cause PostCSS to no longer watch for changes
350350
// in some situations so we instead log the error and continue with an empty stylesheet.
351351
console.error(error)
352-
root.removeAll()
352+
353+
if (error && typeof error === 'object' && 'message' in error) {
354+
throw root.error(`${error.message}`)
355+
}
356+
357+
throw root.error(`${error}`)
353358
}
354359
},
355360
},

0 commit comments

Comments
 (0)