diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts new file mode 100644 index 000000000000..a6b3e0353381 --- /dev/null +++ b/integrations/postcss/index.test.ts @@ -0,0 +1,164 @@ +import path from 'node:path' +import { candidate, css, html, js, json, test, yaml } from '../utils' + +test( + 'production build', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': json` + { + "dependencies": { + "postcss": "^8", + "postcss-cli": "^10", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'project-a/postcss.config.js': js` + module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, + } + `, + 'project-a/index.html': html` +
+ `, + 'project-a/plugin.js': js` + module.exports = function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/utilities'; + @content '../../project-b/src/**/*.js'; + @plugin '../plugin.js'; + `, + 'project-a/src/index.js': js` + const className = "content-['a/src/index.js']" + module.exports = { className } + `, + 'project-b/src/index.js': js` + const className = "content-['b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec }) => { + await exec('pnpm postcss src/index.css --output dist/out.css', { + cwd: path.join(root, 'project-a'), + }) + + await fs.expectFileToContain('project-a/dist/out.css', [ + candidate`underline`, + candidate`content-['a/src/index.js']`, + candidate`content-['b/src/index.js']`, + candidate`inverted:flex`, + candidate`hocus:underline`, + ]) + }, +) + +test( + 'watch mode', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': json` + { + "dependencies": { + "postcss": "^8", + "postcss-cli": "^10", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'project-a/postcss.config.js': js` + module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + }, + } + `, + 'project-a/index.html': html` +
+ `, + 'project-a/plugin.js': js` + module.exports = function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/utilities'; + @content '../../project-b/src/**/*.js'; + @plugin '../plugin.js'; + `, + 'project-a/src/index.js': js` + const className = "content-['a/src/index.js']" + module.exports = { className } + `, + 'project-b/src/index.js': js` + const className = "content-['b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, spawn }) => { + let process = await spawn( + 'pnpm postcss src/index.css --output dist/out.css --watch --verbose', + { cwd: path.join(root, 'project-a') }, + ) + await process.onStderr((message) => message.includes('Waiting for file changes...')) + + await fs.expectFileToContain('project-a/dist/out.css', [ + candidate`underline`, + candidate`content-['a/src/index.js']`, + candidate`content-['b/src/index.js']`, + candidate`inverted:flex`, + candidate`hocus:underline`, + ]) + + await fs.write( + 'project-a/src/index.js', + js` + const className = "[.changed_&]:content-['project-a/src/index.js']" + module.exports = { className } + `, + ) + + await fs.expectFileToContain('project-a/dist/out.css', [ + candidate`[.changed_&]:content-['project-a/src/index.js']`, + ]) + + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" + module.exports = { className } + `, + ) + + await fs.expectFileToContain('project-a/dist/out.css', [ + candidate`[.changed_&]:content-['project-b/src/index.js']`, + ]) + }, +) diff --git a/integrations/utils.ts b/integrations/utils.ts index b4bd0f1d56f6..52ae3572ab57 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -96,10 +96,18 @@ export function test( } let disposables: (() => Promise)[] = [] + async function dispose() { await Promise.all(disposables.map((dispose) => dispose())) - await fs.rm(root, { recursive: true, maxRetries: 3, force: true }) + try { + await fs.rm(root, { recursive: true, maxRetries: 5, force: true }) + } catch (err) { + if (!process.env.CI) { + throw err + } + } } + options.onTestFinished(dispose) let context = { diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 53f379b88d53..ce4699586e31 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -13,7 +13,7 @@ const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css` const css = String.raw beforeEach(async () => { - const { clearCache } = await import('@tailwindcss/oxide') + let { clearCache } = await import('@tailwindcss/oxide') clearCache() }) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 05d10c5465c5..a9a27664102d 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -43,7 +43,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let cache = new DefaultMap(() => { return { mtimes: new Map(), - build: null as null | ReturnType['build'], + compiler: null as null | ReturnType, css: '', optimizedCss: '', } @@ -76,6 +76,23 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { OnceExit(root, { result }) { let inputFile = result.opts.from ?? '' let context = cache.get(inputFile) + let inputBasePath = path.dirname(path.resolve(inputFile)) + + function createCompiler() { + return compile(root.toString(), { + loadPlugin: (pluginPath) => { + if (pluginPath[0] === '.') { + return require(path.resolve(inputBasePath, pluginPath)) + } + + return require(pluginPath) + }, + }) + } + + // Setup the compiler if it doesn't exist yet. This way we can + // guarantee a `compile()` function is available. + context.compiler ??= createCompiler() let rebuildStrategy: 'full' | 'incremental' = 'incremental' @@ -109,10 +126,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { let css = '' // Look for candidates used to generate the CSS - let { candidates, files, globs } = scanDir({ base }) + let scanDirResult = scanDir({ + base, // Root directory, mainly used for auto content detection + contentPaths: context.compiler.globs.map((glob) => ({ + base: inputBasePath, // Globs are relative to the input.css file + glob, + })), + }) // Add all found files as direct dependencies - for (let file of files) { + for (let file of scanDirResult.files) { result.messages.push({ type: 'dependency', plugin: '@tailwindcss/postcss', @@ -124,7 +147,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Register dependencies so changes in `base` cause a rebuild while // giving tools like Vite or Parcel a glob that can be used to limit // the files that cause a rebuild to only those that match it. - for (let { base, glob } of globs) { + for (let { base, glob } of scanDirResult.globs) { result.messages.push({ type: 'dir-dependency', plugin: '@tailwindcss/postcss', @@ -135,20 +158,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } if (rebuildStrategy === 'full') { - let basePath = path.dirname(path.resolve(inputFile)) - let { build } = compile(root.toString(), { - loadPlugin: (pluginPath) => { - if (pluginPath[0] === '.') { - return require(path.resolve(basePath, pluginPath)) - } - - return require(pluginPath) - }, - }) - context.build = build - css = build(hasTailwind ? candidates : []) + context.compiler = createCompiler() + css = context.compiler.build(hasTailwind ? scanDirResult.candidates : []) } else if (rebuildStrategy === 'incremental') { - css = context.build!(candidates) + css = context.compiler.build!(scanDirResult.candidates) } // Replace CSS