From cc228fbfc31d3b5b4576c4d9d842a0472fa792b3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 22 Aug 2024 10:22:12 -0400 Subject: [PATCH] Add support for matching multiple utility definitions for one candidate (#14231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently if a plugin adds a utility called `duration` it will take precedence over the built-in utilities — or any utilities with the same name in previously included plugins. However, in v3, we emitted matches from _all_ plugins where possible. Take this plugin for example which adds utilities for `animation-duration` via the `duration-*` class: ```ts import plugin from 'tailwindcss/plugin' export default plugin( function ({ matchUtilities, theme }) { matchUtilities( { duration: (value) => ({ animationDuration: value }) }, { values: theme("animationDuration") }, ) }, { theme: { extend: { animationDuration: ({ theme }) => ({ ...theme("transitionDuration"), }), } }, } ) ``` Before this PR this plugin's `duration` utility would override the built-in `duration` utility so you'd get this for a class like `duration-3500`: ```css .duration-3000 { animation-duration: 3500ms; } ``` Now, after this PR, we'll emit rules for `transition-duration` (Tailwind's built-in `duration-*` utility) and `animation-duration` (from the above plugin) and you'll get this instead: ```css .duration-3000 { transition-duration: 3500ms; } .duration-3000 { animation-duration: 3500ms; } ``` These are output as separate rules to ensure that they can all be sorted appropriately against other utilities. --------- Co-authored-by: Philipp Spiess --- CHANGELOG.md | 1 + integrations/cli/plugins.test.ts | 40 +- .../__snapshots__/intellisense.test.ts.snap | 2 - packages/tailwindcss/src/candidate.bench.ts | 2 +- packages/tailwindcss/src/candidate.test.ts | 1122 +++++++++-------- packages/tailwindcss/src/candidate.ts | 308 ++--- packages/tailwindcss/src/compile.ts | 158 ++- packages/tailwindcss/src/design-system.ts | 16 +- packages/tailwindcss/src/plugin-api.test.ts | 52 + packages/tailwindcss/src/plugin-api.ts | 8 +- packages/tailwindcss/src/utilities.test.ts | 76 +- packages/tailwindcss/src/utilities.ts | 25 +- playgrounds/vite/src/animate.js | 1 + pnpm-lock.yaml | 6 +- 14 files changed, 1063 insertions(+), 754 deletions(-) create mode 100644 playgrounds/vite/src/animate.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 059e9e828..f1972924a 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 9a17d6dbf..0dce0c2bc 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( + '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/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 026f62ecd..5efd138c8 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.bench.ts b/packages/tailwindcss/src/candidate.bench.ts index 884df3a9c..ef7c97114 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 fcbe692ac..9eb1bbdf0 100644 --- a/packages/tailwindcss/src/candidate.test.ts +++ b/packages/tailwindcss/src/candidate.test.ts @@ -16,15 +16,15 @@ function run( designSystem.utilities = utilities designSystem.variants = variants - return designSystem.parseCandidate(candidate) + return Array.from(designSystem.parseCandidate(candidate)) } 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,29 +212,32 @@ 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": { + [ + { + "important": false, + "kind": "static", + "negative": false, + "raw": "group-[&_p]/parent-name:flex", + "root": "flex", + "variants": [ + { "compounds": true, - "kind": "arbitrary", - "relative": false, - "selector": "& p", + "kind": "compound", + "modifier": { + "kind": "named", + "value": "parent-name", + }, + "root": "group", + "variant": { + "compounds": true, + "kind": "arbitrary", + "relative": false, + "selector": "& p", + }, }, - }, - ], - } + ], + }, + ] `) }) @@ -227,33 +251,36 @@ it('should parse a simple utility with a parameterized variant and a modifier', expect(run('group-aria-[disabled]/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-aria-[disabled]/parent-name:flex", + "root": "flex", + "variants": [ + { "compounds": true, - "kind": "functional", - "modifier": null, - "root": "aria", - "value": { - "kind": "arbitrary", - "value": "disabled", + "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,110 +456,7 @@ 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", - }, - ], - } - `) -}) - -it('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`) -}) - -it('should not parse static utilities with a modifier', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - expect(run('flex/foo', { utilities })).toMatchInlineSnapshot(`null`) -}) - -it('should not parse static utilities with multiple modifiers', () => { - let utilities = new Utilities() - utilities.static('flex', () => []) - - expect(run('flex/foo/bar', { utilities })).toMatchInlineSnapshot(`null`) -}) - -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`) -}) - -it('should parse a utility with an arbitrary value', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-[#0088cc]', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "root": "bg", - "value": { - "dashedIdent": null, - "dataType": null, - "kind": "arbitrary", - "value": "#0088cc", - }, - "variants": [], - } - `) -}) - -it('should parse a utility with an arbitrary value including a typehint', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-[color:var(--value)]', { utilities })).toMatchInlineSnapshot(` - { - "important": false, - "kind": "functional", - "modifier": null, - "negative": false, - "root": "bg", - "value": { - "dashedIdent": null, - "dataType": "color", - "kind": "arbitrary", - "value": "var(--value)", - }, - "variants": [], - } - `) -}) - -it('should parse a utility with an arbitrary value with a modifier', () => { - let utilities = new Utilities() - utilities.functional('bg', () => []) - - expect(run('bg-[#0088cc]/50', { utilities })).toMatchInlineSnapshot(` + [ { "important": false, "kind": "functional", @@ -526,6 +465,67 @@ it('should parse a utility with an arbitrary value with a modifier', () => { "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.skip('should not parse a partial utility', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + utilities.functional('bg', () => []) + + 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(`[]`) +}) + +it('should not parse static utilities with multiple modifiers', () => { + let utilities = new Utilities() + utilities.static('flex', () => []) + + 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(`[]`) +}) + +it('should parse a utility with an arbitrary value', () => { + let utilities = new Utilities() + 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, @@ -534,8 +534,62 @@ it('should parse a utility with an arbitrary value with a modifier', () => { "value": "#0088cc", }, "variants": [], - } - `) + }, + ] + `) +}) + +it('should parse a utility with an arbitrary value including a typehint', () => { + let utilities = new Utilities() + 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, + "dataType": "color", + "kind": "arbitrary", + "value": "var(--value)", + }, + "variants": [], + }, + ] + `) +}) + +it('should parse a utility with an arbitrary value with a modifier', () => { + let utilities = new Utilities() + utilities.functional('bg', () => []) + + expect(run('bg-[#0088cc]/50', { utilities })).toMatchInlineSnapshot(` + [ + { + "important": false, + "kind": "functional", + "modifier": { + "kind": "named", + "value": "50", + }, + "negative": false, + "raw": "bg-[#0088cc]/50", + "root": "bg", + "value": { + "dashedIdent": null, + "dataType": null, + "kind": "arbitrary", + "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)", + [ + { + "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": [], }, - "negative": false, - "root": "bg", - "value": { - "fraction": null, - "kind": "named", - "value": "red-500", - }, - "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", + [ + { + "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", + }, }, - "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", + [ + { + "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", + }, }, - "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/candidate.ts b/packages/tailwindcss/src/candidate.ts index 2a8a6430c..80bf1edbc 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,10 @@ 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 { // hover:focus:underline // ^^^^^ ^^^^^^ -> Variants // ^^^^^^^^^ -> Base @@ -232,7 +235,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 +266,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 +292,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 +306,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 +319,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 +358,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 - value = baseWithoutModifier.slice(idx + 1) + let 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 + for (let [root, value] of roots) { + let candidate: Candidate = { + kind: 'functional', + root, + modifier: modifierSegment === null ? null : parseModifier(modifierSegment), + value: null, + variants: parsedCandidateVariants, + negative, + important, + raw: input, + } - // 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 + if (value === null) { + yield candidate + continue + } - let candidate: Candidate = { - kind: 'functional', - root, - modifier: modifierSegment === null ? null : parseModifier(modifierSegment), - value: null, - variants: parsedCandidateVariants, - negative, - important, - } + { + let startArbitraryIdx = value.indexOf('[') + let valueIsArbitrary = startArbitraryIdx !== -1 - if (value === null) return candidate + if (valueIsArbitrary) { + let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) - { - let startArbitraryIdx = value.indexOf('[') - let valueIsArbitrary = startArbitraryIdx !== -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) - if (valueIsArbitrary) { - let arbitraryValue = value.slice(startArbitraryIdx + 1, -1) + // 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 + } - // 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) + // 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) - } + // 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: '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, + candidate.value = { + kind: 'named', + value, + fraction, + } } } - } - return candidate + yield candidate + } } function parseModifier(modifier: string): CandidateModifier { @@ -548,67 +551,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 + 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 - 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), - } - } - - case 'functional': { - if (value === null) return null - - if (value[0] === '[' && value[value.length - 1] === ']') { return { - kind: 'functional', + kind: 'static', root, - modifier: modifier === null ? null : parseModifier(modifier), - value: { - kind: 'arbitrary', - value: decodeArbitraryValue(value.slice(1, -1)), - }, 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 '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), + } + } + + 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 +618,21 @@ 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 { // 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 +641,9 @@ 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] + return } // Determine the root and value by testing permutations of the incoming input. @@ -648,11 +657,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 64aceb985..5b0d726dd 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -1,8 +1,8 @@ -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' -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) { + 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)) + } + + nodeSorting.set(node, { + properties: propertySort, + variants: variantOrder, + candidate: rawCandidate, + }) + astNodes.push(node) + } + } + + if (!found) { onInvalidCandidate?.(rawCandidate) - continue next } - - let { node, propertySort } = astNode - - // 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) } 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, + nodes, + } + + 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 [] + } + + rules.push({ + node, + propertySort, + }) } - let node: Rule = { - kind: 'rule', - selector: `.${escape(rawCandidate)}`, - nodes, - } - - 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 - } - - return { - 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,24 +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) ?? [] - for (let i = utilities.length - 1; i >= 0; i--) { - let utility = utilities[i] + let asts: AstNode[][] = [] - 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 === null) return null - if (compiledNodes) return compiledNodes + if (compiledNodes === undefined) continue + if (compiledNodes === null) return asts + asts.push(compiledNodes) } - return null + if (asts.length > 0) return asts + + let fallbackUtilities = utilities.filter((u) => isFallbackUtility(u)) + for (let utility of fallbackUtilities) { + if (utility.kind !== candidate.kind) continue + + let compiledNodes = utility.compileFn(candidate) + if (compiledNodes === undefined) continue + if (compiledNodes === null) return asts + asts.push(compiledNodes) + } + + return asts } function applyImportant(ast: AstNode[]): void { diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index bebd82a33..5f6fa6e7c 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,12 @@ 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 +73,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/plugin-api.test.ts b/packages/tailwindcss/src/plugin-api.test.ts index 229d5b7e3..aa0f485a4 100644 --- a/packages/tailwindcss/src/plugin-api.test.ts +++ b/packages/tailwindcss/src/plugin-api.test.ts @@ -814,6 +814,58 @@ 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', + }, + }) + + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), + }, + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) + + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }) + }, + }) + + expect(build(['foo-bar'])).toMatchInlineSnapshot(` + ".foo-bar { + background-color: red; + } + .foo-bar { + color: red; + } + .foo-bar { + --my-prop: bar-valuer; + } + " + `) + }) }) describe('addUtilities()', () => { diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts index 132a479e1..eb366b7be 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 0d04f7caa..5ce50e95b 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); @@ -15096,7 +15131,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 +15151,7 @@ describe('custom utilities', () => { expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` "@layer utilities { .really-round { + --custom-prop: hi; border-radius: 30rem; } }" @@ -15159,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; } `) @@ -15171,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; } }" diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 4ff589710..650c654f7 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -27,14 +27,18 @@ type SuggestionDefinition = hasDefaultValue?: boolean } +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 - }[] - >(() => []) + private utilities = new DefaultMap(() => []) private completions = new Map SuggestionGroup[]>() @@ -42,8 +46,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') { @@ -2441,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']]) diff --git a/playgrounds/vite/src/animate.js b/playgrounds/vite/src/animate.js new file mode 100644 index 000000000..0a5617399 --- /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 a916100c7..9c069ae65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4077,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) @@ -4101,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 @@ -4123,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