Skip to content

Commit

Permalink
Support complex addUtility() configs
Browse files Browse the repository at this point in the history
  • Loading branch information
philipp-spiess authored and RobinMalfait committed Nov 18, 2024
1 parent 08c6c96 commit b053ede
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 30 deletions.
40 changes: 40 additions & 0 deletions integrations/cli/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,46 @@ test(
},
)

test(
'builds the `@tailwindcss/aspect-ratio` plugin utilities',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="aspect-w-16 aspect-h-9">
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/aspect-ratio';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')

await fs.expectFileToContain('dist/out.css', [
//
candidate`aspect-w-16`,
candidate`aspect-h-9`,
])
},
)

test(
'builds the `tailwindcss-animate` plugin utilities',
{
Expand Down
87 changes: 79 additions & 8 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2760,7 +2760,7 @@ describe('addUtilities()', () => {
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
'.text-trim > *': {
':hover > *': {
'text-box-trim': 'both',
'text-box-edge': 'cap alphabetic',
},
Expand Down Expand Up @@ -2842,18 +2842,89 @@ describe('addUtilities()', () => {
},
)

expect(optimizeCss(compiled.build(['form-input', 'lg:form-textarea'])).trim())
.toMatchInlineSnapshot(`
".form-input, .form-input::placeholder {
expect(compiled.build(['form-input', 'lg:form-textarea']).trim()).toMatchInlineSnapshot(`
".form-input {
background-color: red;
&::placeholder {
background-color: red;
}
}
.lg\\:form-textarea {
@media (width >= 1024px) {
.lg\\:form-textarea:hover:focus {
&:hover:focus {
background-color: red;
}
}"
`)
}
}"
`)
})

test('nests complex utility names', async () => {
let compiled = await compile(
css`
@plugin "my-plugin";
@layer utilities {
@tailwind utilities;
}
`,
{
async loadModule(id, base) {
return {
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
'.a .b:hover .c': {
color: 'red',
},
'.d > *': {
color: 'red',
},
'.e .bar:not(.f):has(.g)': {
color: 'red',
},
})
},
}
},
},
)

expect(compiled.build(['a', 'b', 'c', 'd', 'e', 'f', 'g']).trim()).toMatchInlineSnapshot(
`
"@layer utilities {
.a {
& .b:hover .c {
color: red;
}
}
.b {
.a &:hover .c {
color: red;
}
}
.c {
.a .b:hover & {
color: red;
}
}
.d {
& > * {
color: red;
}
}
.e {
& .bar:not(.f):has(.g) {
color: red;
}
}
.g {
.e .bar:not(.f):has(&) {
color: red;
}
}
}"
`,
)
})
})

Expand Down
62 changes: 40 additions & 22 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { substituteFunctions } from '../css-functions'
import * as CSS from '../css-parser'
import type { DesignSystem } from '../design-system'
import { withAlpha } from '../utilities'
import { DefaultMap } from '../utils/default-map'
import { inferDataType } from '../utils/infer-data-type'
import { segment } from '../utils/segment'
import { toKeyPath } from '../utils/to-key-path'
import * as ValueParser from '../value-parser'
import { compoundsForSelectors, substituteAtSlot } from '../variants'
import type { ResolvedConfig, UserConfig } from './config/types'
import { createThemeFn } from './plugin-functions'
Expand Down Expand Up @@ -198,40 +200,56 @@ export function buildPluginApi(
)

// Merge entries for the same class
let utils: Record<string, CssInJs[]> = {}
let utils = new DefaultMap<string, AstNode[]>(() => [])

for (let [name, css] of entries) {
let [className, ...parts] = segment(name, ':')

// Modify classes using pseudo-classes or pseudo-elements to use nested rules
if (parts.length > 0) {
let pseudos = parts.map((p) => `:${p.trim()}`).join('')
css = {
[`&${pseudos}`]: css,
}
}

utils[className] ??= []
css = Array.isArray(css) ? css : [css]
utils[className].push(...css)
}

for (let [name, css] of Object.entries(utils)) {
if (name.startsWith('@keyframes ')) {
ast.push(rule(name, objectToAst(css)))
continue
}

if (name[0] !== '.' || !IS_VALID_UTILITY_NAME.test(name.slice(1))) {
if (name[0] === '.' && IS_VALID_UTILITY_NAME.test(name.slice(1))) {
utils.get(name.slice(1)).push(...objectToAst(css))
continue
}

let selectorAst = ValueParser.parse(name)
let foundValidUtility = false
ValueParser.walk(selectorAst, (node) => {
if (
node.kind === 'word' &&
node.value[0] === '.' &&
IS_VALID_UTILITY_NAME.test(node.value.slice(1))
) {
let value = node.value
node.value = '&'
let selector = ValueParser.toCss(selectorAst)

let className = value.slice(1)
utils.get(className).push(rule(selector, objectToAst(css)))
foundValidUtility = true

node.value = value
return
}

if (node.kind === 'function' && node.value === 'not') {
return ValueParser.ValueWalkAction.Skip
}
})

if (!foundValidUtility) {
throw new Error(
`\`addUtilities({ '${name}' : … })\` defines an invalid utility selector. Utilities must be a single class name and start with a lowercase letter, eg. \`.scrollbar-none\`.`,
)
}
}

designSystem.utilities.static(name.slice(1), () => {
let ast = objectToAst(css)
substituteAtApply(ast, designSystem)
return ast
for (let [className, ast] of utils) {
designSystem.utilities.static(className, () => {
let clonedAst = structuredClone(ast)
substituteAtApply(clonedAst, designSystem)
return clonedAst
})
}
},
Expand Down

0 comments on commit b053ede

Please sign in to comment.