mirror of
https://github.com/hanzoai/tailwindcss.git
synced 2026-09-24 17:03:14 +00:00
Polish match APIs (#5664)
* fix incorrect logic for validating content paths * remove `includeRules` helper * generate keyframes as part of the animate plugin * add matchUtilities * splitup `variantPlugins` and `corePlugins`
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import corePlugins from '../src/corePlugins'
|
||||
import { corePlugins } from '../src/corePlugins'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
let corePluginList = Object.keys(corePlugins).filter((plugin) => !plugin.includes('Variants'))
|
||||
let corePluginList = Object.keys(corePlugins)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(process.cwd(), 'src', 'corePluginList.js'),
|
||||
|
||||
+18
-21
@@ -20,8 +20,7 @@ import {
|
||||
import { version as tailwindVersion } from '../package.json'
|
||||
import log from './util/log'
|
||||
|
||||
export default {
|
||||
// Variant plugins
|
||||
export let variantPlugins = {
|
||||
pseudoElementVariants: ({ config, addVariant }) => {
|
||||
addVariant(
|
||||
'first-letter',
|
||||
@@ -360,8 +359,9 @@ export default {
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Actual plugins
|
||||
export let corePlugins = {
|
||||
preflight: ({ addBase }) => {
|
||||
let preflightStyles = postcss.parse(fs.readFileSync(`${__dirname}/css/preflight.css`, 'utf8'))
|
||||
|
||||
@@ -753,31 +753,28 @@ export default {
|
||||
let prefixName = (name) => prefix(`.${name}`).slice(1)
|
||||
let keyframes = Object.fromEntries(
|
||||
Object.entries(theme('keyframes') ?? {}).map(([key, value]) => {
|
||||
return [key, [{ [`@keyframes ${prefixName(key)}`]: value }]]
|
||||
return [key, { [`@keyframes ${prefixName(key)}`]: value }]
|
||||
})
|
||||
)
|
||||
|
||||
matchUtilities(
|
||||
{
|
||||
animate: (value, { includeRules }) => {
|
||||
animate: (value) => {
|
||||
let animations = parseAnimationValue(value)
|
||||
|
||||
for (let { name } of animations) {
|
||||
if (keyframes[name] !== undefined) {
|
||||
includeRules(keyframes[name], { respectImportant: false })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
animation: animations
|
||||
.map(({ name, value }) => {
|
||||
if (name === undefined || keyframes[name] === undefined) {
|
||||
return value
|
||||
}
|
||||
return value.replace(name, prefixName(name))
|
||||
})
|
||||
.join(', '),
|
||||
}
|
||||
return [
|
||||
...animations.flatMap((animation) => keyframes[animation.name]),
|
||||
{
|
||||
animation: animations
|
||||
.map(({ name, value }) => {
|
||||
if (name === undefined || keyframes[name] === undefined) {
|
||||
return value
|
||||
}
|
||||
return value.replace(name, prefixName(name))
|
||||
})
|
||||
.join(', '),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
{ values: theme('animation') }
|
||||
|
||||
@@ -108,11 +108,6 @@ function applyVariant(variant, matches, context) {
|
||||
let result = []
|
||||
|
||||
for (let [meta, rule] of matches) {
|
||||
if (meta.options.respectVariants === false) {
|
||||
result.push([meta, rule])
|
||||
continue
|
||||
}
|
||||
|
||||
let container = postcss.root({ nodes: [rule.clone()] })
|
||||
|
||||
for (let [variantSort, variantFunction] of variantFunctionTuples) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import escapeClassName from '../util/escapeClassName'
|
||||
import nameClass, { formatClass } from '../util/nameClass'
|
||||
import { coerceValue } from '../util/pluginUtils'
|
||||
import bigSign from '../util/bigSign'
|
||||
import corePlugins from '../corePlugins'
|
||||
import { variantPlugins, corePlugins } from '../corePlugins'
|
||||
import * as sharedState from './sharedState'
|
||||
import { env } from './sharedState'
|
||||
import { toPath } from '../util/toPath'
|
||||
@@ -237,17 +237,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
},
|
||||
addComponents(components, options) {
|
||||
let defaultOptions = {
|
||||
variants: [],
|
||||
respectPrefix: true,
|
||||
respectImportant: false,
|
||||
respectVariants: true,
|
||||
}
|
||||
|
||||
options = Object.assign(
|
||||
{},
|
||||
defaultOptions,
|
||||
Array.isArray(options) ? { variants: options } : options
|
||||
)
|
||||
options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options)
|
||||
|
||||
for (let [identifier, rule] of withIdentifiers(components)) {
|
||||
let prefixedIdentifier = prefixIdentifier(identifier, options)
|
||||
@@ -266,17 +260,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
},
|
||||
addUtilities(utilities, options) {
|
||||
let defaultOptions = {
|
||||
variants: [],
|
||||
respectPrefix: true,
|
||||
respectImportant: true,
|
||||
respectVariants: true,
|
||||
}
|
||||
|
||||
options = Object.assign(
|
||||
{},
|
||||
defaultOptions,
|
||||
Array.isArray(options) ? { variants: options } : options
|
||||
)
|
||||
options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options)
|
||||
|
||||
for (let [identifier, rule] of withIdentifiers(utilities)) {
|
||||
let prefixedIdentifier = prefixIdentifier(identifier, options)
|
||||
@@ -295,10 +283,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
},
|
||||
matchUtilities: function (utilities, options) {
|
||||
let defaultOptions = {
|
||||
variants: [],
|
||||
respectPrefix: true,
|
||||
respectImportant: true,
|
||||
respectVariants: true,
|
||||
}
|
||||
|
||||
options = { ...defaultOptions, ...options }
|
||||
@@ -338,21 +324,14 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
return []
|
||||
}
|
||||
|
||||
let includedRules = []
|
||||
let ruleSets = []
|
||||
.concat(
|
||||
rule(value, {
|
||||
includeRules(rules) {
|
||||
includedRules.push(...rules)
|
||||
},
|
||||
})
|
||||
)
|
||||
.concat(rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
}))
|
||||
|
||||
return [...includedRules, ...ruleSets]
|
||||
return ruleSets
|
||||
}
|
||||
|
||||
let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped]
|
||||
@@ -361,6 +340,68 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
||||
context.candidateRuleMap.set(prefixedIdentifier, [])
|
||||
}
|
||||
|
||||
context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets)
|
||||
}
|
||||
},
|
||||
matchComponents: function (components, options) {
|
||||
let defaultOptions = {
|
||||
respectPrefix: true,
|
||||
respectImportant: false,
|
||||
}
|
||||
|
||||
options = { ...defaultOptions, ...options }
|
||||
|
||||
let offset = offsets.components++
|
||||
|
||||
for (let identifier in components) {
|
||||
let prefixedIdentifier = prefixIdentifier(identifier, options)
|
||||
let rule = components[identifier]
|
||||
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
let { type = 'any' } = options
|
||||
type = [].concat(type)
|
||||
let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!type.includes(coercedType)) {
|
||||
if (isOnlyPlugin) {
|
||||
log.warn([
|
||||
`Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
|
||||
`You can safely update it to \`${identifier}-${modifier.replace(
|
||||
coercedType + ':',
|
||||
''
|
||||
)}\`.`,
|
||||
])
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ruleSets = []
|
||||
.concat(rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
}))
|
||||
|
||||
return ruleSets
|
||||
}
|
||||
|
||||
let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped]
|
||||
|
||||
if (!context.candidateRuleMap.has(prefixedIdentifier)) {
|
||||
context.candidateRuleMap.set(prefixedIdentifier, [])
|
||||
}
|
||||
|
||||
context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets)
|
||||
}
|
||||
},
|
||||
@@ -457,7 +498,7 @@ function collectLayerPlugins(root) {
|
||||
}
|
||||
|
||||
function resolvePlugins(context, root) {
|
||||
let corePluginList = Object.entries(corePlugins)
|
||||
let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins })
|
||||
.map(([name, plugin]) => {
|
||||
if (!context.tailwindConfig.corePlugins.includes(name)) {
|
||||
return null
|
||||
@@ -479,12 +520,15 @@ function resolvePlugins(context, root) {
|
||||
|
||||
// TODO: This is a workaround for backwards compatibility, since custom variants
|
||||
// were historically sorted before screen/stackable variants.
|
||||
let beforeVariants = [corePlugins['pseudoElementVariants'], corePlugins['pseudoClassVariants']]
|
||||
let beforeVariants = [
|
||||
variantPlugins['pseudoElementVariants'],
|
||||
variantPlugins['pseudoClassVariants'],
|
||||
]
|
||||
let afterVariants = [
|
||||
corePlugins['directionVariants'],
|
||||
corePlugins['reducedMotionVariants'],
|
||||
corePlugins['darkVariants'],
|
||||
corePlugins['screenVariants'],
|
||||
variantPlugins['directionVariants'],
|
||||
variantPlugins['reducedMotionVariants'],
|
||||
variantPlugins['darkVariants'],
|
||||
variantPlugins['screenVariants'],
|
||||
]
|
||||
|
||||
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
|
||||
|
||||
+21
-23
@@ -18,14 +18,18 @@ export function normalizeConfig(config) {
|
||||
*/
|
||||
let valid = (() => {
|
||||
// `config.purge` should not exist anymore
|
||||
if (config.purge) return false
|
||||
if (config.purge) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should exist
|
||||
if (!config.content) return false
|
||||
if (!config.content) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should be an object or an array
|
||||
if (
|
||||
!Array.isArray(config.content) ||
|
||||
!Array.isArray(config.content) &&
|
||||
!(typeof config.content === 'object' && config.content !== null)
|
||||
) {
|
||||
return false
|
||||
@@ -38,17 +42,15 @@ export function normalizeConfig(config) {
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
if (
|
||||
// `raw` must be a string
|
||||
typeof path?.raw === 'string' &&
|
||||
// `extension` (if provided) should also be a string
|
||||
path?.extension !== undefined &&
|
||||
typeof path?.extension === 'string'
|
||||
) {
|
||||
return true
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,17 +71,15 @@ export function normalizeConfig(config) {
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
if (
|
||||
// `raw` must be a string
|
||||
typeof path?.raw === 'string' &&
|
||||
// `extension` (if provided) should also be a string
|
||||
path?.extension !== undefined &&
|
||||
typeof path?.extension === 'string'
|
||||
) {
|
||||
return true
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
return true
|
||||
})
|
||||
) {
|
||||
return false
|
||||
@@ -92,7 +92,6 @@ export function normalizeConfig(config) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (
|
||||
!(config.content.extract === undefined || typeof config.content.extract === 'function')
|
||||
) {
|
||||
@@ -106,7 +105,6 @@ export function normalizeConfig(config) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (
|
||||
!(
|
||||
config.content.transform === undefined || typeof config.content.transform === 'function'
|
||||
|
||||
@@ -1500,25 +1500,18 @@ test('the configFunction parameter is optional when using the `createPlugin.with
|
||||
|
||||
test('keyframes are not escaped', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="foo-[abc]"></div>` }],
|
||||
content: [{ raw: html`<div class="foo-[abc] md:foo-[def]"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
plugins: [
|
||||
function ({ matchUtilities }) {
|
||||
matchUtilities({
|
||||
foo: (value, { includeRules }) => {
|
||||
includeRules(
|
||||
[
|
||||
{
|
||||
[`@keyframes ${value}`]: {
|
||||
'25.001%': {
|
||||
color: 'black',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{ respectImportant: false }
|
||||
)
|
||||
|
||||
foo: (value) => {
|
||||
return {
|
||||
[`@keyframes ${value}`]: {
|
||||
'25.001%': {
|
||||
color: 'black',
|
||||
},
|
||||
},
|
||||
animation: `${value} 1s infinite`,
|
||||
}
|
||||
},
|
||||
@@ -1528,15 +1521,27 @@ test('keyframes are not escaped', () => {
|
||||
}
|
||||
|
||||
return run('@tailwind utilities', config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
expect(result.css).toMatchFormattedCss(`
|
||||
@keyframes abc {
|
||||
25.001% {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.foo-\\[abc\\] {
|
||||
animation: abc 1s infinite;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@keyframes def {
|
||||
25.md\\:001\\% {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
.md\\:foo-\\[def\\] {
|
||||
animation: def 1s infinite;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { run, html, css } from './util/run'
|
||||
|
||||
it('should be possible to matchComponents', () => {
|
||||
let config = {
|
||||
content: [
|
||||
{
|
||||
raw: html`<div class="card-[#0088cc] hover:card-[#f0f]">
|
||||
<div class="card-header font-bold"></div>
|
||||
<div class="shadow"></div>
|
||||
<div class="card-footer text-center"></div>
|
||||
</div>`,
|
||||
},
|
||||
],
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
plugins: [
|
||||
function ({ matchComponents }) {
|
||||
matchComponents({
|
||||
card: (value) => {
|
||||
return [
|
||||
{ color: value },
|
||||
{
|
||||
'.card-header': {
|
||||
borderTopWidth: 3,
|
||||
borderTopColor: value,
|
||||
},
|
||||
},
|
||||
{
|
||||
'.card-footer': {
|
||||
borderBottomWidth: 3,
|
||||
borderBottomColor: value,
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return run('@tailwind base; @tailwind components; @tailwind utilities', config).then((result) => {
|
||||
return expect(result.css).toMatchFormattedCss(css`
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
.card-\\[\\#0088cc\\] {
|
||||
color: #0088cc;
|
||||
}
|
||||
|
||||
.card-\\[\\#0088cc\\] .card-header {
|
||||
border-top-width: 3px;
|
||||
border-top-color: #0088cc;
|
||||
}
|
||||
|
||||
.card-\\[\\#0088cc\\] .card-footer {
|
||||
border-bottom-width: 3px;
|
||||
border-bottom-color: #0088cc;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
||||
var(--tw-shadow);
|
||||
}
|
||||
|
||||
.hover\\:card-\\[\\#f0f\\]:hover {
|
||||
color: #f0f;
|
||||
}
|
||||
|
||||
.hover\\:card-\\[\\#f0f\\]:hover .hover\\:card-header:hover {
|
||||
border-top-width: 3px;
|
||||
border-top-color: #f0f;
|
||||
}
|
||||
|
||||
.hover\\:card-\\[\\#f0f\\]:hover .hover\\:card-footer:hover {
|
||||
border-bottom-width: 3px;
|
||||
border-bottom-color: #f0f;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
@@ -196,6 +196,14 @@
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
||||
var(--tw-shadow);
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.hover\:animate-spin:hover {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.hover\:shadow-md:hover {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
||||
@@ -622,6 +630,14 @@
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.lg\:animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.lg\:shadow-md {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
<div class="lg:shadow-md"></div>
|
||||
<div class="xl:shadow-md"></div>
|
||||
<div class="2xl:shadow-md"></div>
|
||||
<div class="lg:animate-spin"></div>
|
||||
<div class="hover:animate-spin"></div>
|
||||
|
||||
<!-- Stacked variants -->
|
||||
<div class="open:hover:bg-red-200"></div>
|
||||
|
||||
@@ -46,3 +46,83 @@ test('stacked peer variants', async () => {
|
||||
let result = await run(input, config)
|
||||
expect(result.css).toIncludeCss(expected)
|
||||
})
|
||||
|
||||
it('should properly handle keyframes with multiple variants', async () => {
|
||||
let config = {
|
||||
content: [
|
||||
{
|
||||
raw: 'animate-spin hover:animate-spin focus:animate-spin hover:animate-bounce focus:animate-bounce',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
let result = await run(input, config)
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.hover\\:animate-spin:hover {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.hover\\:animate-bounce:hover {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.focus\\:animate-spin:focus {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.focus\\:animate-bounce:focus {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user