Skip to content

Re-throw errors from PostCSS nodes #18373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
64 changes: 56 additions & 8 deletions integrations/postcss/index.test.ts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path'
import { candidate, css, html, js, json, retryAssertion, test, ts, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'

test(
'production build (string)',
Expand Down Expand Up @@ -662,35 +662,72 @@ test(
`,
'src/index.css': css` @import './tailwind.css'; `,
'src/tailwind.css': css`
@reference 'tailwindcss/does-not-exist';
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
},
},
async ({ fs, expect, spawn }) => {
// 1. Start the watcher
//
// It must have valid CSS for the initial build
let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')

await process.onStderr((message) => message.includes('Waiting for file changes...'))

expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
--- dist/out.css ---
.underline {
text-decoration-line: underline;
}
"
`)

// 2. Cause an error
await fs.write(
'src/tailwind.css',
css`
@reference 'tailwindcss/does-not-exist';
@import 'tailwindcss/utilities';
`,
)

// 2.5 Write to a content file
await fs.write('src/index.html', html`
<div class="flex underline"></div>
`)

await process.onStderr((message) =>
message.includes('does-not-exist is not exported from package'),
)

await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))

await process.onStderr((message) => message.includes('Waiting for file changes...'))
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
--- dist/out.css ---
.underline {
text-decoration-line: underline;
}
"
`)

// Fix the CSS file
// 3. Fix the CSS file
await fs.write(
'src/tailwind.css',
css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
)
await process.onStderr((message) => message.includes('Finished'))

await process.onStderr((message) => message.includes('Waiting for file changes...'))

expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
--- dist/out.css ---
.flex {
display: flex;
}
.underline {
text-decoration-line: underline;
}
Expand All @@ -705,11 +742,22 @@ test(
@import 'tailwindcss/utilities';
`,
)

await process.onStderr((message) =>
message.includes('does-not-exist is not exported from package'),
)

await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))
expect(await fs.dumpFiles('dist/*.css')).toMatchInlineSnapshot(`
"
--- dist/out.css ---
.flex {
display: flex;
}
.underline {
text-decoration-line: underline;
}
"
`)
},
)

Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-postcss/src/ast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ it('should convert a Tailwind CSS AST into a PostCSS AST', () => {
`

let ast = parse(input)
let transformedAst = cssAstToPostCssAst(ast)
let transformedAst = cssAstToPostCssAst(ast, undefined, postcss)

expect(transformedAst.toString()).toMatchInlineSnapshot(`
"@charset "UTF-8";
Expand Down
32 changes: 15 additions & 17 deletions packages/@tailwindcss-postcss/src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import postcss, {
Input,
type ChildNode as PostCssChildNode,
type Container as PostCssContainerNode,
type Root as PostCssRoot,
type Source as PostcssSource,
} from 'postcss'
import type * as postcss from 'postcss'
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table'
import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source'
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'

const EXCLAMATION_MARK = 0x21

export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot {
let inputMap = new DefaultMap<Source, Input>((src) => {
return new Input(src.code, {
export function cssAstToPostCssAst(
ast: AstNode[],
source: postcss.Source | undefined,
postcss: postcss.Postcss,
): postcss.Root {
let inputMap = new DefaultMap<Source, postcss.Input>((src) => {
return new postcss.Input(src.code, {
map: source?.input.map,
from: src.file ?? undefined,
})
Expand All @@ -25,7 +23,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
let root = postcss.root()
root.source = source

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

function updateSource(astNode: PostCssChildNode, loc: SourceLocation | undefined) {
function updateSource(astNode: postcss.ChildNode, loc: SourceLocation | undefined) {
let source = toSource(loc)

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

function transform(node: AstNode, parent: PostCssContainerNode) {
function transform(node: AstNode, parent: postcss.Container) {
// Declaration
if (node.kind === 'declaration') {
let astNode = postcss.decl({
Expand Down Expand Up @@ -125,13 +123,13 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
return root
}

export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
let inputMap = new DefaultMap<Input, Source>((input) => ({
export function postCssAstToCssAst(root: postcss.Root): AstNode[] {
let inputMap = new DefaultMap<postcss.Input, Source>((input) => ({
file: input.file ?? input.id ?? null,
code: input.css,
}))

function toSource(node: PostCssChildNode): SourceLocation | undefined {
function toSource(node: postcss.ChildNode): SourceLocation | undefined {
let source = node.source
if (!source) return

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

function transform(
node: PostCssChildNode,
node: postcss.ChildNode,
parent: Extract<AstNode, { nodes: AstNode[] }>['nodes'],
) {
// Declaration
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-postcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ describe('concurrent builds', () => {
let ast = postcss.parse(input)
for (let runner of (plugin as any).plugins) {
if (runner.Once) {
await runner.Once(ast, { result: { opts: { from }, messages: [] } })
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
}
}
return ast.toString()
Expand Down
21 changes: 13 additions & 8 deletions packages/@tailwindcss-postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs'
import path, { relative } from 'node:path'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
import type { AcceptedPlugin, PluginCreator, Postcss, Root } from 'postcss'
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
import { cssAstToPostCssAst, postCssAstToCssAst } from './ast'
import fixRelativePathsPlugin from './postcss-fix-relative-paths'
Expand All @@ -23,13 +23,13 @@ interface CacheEntry {
compiler: null | ReturnType<typeof compileAst>
scanner: null | Scanner
tailwindCssAst: AstNode[]
cachedPostCssAst: postcss.Root
optimizedPostCssAst: postcss.Root
cachedPostCssAst: Root
optimizedPostCssAst: Root
fullRebuildPaths: string[]
}
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })

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

{
postcssPlugin: 'tailwindcss',
async Once(root, { result }) {
async Once(root, { result, postcss }) {
using I = new Instrumentation()

let inputFile = result.opts.from ?? ''
Expand Down Expand Up @@ -100,7 +100,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
DEBUG && I.end('Quick bail check')
}

let context = getContextFromCache(inputFile, opts)
let context = getContextFromCache(inputFile, opts, postcss)
let inputBasePath = path.dirname(path.resolve(inputFile))

// Whether this is the first build or not, if it is, then we can
Expand Down Expand Up @@ -296,7 +296,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
} else {
// Convert our AST to a PostCSS AST
DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source)
context.cachedPostCssAst = cssAstToPostCssAst(tailwindCssAst, root.source, postcss)
DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
}
}
Expand Down Expand Up @@ -335,7 +335,12 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
// We found that throwing the error will cause PostCSS to no longer watch for changes
// in some situations so we instead log the error and continue with an empty stylesheet.
console.error(error)
root.removeAll()

if (error && typeof error === 'object' && 'message' in error) {
throw root.error(`${error.message}`)
}

throw root.error(`${error}`)
}
},
},
Expand Down