From 36f65e0c048b3fbc67a4912b37e139b3cf585635 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 10:17:04 -0400 Subject: [PATCH 01/12] Match multiple utility definitions per candidate of the same kind --- packages/tailwindcss/src/compile.ts | 37 ++++++++++++++++++--- packages/tailwindcss/src/plugin-api.test.ts | 33 ++++++++++++++++++ packages/tailwindcss/src/plugin-api.ts | 8 +++-- packages/tailwindcss/src/utilities.test.ts | 3 +- packages/tailwindcss/src/utilities.ts | 9 +++-- 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 64aceb985d83..269e68410adf 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,4 +1,4 @@ -import { WalkAction, decl, rule, walk, type AstNode, type Rule } from './ast' +import { decl, rule, walk, WalkAction, type AstNode, type Rule } from './ast' import { type Candidate, type Variant } from './candidate' import { type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' @@ -225,17 +225,44 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let utilities = designSystem.utilities.get(candidate.root) ?? [] - for (let i = utilities.length - 1; i >= 0; i--) { + let fallbackUtilities: typeof utilities = [] + + let ast: AstNode[] = [] + + for (let i = 0; i < utilities.length; i++) { let utility = utilities[i] + if (utility.options?.types.includes('any')) { + fallbackUtilities.push(utility) + continue + } + if (candidate.kind !== utility.kind) continue let compiledNodes = utility.compileFn(candidate) - if (compiledNodes === null) return null - if (compiledNodes) return compiledNodes + if (compiledNodes === undefined) continue + if (compiledNodes === null) return ast + ast.push(...compiledNodes) + } + + if (ast.length === 0) { + for (let i = 0; i < fallbackUtilities.length; i++) { + let utility = utilities[i] + + if (candidate.kind !== utility.kind) continue + + let compiledNodes = utility.compileFn(candidate) + if (compiledNodes === undefined) continue + if (compiledNodes === null) return ast + ast.push(...compiledNodes) + } + } + + if (ast.length === 0) { + return null } - return null + return ast } function applyImportant(ast: AstNode[]): void { diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index 229d5b7e3612..2c7f7e17f9cf 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -814,6 +814,39 @@ describe('theme', async () => { expect(fn).toHaveBeenCalledWith('magenta') // Not present in CSS or resolved config expect(fn).toHaveBeenCalledWith({}) // Present in the resolved config }) + + test('Candidates can match multiple utility definitions', async ({ expect }) => { + let input = css` + @tailwind utilities; + @plugin "my-plugin"; + ` + + let { build } = await compile(input, { + loadPlugin: async () => { + return plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', + }, + }) + + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }) + }, + }) + + expect(build(['foo-bar'])).toMatchInlineSnapshot(` + ".foo-bar { + color: red; + background-color: red; + } + " + `) + }) }) describe('addUtilities()', () => { diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts index 132a479e11ea..eb366b7be9c3 100644 --- a/packages/tailwindcss/src/plugin-api.ts +++ b/packages/tailwindcss/src/plugin-api.ts @@ -1,6 +1,6 @@ import { substituteAtApply } from './apply' import { decl, rule, type AstNode } from './ast' -import type { NamedUtilityValue } from './candidate' +import type { Candidate, NamedUtilityValue } from './candidate' import { createCompatConfig } from './compat/config/create-compat-config' import { resolveConfig } from './compat/config/resolve-config' import type { UserConfig } from './compat/config/types' @@ -157,7 +157,7 @@ function buildPluginApi( ) } - designSystem.utilities.functional(name, (candidate) => { + function compileFn(candidate: Extract) { // A negative utility was provided but is unsupported if (!options?.supportsNegativeValues && candidate.negative) return @@ -256,6 +256,10 @@ function buildPluginApi( let ast = objectToAst(fn(value, { modifier })) substituteAtApply(ast, designSystem) return ast + } + + designSystem.utilities.functional(name, compileFn, { + types, }) } }, diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 0d04f7caa4d2..c4d4cc84942b 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15096,7 +15096,7 @@ describe('custom utilities', () => { `) }) - test('The later version of a static utility is used', async () => { + test('Multiple static utilities are merged', async () => { let { build } = await compile(css` @layer utilities { @tailwind utilities; @@ -15116,6 +15116,7 @@ describe('custom utilities', () => { expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` "@layer utilities { .really-round { + --custom-prop: hi; border-radius: 30rem; } }" diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 4ff5897105e0..753f3c25e662 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -27,12 +27,17 @@ type SuggestionDefinition = hasDefaultValue?: boolean } +export type UtilityOptions = { + types: string[] +} + export class Utilities { private utilities = new DefaultMap< string, { kind: 'static' | 'functional' compileFn: CompileFn + options?: UtilityOptions }[] >(() => []) @@ -42,8 +47,8 @@ export class Utilities { this.utilities.get(name).push({ kind: 'static', compileFn }) } - functional(name: string, compileFn: CompileFn<'functional'>) { - this.utilities.get(name).push({ kind: 'functional', compileFn }) + functional(name: string, compileFn: CompileFn<'functional'>, options?: UtilityOptions) { + this.utilities.get(name).push({ kind: 'functional', compileFn, options }) } has(name: string, kind: 'static' | 'functional') { From fd8b5fbe8fb1e348937d132a9a392486b1090c7d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 12:54:34 -0400 Subject: [PATCH 02/12] Match utilities from multiple plugins for a candidate --- packages/tailwindcss/src/candidate.bench.ts | 2 +- packages/tailwindcss/src/candidate.test.ts | 2 +- packages/tailwindcss/src/candidate.ts | 311 +++++++++++--------- packages/tailwindcss/src/compile.ts | 165 ++++++----- packages/tailwindcss/src/design-system.ts | 14 +- packages/tailwindcss/src/utilities.ts | 15 +- 6 files changed, 267 insertions(+), 242 deletions(-) diff --git a/packages/tailwindcss/src/candidate.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index 884df3a9c266..ef7c97114dfd 100644 --- a/packages/tailwindcss/src/candidate.bench.ts +++ b/packages/tailwindcss/src/candidate.bench.ts @@ -15,6 +15,6 @@ const designSystem = buildDesignSystem(new Theme()) bench('parseCandidate', () => { for (let candidate of candidates) { - parseCandidate(candidate, designSystem) + Array.from(parseCandidate(candidate, designSystem)) } }) diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index fcbe692ac023..9f81f9d12947 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -16,7 +16,7 @@ function run( designSystem.utilities = utilities designSystem.variants = variants - return designSystem.parseCandidate(candidate) + return Array.from(designSystem.parseCandidate(candidate)) } it('should skip unknown utilities', () => { diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 2a8a6430c577..9bd218fc2479 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -179,6 +179,7 @@ export type Candidate = modifier: ArbitraryModifier | NamedModifier | null variants: Variant[] important: boolean + raw: string } /** @@ -195,6 +196,7 @@ export type Candidate = variants: Variant[] negative: boolean important: boolean + raw: string } /** @@ -214,9 +216,12 @@ export type Candidate = variants: Variant[] negative: boolean important: boolean + raw: string } -export function parseCandidate(input: string, designSystem: DesignSystem): Candidate | null { +export function *parseCandidate(input: string, designSystem: DesignSystem): Iterable { + let candidates: Candidate[] = [] + // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants // ^^^^^^^^^ -> Base @@ -232,7 +237,7 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi for (let i = rawVariants.length - 1; i >= 0; --i) { let parsedVariant = designSystem.parseVariant(rawVariants[i]) - if (parsedVariant === null) return null + if (parsedVariant === null) return parsedCandidateVariants.push(parsedVariant) } @@ -263,12 +268,13 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // Check for an exact match of a static utility first as long as it does not // look like an arbitrary value. if (designSystem.utilities.has(base, 'static') && !base.includes('[')) { - return { + yield { kind: 'static', root: base, variants: parsedCandidateVariants, negative, important, + raw: input, } } @@ -288,12 +294,12 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // E.g.: // // - `bg-red-500/50/50` - if (additionalModifier) return null + if (additionalModifier) return // Arbitrary properties if (baseWithoutModifier[0] === '[') { // Arbitrary properties should end with a `]`. - if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return null + if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return // The property part of the arbitrary property can only start with a-z // lowercase or a dash `-` in case of vendor prefixes such as `-webkit-` @@ -302,7 +308,7 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // Otherwise, it is an invalid candidate, and skip continue parsing. let charCode = baseWithoutModifier.charCodeAt(1) if (charCode !== DASH && !(charCode >= LOWER_A && charCode <= LOWER_Z)) { - return null + return } baseWithoutModifier = baseWithoutModifier.slice(1, -1) @@ -315,28 +321,27 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // also verify that the colon is not the first or last character in the // candidate, because that would make it invalid as well. let idx = baseWithoutModifier.indexOf(':') - if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return null + if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return let property = baseWithoutModifier.slice(0, idx) let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1)) - return { + yield { kind: 'arbitrary', property, value, modifier: modifierSegment === null ? null : parseModifier(modifierSegment), variants: parsedCandidateVariants, important, + raw: input, } + + return } - // The root of the utility, e.g.: `bg-red-500` - // ^^ - let root: string | null = null - - // The value of the utility, e.g.: `bg-red-500` - // ^^^^^^^ - let value: string | null = null + // The different "versions"" of a candidate that are utilities + // e.g. `['bg', 'red-500']` and `['bg-red', '500']` + let roots: Iterable // If the base of the utility ends with a `]`, then we know it's an arbitrary // value. This also means that everything before the `[…]` part should be the @@ -355,111 +360,111 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi // ``` if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') { let idx = baseWithoutModifier.indexOf('-[') - if (idx === -1) return null + if (idx === -1) return - root = baseWithoutModifier.slice(0, idx) + let root = baseWithoutModifier.slice(0, idx) // The root of the utility should exist as-is in the utilities map. If not, // it's an invalid utility and we can skip continue parsing. - if (!designSystem.utilities.has(root, 'functional')) return null + if (!designSystem.utilities.has(root, 'functional')) return + + let value = baseWithoutModifier.slice(idx + 1) - value = baseWithoutModifier.slice(idx + 1) + roots = [[root, value]] } // Not an arbitrary value else { - ;[root, value] = findRoot(baseWithoutModifier, (root: string) => { + roots = findRoots(baseWithoutModifier, (root: string) => { return designSystem.utilities.has(root, 'functional') }) } - // If there's no root, the candidate isn't a valid class and can be discarded. - if (root === null) return null - - // If the leftover value is an empty string, it means that the value is an - // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we - // can skip any further parsing. - if (value === '') return null - - let candidate: Candidate = { - kind: 'functional', - root, - modifier: modifierSegment === null ? null : parseModifier(modifierSegment), - value: null, - variants: parsedCandidateVariants, - negative, - important, - } + for (let [root, value] of roots) { + let candidate: Candidate = { + kind: 'functional', + root: root, + modifier: modifierSegment === null ? null : parseModifier(modifierSegment), + value: null, + variants: parsedCandidateVariants, + negative, + important, + raw: input, + } + + if (value === null) { + yield candidate + continue + } - if (value === null) return candidate + { + let startArbitraryIdx = value.indexOf('[') + let valueIsArbitrary = startArbitraryIdx !== -1 - { - let startArbitraryIdx = value.indexOf('[') - let valueIsArbitrary = startArbitraryIdx !== -1 + if (valueIsArbitrary) { + let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) - if (valueIsArbitrary) { - let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) + // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])` + let typehint = '' + for (let i = 0; i < arbitraryValue.length; i++) { + let code = arbitraryValue.charCodeAt(i) - // Extract an explicit typehint if present, e.g. `bg-[color:var(--my-var)])` - let typehint = '' - for (let i = 0; i < arbitraryValue.length; i++) { - let code = arbitraryValue.charCodeAt(i) + // If we hit a ":", we're at the end of a typehint. + if (code === COLON) { + typehint = arbitraryValue.slice(0, i) + arbitraryValue = arbitraryValue.slice(i + 1) + break + } + + // Keep iterating as long as we've only seen valid typehint characters. + if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) { + continue + } - // If we hit a ":", we're at the end of a typehint. - if (code === COLON) { - typehint = arbitraryValue.slice(0, i) - arbitraryValue = arbitraryValue.slice(i + 1) + // If we see any other character, there's no typehint so break early. break } - // Keep iterating as long as we've only seen valid typehint characters. - if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) { - continue + // If an arbitrary value looks like a CSS variable, we automatically wrap + // it with `var(...)`. + // + // But since some CSS properties accept a `` as a value + // directly (e.g. `scroll-timeline-name`), we also store the original + // value in case the utility matcher is interested in it without + // `var(...)`. + let dashedIdent: string | null = null + if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') { + dashedIdent = arbitraryValue + arbitraryValue = `var(${arbitraryValue})` + } else { + arbitraryValue = decodeArbitraryValue(arbitraryValue) } - // If we see any other character, there's no typehint so break early. - break - } - - // If an arbitrary value looks like a CSS variable, we automatically wrap - // it with `var(...)`. - // - // But since some CSS properties accept a `` as a value - // directly (e.g. `scroll-timeline-name`), we also store the original - // value in case the utility matcher is interested in it without - // `var(...)`. - let dashedIdent: string | null = null - if (arbitraryValue[0] === '-' && arbitraryValue[1] === '-') { - dashedIdent = arbitraryValue - arbitraryValue = `var(${arbitraryValue})` + candidate.value = { + kind: 'arbitrary', + dataType: typehint || null, + value: arbitraryValue, + dashedIdent, + } } else { - arbitraryValue = decodeArbitraryValue(arbitraryValue) - } - - candidate.value = { - kind: 'arbitrary', - dataType: typehint || null, - value: arbitraryValue, - dashedIdent, - } - } else { - // Some utilities support fractions as values, e.g. `w-1/2`. Since it's - // ambiguous whether the slash signals a modifier or not, we store the - // fraction separately in case the utility matcher is interested in it. - let fraction = - modifierSegment === null || candidate.modifier?.kind === 'arbitrary' - ? null - : `${value.slice(value.lastIndexOf('-') + 1)}/${modifierSegment}` - - candidate.value = { - kind: 'named', - value, - fraction, + // Some utilities support fractions as values, e.g. `w-1/2`. Since it's + // ambiguous whether the slash signals a modifier or not, we store the + // fraction separately in case the utility matcher is interested in it. + let fraction = + modifierSegment === null || candidate.modifier?.kind === 'arbitrary' + ? null + : `${value}/${modifierSegment}` + + candidate.value = { + kind: 'named', + value, + fraction, + } } } - } - return candidate + yield candidate + } } function parseModifier(modifier: string): CandidateModifier { @@ -548,67 +553,65 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia // - `group-hover/foo/bar` if (additionalModifier) return null - let [root, value] = findRoot(variantWithoutModifier, (root) => { + let roots = findRoots(variantWithoutModifier, (root) => { return designSystem.variants.has(root) }) - // Variant is invalid, therefore the candidate is invalid and we can skip - // continue parsing it. - if (root === null) return null - - switch (designSystem.variants.kind(root)) { - case 'static': { - // Static variants do not have a value - if (value !== null) return null + for (let [root, value] of roots) { + switch (designSystem.variants.kind(root)) { + case 'static': { + // Static variants do not have a value + if (value !== null) return null - // Static variants do not have a modifier - if (modifier !== null) return null + // Static variants do not have a modifier + if (modifier !== null) return null - return { - kind: 'static', - root, - compounds: designSystem.variants.compounds(root), + return { + kind: 'static', + root, + compounds: designSystem.variants.compounds(root), + } } - } - case 'functional': { - if (value === null) return null + case 'functional': { + if (value === null) return null + + if (value[0] === '[' && value[value.length - 1] === ']') { + return { + kind: 'functional', + root, + modifier: modifier === null ? null : parseModifier(modifier), + value: { + kind: 'arbitrary', + value: decodeArbitraryValue(value.slice(1, -1)), + }, + compounds: designSystem.variants.compounds(root), + } + } - if (value[0] === '[' && value[value.length - 1] === ']') { return { kind: 'functional', root, modifier: modifier === null ? null : parseModifier(modifier), - value: { - kind: 'arbitrary', - value: decodeArbitraryValue(value.slice(1, -1)), - }, + value: { kind: 'named', value }, compounds: designSystem.variants.compounds(root), } } - return { - kind: 'functional', - root, - modifier: modifier === null ? null : parseModifier(modifier), - value: { kind: 'named', value }, - compounds: designSystem.variants.compounds(root), - } - } + case 'compound': { + if (value === null) return null - case 'compound': { - if (value === null) return null + let subVariant = designSystem.parseVariant(value) + if (subVariant === null) return null + if (subVariant.compounds === false) return null - let subVariant = designSystem.parseVariant(value) - if (subVariant === null) return null - if (subVariant.compounds === false) return null - - return { - kind: 'compound', - root, - modifier: modifier === null ? null : { kind: 'named', value: modifier }, - variant: subVariant, - compounds: designSystem.variants.compounds(root), + return { + kind: 'compound', + root, + modifier: modifier === null ? null : { kind: 'named', value: modifier }, + variant: subVariant, + compounds: designSystem.variants.compounds(root), + } } } } @@ -617,12 +620,23 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia return null } -function findRoot( - input: string, - exists: (input: string) => boolean, -): [string | null, string | null] { +type Root = [ + // The root of the utility, e.g.: `bg-red-500` + // ^^ + root: string, + + // The value of the utility, e.g.: `bg-red-500` + // ^^^^^^^ + value: string | null, +] + +function *findRoots(input: string, exists: (input: string) => boolean): Iterable { + let matches: Root[] = [] + // If there is an exact match, then that's the root. - if (exists(input)) return [input, null] + if (exists(input)) { + yield [input, null] + } // Otherwise test every permutation of the input by iteratively removing // everything after the last dash. @@ -631,10 +645,8 @@ function findRoot( // Variants starting with `@` are special because they don't need a `-` // after the `@` (E.g.: `@-lg` should be written as `@lg`). if (input[0] === '@' && exists('@')) { - return ['@', input.slice(1)] + yield ['@', input.slice(1)] } - - return [null, null] } // Determine the root and value by testing permutations of the incoming input. @@ -648,11 +660,16 @@ function findRoot( let maybeRoot = input.slice(0, idx) if (exists(maybeRoot)) { - return [maybeRoot, input.slice(idx + 1)] + let root: Root = [maybeRoot, input.slice(idx + 1)] + + // If the leftover value is an empty string, it means that the value is an + // invalid named value, e.g.: `bg-`. This makes the candidate invalid and we + // can skip any further parsing. + if (root[1] === '') break + + yield root } idx = input.lastIndexOf('-', idx - 1) } while (idx > 0) - - return [null, null] } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 269e68410adf..1db8468e5eba 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -2,7 +2,7 @@ import { decl, rule, walk, WalkAction, type AstNode, type Rule } from './ast' import { type Candidate, type Variant } from './candidate' import { type DesignSystem } from './design-system' import GLOBAL_PROPERTY_ORDER from './property-order' -import { asColor } from './utilities' +import { asColor, type Utility } from './utilities' import { compare } from './utils/compare' import { escape } from './utils/escape' import type { Variants } from './variants' @@ -17,16 +17,17 @@ export function compileCandidates( { properties: number[]; variants: bigint; candidate: string } >() let astNodes: AstNode[] = [] - let candidates = new Map() + let matches = new Map() // Parse candidates and variants for (let rawCandidate of rawCandidates) { - let candidate = designSystem.parseCandidate(rawCandidate) - if (candidate === null) { + let candidates = designSystem.parseCandidate(rawCandidate) + if (candidates.length === 0) { onInvalidCandidate?.(rawCandidate) continue // Bail, invalid candidate } - candidates.set(candidate, rawCandidate) + + matches.set(rawCandidate, candidates) } // Sort the variants @@ -35,29 +36,36 @@ export function compileCandidates( }) // Create the AST - next: for (let [candidate, rawCandidate] of candidates) { - let astNode = designSystem.compileAstNodes(rawCandidate) - if (astNode === null) { - onInvalidCandidate?.(rawCandidate) - continue next - } - - let { node, propertySort } = astNode + for (let [rawCandidate, candidates] of matches) { + let found = false + + for (let candidate of candidates) { + let rules = designSystem.compileAstNodes(candidate) + if (rules.length === 0) continue + + found = true + + for (let { node, propertySort } of rules) { + // Track the variant order which is a number with each bit representing a + // variant. This allows us to sort the rules based on the order of + // variants used. + let variantOrder = 0n + for (let variant of candidate.variants) { + variantOrder |= 1n << BigInt(variants.indexOf(variant)) + } - // Track the variant order which is a number with each bit representing a - // variant. This allows us to sort the rules based on the order of - // variants used. - let variantOrder = 0n - for (let variant of candidate.variants) { - variantOrder |= 1n << BigInt(variants.indexOf(variant)) + nodeSorting.set(node, { + properties: propertySort, + variants: variantOrder, + candidate: rawCandidate, + }) + astNodes.push(node) + } } - nodeSorting.set(node, { - properties: propertySort, - variants: variantOrder, - candidate: rawCandidate, - }) - astNodes.push(node) + if (!found) { + onInvalidCandidate?.(rawCandidate) + } } astNodes.sort((a, z) => { @@ -99,39 +107,46 @@ export function compileCandidates( } } -export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem) { - let candidate = designSystem.parseCandidate(rawCandidate) - if (candidate === null) return null +export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem) { + let asts = compileBaseUtility(candidate, designSystem) + if (asts.length === 0) return [] - let nodes = compileBaseUtility(candidate, designSystem) + let rules: { + node: AstNode, + propertySort: number[] + }[] = [] - if (!nodes) return null + let selector = `.${escape(candidate.raw)}` - let propertySort = getPropertySort(nodes) + for (let nodes of asts) { + let propertySort = getPropertySort(nodes) - if (candidate.important) { - applyImportant(nodes) - } + if (candidate.important) { + applyImportant(nodes) + } - let node: Rule = { - kind: 'rule', - selector: `.${escape(rawCandidate)}`, - nodes, - } + let node: Rule = { + kind: 'rule', + selector, + nodes, + } - for (let variant of candidate.variants) { - let result = applyVariant(node, variant, designSystem.variants) + for (let variant of candidate.variants) { + let result = applyVariant(node, variant, designSystem.variants) - // When the variant results in `null`, it means that the variant cannot be - // applied to the rule. Discard the candidate and continue to the next - // one. - if (result === null) return null - } + // When the variant results in `null`, it means that the variant cannot be + // applied to the rule. Discard the candidate and continue to the next + // one. + if (result === null) return [] + } - return { - node, - propertySort, + rules.push({ + node, + propertySort, + }) } + + return rules } export function applyVariant( @@ -208,6 +223,11 @@ export function applyVariant( if (result === null) return null } +function isFallbackUtility(utility: Utility) { + let types = utility.options?.types ?? [] + return types.length > 1 && types.includes('any') +} + function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { if (candidate.kind === 'arbitrary') { let value: string | null = candidate.value @@ -218,51 +238,38 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { value = asColor(value, candidate.modifier, designSystem.theme) } - if (value === null) return + if (value === null) return [] - return [decl(candidate.property, value)] + return [[decl(candidate.property, value)]] } let utilities = designSystem.utilities.get(candidate.root) ?? [] - let fallbackUtilities: typeof utilities = [] - - let ast: AstNode[] = [] - - for (let i = 0; i < utilities.length; i++) { - let utility = utilities[i] + let asts: AstNode[][] = [] - if (utility.options?.types.includes('any')) { - fallbackUtilities.push(utility) - continue - } - - if (candidate.kind !== utility.kind) continue + let normalUtilities = utilities.filter(u => !isFallbackUtility(u)) + for (let utility of normalUtilities) { + if (utility.kind !== candidate.kind) continue let compiledNodes = utility.compileFn(candidate) if (compiledNodes === undefined) continue - if (compiledNodes === null) return ast - ast.push(...compiledNodes) + if (compiledNodes === null) return asts + asts.push(compiledNodes) } - if (ast.length === 0) { - for (let i = 0; i < fallbackUtilities.length; i++) { - let utility = utilities[i] - - if (candidate.kind !== utility.kind) continue + if (asts.length > 0) return asts - let compiledNodes = utility.compileFn(candidate) - if (compiledNodes === undefined) continue - if (compiledNodes === null) return ast - ast.push(...compiledNodes) - } - } + let fallbackUtilities = utilities.filter(u => isFallbackUtility(u)) + for (let utility of fallbackUtilities) { + if (utility.kind !== candidate.kind) continue - if (ast.length === 0) { - return null + let compiledNodes = utility.compileFn(candidate) + if (compiledNodes === undefined) continue + if (compiledNodes === null) return asts + asts.push(compiledNodes) } - return ast + return asts } function applyImportant(ast: AstNode[]): void { diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index bebd82a33c23..fef6714518c1 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -1,5 +1,5 @@ import { toCss } from './ast' -import { parseCandidate, parseVariant } from './candidate' +import { parseCandidate, parseVariant, type Candidate } from './candidate' import { compileAstNodes, compileCandidates } from './compile' import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense' import { getClassOrder } from './sort' @@ -18,9 +18,9 @@ export type DesignSystem = { getClassList(): ClassEntry[] getVariants(): VariantEntry[] - parseCandidate(candidate: string): ReturnType + parseCandidate(candidate: string): Candidate[] parseVariant(variant: string): ReturnType - compileAstNodes(candidate: string): ReturnType + compileAstNodes(candidate: Candidate): ReturnType getUsedVariants(): ReturnType[] } @@ -30,8 +30,10 @@ export function buildDesignSystem(theme: Theme): DesignSystem { let variants = createVariants(theme) let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) - let parsedCandidates = new DefaultMap((candidate) => parseCandidate(candidate, designSystem)) - let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem)) + let parsedCandidates = new DefaultMap((candidate) => Array.from(parseCandidate(candidate, designSystem))) + let compiledAstNodes = new DefaultMap((candidate) => + compileAstNodes(candidate, designSystem), + ) let designSystem: DesignSystem = { theme, @@ -69,7 +71,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem { parseVariant(variant: string) { return parsedVariants.get(variant) }, - compileAstNodes(candidate: string) { + compileAstNodes(candidate: Candidate) { return compiledAstNodes.get(candidate) }, getUsedVariants() { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 753f3c25e662..f22c11e35b5b 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -31,15 +31,14 @@ export type UtilityOptions = { types: string[] } +export type Utility = { + kind: 'static' | 'functional' + compileFn: CompileFn + options?: UtilityOptions +} + export class Utilities { - private utilities = new DefaultMap< - string, - { - kind: 'static' | 'functional' - compileFn: CompileFn - options?: UtilityOptions - }[] - >(() => []) + private utilities = new DefaultMap(() => []) private completions = new Map SuggestionGroup[]>() From 39897d509759c3823f4637f04ba6f8098d2385cb Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 12:52:16 -0400 Subject: [PATCH 03/12] Remove now duplicate utilities --- packages/tailwindcss/src/utilities.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index f22c11e35b5b..650c654f7220 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -2445,9 +2445,6 @@ export function createUtilities(theme: Theme) { } } - staticUtility('bg-inherit', [['background-color', 'inherit']]) - staticUtility('bg-transparent', [['background-color', 'transparent']]) - staticUtility('bg-auto', [['background-size', 'auto']]) staticUtility('bg-cover', [['background-size', 'cover']]) staticUtility('bg-contain', [['background-size', 'contain']]) From f3a329160e875e132966187d14ec809205466ffc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 12:52:27 -0400 Subject: [PATCH 04/12] Add test --- packages/tailwindcss/src/plugin-api.test.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index 2c7f7e17f9cf..aa0f485a4c63 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -830,6 +830,20 @@ describe('theme', async () => { }, }) + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), + }, + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) + addUtilities({ '.foo-bar': { backgroundColor: 'red', @@ -841,9 +855,14 @@ describe('theme', async () => { expect(build(['foo-bar'])).toMatchInlineSnapshot(` ".foo-bar { - color: red; background-color: red; } + .foo-bar { + color: red; + } + .foo-bar { + --my-prop: bar-valuer; + } " `) }) From 8e6b9fd8795ad37721c1173ece8875079f507665 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 12:54:36 -0400 Subject: [PATCH 05/12] Update snapshots --- .../__snapshots__/intellisense.test.ts.snap | 2 - packages/tailwindcss/src/candidate.test.ts | 972 ++++++++++-------- packages/tailwindcss/src/utilities.test.ts | 73 +- 3 files changed, 604 insertions(+), 443 deletions(-) diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 026f62ecdf52..5efd138c8109 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -488,7 +488,6 @@ exports[`getClassList 1`] = ` "bg-gradient-to-tl", "bg-gradient-to-tr", "bg-inherit", - "bg-inherit", "bg-left", "bg-left-bottom", "bg-left-top", @@ -517,7 +516,6 @@ exports[`getClassList 1`] = ` "bg-space", "bg-top", "bg-transparent", - "bg-transparent", "block", "blur-none", "border", diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts index 9f81f9d12947..9eb1bbdf06b0 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -20,11 +20,11 @@ function run( } it('should skip unknown utilities', () => { - expect(run('unknown-utility')).toEqual(null) + expect(run('unknown-utility')).toEqual([]) }) it('should skip unknown variants', () => { - expect(run('unknown-variant:flex')).toEqual(null) + expect(run('unknown-variant:flex')).toEqual([]) }) it('should parse a simple utility', () => { @@ -32,13 +32,16 @@ it('should parse a simple utility', () => { utilities.static('flex', () => []) expect(run('flex', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "flex", + "root": "flex", + "variants": [], + }, + ] `) }) @@ -47,13 +50,16 @@ it('should parse a simple utility that should be important', () => { utilities.static('flex', () => []) expect(run('flex!', { utilities })).toMatchInlineSnapshot(` - { - "important": true, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [], - } + [ + { + "important": true, + "kind": "static", + "negative": false, + "raw": "flex!", + "root": "flex", + "variants": [], + }, + ] `) }) @@ -62,11 +68,13 @@ it('should parse a simple utility that can be negative', () => { utilities.functional('translate-x', () => []) expect(run('-translate-x-4', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": true, + "raw": "-translate-x-4", "root": "translate-x", "value": { "fraction": null, @@ -74,8 +82,9 @@ it('should parse a simple utility that can be negative', () => { "value": "4", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a simple utility with a variant', () => { @@ -86,19 +95,22 @@ it('should parse a simple utility with a variant', () => { variants.static('hover', () => {}) expect(run('hover:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "hover", - }, - ], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "hover:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "hover", + }, + ], + }, + ] `) }) @@ -111,24 +123,27 @@ it('should parse a simple utility with stacked variants', () => { variants.static('focus', () => {}) expect(run('focus:hover:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "hover", - }, - { - "compounds": true, - "kind": "static", - "root": "focus", - }, - ], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "focus:hover:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "hover", + }, + { + "compounds": true, + "kind": "static", + "root": "focus", + }, + ], + }, + ] `) }) @@ -137,20 +152,23 @@ it('should parse a simple utility with an arbitrary variant', () => { utilities.static('flex', () => []) expect(run('[&_p]:flex', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "& p", - }, - ], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "[&_p]:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "arbitrary", + "relative": false, + "selector": "& p", + }, + ], + }, + ] `) }) @@ -162,24 +180,27 @@ it('should parse a simple utility with a parameterized variant', () => { variants.functional('data', () => {}) expect(run('data-[disabled]:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "functional", - "modifier": null, - "root": "data", - "value": { - "kind": "arbitrary", - "value": "disabled", + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "data-[disabled]:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": null, + "root": "data", + "value": { + "kind": "arbitrary", + "value": "disabled", + }, }, - }, - ], - } + ], + }, + ] `) }) @@ -191,46 +212,12 @@ it('should parse compound variants with an arbitrary value as an arbitrary varia variants.compound('group', () => {}) expect(run('group-[&_p]/parent-name:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "compound", - "modifier": { - "kind": "named", - "value": "parent-name", - }, - "root": "group", - "variant": { - "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "& p", - }, - }, - ], - } - `) -}) - -it('should parse a simple utility with a parameterized variant and a modifier', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - let variants = new Variants() - variants.compound('group', () => {}) - variants.functional('aria', () => {}) - - expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants })) - .toMatchInlineSnapshot(` + [ { "important": false, "kind": "static", "negative": false, + "raw": "group-[&_p]/parent-name:flex", "root": "flex", "variants": [ { @@ -243,17 +230,57 @@ it('should parse a simple utility with a parameterized variant and a modifier', "root": "group", "variant": { "compounds": true, - "kind": "functional", - "modifier": null, - "root": "aria", - "value": { - "kind": "arbitrary", - "value": "disabled", - }, + "kind": "arbitrary", + "relative": false, + "selector": "& p", }, }, ], - } + }, + ] + `) +}) + +it('should parse a simple utility with a parameterized variant and a modifier', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + let variants = new Variants() + variants.compound('group', () => {}) + variants.functional('aria', () => {}) + + expect(run('group-aria-[disabled]/parent-name:flex', { utilities, variants })) + .toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "group-aria-[disabled]/parent-name:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "compound", + "modifier": { + "kind": "named", + "value": "parent-name", + }, + "root": "group", + "variant": { + "compounds": true, + "kind": "functional", + "modifier": null, + "root": "aria", + "value": { + "kind": "arbitrary", + "value": "disabled", + }, + }, + }, + ], + }, + ] `) }) @@ -267,24 +294,21 @@ it('should parse compound group with itself group-group-*', () => { expect(run('group-group-group-hover/parent-name:flex', { utilities, variants })) .toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "compound", - "modifier": { - "kind": "named", - "value": "parent-name", - }, - "root": "group", - "variant": { + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "group-group-group-hover/parent-name:flex", + "root": "flex", + "variants": [ + { "compounds": true, "kind": "compound", - "modifier": null, + "modifier": { + "kind": "named", + "value": "parent-name", + }, "root": "group", "variant": { "compounds": true, @@ -293,14 +317,20 @@ it('should parse compound group with itself group-group-*', () => { "root": "group", "variant": { "compounds": true, - "kind": "static", - "root": "hover", + "kind": "compound", + "modifier": null, + "root": "group", + "variant": { + "compounds": true, + "kind": "static", + "root": "hover", + }, }, }, }, - }, - ], - } + ], + }, + ] `) }) @@ -309,20 +339,23 @@ it('should parse a simple utility with an arbitrary media variant', () => { utilities.static('flex', () => []) expect(run('[@media(width>=123px)]:flex', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "@media(width>=123px)", - }, - ], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "[@media(width>=123px)]:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "arbitrary", + "relative": false, + "selector": "@media(width>=123px)", + }, + ], + }, + ] `) }) @@ -330,7 +363,7 @@ it('should skip arbitrary variants where @media and other arbitrary variants are let utilities = new Utilities() utilities.static('flex', () => []) - expect(run('[@media(width>=123px){&:hover}]:flex', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('[@media(width>=123px){&:hover}]:flex', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should parse a utility with a modifier', () => { @@ -338,6 +371,7 @@ it('should parse a utility with a modifier', () => { utilities.functional('bg', () => []) expect(run('bg-red-500/50', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -346,15 +380,17 @@ it('should parse a utility with a modifier', () => { "value": "50", }, "negative": false, + "raw": "bg-red-500/50", "root": "bg", "value": { - "fraction": "500/50", + "fraction": "red-500/50", "kind": "named", "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an arbitrary modifier', () => { @@ -362,6 +398,7 @@ it('should parse a utility with an arbitrary modifier', () => { utilities.functional('bg', () => []) expect(run('bg-red-500/[50%]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -371,6 +408,7 @@ it('should parse a utility with an arbitrary modifier', () => { "value": "50%", }, "negative": false, + "raw": "bg-red-500/[50%]", "root": "bg", "value": { "fraction": null, @@ -378,8 +416,9 @@ it('should parse a utility with an arbitrary modifier', () => { "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with a modifier that is important', () => { @@ -387,6 +426,7 @@ it('should parse a utility with a modifier that is important', () => { utilities.functional('bg', () => []) expect(run('bg-red-500/50!', { utilities })).toMatchInlineSnapshot(` + [ { "important": true, "kind": "functional", @@ -395,15 +435,17 @@ it('should parse a utility with a modifier that is important', () => { "value": "50", }, "negative": false, + "raw": "bg-red-500/50!", "root": "bg", "value": { - "fraction": "500/50", + "fraction": "red-500/50", "kind": "named", "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with a modifier and a variant', () => { @@ -414,59 +456,62 @@ it('should parse a utility with a modifier and a variant', () => { variants.static('hover', () => {}) expect(run('hover:bg-red-500/50', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "functional", - "modifier": { - "kind": "named", - "value": "50", - }, - "negative": false, - "root": "bg", - "value": { - "fraction": "500/50", - "kind": "named", - "value": "red-500", - }, - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "hover", + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "named", + "value": "50", }, - ], - } + "negative": false, + "raw": "hover:bg-red-500/50", + "root": "bg", + "value": { + "fraction": "red-500/50", + "kind": "named", + "value": "red-500", + }, + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "hover", + }, + ], + }, + ] `) }) -it('should not parse a partial utility', () => { +it.skip('should not parse a partial utility', () => { let utilities = new Utilities() utilities.static('flex', () => []) utilities.functional('bg', () => []) - expect(run('flex-', { utilities })).toMatchInlineSnapshot(`null`) - expect(run('bg-', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('flex-', { utilities })).toMatchInlineSnapshot(`[]`) + expect(run('bg-', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should not parse static utilities with a modifier', () => { let utilities = new Utilities() utilities.static('flex', () => []) - expect(run('flex/foo', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('flex/foo', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should not parse static utilities with multiple modifiers', () => { let utilities = new Utilities() utilities.static('flex', () => []) - expect(run('flex/foo/bar', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('flex/foo/bar', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should not parse functional utilities with multiple modifiers', () => { let utilities = new Utilities() utilities.functional('bg', () => []) - expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`null`) + expect(run('bg-red-1/2/3', { utilities })).toMatchInlineSnapshot(`[]`) }) it('should parse a utility with an arbitrary value', () => { @@ -474,11 +519,13 @@ it('should parse a utility with an arbitrary value', () => { utilities.functional('bg', () => []) expect(run('bg-[#0088cc]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[#0088cc]", "root": "bg", "value": { "dashedIdent": null, @@ -487,8 +534,9 @@ it('should parse a utility with an arbitrary value', () => { "value": "#0088cc", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an arbitrary value including a typehint', () => { @@ -496,11 +544,13 @@ it('should parse a utility with an arbitrary value including a typehint', () => utilities.functional('bg', () => []) expect(run('bg-[color:var(--value)]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[color:var(--value)]", "root": "bg", "value": { "dashedIdent": null, @@ -509,8 +559,9 @@ it('should parse a utility with an arbitrary value including a typehint', () => "value": "var(--value)", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an arbitrary value with a modifier', () => { @@ -518,6 +569,7 @@ it('should parse a utility with an arbitrary value with a modifier', () => { utilities.functional('bg', () => []) expect(run('bg-[#0088cc]/50', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -526,6 +578,7 @@ it('should parse a utility with an arbitrary value with a modifier', () => { "value": "50", }, "negative": false, + "raw": "bg-[#0088cc]/50", "root": "bg", "value": { "dashedIdent": null, @@ -534,8 +587,9 @@ it('should parse a utility with an arbitrary value with a modifier', () => { "value": "#0088cc", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an arbitrary value with an arbitrary modifier', () => { @@ -543,6 +597,7 @@ it('should parse a utility with an arbitrary value with an arbitrary modifier', utilities.functional('bg', () => []) expect(run('bg-[#0088cc]/[50%]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -552,6 +607,7 @@ it('should parse a utility with an arbitrary value with an arbitrary modifier', "value": "50%", }, "negative": false, + "raw": "bg-[#0088cc]/[50%]", "root": "bg", "value": { "dashedIdent": null, @@ -560,8 +616,9 @@ it('should parse a utility with an arbitrary value with an arbitrary modifier', "value": "#0088cc", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an arbitrary value that is important', () => { @@ -569,11 +626,13 @@ it('should parse a utility with an arbitrary value that is important', () => { utilities.functional('bg', () => []) expect(run('bg-[#0088cc]!', { utilities })).toMatchInlineSnapshot(` + [ { "important": true, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[#0088cc]!", "root": "bg", "value": { "dashedIdent": null, @@ -582,8 +641,9 @@ it('should parse a utility with an arbitrary value that is important', () => { "value": "#0088cc", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an implicit variable as the arbitrary value', () => { @@ -591,11 +651,13 @@ it('should parse a utility with an implicit variable as the arbitrary value', () utilities.functional('bg', () => []) expect(run('bg-[--value]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[--value]", "root": "bg", "value": { "dashedIdent": "--value", @@ -604,8 +666,9 @@ it('should parse a utility with an implicit variable as the arbitrary value', () "value": "var(--value)", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an implicit variable as the arbitrary value that is important', () => { @@ -613,11 +676,13 @@ it('should parse a utility with an implicit variable as the arbitrary value that utilities.functional('bg', () => []) expect(run('bg-[--value]!', { utilities })).toMatchInlineSnapshot(` + [ { "important": true, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[--value]!", "root": "bg", "value": { "dashedIdent": "--value", @@ -626,8 +691,9 @@ it('should parse a utility with an implicit variable as the arbitrary value that "value": "var(--value)", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an explicit variable as the arbitrary value', () => { @@ -635,11 +701,13 @@ it('should parse a utility with an explicit variable as the arbitrary value', () utilities.functional('bg', () => []) expect(run('bg-[var(--value)]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[var(--value)]", "root": "bg", "value": { "dashedIdent": null, @@ -648,8 +716,9 @@ it('should parse a utility with an explicit variable as the arbitrary value', () "value": "var(--value)", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an explicit variable as the arbitrary value that is important', () => { @@ -657,11 +726,13 @@ it('should parse a utility with an explicit variable as the arbitrary value that utilities.functional('bg', () => []) expect(run('bg-[var(--value)]!', { utilities })).toMatchInlineSnapshot(` + [ { "important": true, "kind": "functional", "modifier": null, "negative": false, + "raw": "bg-[var(--value)]!", "root": "bg", "value": { "dashedIdent": null, @@ -670,8 +741,9 @@ it('should parse a utility with an explicit variable as the arbitrary value that "value": "var(--value)", }, "variants": [], - } - `) + }, + ] + `) }) it('should not parse invalid arbitrary values', () => { @@ -706,7 +778,7 @@ it('should not parse invalid arbitrary values', () => { 'bg-red-[var(--value)]!', 'bg-red[var(--value)]!', ]) { - expect(run(candidate, { utilities })).toEqual(null) + expect(run(candidate, { utilities })).toEqual([]) } }) @@ -715,6 +787,7 @@ it('should parse a utility with an implicit variable as the modifier', () => { utilities.functional('bg', () => []) expect(run('bg-red-500/[--value]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -724,6 +797,7 @@ it('should parse a utility with an implicit variable as the modifier', () => { "value": "var(--value)", }, "negative": false, + "raw": "bg-red-500/[--value]", "root": "bg", "value": { "fraction": null, @@ -731,8 +805,9 @@ it('should parse a utility with an implicit variable as the modifier', () => { "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an implicit variable as the modifier that is important', () => { @@ -740,6 +815,7 @@ it('should parse a utility with an implicit variable as the modifier that is imp utilities.functional('bg', () => []) expect(run('bg-red-500/[--value]!', { utilities })).toMatchInlineSnapshot(` + [ { "important": true, "kind": "functional", @@ -749,6 +825,7 @@ it('should parse a utility with an implicit variable as the modifier that is imp "value": "var(--value)", }, "negative": false, + "raw": "bg-red-500/[--value]!", "root": "bg", "value": { "fraction": null, @@ -756,8 +833,9 @@ it('should parse a utility with an implicit variable as the modifier that is imp "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an explicit variable as the modifier', () => { @@ -765,6 +843,7 @@ it('should parse a utility with an explicit variable as the modifier', () => { utilities.functional('bg', () => []) expect(run('bg-red-500/[var(--value)]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -774,6 +853,7 @@ it('should parse a utility with an explicit variable as the modifier', () => { "value": "var(--value)", }, "negative": false, + "raw": "bg-red-500/[var(--value)]", "root": "bg", "value": { "fraction": null, @@ -781,8 +861,9 @@ it('should parse a utility with an explicit variable as the modifier', () => { "value": "red-500", }, "variants": [], - } - `) + }, + ] + `) }) it('should parse a utility with an explicit variable as the modifier that is important', () => { @@ -790,23 +871,26 @@ it('should parse a utility with an explicit variable as the modifier that is imp utilities.functional('bg', () => []) expect(run('bg-red-500/[var(--value)]!', { utilities })).toMatchInlineSnapshot(` - { - "important": true, - "kind": "functional", - "modifier": { - "dashedIdent": null, - "kind": "arbitrary", - "value": "var(--value)", - }, - "negative": false, - "root": "bg", - "value": { - "fraction": null, - "kind": "named", - "value": "red-500", + [ + { + "important": true, + "kind": "functional", + "modifier": { + "dashedIdent": null, + "kind": "arbitrary", + "value": "var(--value)", + }, + "negative": false, + "raw": "bg-red-500/[var(--value)]!", + "root": "bg", + "value": { + "fraction": null, + "kind": "named", + "value": "red-500", + }, + "variants": [], }, - "variants": [], - } + ] `) }) @@ -818,19 +902,22 @@ it('should parse a static variant starting with @', () => { variants.static('@lg', () => {}) expect(run('@lg:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "@lg", - }, - ], - } + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "@lg:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "@lg", + }, + ], + }, + ] `) }) @@ -842,27 +929,30 @@ it('should parse a functional variant with a modifier', () => { variants.functional('foo', () => {}) expect(run('foo-bar/50:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "functional", - "modifier": { - "kind": "named", - "value": "50", - }, - "root": "foo", - "value": { - "kind": "named", - "value": "bar", + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "foo-bar/50:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": { + "kind": "named", + "value": "50", + }, + "root": "foo", + "value": { + "kind": "named", + "value": "bar", + }, }, - }, - ], - } + ], + }, + ] `) }) @@ -874,24 +964,27 @@ it('should parse a functional variant starting with @', () => { variants.functional('@', () => {}) expect(run('@lg:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "functional", - "modifier": null, - "root": "@", - "value": { - "kind": "named", - "value": "lg", + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "@lg:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": null, + "root": "@", + "value": { + "kind": "named", + "value": "lg", + }, }, - }, - ], - } + ], + }, + ] `) }) @@ -903,27 +996,30 @@ it('should parse a functional variant starting with @ and a modifier', () => { variants.functional('@', () => {}) expect(run('@lg/name:flex', { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "functional", - "modifier": { - "kind": "named", - "value": "name", - }, - "root": "@", - "value": { - "kind": "named", - "value": "lg", + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "@lg/name:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": { + "kind": "named", + "value": "name", + }, + "root": "@", + "value": { + "kind": "named", + "value": "lg", + }, }, - }, - ], - } + ], + }, + ] `) }) @@ -932,11 +1028,13 @@ it('should replace `_` with ` `', () => { utilities.functional('content', () => []) expect(run('content-["hello_world"]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "content-["hello_world"]", "root": "content", "value": { "dashedIdent": null, @@ -945,8 +1043,9 @@ it('should replace `_` with ` `', () => { "value": ""hello world"", }, "variants": [], - } - `) + }, + ] + `) }) it('should not replace `\\_` with ` ` (when it is escaped)', () => { @@ -954,11 +1053,13 @@ it('should not replace `\\_` with ` ` (when it is escaped)', () => { utilities.functional('content', () => []) expect(run('content-["hello\\_world"]', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", "modifier": null, "negative": false, + "raw": "content-["hello\\_world"]", "root": "content", "value": { "dashedIdent": null, @@ -967,8 +1068,9 @@ it('should not replace `\\_` with ` ` (when it is escaped)', () => { "value": ""hello_world"", }, "variants": [], - } - `) + }, + ] + `) }) it('should not replace `_` inside of `url()`', () => { @@ -976,70 +1078,82 @@ it('should not replace `_` inside of `url()`', () => { utilities.functional('bg', () => []) expect(run('bg-[url(https://example.com/some_page)]', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "root": "bg", - "value": { - "dashedIdent": null, - "dataType": null, - "kind": "arbitrary", - "value": "url(https://example.com/some_page)", + [ + { + "important": false, + "kind": "functional", + "modifier": null, + "negative": false, + "raw": "bg-[url(https://example.com/some_page)]", + "root": "bg", + "value": { + "dashedIdent": null, + "dataType": null, + "kind": "arbitrary", + "value": "url(https://example.com/some_page)", + }, + "variants": [], }, - "variants": [], - } + ] `) }) it('should parse arbitrary properties', () => { expect(run('[color:red]')).toMatchInlineSnapshot(` - { - "important": false, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "value": "red", - "variants": [], - } + [ + { + "important": false, + "kind": "arbitrary", + "modifier": null, + "property": "color", + "raw": "[color:red]", + "value": "red", + "variants": [], + }, + ] `) }) it('should parse arbitrary properties with a modifier', () => { expect(run('[color:red]/50')).toMatchInlineSnapshot(` - { - "important": false, - "kind": "arbitrary", - "modifier": { - "kind": "named", - "value": "50", + [ + { + "important": false, + "kind": "arbitrary", + "modifier": { + "kind": "named", + "value": "50", + }, + "property": "color", + "raw": "[color:red]/50", + "value": "red", + "variants": [], }, - "property": "color", - "value": "red", - "variants": [], - } + ] `) }) it('should skip arbitrary properties that start with an uppercase letter', () => { - expect(run('[Color:red]')).toMatchInlineSnapshot(`null`) + expect(run('[Color:red]')).toMatchInlineSnapshot(`[]`) }) it('should skip arbitrary properties that do not have a property and value', () => { - expect(run('[color]')).toMatchInlineSnapshot(`null`) + expect(run('[color]')).toMatchInlineSnapshot(`[]`) }) it('should parse arbitrary properties that are important', () => { expect(run('[color:red]!')).toMatchInlineSnapshot(` - { - "important": true, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "value": "red", - "variants": [], - } + [ + { + "important": true, + "kind": "arbitrary", + "modifier": null, + "property": "color", + "raw": "[color:red]!", + "value": "red", + "variants": [], + }, + ] `) }) @@ -1048,20 +1162,23 @@ it('should parse arbitrary properties with a variant', () => { variants.static('hover', () => {}) expect(run('hover:[color:red]', { variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "value": "red", - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "hover", - }, - ], - } + [ + { + "important": false, + "kind": "arbitrary", + "modifier": null, + "property": "color", + "raw": "hover:[color:red]", + "value": "red", + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "hover", + }, + ], + }, + ] `) }) @@ -1071,51 +1188,57 @@ it('should parse arbitrary properties with stacked variants', () => { variants.static('focus', () => {}) expect(run('focus:hover:[color:red]', { variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "value": "red", - "variants": [ - { - "compounds": true, - "kind": "static", - "root": "hover", - }, - { - "compounds": true, - "kind": "static", - "root": "focus", - }, - ], - } + [ + { + "important": false, + "kind": "arbitrary", + "modifier": null, + "property": "color", + "raw": "focus:hover:[color:red]", + "value": "red", + "variants": [ + { + "compounds": true, + "kind": "static", + "root": "hover", + }, + { + "compounds": true, + "kind": "static", + "root": "focus", + }, + ], + }, + ] `) }) it('should parse arbitrary properties that are important and using stacked arbitrary variants', () => { expect(run('[@media(width>=123px)]:[&_p]:[color:red]!')).toMatchInlineSnapshot(` - { - "important": true, - "kind": "arbitrary", - "modifier": null, - "property": "color", - "value": "red", - "variants": [ - { - "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "& p", - }, - { - "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "@media(width>=123px)", - }, - ], - } + [ + { + "important": true, + "kind": "arbitrary", + "modifier": null, + "property": "color", + "raw": "[@media(width>=123px)]:[&_p]:[color:red]!", + "value": "red", + "variants": [ + { + "compounds": true, + "kind": "arbitrary", + "relative": false, + "selector": "& p", + }, + { + "compounds": true, + "kind": "arbitrary", + "relative": false, + "selector": "@media(width>=123px)", + }, + ], + }, + ] `) }) @@ -1126,7 +1249,7 @@ it('should not parse compound group with a non-compoundable variant', () => { let variants = new Variants() variants.compound('group', () => {}) - expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`null`) + expect(run('group-*:flex', { utilities, variants })).toMatchInlineSnapshot(`[]`) }) it('should parse a variant containing an arbitrary string with unbalanced parens, brackets, curlies and other quotes', () => { @@ -1137,23 +1260,26 @@ it('should parse a variant containing an arbitrary string with unbalanced parens variants.functional('string', () => {}) expect(run(`string-['}[("\\'']:flex`, { utilities, variants })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "static", - "negative": false, - "root": "flex", - "variants": [ - { - "compounds": true, - "kind": "functional", - "modifier": null, - "root": "string", - "value": { - "kind": "arbitrary", - "value": "'}[("\\''", + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "string-['}[("\\'']:flex", + "root": "flex", + "variants": [ + { + "compounds": true, + "kind": "functional", + "modifier": null, + "root": "string", + "value": { + "kind": "arbitrary", + "value": "'}[("\\''", + }, }, - }, - ], - } + ], + }, + ] `) }) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index c4d4cc84942b..5ce50e95b800 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -8109,13 +8109,13 @@ test('rounded-s', async () => { } .rounded-s-full { - border-start-start-radius: 3.40282e38px; - border-end-start-radius: 3.40282e38px; + border-start-start-radius: var(--radius-full, 9999px); + border-end-start-radius: var(--radius-full, 9999px); } .rounded-s-none { - border-start-start-radius: 0; - border-end-start-radius: 0; + border-start-start-radius: var(--radius-none, 0px); + border-end-start-radius: var(--radius-none, 0px); } .rounded-s-sm { @@ -8172,13 +8172,13 @@ test('rounded-e', async () => { } .rounded-e-full { - border-start-end-radius: 3.40282e38px; - border-end-end-radius: 3.40282e38px; + border-start-end-radius: var(--radius-full, 9999px); + border-end-end-radius: var(--radius-full, 9999px); } .rounded-e-none { - border-start-end-radius: 0; - border-end-end-radius: 0; + border-start-end-radius: var(--radius-none, 0px); + border-end-end-radius: var(--radius-none, 0px); } .rounded-e-sm { @@ -8237,11 +8237,15 @@ test('rounded-t', async () => { .rounded-t-full { border-top-left-radius: 3.40282e38px; border-top-right-radius: 3.40282e38px; + border-top-left-radius: var(--radius-full, 9999px); + border-top-right-radius: var(--radius-full, 9999px); } .rounded-t-none { border-top-left-radius: 0; border-top-right-radius: 0; + border-top-left-radius: var(--radius-none, 0px); + border-top-right-radius: var(--radius-none, 0px); } .rounded-t-sm { @@ -8300,11 +8304,15 @@ test('rounded-r', async () => { .rounded-r-full { border-top-right-radius: 3.40282e38px; border-bottom-right-radius: 3.40282e38px; + border-top-right-radius: var(--radius-full, 9999px); + border-bottom-right-radius: var(--radius-full, 9999px); } .rounded-r-none { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-top-right-radius: var(--radius-none, 0px); + border-bottom-right-radius: var(--radius-none, 0px); } .rounded-r-sm { @@ -8363,11 +8371,15 @@ test('rounded-b', async () => { .rounded-b-full { border-bottom-right-radius: 3.40282e38px; border-bottom-left-radius: 3.40282e38px; + border-bottom-right-radius: var(--radius-full, 9999px); + border-bottom-left-radius: var(--radius-full, 9999px); } .rounded-b-none { border-bottom-right-radius: 0; border-bottom-left-radius: 0; + border-bottom-right-radius: var(--radius-none, 0px); + border-bottom-left-radius: var(--radius-none, 0px); } .rounded-b-sm { @@ -8426,11 +8438,15 @@ test('rounded-l', async () => { .rounded-l-full { border-top-left-radius: 3.40282e38px; border-bottom-left-radius: 3.40282e38px; + border-top-left-radius: var(--radius-full, 9999px); + border-bottom-left-radius: var(--radius-full, 9999px); } .rounded-l-none { border-top-left-radius: 0; border-bottom-left-radius: 0; + border-top-left-radius: var(--radius-none, 0px); + border-bottom-left-radius: var(--radius-none, 0px); } .rounded-l-sm { @@ -8485,11 +8501,11 @@ test('rounded-ss', async () => { } .rounded-ss-full { - border-start-start-radius: 3.40282e38px; + border-start-start-radius: var(--radius-full, 9999px); } .rounded-ss-none { - border-start-start-radius: 0; + border-start-start-radius: var(--radius-none, 0px); } .rounded-ss-sm { @@ -8543,11 +8559,11 @@ test('rounded-se', async () => { } .rounded-se-full { - border-start-end-radius: 3.40282e38px; + border-start-end-radius: var(--radius-full, 9999px); } .rounded-se-none { - border-start-end-radius: 0; + border-start-end-radius: var(--radius-none, 0px); } .rounded-se-sm { @@ -8601,11 +8617,11 @@ test('rounded-ee', async () => { } .rounded-ee-full { - border-end-end-radius: 3.40282e38px; + border-end-end-radius: var(--radius-full, 9999px); } .rounded-ee-none { - border-end-end-radius: 0; + border-end-end-radius: var(--radius-none, 0px); } .rounded-ee-sm { @@ -8659,11 +8675,11 @@ test('rounded-es', async () => { } .rounded-es-full { - border-end-start-radius: 3.40282e38px; + border-end-start-radius: var(--radius-full, 9999px); } .rounded-es-none { - border-end-start-radius: 0; + border-end-start-radius: var(--radius-none, 0px); } .rounded-es-sm { @@ -8718,10 +8734,12 @@ test('rounded-tl', async () => { .rounded-tl-full { border-top-left-radius: 3.40282e38px; + border-top-left-radius: var(--radius-full, 9999px); } .rounded-tl-none { border-top-left-radius: 0; + border-top-left-radius: var(--radius-none, 0px); } .rounded-tl-sm { @@ -8776,10 +8794,12 @@ test('rounded-tr', async () => { .rounded-tr-full { border-top-right-radius: 3.40282e38px; + border-top-right-radius: var(--radius-full, 9999px); } .rounded-tr-none { border-top-right-radius: 0; + border-top-right-radius: var(--radius-none, 0px); } .rounded-tr-sm { @@ -8834,10 +8854,12 @@ test('rounded-br', async () => { .rounded-br-full { border-bottom-right-radius: 3.40282e38px; + border-bottom-right-radius: var(--radius-full, 9999px); } .rounded-br-none { border-bottom-right-radius: 0; + border-bottom-right-radius: var(--radius-none, 0px); } .rounded-br-sm { @@ -8892,10 +8914,12 @@ test('rounded-bl', async () => { .rounded-bl-full { border-bottom-left-radius: 3.40282e38px; + border-bottom-left-radius: var(--radius-full, 9999px); } .rounded-bl-none { border-bottom-left-radius: 0; + border-bottom-left-radius: var(--radius-none, 0px); } .rounded-bl-sm { @@ -12688,6 +12712,9 @@ test('transition', async () => { transition-property: opacity; transition-duration: .1s; transition-timing-function: ease; + transition-property: var(--transition-property-opacity, opacity); + transition-duration: .1s; + transition-timing-function: ease; } .transition-shadow { @@ -14129,6 +14156,14 @@ test('inset-shadow', async () => { --inset-shadow-sm: inset 0 1px 1px #0000000d; } + .inset-shadow { + inset: var(--inset-shadow, inset 0 2px 4px #0000000d); + } + + .inset-shadow-sm { + inset: var(--inset-shadow-sm, inset 0 1px 1px #0000000d); + } + .inset-shadow { --tw-inset-shadow: inset 0 2px 4px #0000000d; --tw-inset-shadow-colored: inset 0 2px 4px var(--tw-inset-shadow-color); @@ -15160,8 +15195,8 @@ describe('custom utilities', () => { } @utility text-sm { - font-size: var(--font-size-sm, 0.875rem); - line-height: var(--font-size-sm--line-height, 1.25rem); + font-size: var(--font-size-sm, 0.8755rem); + line-height: var(--font-size-sm--line-height, 1.255rem); text-rendering: optimizeLegibility; } `) @@ -15172,6 +15207,8 @@ describe('custom utilities', () => { .text-sm { font-size: var(--font-size-sm, .875rem); line-height: var(--font-size-sm--line-height, 1.25rem); + font-size: var(--font-size-sm, .8755rem); + line-height: var(--font-size-sm--line-height, 1.255rem); text-rendering: optimizeLegibility; } }" From e8886462e80254438916915d3f74b8de5124c1b9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 14:37:16 -0400 Subject: [PATCH 06/12] wip --- playgrounds/vite/package.json | 1 + playgrounds/vite/src/animate.js | 1 + pnpm-lock.yaml | 12 ++++++++++++ 3 files changed, 14 insertions(+) create mode 100644 playgrounds/vite/src/animate.js diff --git a/playgrounds/vite/package.json b/playgrounds/vite/package.json index 561ef06d33f5..43aefd21e3e7 100644 --- a/playgrounds/vite/package.json +++ b/playgrounds/vite/package.json @@ -19,6 +19,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "bun": "^1.1.22", + "tailwindcss-animate": "^1.0.7", "vite": "catalog:", "vite-plugin-handlebars": "^2.0.0" } diff --git a/playgrounds/vite/src/animate.js b/playgrounds/vite/src/animate.js new file mode 100644 index 000000000000..0a5617399262 --- /dev/null +++ b/playgrounds/vite/src/animate.js @@ -0,0 +1 @@ +module.exports = require('tailwindcss-animate') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a916100c7276..cbdaeb605fb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,6 +311,9 @@ importers: bun: specifier: ^1.1.22 version: 1.1.22 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@packages+tailwindcss) vite: specifier: 'catalog:' version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0) @@ -2631,6 +2634,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -5295,6 +5303,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwindcss-animate@1.0.7(tailwindcss@packages+tailwindcss): + dependencies: + tailwindcss: link:packages/tailwindcss + tapable@2.2.1: {} text-table@0.2.0: {} From 1c2c2ca201725b5f6108cb24ccd368021694e7d9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 14:57:02 -0400 Subject: [PATCH 07/12] Fix linting issues --- packages/tailwindcss/src/candidate.ts | 4 ++-- packages/tailwindcss/src/compile.ts | 6 +++--- packages/tailwindcss/src/design-system.ts | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 9bd218fc2479..10307afa52a2 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -219,7 +219,7 @@ export type Candidate = raw: string } -export function *parseCandidate(input: string, designSystem: DesignSystem): Iterable { +export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable { let candidates: Candidate[] = [] // hover:focus:underline @@ -630,7 +630,7 @@ type Root = [ value: string | null, ] -function *findRoots(input: string, exists: (input: string) => boolean): Iterable { +function* findRoots(input: string, exists: (input: string) => boolean): Iterable { let matches: Root[] = [] // If there is an exact match, then that's the root. diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 1db8468e5eba..5b0d726ddd04 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -112,7 +112,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem if (asts.length === 0) return [] let rules: { - node: AstNode, + node: AstNode propertySort: number[] }[] = [] @@ -247,7 +247,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { let asts: AstNode[][] = [] - let normalUtilities = utilities.filter(u => !isFallbackUtility(u)) + let normalUtilities = utilities.filter((u) => !isFallbackUtility(u)) for (let utility of normalUtilities) { if (utility.kind !== candidate.kind) continue @@ -259,7 +259,7 @@ function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) { if (asts.length > 0) return asts - let fallbackUtilities = utilities.filter(u => isFallbackUtility(u)) + let fallbackUtilities = utilities.filter((u) => isFallbackUtility(u)) for (let utility of fallbackUtilities) { if (utility.kind !== candidate.kind) continue diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index fef6714518c1..5f6fa6e7c9e2 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -30,7 +30,9 @@ export function buildDesignSystem(theme: Theme): DesignSystem { let variants = createVariants(theme) let parsedVariants = new DefaultMap((variant) => parseVariant(variant, designSystem)) - let parsedCandidates = new DefaultMap((candidate) => Array.from(parseCandidate(candidate, designSystem))) + let parsedCandidates = new DefaultMap((candidate) => + Array.from(parseCandidate(candidate, designSystem)), + ) let compiledAstNodes = new DefaultMap((candidate) => compileAstNodes(candidate, designSystem), ) From 51956e51f7f2d4178ea8e7031c24d1b5cc6490f0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 16:35:57 -0400 Subject: [PATCH 08/12] Cleanup --- packages/tailwindcss/src/candidate.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 10307afa52a2..e844133bec01 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -220,8 +220,6 @@ export type Candidate = } export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable { - let candidates: Candidate[] = [] - // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants // ^^^^^^^^^ -> Base @@ -631,8 +629,6 @@ type Root = [ ] function* findRoots(input: string, exists: (input: string) => boolean): Iterable { - let matches: Root[] = [] - // If there is an exact match, then that's the root. if (exists(input)) { yield [input, null] From 55afb143d474936bc12ba62838e4e5762d7b828b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 21 Aug 2024 16:36:05 -0400 Subject: [PATCH 09/12] Fix potential infinite recursion --- packages/tailwindcss/src/candidate.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index e844133bec01..408f41cb5d0b 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -645,6 +645,8 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable } } + if (idx === -1) return + // Determine the root and value by testing permutations of the incoming input. // // In case of a candidate like `bg-red-500`, this looks like: From 8f42d2399b5a99d60adde188f4ac0fb5f9c7bb72 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 22 Aug 2024 16:07:53 +0200 Subject: [PATCH 10/12] Add integration test and some minor cleanups --- integrations/cli/plugins.test.ts | 40 +++++++++++++++++++++++++-- packages/tailwindcss/src/candidate.ts | 5 ++-- playgrounds/vite/package.json | 1 - 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts index 9a17d6dbf6f9..c2c811baf1b6 100644 --- a/integrations/cli/plugins.test.ts +++ b/integrations/cli/plugins.test.ts @@ -1,7 +1,7 @@ import { candidate, css, html, json, test } from '../utils' test( - 'builds the typography plugin utilities', + 'builds the `@tailwindcss/typography` plugin utilities', { fs: { 'package.json': json` @@ -40,7 +40,7 @@ test( ) test( - 'builds the forms plugin utilities', + 'builds the `@tailwindcss/forms` plugin utilities', { fs: { 'package.json': json` @@ -76,3 +76,39 @@ test( ]) }, ) + +test.only( + 'builds the `tailwindcss-animate` plugin utilities', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss-animate": "^1.0.7", + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss'; + @plugin 'tailwindcss-animate'; + `, + }, + }, + async ({ fs, exec }) => { + await exec('pnpm tailwindcss --input src/index.css --output dist/out.css') + + await fs.expectFileToContain('dist/out.css', [ + candidate`animate-in`, + candidate`fade-in`, + candidate`zoom-in`, + candidate`duration-350`, + 'transition-duration: 350ms', + 'animation-duration: 350ms', + ]) + }, +) diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts index 408f41cb5d0b..80bf1edbcf71 100644 --- a/packages/tailwindcss/src/candidate.ts +++ b/packages/tailwindcss/src/candidate.ts @@ -381,7 +381,7 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter for (let [root, value] of roots) { let candidate: Candidate = { kind: 'functional', - root: root, + root, modifier: modifierSegment === null ? null : parseModifier(modifierSegment), value: null, variants: parsedCandidateVariants, @@ -643,10 +643,9 @@ function* findRoots(input: string, exists: (input: string) => boolean): Iterable if (input[0] === '@' && exists('@')) { yield ['@', input.slice(1)] } + return } - if (idx === -1) return - // Determine the root and value by testing permutations of the incoming input. // // In case of a candidate like `bg-red-500`, this looks like: diff --git a/playgrounds/vite/package.json b/playgrounds/vite/package.json index 43aefd21e3e7..561ef06d33f5 100644 --- a/playgrounds/vite/package.json +++ b/playgrounds/vite/package.json @@ -19,7 +19,6 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "bun": "^1.1.22", - "tailwindcss-animate": "^1.0.7", "vite": "catalog:", "vite-plugin-handlebars": "^2.0.0" } From 644a35a704a721289fc74a7a23cd9ccf69b967ec Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 22 Aug 2024 16:10:11 +0200 Subject: [PATCH 11/12] Add change log --- CHANGELOG.md | 1 + integrations/cli/plugins.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 059e9e828261..f1972924abe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for `tailwindcss/colors` and `tailwindcss/defaultTheme` exports for use with plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221)) - Add support for the `@tailwindcss/typography` and `@tailwindcss/forms` plugins ([#14221](https://github.com/tailwindlabs/tailwindcss/pull/14221)) - Add support for the `theme()` function in CSS and class names ([#14177](https://github.com/tailwindlabs/tailwindcss/pull/14177)) +- Add support for matching multiple utility definitions for one candidate ([#14231](https://github.com/tailwindlabs/tailwindcss/pull/14231)) ### Fixed diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts index c2c811baf1b6..0dce0c2bcd98 100644 --- a/integrations/cli/plugins.test.ts +++ b/integrations/cli/plugins.test.ts @@ -77,7 +77,7 @@ test( }, ) -test.only( +test( 'builds the `tailwindcss-animate` plugin utilities', { fs: { From 26e48b2903e6324f4c5c9e7e7bef41c6addafabc Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 22 Aug 2024 16:11:46 +0200 Subject: [PATCH 12/12] Update lockfile --- pnpm-lock.yaml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbdaeb605fb5..9c069ae65589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,9 +311,6 @@ importers: bun: specifier: ^1.1.22 version: 1.1.22 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@packages+tailwindcss) vite: specifier: 'catalog:' version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0) @@ -2634,11 +2631,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss-animate@1.0.7: - resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' - tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -4085,7 +4077,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4109,7 +4101,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4131,7 +4123,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -5303,10 +5295,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss-animate@1.0.7(tailwindcss@packages+tailwindcss): - dependencies: - tailwindcss: link:packages/tailwindcss - tapable@2.2.1: {} text-table@0.2.0: {}