diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index a811cf637661..dd07fd90ebbd 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -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)', @@ -662,23 +662,56 @@ 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` +
+ `) + 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` @@ -686,11 +719,15 @@ test( @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; } @@ -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; + } + " + `) }, ) diff --git a/packages/@tailwindcss-postcss/src/ast.test.ts b/packages/@tailwindcss-postcss/src/ast.test.ts index dd7095428e1c..cb0161bcea1b 100644 --- a/packages/@tailwindcss-postcss/src/ast.test.ts +++ b/packages/@tailwindcss-postcss/src/ast.test.ts @@ -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"; diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts index 1912fc84b789..2ec8b88b6caf 100644 --- a/packages/@tailwindcss-postcss/src/ast.ts +++ b/packages/@tailwindcss-postcss/src/ast.ts @@ -1,10 +1,4 @@ -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' @@ -12,9 +6,13 @@ 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((src) => { - return new Input(src.code, { +export function cssAstToPostCssAst( + ast: AstNode[], + source: postcss.Source | undefined, + postcss: postcss.Postcss, +): postcss.Root { + let inputMap = new DefaultMap((src) => { + return new postcss.Input(src.code, { map: source?.input.map, from: src.file ?? undefined, }) @@ -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 @@ -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 @@ -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({ @@ -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) => ({ +export function postCssAstToCssAst(root: postcss.Root): AstNode[] { + let inputMap = new DefaultMap((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 @@ -144,7 +142,7 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] { } function transform( - node: PostCssChildNode, + node: postcss.ChildNode, parent: Extract['nodes'], ) { // Declaration diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 4227d6fbbf93..593ef70cff5a 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -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() diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 8a37f542c94f..5e9cf49b09d7 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -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' @@ -23,13 +23,13 @@ interface CacheEntry { compiler: null | ReturnType scanner: null | Scanner tailwindCssAst: AstNode[] - cachedPostCssAst: postcss.Root - optimizedPostCssAst: postcss.Root + cachedPostCssAst: Root + optimizedPostCssAst: Root fullRebuildPaths: string[] } const cache = new QuickLRU({ 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 = { @@ -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 ?? '' @@ -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 @@ -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') } } @@ -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}`) } }, },