From 544853848dd00f25be58ac4e74aad9e7d198e725 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Jul 2024 15:58:15 +0200 Subject: [PATCH 1/9] setup tests for `@content` support We also setup a sibling fixture project so that we can use `@content` to include files that are not detected by the auto content detection. --- .../src/fixtures/other-project/src/index.js | 1 + .../@tailwindcss-postcss/src/index.test.ts | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js diff --git a/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js b/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js new file mode 100644 index 000000000000..60abb39c0a9b --- /dev/null +++ b/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js @@ -0,0 +1 @@ +const className = "content-['other-project']" diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 53f379b88d53..013457f4b265 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() }) @@ -226,3 +226,59 @@ describe('plugins', () => { `) }) }) + +describe('@content', () => { + test('scans custom @content files', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + @import 'tailwindcss/utilities'; + @content '../other-project/src/**/*.js'; + `, + { from: INPUT_CSS_PATH }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline; + } + + .content-\\[\\'other-project\\'\\] { + --tw-content: "other-project"; + content: var(--tw-content); + } + + @supports (-moz-orient: inline) { + @layer base { + *, :before, :after, ::backdrop { + --tw-content: ""; + } + } + } + + @property --tw-content { + syntax: "*"; + inherits: false; + initial-value: ""; + }" + `) + + expect(result.messages).toContainEqual({ + type: 'dependency', + file: expect.stringMatching(/other-project\/src\/index.js$/g), + parent: expect.any(String), + plugin: '@tailwindcss/postcss', + }) + + expect(result.messages).toContainEqual({ + type: 'dir-dependency', + dir: expect.stringMatching(/other-project\/src/), + glob: '**/*.js', + parent: expect.any(String), + plugin: '@tailwindcss/postcss', + }) + }) +}) From ee829e99523c91b8d68b774c5384ae255de9f7fa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 29 Jul 2024 16:00:14 +0200 Subject: [PATCH 2/9] track globs, and pass them to Rust to scan --- packages/@tailwindcss-postcss/src/index.ts | 86 +++++++++++----------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 05d10c5465c5..b47cd9fc5fbc 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -106,50 +106,50 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Do nothing if neither `@tailwind` nor `@apply` is used if (!hasTailwind && !hasApply) return - let css = '' - - // Look for candidates used to generate the CSS - let { candidates, files, globs } = scanDir({ base }) - - // Add all found files as direct dependencies - for (let file of files) { - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file, - parent: result.opts.from, - }) - } - - // 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) { - result.messages.push({ - type: 'dir-dependency', - plugin: '@tailwindcss/postcss', - dir: base, - glob, - parent: result.opts.from, - }) - } - - 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)) - } + let css = '' + + // Look for candidates used to generate the CSS + let { candidates, files, globs } = scanDir({ base }) + + // Add all found files as direct dependencies + for (let file of files) { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file, + parent: result.opts.from, + }) + } + + // 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) { + result.messages.push({ + type: 'dir-dependency', + plugin: '@tailwindcss/postcss', + dir: base, + glob, + parent: result.opts.from, + }) + } + + 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 : []) - } else if (rebuildStrategy === 'incremental') { - css = context.build!(candidates) - } + return require(pluginPath) + }, + }) + context.build = build + css = build(hasTailwind ? candidates : []) + } else if (rebuildStrategy === 'incremental') { + css = context.build!(candidates) + } // Replace CSS if (css !== context.css && optimize) { From 7e05b1bef25a460896843686056ff9c0f979c8ab Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 30 Jul 2024 12:34:38 +0200 Subject: [PATCH 3/9] map `globs` to `Vec` We also made sure to store the actual "compiler" in the cache. This way we keep track of both the `globs` and `build` function. Whenever we need to do a full rebuild, we can create a new compiler instance. --- packages/@tailwindcss-postcss/src/index.ts | 97 ++++++++++++---------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index b47cd9fc5fbc..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' @@ -106,50 +123,46 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Do nothing if neither `@tailwind` nor `@apply` is used if (!hasTailwind && !hasApply) return - let css = '' - - // Look for candidates used to generate the CSS - let { candidates, files, globs } = scanDir({ base }) + let css = '' - // Add all found files as direct dependencies - for (let file of files) { - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file, - parent: result.opts.from, + // Look for candidates used to generate the CSS + 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, + })), }) - } - - // 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) { - result.messages.push({ - type: 'dir-dependency', - plugin: '@tailwindcss/postcss', - dir: base, - glob, - parent: result.opts.from, - }) - } - - 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 : []) - } else if (rebuildStrategy === 'incremental') { - css = context.build!(candidates) - } + // Add all found files as direct dependencies + for (let file of scanDirResult.files) { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file, + parent: result.opts.from, + }) + } + + // 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 scanDirResult.globs) { + result.messages.push({ + type: 'dir-dependency', + plugin: '@tailwindcss/postcss', + dir: base, + glob, + parent: result.opts.from, + }) + } + + if (rebuildStrategy === 'full') { + context.compiler = createCompiler() + css = context.compiler.build(hasTailwind ? scanDirResult.candidates : []) + } else if (rebuildStrategy === 'incremental') { + css = context.compiler.build!(scanDirResult.candidates) + } // Replace CSS if (css !== context.css && optimize) { From 21790f7f08b7f5e51225f5c72fcaabcb9898186e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 15:17:26 +0200 Subject: [PATCH 4/9] add postcss integration test --- integrations/postcss/index.test.ts | 160 +++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 integrations/postcss/index.test.ts diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts new file mode 100644 index 000000000000..3686625e63a1 --- /dev/null +++ b/integrations/postcss/index.test.ts @@ -0,0 +1,160 @@ +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 }) => { + await spawn('pnpm postcss src/index.css --output dist/out.css --watch', { + 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`, + ]) + + 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']`, + ]) + }, +) From f2d9ca8ba80a77b78bbc905f062b09ac33f0be94 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 15:46:35 +0200 Subject: [PATCH 5/9] use Windows and unix type separators to regex --- packages/@tailwindcss-postcss/src/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 013457f4b265..4d0ec31d2ecd 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -268,14 +268,14 @@ describe('@content', () => { expect(result.messages).toContainEqual({ type: 'dependency', - file: expect.stringMatching(/other-project\/src\/index.js$/g), + file: expect.stringMatching(/other-project[\\/]src[\\/]index.js$/g), parent: expect.any(String), plugin: '@tailwindcss/postcss', }) expect(result.messages).toContainEqual({ type: 'dir-dependency', - dir: expect.stringMatching(/other-project\/src/), + dir: expect.stringMatching(/other-project[\\/]src/), glob: '**/*.js', parent: expect.any(String), plugin: '@tailwindcss/postcss', From 339a1d7f0520c5af991c9c58409ba27db0790e53 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 17:45:08 +0200 Subject: [PATCH 6/9] remove test in favor of integration tests --- .../src/fixtures/other-project/src/index.js | 1 - .../@tailwindcss-postcss/src/index.test.ts | 56 ------------------- 2 files changed, 57 deletions(-) delete mode 100644 packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js diff --git a/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js b/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js deleted file mode 100644 index 60abb39c0a9b..000000000000 --- a/packages/@tailwindcss-postcss/src/fixtures/other-project/src/index.js +++ /dev/null @@ -1 +0,0 @@ -const className = "content-['other-project']" diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 4d0ec31d2ecd..ce4699586e31 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -226,59 +226,3 @@ describe('plugins', () => { `) }) }) - -describe('@content', () => { - test('scans custom @content files', async () => { - let processor = postcss([ - tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), - ]) - - let result = await processor.process( - css` - @import 'tailwindcss/utilities'; - @content '../other-project/src/**/*.js'; - `, - { from: INPUT_CSS_PATH }, - ) - - expect(result.css.trim()).toMatchInlineSnapshot(` - ".underline { - text-decoration-line: underline; - } - - .content-\\[\\'other-project\\'\\] { - --tw-content: "other-project"; - content: var(--tw-content); - } - - @supports (-moz-orient: inline) { - @layer base { - *, :before, :after, ::backdrop { - --tw-content: ""; - } - } - } - - @property --tw-content { - syntax: "*"; - inherits: false; - initial-value: ""; - }" - `) - - expect(result.messages).toContainEqual({ - type: 'dependency', - file: expect.stringMatching(/other-project[\\/]src[\\/]index.js$/g), - parent: expect.any(String), - plugin: '@tailwindcss/postcss', - }) - - expect(result.messages).toContainEqual({ - type: 'dir-dependency', - dir: expect.stringMatching(/other-project[\\/]src/), - glob: '**/*.js', - parent: expect.any(String), - plugin: '@tailwindcss/postcss', - }) - }) -}) From daab0a6b516b317d0a504de94fdb132931f4ba90 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 17:56:30 +0200 Subject: [PATCH 7/9] increase retries, and ignore errors in CI --- integrations/utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 = { From f523c72bcc8d561d05a8e19d0de9022ca5e4c4d4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 18:22:26 +0200 Subject: [PATCH 8/9] wait for postcss-cli to be ready --- integrations/postcss/index.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index 3686625e63a1..facbaeec2f56 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -123,9 +123,11 @@ test( }, }, async ({ root, fs, spawn }) => { - await spawn('pnpm postcss src/index.css --output dist/out.css --watch', { - cwd: path.join(root, 'project-a'), - }) + 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('Finished')) await fs.expectFileToContain('project-a/dist/out.css', [ candidate`underline`, @@ -142,6 +144,7 @@ test( module.exports = { className } `, ) + await fs.expectFileToContain('project-a/dist/out.css', [ candidate`[.changed_&]:content-['project-a/src/index.js']`, ]) @@ -153,6 +156,7 @@ test( module.exports = { className } `, ) + await fs.expectFileToContain('project-a/dist/out.css', [ candidate`[.changed_&]:content-['project-b/src/index.js']`, ]) From 1829984a9594a3cfa27d382c23816ed5ce67cf32 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 5 Aug 2024 18:27:16 +0200 Subject: [PATCH 9/9] watch for the correct message --- integrations/postcss/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index facbaeec2f56..a6b3e0353381 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -127,7 +127,7 @@ test( 'pnpm postcss src/index.css --output dist/out.css --watch --verbose', { cwd: path.join(root, 'project-a') }, ) - await process.onStderr((message) => message.includes('Finished')) + await process.onStderr((message) => message.includes('Waiting for file changes...')) await fs.expectFileToContain('project-a/dist/out.css', [ candidate`underline`,