Skip to content
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 @@ -83,7 +83,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 @@ -114,7 +114,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 @@ -310,7 +310,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 @@ -349,7 +349,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