Skip to content

Commit

Permalink
Add @content support to @tailwindcss/postcss (#14080)
Browse files Browse the repository at this point in the history
This PR adds support for `@content` and makes sure that globs are
registered within PostCSS.

From a configuration perspective nothing changes for the end user. It
does however make sure that `@content` can be used and changes in those
globs will be picked up.
  • Loading branch information
RobinMalfait committed Aug 7, 2024
1 parent c576824 commit 2424fb5
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 19 deletions.
164 changes: 164 additions & 0 deletions integrations/postcss/index.test.ts
Original file line number Diff line number Diff line change
@@ -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`
<div
class="underline 2xl:font-bold hocus:underline inverted:flex"
></div>
`,
'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`
<div
class="underline 2xl:font-bold hocus:underline inverted:flex"
></div>
`,
'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']`,
])
},
)
10 changes: 9 additions & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,18 @@ export function test(
}

let disposables: (() => Promise<void>)[] = []

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 = {
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 @@ -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()
})

Expand Down
47 changes: 30 additions & 17 deletions packages/@tailwindcss-postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
let cache = new DefaultMap(() => {
return {
mtimes: new Map<string, number>(),
build: null as null | ReturnType<typeof compile>['build'],
compiler: null as null | ReturnType<typeof compile>,
css: '',
optimizedCss: '',
}
Expand Down Expand Up @@ -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'

Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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
Expand Down

0 comments on commit 2424fb5

Please sign in to comment.