From 3a9a319475b86d58f81634b67b5495e1d661837d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 10:43:43 -0400 Subject: [PATCH 01/20] Change `matchVariant` API to use positional arguments --- src/corePlugins.js | 5 ++--- src/lib/setupContextUtils.js | 4 ++-- tests/match-variants.test.js | 36 ++++++++++++++++++------------------ types/config.d.ts | 18 +++++++----------- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/corePlugins.js b/src/corePlugins.js index bd1e43c160ff..504ef5451a48 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -153,8 +153,7 @@ export let variantPlugins = { for (let [name, fn] of Object.entries(variants)) { matchVariant( name, - (ctx = {}) => { - let { modifier, value = '' } = ctx + (value = '', modifier) => { if (modifier) { log.warn(`modifier-${name}-experimental`, [ `The ${name} variant modifier feature in Tailwind CSS is currently in preview.`, @@ -232,7 +231,7 @@ export let variantPlugins = { supportsVariants: ({ matchVariant, theme }) => { matchVariant( 'supports', - ({ value = '' }) => { + (value = '') => { let check = normalize(value) let isRaw = /^\w*\s*\(/.test(check) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index fa0969ffb1a4..bfd9483ef8ae 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -525,7 +525,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs for (let [key, value] of Object.entries(options?.values ?? {})) { api.addVariant( isSpecial ? `${variant}${key}` : `${variant}-${key}`, - Object.assign(({ args, container }) => variantFn({ ...args, container, value }), { + Object.assign(({ args }) => variantFn(value, args.modifier), { [MATCH_VARIANT]: true, }), { ...options, value, id } @@ -534,7 +534,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs api.addVariant( variant, - Object.assign(({ args, container }) => variantFn({ ...args, container }), { + Object.assign(({ args }) => variantFn(args.value, args.modifier), { [MATCH_VARIANT]: true, }), { ...options, id } diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js index 7b80a0639eb3..fad17193617d 100644 --- a/tests/match-variants.test.js +++ b/tests/match-variants.test.js @@ -10,7 +10,7 @@ test('partial arbitrary variants', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('potato', ({ value: flavor }) => `.potato-${flavor} &`) + matchVariant('potato', (flavor) => `.potato-${flavor} &`) }, ], } @@ -43,7 +43,7 @@ test('partial arbitrary variants with at-rules', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor})`) + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) }, ], } @@ -79,7 +79,7 @@ test('partial arbitrary variants with at-rules and placeholder', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor}) { &:potato }`) + matchVariant('potato', (flavor) => `@media (potato: ${flavor}) { &:potato }`) }, ], } @@ -115,7 +115,7 @@ test('partial arbitrary variants with default values', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('tooltip', ({ value: side }) => `&${side}`, { + matchVariant('tooltip', (side) => `&${side}`, { values: { bottom: '[data-location="bottom"]', top: '[data-location="top"]', @@ -154,7 +154,7 @@ test('matched variant values maintain the sort order they are registered in', () corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('alphabet', ({ value: side }) => `&${side}`, { + matchVariant('alphabet', (side) => `&${side}`, { values: { a: '[data-value="a"]', b: '[data-value="b"]', @@ -201,7 +201,7 @@ test('matchVariant can return an array of format strings from the function', () corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('test', ({ value: selector }) => + matchVariant('test', (selector) => selector.split(',').map((selector) => `&.${selector} > *`) ) }, @@ -243,7 +243,7 @@ it('should be possible to sort variants', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, @@ -287,7 +287,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', () corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { values: { example: '600px', }, @@ -347,13 +347,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', ({ value }) => `@media (max-width: ${value})`, { + matchVariant('max', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -412,13 +412,13 @@ it('should maintain sort from other variants, if sort functions of arbitrary var corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', ({ value }) => `@media (max-width: ${value})`, { + matchVariant('max', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -464,12 +464,12 @@ it('should sort arbitrary variants left to right (1)', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', ({ value }) => `@media (max-width: ${value})`, { + matchVariant('max', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -532,12 +532,12 @@ it('should sort arbitrary variants left to right (2)', () => { corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { return parseInt(a.value) - parseInt(z.value) }, }) - matchVariant('max', ({ value }) => `@media (max-width: ${value})`, { + matchVariant('max', (value) => `@media (max-width: ${value})`, { sort(a, z) { return parseInt(z.value) - parseInt(a.value) }, @@ -598,7 +598,7 @@ it('should guarantee that we are not passing values from other variants to the w corePlugins: { preflight: false }, plugins: [ ({ matchVariant }) => { - matchVariant('min', ({ value }) => `@media (min-width: ${value})`, { + matchVariant('min', (value) => `@media (min-width: ${value})`, { sort(a, z) { let lookup = ['100px', '200px'] if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { @@ -607,7 +607,7 @@ it('should guarantee that we are not passing values from other variants to the w return lookup.indexOf(a.value) - lookup.indexOf(z.value) }, }) - matchVariant('max', ({ value }) => `@media (max-width: ${value})`, { + matchVariant('max', (value) => `@media (max-width: ${value})`, { sort(a, z) { let lookup = ['300px', '400px'] if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { diff --git a/types/config.d.ts b/types/config.d.ts index 2e6fb1438019..3412d377a50f 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -296,18 +296,14 @@ export interface PluginAPI { addBase(base: CSSRuleObject | CSSRuleObject[]): void // for registering custom variants addVariant(name: string, definition: string | string[] | (() => string) | (() => string)[]): void - matchVariant( + matchVariant( name: string, - cb: (options: { value: string; modifier: string | null }) => string | string[] - ): void - matchVariant( - name: string, - cb: (options: { value: string; modifier: string | null }) => string | string[], - options: { - values: Values - sort( - a: { value: keyof Values | string; modifier: string | null }, - b: { value: keyof Values | string; modifier: string | null } + cb: (value: T | string, modifier: string | null) => string | string[], + options?: { + values?: KeyValuePair, + sort?( + a: { value: T | string; modifier: string | null }, + b: { value: T | string; modifier: string | null } ): number } ): void From 605843f04cd2d31f9f62085d0008c12de19ece79 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 17:11:50 -0400 Subject: [PATCH 02/20] Fix CS wip --- types/config.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/types/config.d.ts b/types/config.d.ts index 3412d377a50f..0d980284e1bd 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -323,7 +323,10 @@ export type PluginCreator = (api: PluginAPI) => void export type PluginsConfig = ( | PluginCreator | { handler: PluginCreator; config?: Partial } - | { (options: any): { handler: PluginCreator; config?: Partial }; __isOptionsFunction: true } + | { + (options: any): { handler: PluginCreator; config?: Partial } + __isOptionsFunction: true + } )[] // Top level config related From c21a849c5fe8c3b5b0bace77f190310fa8647556 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 17:12:47 -0400 Subject: [PATCH 03/20] Change match variant wrap modifier in an object Needed for compat w/ some group and peer plugins --- src/corePlugins.js | 12 ++++++------ src/lib/setupContextUtils.js | 18 ++++++++++++------ types/config.d.ts | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/corePlugins.js b/src/corePlugins.js index 504ef5451a48..0ae64d9b0762 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -144,27 +144,27 @@ export let variantPlugins = { } let variants = { - group: ({ modifier }) => + group: (_, { modifier }) => modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'], - peer: ({ modifier }) => + peer: (_, { modifier }) => modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'], } for (let [name, fn] of Object.entries(variants)) { matchVariant( name, - (value = '', modifier) => { - if (modifier) { + (value = '', extra) => { + if (extra.modifier) { log.warn(`modifier-${name}-experimental`, [ `The ${name} variant modifier feature in Tailwind CSS is currently in preview.`, 'Preview features are not covered by semver, and may be improved in breaking ways at any time.', ]) } - let result = normalize(typeof value === 'function' ? value(ctx) : value) + let result = normalize(typeof value === 'function' ? value(extra) : value) if (!result.includes('&')) result = '&' + result - let [a, b] = fn({ modifier }) + let [a, b] = fn('', extra) return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b) }, { values: Object.fromEntries(pseudoVariants) } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index bfd9483ef8ae..bbc2454c4376 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -525,18 +525,24 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs for (let [key, value] of Object.entries(options?.values ?? {})) { api.addVariant( isSpecial ? `${variant}${key}` : `${variant}-${key}`, - Object.assign(({ args }) => variantFn(value, args.modifier), { - [MATCH_VARIANT]: true, - }), + Object.assign( + ({ args, container }) => variantFn(value, { modifier: args.modifier, container }), + { + [MATCH_VARIANT]: true, + } + ), { ...options, value, id } ) } api.addVariant( variant, - Object.assign(({ args }) => variantFn(args.value, args.modifier), { - [MATCH_VARIANT]: true, - }), + Object.assign( + ({ args, container }) => variantFn(args.value, { modifier: args.modifier, container }), + { + [MATCH_VARIANT]: true, + } + ), { ...options, id } ) }, diff --git a/types/config.d.ts b/types/config.d.ts index 0d980284e1bd..b08490cd1641 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -298,9 +298,9 @@ export interface PluginAPI { addVariant(name: string, definition: string | string[] | (() => string) | (() => string)[]): void matchVariant( name: string, - cb: (value: T | string, modifier: string | null) => string | string[], + cb: (value: T | string, extra: { modifier: string | null }) => string | string[], options?: { - values?: KeyValuePair, + values?: KeyValuePair sort?( a: { value: T | string; modifier: string | null }, b: { value: T | string; modifier: string | null } From d5692e382644247e20871fbe372706a6a223144b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 17:22:18 -0400 Subject: [PATCH 04/20] Add modifier support to matchUtilities --- types/config.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/types/config.d.ts b/types/config.d.ts index b08490cd1641..feef89adc732 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -12,7 +12,7 @@ interface RecursiveKeyValuePair { [key: string]: V | RecursiveKeyValuePair } type ResolvableTo = T | ((utils: PluginUtils) => T) -type CSSRuleObject = RecursiveKeyValuePair +type CSSRuleObject = RecursiveKeyValuePair interface PluginUtils { colors: DefaultColors @@ -263,8 +263,11 @@ export interface PluginAPI { }> ): void // for registering new dynamic utility styles - matchUtilities( - utilities: KeyValuePair CSSRuleObject>, + matchUtilities( + utilities: KeyValuePair< + string, + (value: T | string, extra: { modifier: string | null }) => CSSRuleObject + >, options?: Partial<{ respectPrefix: boolean respectImportant: boolean From d4a08a72813bd50809dc64f9597656e189c93eff Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 18:02:15 -0400 Subject: [PATCH 05/20] refactor --- src/lib/generateRules.js | 18 +++++++----------- src/util/pluginUtils.js | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 202004903c21..90363a406a48 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser' import parseObjectStyles from '../util/parseObjectStyles' import isPlainObject from '../util/isPlainObject' import prefixSelector from '../util/prefixSelector' -import { updateAllClasses, typeMap } from '../util/pluginUtils' +import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils' import log from '../util/log' import * as sharedState from './sharedState' import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector' @@ -552,16 +552,12 @@ function* resolveMatches(candidate, context, original = candidate) { } if (matchesPerPlugin.length > 0) { - let matchingTypes = (sort.options?.types ?? []) - .map(({ type }) => type) - // Only track the types for this plugin that resulted in some result - .filter((type) => { - return Boolean( - typeMap[type](modifier, sort.options, { - tailwindConfig: context.tailwindConfig, - }) - ) - }) + let matchingTypes = Array.from(getMatchingTypes( + sort.options?.types ?? [], + modifier, + sort.options ?? {}, + context.tailwindConfig + )).map(([_, type]) => type) if (matchingTypes.length > 0) { typesByMatches.set(matchesPerPlugin, matchingTypes) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index c56f6a1dd412..f25eefe0c181 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -106,7 +106,9 @@ export function parseColorFormat(value) { return value } -export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) { +export function asColor(modifier, options = {}, { tailwindConfig = {}, rawModifier } = {}) { + modifier = rawModifier + if (options.values?.[modifier] !== undefined) { return parseColorFormat(options.values?.[modifier]) } @@ -134,7 +136,7 @@ export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) { return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha]) } - return asValue(modifier, options, { validate: validateColor }) + return asValue(modifier, options, { rawModifier, validate: validateColor }) } export function asLookupValue(modifier, options = {}) { @@ -142,8 +144,8 @@ export function asLookupValue(modifier, options = {}) { } function guess(validate) { - return (modifier, options) => { - return asValue(modifier, options, { validate }) + return (modifier, options, extras) => { + return asValue(modifier, options, { ...extras, validate }) } } @@ -196,11 +198,33 @@ export function coerceValue(types, modifier, options, tailwindConfig) { } } + let matches = getMatchingTypes(types, modifier, options, tailwindConfig) + // Find first matching type - for (let { type } of types) { - let result = typeMap[type](modifier, options, { tailwindConfig }) - if (result !== undefined) return [result, type] + for (let match of matches) { + return match } return [] } + +/** + * + * @param {{type: string}[]} types + * @param {string} modifier + * @param {any} options + * @param {any} tailwindConfig + * @returns {Iterator<[value: string, type: string]>} + */ +export function* getMatchingTypes(types, modifier, options, tailwindConfig) { + for (const { type } of types ?? []) { + let result = typeMap[type](modifier, options, { + rawModifier: modifier, + tailwindConfig, + }) + + if (result !== undefined) { + yield [result, type] + } + } +} From f1ddc1fa4094dfbe2ab14574bb343aa84447acba Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 18:22:53 -0400 Subject: [PATCH 06/20] Hoist utility modifier splitting --- src/util/pluginUtils.js | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index f25eefe0c181..1f6278682b19 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -90,7 +90,16 @@ function splitAlpha(modifier) { let slashIdx = modifier.lastIndexOf('/') if (slashIdx === -1 || slashIdx === modifier.length - 1) { - return [modifier] + return [modifier, undefined] + } + + let arbitrary = isArbitraryValue(modifier) + + // The modifier could be of the form `[foo]/[bar]` + // We want to handle this case properly + // without affecting `[foo/bar]` + if (arbitrary && !modifier.includes(']/[')) { + return [modifier, undefined] } return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)] @@ -106,14 +115,12 @@ export function parseColorFormat(value) { return value } -export function asColor(modifier, options = {}, { tailwindConfig = {}, rawModifier } = {}) { - modifier = rawModifier - - if (options.values?.[modifier] !== undefined) { - return parseColorFormat(options.values?.[modifier]) +export function asColor(modifier, options = {}, { tailwindConfig = {}, utilityModifier, rawModifier } = {}) { + if (options.values?.[rawModifier] !== undefined) { + return parseColorFormat(options.values?.[rawModifier]) } - let [color, alpha] = splitAlpha(modifier) + let [color, alpha] = [modifier, utilityModifier] if (alpha !== undefined) { let normalizedColor = @@ -136,7 +143,7 @@ export function asColor(modifier, options = {}, { tailwindConfig = {}, rawModifi return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha]) } - return asValue(modifier, options, { rawModifier, validate: validateColor }) + return asValue(rawModifier, options, { rawModifier, utilityModifier, validate: validateColor }) } export function asLookupValue(modifier, options = {}) { @@ -211,15 +218,26 @@ export function coerceValue(types, modifier, options, tailwindConfig) { /** * * @param {{type: string}[]} types - * @param {string} modifier + * @param {string} rawModifier * @param {any} options * @param {any} tailwindConfig * @returns {Iterator<[value: string, type: string]>} */ -export function* getMatchingTypes(types, modifier, options, tailwindConfig) { +export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { + let [modifier, utilityModifier] = splitAlpha(rawModifier) + for (const { type } of types ?? []) { + // TODO: This feels sus but it's required for certain lookup-based stuff to work as expected + // And for the color plugins otherwise we get output we shouldn't for unknown opacity utilities + + // Basically asValue and asLookupValue need special treatment + if ((type === 'any' || type === 'lookup') && utilityModifier) { + modifier = rawModifier + } + let result = typeMap[type](modifier, options, { - rawModifier: modifier, + rawModifier, + utilityModifier, tailwindConfig, }) From 6d065cae47d925830ee2897c4b9477e3e987f2d1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 18:23:15 -0400 Subject: [PATCH 07/20] Rename fn --- src/util/pluginUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 1f6278682b19..9548dce228a0 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -86,7 +86,7 @@ function isArbitraryValue(input) { return input.startsWith('[') && input.endsWith(']') } -function splitAlpha(modifier) { +function splitUtilityModifier(modifier) { let slashIdx = modifier.lastIndexOf('/') if (slashIdx === -1 || slashIdx === modifier.length - 1) { @@ -224,7 +224,7 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @returns {Iterator<[value: string, type: string]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { - let [modifier, utilityModifier] = splitAlpha(rawModifier) + let [modifier, utilityModifier] = splitUtilityModifier(rawModifier) for (const { type } of types ?? []) { // TODO: This feels sus but it's required for certain lookup-based stuff to work as expected From 397d8cf541b07cbc0bc4a37873198febd90a925b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 18:28:13 -0400 Subject: [PATCH 08/20] refactor --- src/util/pluginUtils.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 9548dce228a0..501ddf5132d4 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -229,9 +229,10 @@ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { for (const { type } of types ?? []) { // TODO: This feels sus but it's required for certain lookup-based stuff to work as expected // And for the color plugins otherwise we get output we shouldn't for unknown opacity utilities - // Basically asValue and asLookupValue need special treatment - if ((type === 'any' || type === 'lookup') && utilityModifier) { + let canUseUtilityModifier = type === 'any' || type === 'lookup' + + if (utilityModifier && canUseUtilityModifier) { modifier = rawModifier } @@ -241,8 +242,13 @@ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { tailwindConfig, }) - if (result !== undefined) { - yield [result, type] + if (result === undefined) { + continue } + + yield [ + result, + type, + ] } } From 8679e76bd208e11e27d87fa5801591f70e1b6156 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 12 Oct 2022 18:32:51 -0400 Subject: [PATCH 09/20] Add support for generic utility modifiers --- src/lib/generateRules.js | 28 ++++++++++++++--- src/lib/setupContextUtils.js | 8 ++--- src/util/nameClass.js | 4 +++ src/util/pluginUtils.js | 15 ++++++--- tests/match-utilities.test.js | 59 +++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 tests/match-utilities.test.js diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 90363a406a48..acd40f25b2fa 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -34,13 +34,24 @@ function* candidatePermutations(candidate) { while (lastIndex >= 0) { let dashIdx + let wasSlash = false if (lastIndex === Infinity && candidate.endsWith(']')) { let bracketIdx = candidate.indexOf('[') // If character before `[` isn't a dash or a slash, this isn't a dynamic class // eg. string[] - dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1 + if (candidate[bracketIdx - 1] === '-') { + dashIdx = bracketIdx - 1 + } else if (candidate[bracketIdx - 1] === '/') { + dashIdx = bracketIdx - 1 + wasSlash = true + } else { + dashIdx = -1 + } + } else if (lastIndex === Infinity && candidate.includes('/')) { + dashIdx = candidate.lastIndexOf('/') + wasSlash = true } else { dashIdx = candidate.lastIndexOf('-', lastIndex) } @@ -50,11 +61,20 @@ function* candidatePermutations(candidate) { } let prefix = candidate.slice(0, dashIdx) - let modifier = candidate.slice(dashIdx + 1) - - yield [prefix, modifier] + let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1) lastIndex = dashIdx - 1 + + if (candidate === 'test/[foo]' || candidate === 'test-1/[foo]') { + console.log({ prefix, modifier }) + } + + // TODO: This feels a bit hacky + if (prefix === '' || modifier === '/') { + continue + } + + yield [prefix, modifier] } } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index bbc2454c4376..64c04561fca4 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -371,7 +371,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs classList.add([prefixedIdentifier, options]) function wrapped(modifier, { isOnlyPlugin }) { - let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig) + let [value, coercedType, utilityModifier] = coerceValue(options.types, modifier, options, tailwindConfig) if (value === undefined) { return [] @@ -396,7 +396,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs } let ruleSets = [] - .concat(rule(value)) + .concat(rule(value, { modifier: utilityModifier })) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, @@ -431,7 +431,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs classList.add([prefixedIdentifier, options]) function wrapped(modifier, { isOnlyPlugin }) { - let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig) + let [value, coercedType, utilityModifier] = coerceValue(options.types, modifier, options, tailwindConfig) if (value === undefined) { return [] @@ -456,7 +456,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs } let ruleSets = [] - .concat(rule(value)) + .concat(rule(value, { modifier: utilityModifier })) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, diff --git a/src/util/nameClass.js b/src/util/nameClass.js index ae737012901b..cb37dba20d09 100644 --- a/src/util/nameClass.js +++ b/src/util/nameClass.js @@ -22,5 +22,9 @@ export function formatClass(classPrefix, key) { return `-${classPrefix}${key}` } + if (key.startsWith('/')) { + return `${classPrefix}${key}` + } + return `${classPrefix}-${key}` } diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 501ddf5132d4..a79583b36e44 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -201,7 +201,7 @@ export function coerceValue(types, modifier, options, tailwindConfig) { } if (value.length > 0 && supportedTypes.includes(explicitType)) { - return [asValue(`[${value}]`, options), explicitType] + return [asValue(`[${value}]`, options), explicitType, null] } } @@ -221,18 +221,22 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @param {string} rawModifier * @param {any} options * @param {any} tailwindConfig - * @returns {Iterator<[value: string, type: string]>} + * @returns {Iterator<[value: string, type: string, modifier: string | null]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { let [modifier, utilityModifier] = splitUtilityModifier(rawModifier) + if (utilityModifier !== undefined && modifier === '') { + modifier = 'DEFAULT' + } + for (const { type } of types ?? []) { // TODO: This feels sus but it's required for certain lookup-based stuff to work as expected // And for the color plugins otherwise we get output we shouldn't for unknown opacity utilities // Basically asValue and asLookupValue need special treatment - let canUseUtilityModifier = type === 'any' || type === 'lookup' + let canUseUtilityModifier = type !== 'any' && type !== 'lookup' - if (utilityModifier && canUseUtilityModifier) { + if (utilityModifier && !canUseUtilityModifier) { modifier = rawModifier } @@ -249,6 +253,9 @@ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { yield [ result, type, + canUseUtilityModifier + ? (utilityModifier ?? null) + : null ] } } diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js new file mode 100644 index 000000000000..adf96208c18e --- /dev/null +++ b/tests/match-utilities.test.js @@ -0,0 +1,59 @@ +import { run, html, css } from './util/run' + +test('match utilities with modifiers', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + ({ matchUtilities }) => { + matchUtilities( + { + test: (value, { modifier }) => ({ + color: `${value}_${modifier}`, + }), + }, + { + values: { + DEFAULT: 'default', + bar: 'bar', + '1': 'one', + '2': 'two', + '1/foo': 'onefoo', + }, + } + ) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .test { + color: default_null; + } + .test\/foo { + color: default_foo; + } + .test-1\/foo { + color: onefoo_null; + } + .test-2\/foo { + color: two_foo; + } + .test\/\[foo\] { + color: default_[foo]; + } + .test-1\/\[foo\] { + color: one_[foo]; + } + `) +}) From c262525960cd9bddd0dc64395fc7559af5ecb691 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 09:00:42 -0400 Subject: [PATCH 10/20] Fix CS --- src/lib/generateRules.js | 14 ++++++------ src/lib/setupContextUtils.js | 14 ++++++++++-- src/util/pluginUtils.js | 40 +++++++++++++++++++---------------- tests/match-utilities.test.js | 1 + 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index acd40f25b2fa..2b12cc471594 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -572,12 +572,14 @@ function* resolveMatches(candidate, context, original = candidate) { } if (matchesPerPlugin.length > 0) { - let matchingTypes = Array.from(getMatchingTypes( - sort.options?.types ?? [], - modifier, - sort.options ?? {}, - context.tailwindConfig - )).map(([_, type]) => type) + let matchingTypes = Array.from( + getMatchingTypes( + sort.options?.types ?? [], + modifier, + sort.options ?? {}, + context.tailwindConfig + ) + ).map(([_, type]) => type) if (matchingTypes.length > 0) { typesByMatches.set(matchesPerPlugin, matchingTypes) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 64c04561fca4..40672023ae6d 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -371,7 +371,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs classList.add([prefixedIdentifier, options]) function wrapped(modifier, { isOnlyPlugin }) { - let [value, coercedType, utilityModifier] = coerceValue(options.types, modifier, options, tailwindConfig) + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) if (value === undefined) { return [] @@ -431,7 +436,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs classList.add([prefixedIdentifier, options]) function wrapped(modifier, { isOnlyPlugin }) { - let [value, coercedType, utilityModifier] = coerceValue(options.types, modifier, options, tailwindConfig) + let [value, coercedType, utilityModifier] = coerceValue( + options.types, + modifier, + options, + tailwindConfig + ) if (value === undefined) { return [] diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index a79583b36e44..eb3488703988 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -115,12 +115,18 @@ export function parseColorFormat(value) { return value } -export function asColor(modifier, options = {}, { tailwindConfig = {}, utilityModifier, rawModifier } = {}) { +export function asColor( + modifier, + options = {}, + { tailwindConfig = {}, utilityModifier, rawModifier } = {} +) { if (options.values?.[rawModifier] !== undefined) { return parseColorFormat(options.values?.[rawModifier]) } - let [color, alpha] = [modifier, utilityModifier] + // TODO: Hoist this up to getMatchingTypes or something + // We do this here because we need the alpha value (if any) + let [color, alpha] = splitUtilityModifier(rawModifier) if (alpha !== undefined) { let normalizedColor = @@ -224,22 +230,26 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @returns {Iterator<[value: string, type: string, modifier: string | null]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { - let [modifier, utilityModifier] = splitUtilityModifier(rawModifier) + let canUseUtilityModifier = options.modifiers === true // || typeof options.modifiers === 'object' + + let [modifier, utilityModifier] = canUseUtilityModifier + ? splitUtilityModifier(rawModifier) + : [rawModifier, undefined] if (utilityModifier !== undefined && modifier === '') { modifier = 'DEFAULT' } - for (const { type } of types ?? []) { - // TODO: This feels sus but it's required for certain lookup-based stuff to work as expected - // And for the color plugins otherwise we get output we shouldn't for unknown opacity utilities - // Basically asValue and asLookupValue need special treatment - let canUseUtilityModifier = type !== 'any' && type !== 'lookup' - - if (utilityModifier && !canUseUtilityModifier) { - modifier = rawModifier + // Check the full value first + // TODO: Move to asValue… somehow + if (utilityModifier !== undefined) { + let result = asValue(rawModifier, options, { rawModifier, utilityModifier, tailwindConfig }) + if (result !== undefined) { + yield [result, 'any', null] } + } + for (const { type } of types ?? []) { let result = typeMap[type](modifier, options, { rawModifier, utilityModifier, @@ -250,12 +260,6 @@ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { continue } - yield [ - result, - type, - canUseUtilityModifier - ? (utilityModifier ?? null) - : null - ] + yield [result, type, utilityModifier ?? null] } } diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js index adf96208c18e..d678ff5248bf 100644 --- a/tests/match-utilities.test.js +++ b/tests/match-utilities.test.js @@ -24,6 +24,7 @@ test('match utilities with modifiers', async () => { '2': 'two', '1/foo': 'onefoo', }, + modifiers: true, } ) }, From 35769a9acc5e0d3ed2afd8b8b705c100a71b4f48 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 09:02:29 -0400 Subject: [PATCH 11/20] wip --- src/lib/generateRules.js | 4 ---- src/util/pluginUtils.js | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 2b12cc471594..ab01db78ddc8 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -65,10 +65,6 @@ function* candidatePermutations(candidate) { lastIndex = dashIdx - 1 - if (candidate === 'test/[foo]' || candidate === 'test-1/[foo]') { - console.log({ prefix, modifier }) - } - // TODO: This feels a bit hacky if (prefix === '' || modifier === '/') { continue diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index eb3488703988..f614f8964fc0 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -116,7 +116,7 @@ export function parseColorFormat(value) { } export function asColor( - modifier, + _, options = {}, { tailwindConfig = {}, utilityModifier, rawModifier } = {} ) { @@ -230,7 +230,7 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @returns {Iterator<[value: string, type: string, modifier: string | null]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { - let canUseUtilityModifier = options.modifiers === true // || typeof options.modifiers === 'object' + let canUseUtilityModifier = options.modifiers === true let [modifier, utilityModifier] = canUseUtilityModifier ? splitUtilityModifier(rawModifier) From 1b9e5ed13e69dcbb3411088bf72eb60b96580f10 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 09:10:23 -0400 Subject: [PATCH 12/20] update types --- types/config.d.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/types/config.d.ts b/types/config.d.ts index feef89adc732..5b1438250ecb 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -273,6 +273,7 @@ export interface PluginAPI { respectImportant: boolean type: ValueType | ValueType[] values: KeyValuePair + modifiers: false | true supportsNegativeValues: boolean }> ): void @@ -285,13 +286,17 @@ export interface PluginAPI { }> ): void // for registering new dynamic component styles - matchComponents( - components: KeyValuePair CSSRuleObject>, + matchComponents( + components: KeyValuePair< + string, + (value: T | string, extra: { modifier: string | null }) => CSSRuleObject + >, options?: Partial<{ respectPrefix: boolean respectImportant: boolean type: ValueType | ValueType[] values: KeyValuePair + modifiers: false | true supportsNegativeValues: boolean }> ): void From 0c67f9ca3e6a596b619508b2d1ffaf9e408132a3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 09:10:52 -0400 Subject: [PATCH 13/20] Warn when using modifiers without the option --- src/lib/setupContextUtils.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 40672023ae6d..01b6dc788481 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -358,6 +358,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs let defaultOptions = { respectPrefix: true, respectImportant: true, + modifiers: false, } options = normalizeOptionTypes({ ...defaultOptions, ...options }) @@ -400,8 +401,20 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return [] } + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + } + } + let ruleSets = [] - .concat(rule(value, { modifier: utilityModifier })) + .concat(rule(value, extras)) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, @@ -423,6 +436,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs let defaultOptions = { respectPrefix: true, respectImportant: false, + modifiers: false, } options = normalizeOptionTypes({ ...defaultOptions, ...options }) @@ -465,8 +479,20 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs return [] } + let extras = { + get modifier() { + if (!options.modifiers) { + log.warn(`modifier-used-without-options-for-${identifier}`, [ + 'Your plugin must set `modifiers: true` in its options to support modifiers.', + ]) + } + + return utilityModifier + } + } + let ruleSets = [] - .concat(rule(value, { modifier: utilityModifier })) + .concat(rule(value, extras)) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, From 93d17098c83ee141d307f2623b99c5a22d4dc50d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 11:10:31 -0400 Subject: [PATCH 14/20] Allow modifiers to be a config object --- src/util/pluginUtils.js | 13 +++++++- tests/match-utilities.test.js | 62 +++++++++++++++++++++++++++++++++-- types/config.d.ts | 12 +++---- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index f614f8964fc0..704bfdb82cb3 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -230,7 +230,9 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @returns {Iterator<[value: string, type: string, modifier: string | null]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { - let canUseUtilityModifier = options.modifiers === true + let canUseUtilityModifier = options.modifiers != null && ( + options.modifiers === 'any' || typeof options.modifiers === 'object' + ) let [modifier, utilityModifier] = canUseUtilityModifier ? splitUtilityModifier(rawModifier) @@ -243,6 +245,15 @@ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { // Check the full value first // TODO: Move to asValue… somehow if (utilityModifier !== undefined) { + if (typeof options.modifiers === 'object') { + let configValue = options.modifiers?.[utilityModifier] ?? null + if (configValue !== null) { + utilityModifier = configValue + } else if (isArbitraryValue(utilityModifier)) { + utilityModifier = utilityModifier.slice(1, -1) + } + } + let result = asValue(rawModifier, options, { rawModifier, utilityModifier, tailwindConfig }) if (result !== undefined) { yield [result, 'any', null] diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js index d678ff5248bf..35066d51d538 100644 --- a/tests/match-utilities.test.js +++ b/tests/match-utilities.test.js @@ -8,8 +8,9 @@ test('match utilities with modifiers', async () => { }, ], corePlugins: { preflight: false }, + plugins: [ - ({ matchUtilities }) => { + ({ matchUtilities, theme }) => { matchUtilities( { test: (value, { modifier }) => ({ @@ -24,7 +25,7 @@ test('match utilities with modifiers', async () => { '2': 'two', '1/foo': 'onefoo', }, - modifiers: true, + modifiers: 'any', } ) }, @@ -58,3 +59,60 @@ test('match utilities with modifiers', async () => { } `) }) + +test('match utilities with modifiers in the config', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + + plugins: [ + ({ matchUtilities, theme }) => { + matchUtilities( + { + test: (value, { modifier }) => ({ + color: `${value}_${modifier}`, + }), + }, + { + values: { + DEFAULT: 'default', + bar: 'bar', + '1': 'one', + }, + modifiers: { + foo: 'mewtwo', + }, + } + ) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .test { + color: default_null; + } + .test\/foo { + color: default_mewtwo; + } + .test-1\/foo { + color: one_mewtwo; + } + .test\/\[bar\] { + color: default_bar; + } + .test-1\/\[bar\] { + color: one_bar; + } + `) +}) diff --git a/types/config.d.ts b/types/config.d.ts index 5b1438250ecb..cf9e91427d53 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -263,17 +263,17 @@ export interface PluginAPI { }> ): void // for registering new dynamic utility styles - matchUtilities( + matchUtilities( utilities: KeyValuePair< string, - (value: T | string, extra: { modifier: string | null }) => CSSRuleObject + (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject >, options?: Partial<{ respectPrefix: boolean respectImportant: boolean type: ValueType | ValueType[] values: KeyValuePair - modifiers: false | true + modifiers: 'any' | KeyValuePair supportsNegativeValues: boolean }> ): void @@ -286,17 +286,17 @@ export interface PluginAPI { }> ): void // for registering new dynamic component styles - matchComponents( + matchComponents( components: KeyValuePair< string, - (value: T | string, extra: { modifier: string | null }) => CSSRuleObject + (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject >, options?: Partial<{ respectPrefix: boolean respectImportant: boolean type: ValueType | ValueType[] values: KeyValuePair - modifiers: false | true + modifiers: 'any' | KeyValuePair supportsNegativeValues: boolean }> ): void From a7226047ffdf320f48d217516ce1b73d35990b7a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 11:14:40 -0400 Subject: [PATCH 15/20] Make sure we can return null from matchUtilities to omit rules --- tests/match-utilities.test.js | 46 +++++++++++++++++++++++++++++++++++ types/config.d.ts | 4 +-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js index 35066d51d538..ac3dcedcd402 100644 --- a/tests/match-utilities.test.js +++ b/tests/match-utilities.test.js @@ -116,3 +116,49 @@ test('match utilities with modifiers in the config', async () => { } `) }) + +test('match utilities can omit utilities by returning null', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + + plugins: [ + ({ matchUtilities, theme }) => { + matchUtilities( + { + test: (value, { modifier }) => (modifier === 'bad' ? null : { + color: `${value}_${modifier}`, + }), + }, + { + values: { + DEFAULT: 'default', + bar: 'bar', + '1': 'one', + }, + modifiers: 'any', + } + ) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + .test { + color: default_null; + } + .test\/good { + color: default_good; + } + `) +}) diff --git a/types/config.d.ts b/types/config.d.ts index cf9e91427d53..0df3620611c9 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -266,7 +266,7 @@ export interface PluginAPI { matchUtilities( utilities: KeyValuePair< string, - (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject + (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null >, options?: Partial<{ respectPrefix: boolean @@ -289,7 +289,7 @@ export interface PluginAPI { matchComponents( components: KeyValuePair< string, - (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject + (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null >, options?: Partial<{ respectPrefix: boolean From 700cee4b6e5e1cf6e29185794c4521ea2c611afb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 13:20:17 -0400 Subject: [PATCH 16/20] Feature flag generalized modifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re putting a flag for modifiers in front of matchVariant and matchUtilities --- src/featureFlags.js | 1 + src/lib/setupContextUtils.js | 27 +++++++++++++++++++++------ src/util/pluginUtils.js | 10 +++++++--- tests/arbitrary-variants.test.js | 8 +++++++- tests/match-utilities.test.js | 19 ++++++++++++++++--- 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/featureFlags.js b/src/featureFlags.js index 2bfee94d97ab..13c424184768 100644 --- a/src/featureFlags.js +++ b/src/featureFlags.js @@ -14,6 +14,7 @@ let featureFlags = { ], experimental: [ 'optimizeUniversalDefaults', + 'generalizedModifiers', // 'variantGrouping', ], } diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 01b6dc788481..2f0ca84084b5 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -21,6 +21,7 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue' import { generateRules } from './generateRules' import { hasContentChanged } from './cacheInvalidation.js' import { Offsets } from './offsets.js' +import { flagEnabled } from '../featureFlags.js' let MATCH_VARIANT = Symbol() @@ -410,11 +411,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs } return utilityModifier - } + }, } + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + let ruleSets = [] - .concat(rule(value, extras)) + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, @@ -488,11 +491,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs } return utilityModifier - } + }, } + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + let ruleSets = [] - .concat(rule(value, extras)) + .concat(modifiersEnabled ? rule(value, extras) : rule(value)) .filter(Boolean) .map((declaration) => ({ [nameClass(identifier, modifier)]: declaration, @@ -558,11 +563,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs let id = ++variantIdentifier // A unique identifier that "groups" these variables together. let isSpecial = variant === '@' + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + for (let [key, value] of Object.entries(options?.values ?? {})) { api.addVariant( isSpecial ? `${variant}${key}` : `${variant}-${key}`, Object.assign( - ({ args, container }) => variantFn(value, { modifier: args.modifier, container }), + ({ args, container }) => + variantFn( + value, + modifiersEnabled ? { modifier: args.modifier, container } : { container } + ), { [MATCH_VARIANT]: true, } @@ -574,7 +585,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs api.addVariant( variant, Object.assign( - ({ args, container }) => variantFn(args.value, { modifier: args.modifier, container }), + ({ args, container }) => + variantFn( + args.value, + modifiersEnabled ? { modifier: args.modifier, container } : { container } + ), { [MATCH_VARIANT]: true, } diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 704bfdb82cb3..d5ad3fa3142b 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -19,6 +19,7 @@ import { } from './dataTypes' import negateValue from './negateValue' import { backgroundSize } from './validateFormalSyntax' +import { flagEnabled } from '../featureFlags.js' export function updateAllClasses(selectors, updateClass) { let parser = selectorParser((selectors) => { @@ -230,9 +231,12 @@ export function coerceValue(types, modifier, options, tailwindConfig) { * @returns {Iterator<[value: string, type: string, modifier: string | null]>} */ export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) { - let canUseUtilityModifier = options.modifiers != null && ( - options.modifiers === 'any' || typeof options.modifiers === 'object' - ) + let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers') + + let canUseUtilityModifier = + modifiersEnabled && + options.modifiers != null && + (options.modifiers === 'any' || typeof options.modifiers === 'object') let [modifier, utilityModifier] = canUseUtilityModifier ? splitUtilityModifier(rawModifier) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index d259971fe57a..35acf35fa7e1 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -107,7 +107,7 @@ test('variants without & or an at-rule are ignored', () => { test('arbitrary variants are sorted after other variants', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -709,6 +709,9 @@ it('should support supports', () => { it('should be possible to use modifiers and arbitrary groups', () => { let config = { + experimental: { + generalizedModifiers: true, + }, content: [ { raw: html` @@ -810,6 +813,9 @@ it('should be possible to use modifiers and arbitrary groups', () => { it('should be possible to use modifiers and arbitrary peers', () => { let config = { + experimental: { + generalizedModifiers: true, + }, content: [ { raw: html` diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js index ac3dcedcd402..68a67f870d07 100644 --- a/tests/match-utilities.test.js +++ b/tests/match-utilities.test.js @@ -2,6 +2,10 @@ import { run, html, css } from './util/run' test('match utilities with modifiers', async () => { let config = { + experimental: { + generalizedModifiers: true, + }, + content: [ { raw: html`
`, @@ -62,6 +66,9 @@ test('match utilities with modifiers', async () => { test('match utilities with modifiers in the config', async () => { let config = { + experimental: { + generalizedModifiers: true, + }, content: [ { raw: html`
`, @@ -119,6 +126,9 @@ test('match utilities with modifiers in the config', async () => { test('match utilities can omit utilities by returning null', async () => { let config = { + experimental: { + generalizedModifiers: true, + }, content: [ { raw: html`
`, @@ -130,9 +140,12 @@ test('match utilities can omit utilities by returning null', async () => { ({ matchUtilities, theme }) => { matchUtilities( { - test: (value, { modifier }) => (modifier === 'bad' ? null : { - color: `${value}_${modifier}`, - }), + test: (value, { modifier }) => + modifier === 'bad' + ? null + : { + color: `${value}_${modifier}`, + }, }, { values: { From 37922a470a9e9e4f7c111a9cd13d7442e23ed6da Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 13:25:03 -0400 Subject: [PATCH 17/20] cleanup --- tests/match-utilities.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js index 68a67f870d07..bae11e5a450a 100644 --- a/tests/match-utilities.test.js +++ b/tests/match-utilities.test.js @@ -14,7 +14,7 @@ test('match utilities with modifiers', async () => { corePlugins: { preflight: false }, plugins: [ - ({ matchUtilities, theme }) => { + ({ matchUtilities }) => { matchUtilities( { test: (value, { modifier }) => ({ @@ -77,7 +77,7 @@ test('match utilities with modifiers in the config', async () => { corePlugins: { preflight: false }, plugins: [ - ({ matchUtilities, theme }) => { + ({ matchUtilities }) => { matchUtilities( { test: (value, { modifier }) => ({ @@ -137,7 +137,7 @@ test('match utilities can omit utilities by returning null', async () => { corePlugins: { preflight: false }, plugins: [ - ({ matchUtilities, theme }) => { + ({ matchUtilities }) => { matchUtilities( { test: (value, { modifier }) => From 77e46b6a0f3f935b2b9292eaffd454071dd5aa3e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 13:28:12 -0400 Subject: [PATCH 18/20] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fcaccedde7a..c149d888e886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added 'place-items-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507)) - Added 'content-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507)) - Prepare for container queries setup ([#9526](https://github.com/tailwindlabs/tailwindcss/pull/9526)) +- Add support for modifiers to `matchUtilities` ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541)) +- Switch to positional argument + object for modifiers ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541)) ### Fixed From cb5e2dd8617ca7db7d70efc555a99fad7244e568 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 13:45:55 -0400 Subject: [PATCH 19/20] Properly flag variants using modifiers --- src/lib/generateRules.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index ab01db78ddc8..961b6fa5e5af 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -153,6 +153,10 @@ function applyVariant(variant, matches, context) { if (match) { variant = match[1] args.modifier = match[2] + + if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) { + return [] + } } } From 5bebcde3d98b5f51de6239f795f6a8d1dafde900 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Oct 2022 13:56:57 -0400 Subject: [PATCH 20/20] Fix test --- tests/arbitrary-variants.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 35acf35fa7e1..39484315d066 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -107,7 +107,7 @@ test('variants without & or an at-rule are ignored', () => { test('arbitrary variants are sorted after other variants', () => { let config = { - content: [{ raw: html`
` }], + content: [{ raw: html`
` }], corePlugins: { preflight: false }, }