Add support for basic addVariant plugins with new @plugin directive (#13982)

* Add basic `addVariant` plugin support

* Return early

* Load plugins right away instead later

* Use correct type for variant name

* Preliminary support for addVariant plugins in PostCSS plugin

* Add test for compounding plugin variants

* Add basic `loadPlugin` support to Vite plugin

* Add basic `loadPlugin` support to CLI

* add `io.ts` for integrations

* use shared `loadPlugin` from `tailwindcss/io`

* add `tailwindcss-test-utils` to `@tailwindcss/cli` and `@tailwindcss/vite`

* only add `tailwindcss-test-utils` to `tailwindcss` as a dev dependency

Because `src/io.ts` is requiring the plugin.

* move `tailwindcss-test-utils` to `@tailwindcss/postcss `

This is the spot where we actually need it.

* use newer pnpm version

* Duplicate loadPlugin implementation instead of exporting io file

* Remove another io reference

* update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
Adam Wathan 2024-07-11 09:47:26 -04:00 committed by GitHub
parent f7686e1982
commit 54474086c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 247 additions and 24 deletions

View File

@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v3
with:
version: ^8.15.0
version: ^9.5.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3

View File

@ -15,7 +15,7 @@ permissions:
env:
APP_NAME: tailwindcss-oxide
NODE_VERSION: 20
PNPM_VERSION: ^8.15.0
PNPM_VERSION: ^9.5.0
OXIDE_LOCATION: ./crates/node
jobs:

View File

@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Discard invalid `variants` and `utilities` with modifiers ([#13977](https://github.com/tailwindlabs/tailwindcss/pull/13977))
- Add missing utilities that exist in v3, such as `resize`, `fill-none`, `accent-none`, `drop-shadow-none`, and negative `hue-rotate` and `backdrop-hue-rotate` utilities ([#13971](https://github.com/tailwindlabs/tailwindcss/pull/13971))
### Added
- Add support for basic `addVariant` plugins with new `@plugin` directive ([#13982](https://github.com/tailwindlabs/tailwindcss/pull/13982))
## [4.0.0-alpha.17] - 2024-07-04
### Added

View File

@ -6,7 +6,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import postcss from 'postcss'
import atImport from 'postcss-import'
import { compile } from 'tailwindcss'
import * as tailwindcss from 'tailwindcss'
import type { Arg, Result } from '../../utils/args'
import {
eprintln,
@ -124,6 +124,22 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
}
}
let inputFile = args['--input'] && args['--input'] !== '-' ? args['--input'] : process.cwd()
let basePath = path.dirname(path.resolve(inputFile))
function compile(css: string) {
return tailwindcss.compile(css, {
loadPlugin: (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(basePath, pluginPath))
}
return require(pluginPath)
},
})
}
// Compile the input
let { build } = compile(input)

View File

@ -38,6 +38,7 @@
"devDependencies": {
"@types/node": "^20.12.12",
"@types/postcss-import": "^14.0.3",
"postcss": "8.4.24"
"postcss": "8.4.24",
"tailwindcss-test-utils": "workspace:*"
}
}

View File

@ -1 +1 @@
<div class="underline 2xl:font-bold"></div>
<div class="underline 2xl:font-bold hocus:underline inverted:flex"></div>

View File

@ -0,0 +1,4 @@
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}

View File

@ -134,3 +134,65 @@ describe('processing without specifying a base path', () => {
})
})
})
describe('plugins', () => {
test('local CJS plugin', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@import 'tailwindcss/utilities';
@plugin 'tailwindcss-test-utils';
`,
{ from: INPUT_CSS_PATH },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}
@media (inverted-colors: inverted) {
.inverted\\:flex {
display: flex;
}
}
.hocus\\:underline:focus, .hocus\\:underline:hover {
text-decoration-line: underline;
}"
`)
})
test('published CJS plugin', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])
let result = await processor.process(
css`
@import 'tailwindcss/utilities';
@plugin 'tailwindcss-test-utils';
`,
{ from: INPUT_CSS_PATH },
)
expect(result.css.trim()).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}
@media (inverted-colors: inverted) {
.inverted\\:flex {
display: flex;
}
}
.hocus\\:underline:focus, .hocus\\:underline:hover {
text-decoration-line: underline;
}"
`)
})
})

View File

@ -1,6 +1,7 @@
import { scanDir } from '@tailwindcss/oxide'
import fs from 'fs'
import { Features, transform } from 'lightningcss'
import path from 'path'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
import postcssImport from 'postcss-import'
import { compile } from 'tailwindcss'
@ -130,7 +131,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
}
if (rebuildStrategy === 'full') {
let { build } = compile(root.toString())
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') {

View File

@ -72,12 +72,22 @@ export default function tailwindcss(): Plugin[] {
return updated
}
function generateCss(css: string) {
return compile(css).build(Array.from(candidates))
function generateCss(css: string, inputPath: string) {
let basePath = path.dirname(path.resolve(inputPath))
return compile(css, {
loadPlugin: (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(basePath, pluginPath))
}
return require(pluginPath)
},
}).build(Array.from(candidates))
}
function generateOptimizedCss(css: string) {
return optimizeCss(generateCss(css), { minify })
function generateOptimizedCss(css: string, inputPath: string) {
return optimizeCss(generateCss(css, inputPath), { minify })
}
// Manually run the transform functions of non-Tailwind plugins on the given CSS
@ -189,7 +199,7 @@ export default function tailwindcss(): Plugin[] {
await server?.waitForRequestsIdle?.(id)
}
let code = await transformWithPlugins(this, id, generateCss(src))
let code = await transformWithPlugins(this, id, generateCss(src, id))
return { code }
},
},
@ -213,7 +223,7 @@ export default function tailwindcss(): Plugin[] {
continue
}
let css = generateOptimizedCss(file.content)
let css = generateOptimizedCss(file.content, id)
// These plugins have side effects which, during build, results in CSS
// being written to the output dir. We need to run them here to ensure

View File

@ -1,4 +1,4 @@
import { toCss } from './ast'
import { rule, toCss } from './ast'
import { parseCandidate, parseVariant } from './candidate'
import { compileAstNodes, compileCandidates } from './compile'
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
@ -8,6 +8,10 @@ import { Utilities, createUtilities } from './utilities'
import { DefaultMap } from './utils/default-map'
import { Variants, createVariants } from './variants'
export type Plugin = (api: {
addVariant: (name: string, selector: string | string[]) => void
}) => void
export type DesignSystem = {
theme: Theme
utilities: Utilities
@ -25,7 +29,7 @@ export type DesignSystem = {
getUsedVariants(): ReturnType<typeof parseVariant>[]
}
export function buildDesignSystem(theme: Theme): DesignSystem {
export function buildDesignSystem(theme: Theme, plugins: Plugin[] = []): DesignSystem {
let utilities = createUtilities(theme)
let variants = createVariants(theme)
@ -77,5 +81,15 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
},
}
for (let plugin of plugins) {
plugin({
addVariant: (name: string, selectors: string | string[]) => {
variants.static(name, (r) => {
r.nodes = ([] as string[]).concat(selectors).map((selector) => rule(selector, r.nodes))
})
},
})
}
return designSystem
}

View File

@ -1,7 +1,8 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, it, test } from 'vitest'
import { compileCss, run } from './test-utils/run'
import { compile } from '.'
import { compileCss, optimizeCss, run } from './test-utils/run'
const css = String.raw
@ -1123,3 +1124,61 @@ describe('Parsing themes values from CSS', () => {
)
})
})
describe('plugins', () => {
test('addVariant with string selector', () => {
let compiled = compile(
css`
@plugin "my-plugin";
@layer utilities {
@tailwind utilities;
}
`,
{
loadPlugin: () => {
return ({ addVariant }) => {
addVariant('hocus', '&:hover, &:focus')
}
},
},
).build(['hocus:underline'])
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.hocus\\:underline:hover, .hocus\\:underline:focus {
text-decoration-line: underline;
}
}"
`)
})
test('addVariant with array of selectors', () => {
let compiled = compile(
css`
@plugin "my-plugin";
@layer utilities {
@tailwind utilities;
}
`,
{
loadPlugin: () => {
return ({ addVariant }) => {
addVariant('hocus', ['&:hover', '&:focus'])
}
},
},
).build(['hocus:underline', 'group-hocus:flex'])
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
"@layer utilities {
.group-hocus\\:flex:is(:where(.group):hover *), .group-hocus\\:flex:is(:where(.group):focus *) {
display: flex;
}
.hocus\\:underline:hover, .hocus\\:underline:focus {
text-decoration-line: underline;
}
}"
`)
})
})

View File

@ -2,10 +2,21 @@ import { version } from '../package.json'
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
import { compileCandidates } from './compile'
import * as CSS from './css-parser'
import { buildDesignSystem } from './design-system'
import { buildDesignSystem, type Plugin } from './design-system'
import { Theme } from './theme'
export function compile(css: string): {
type CompileOptions = {
loadPlugin?: (path: string) => Plugin
}
function throwOnPlugin(): never {
throw new Error('No `loadPlugin` function provided to `compile`')
}
export function compile(
css: string,
{ loadPlugin = throwOnPlugin }: CompileOptions = {},
): {
build(candidates: string[]): string
} {
let ast = CSS.parse(css)
@ -22,12 +33,20 @@ export function compile(css: string): {
// Find all `@theme` declarations
let theme = new Theme()
let plugins: Plugin[] = []
let firstThemeRule: Rule | null = null
let keyframesRules: Rule[] = []
walk(ast, (node, { replaceWith }) => {
if (node.kind !== 'rule') return
// Collect paths from `@plugin` at-rules
if (node.selector.startsWith('@plugin ')) {
plugins.push(loadPlugin(node.selector.slice(9, -1)))
replaceWith([])
return
}
// Drop instances of `@media reference`
//
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
@ -125,7 +144,7 @@ export function compile(css: string): {
firstThemeRule.nodes = nodes
}
let designSystem = buildDesignSystem(theme)
let designSystem = buildDesignSystem(theme, plugins)
let tailwindUtilitiesNode: Rule | null = null

View File

@ -0,0 +1,4 @@
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}

View File

@ -0,0 +1,5 @@
{
"name": "tailwindcss-test-utils",
"private": true,
"main": "index.js"
}

View File

@ -1 +1,2 @@
@import 'tailwindcss';
@plugin "./plugin.js";

View File

@ -4,6 +4,7 @@ export function App() {
return (
<div className="m-3 p-3 border">
<h1 className="text-blue-500">Hello World</h1>
<button className="hocus:underline">Click me</button>
<Foo />
</div>
)

View File

@ -0,0 +1,4 @@
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}

21
pnpm-lock.yaml generated
View File

@ -156,6 +156,9 @@ importers:
postcss:
specifier: 8.4.24
version: 8.4.24
tailwindcss-test-utils:
specifier: workspace:*
version: link:../test-utils
packages/@tailwindcss-vite:
dependencies:
@ -188,6 +191,8 @@ importers:
specifier: ^1.25.1
version: 1.25.1
packages/test-utils: {}
playgrounds/nextjs:
dependencies:
'@tailwindcss/postcss':
@ -675,6 +680,7 @@ packages:
'@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
@ -682,6 +688,7 @@ packages:
'@humanwhocodes/object-schema@2.0.3':
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
@ -1678,6 +1685,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
@ -2444,6 +2452,7 @@ packages:
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rollup@4.18.0:
@ -4083,7 +4092,7 @@ snapshots:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.1(eslint@8.57.0)
@ -4102,12 +4111,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 4.3.4
enhanced-resolve: 5.16.1
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@ -4119,14 +4128,14 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
@ -4140,7 +4149,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3