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:
Robin Malfait
2021-10-01 17:31:09 +02:00
committed by GitHub
parent 241109b958
commit 3a14e2ba6f
10 changed files with 331 additions and 99 deletions
+2 -2
View File
@@ -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
View File
@@ -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') }
-5
View File
@@ -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) {
+76 -32
View File
@@ -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
View File
@@ -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'
+21 -16
View File
@@ -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;
}
}
`)
})
})
+95
View File
@@ -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;
}
`)
})
})
+16
View File
@@ -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),
+2
View File
@@ -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>
+80
View File
@@ -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;
}
`)
})