From 284182ec88bd13ed69bd8fe7914df33fb7f204a4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 23 Sep 2024 18:09:33 -0400 Subject: [PATCH 01/15] Add support for wrapping utilities with a selector --- packages/tailwindcss/src/compile.ts | 4 ++ packages/tailwindcss/src/design-system.ts | 7 ++++ packages/tailwindcss/src/important.test.ts | 36 ++++++++++++++++++ packages/tailwindcss/src/index.ts | 19 ++++++++++ packages/tailwindcss/src/intellisense.test.ts | 37 +++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 packages/tailwindcss/src/important.test.ts diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 9b7b3ce468b0..5c5f1e6e53cb 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -154,6 +154,10 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem if (result === null) return [] } + if (typeof designSystem.important === 'string') { + node.nodes = [rule(designSystem.important, node.nodes)] + } + rules.push({ node, propertySort, diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 0bac8102d7de..8b0718461112 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -13,6 +13,8 @@ export type DesignSystem = { utilities: Utilities variants: Variants + important: string | boolean + getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] @@ -45,6 +47,11 @@ export function buildDesignSystem(theme: Theme): DesignSystem { utilities, variants, + // How to mark important utilities + // - wrap with a selector (any string) + // - do nothing (false) + important: false, + candidatesToCss(classes: string[]) { let result: (string | null)[] = [] diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts new file mode 100644 index 000000000000..7fc0a52a65ff --- /dev/null +++ b/packages/tailwindcss/src/important.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from 'vitest' +import { compile } from '.' + +const css = String.raw + +test('Utilities can be wrapped in a selector', async () => { + // This is the v4 equivalent of `important: "#app"` from v3 + let input = css` + @import 'tailwindcss/utilities' selector(#app); + ` + + let compiler = await compile(input, { + loadStylesheet: async (id: string, base: string) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + ".underline { + #app & { + text-decoration-line: underline; + } + } + .hover\\:line-through { + #app & { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f3c7ee674370..d5ade032179d 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -76,6 +76,7 @@ async function parseCss( await substituteAtImports(ast, base, loadStylesheet) // Find all `@theme` declarations + let important: string | true | null = null let theme = new Theme() let customVariants: ((designSystem: DesignSystem) => void)[] = [] let customUtilities: ((designSystem: DesignSystem) => void)[] = [] @@ -232,6 +233,20 @@ async function parseCss( return WalkAction.Skip } + // Drop instances of `@media selector(…)` + // + // We support `@import "tailwindcss" selector(…)` as a way to + // nest utilities under a custom selector. + if (node.selector.startsWith('@media selector(')) { + let themeParams = node.selector.slice(16, -1) + + important = `${themeParams} &` + + replaceWith(node.nodes) + + return WalkAction.Skip + } + if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return let [themeOptions, themePrefix] = parseThemeOptions(node.selector) @@ -284,6 +299,10 @@ async function parseCss( let designSystem = buildDesignSystem(theme) + if (important) { + designSystem.important = important + } + // Apply hooks from backwards compatibility layer. This function takes a lot // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 0eb4d16cc94f..ee7da069ef18 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -1,7 +1,10 @@ import { expect, test } from 'vitest' +import { __unstable__loadDesignSystem } from '.' import { buildDesignSystem } from './design-system' import { Theme } from './theme' +const css = String.raw + function loadDesignSystem() { let theme = new Theme() theme.add('--spacing-0_5', '0.125rem') @@ -83,3 +86,37 @@ test('The variant `has-force` does not crash', () => { expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`) }) + +test('Utilities show when nested in a selector in intellisense', async () => { + let input = css` + @import 'tailwindcss/utilities' selector(#app); + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + #app & { + text-decoration-line: underline; + } + } + ", + ".hover\\:line-through { + #app & { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + } + ", + ] + `) +}) From d5ba6ed5c5f1eb8f034a3eef75e6ff00c3962fd1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 24 Sep 2024 16:29:45 -0400 Subject: [PATCH 02/15] Allow marking utility declarations as `!important` --- packages/tailwindcss/src/compile.ts | 2 +- packages/tailwindcss/src/design-system.ts | 1 + packages/tailwindcss/src/important.test.ts | 28 +++++++++++++++++ packages/tailwindcss/src/index.ts | 12 ++++++++ packages/tailwindcss/src/intellisense.test.ts | 30 +++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 5c5f1e6e53cb..1f5a1d1abff8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -135,7 +135,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem for (let nodes of asts) { let propertySort = getPropertySort(nodes) - if (candidate.important) { + if (candidate.important || designSystem.important === true) { applyImportant(nodes) } diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index 8b0718461112..f95349600437 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -49,6 +49,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem { // How to mark important utilities // - wrap with a selector (any string) + // - add an !important (true) // - do nothing (false) important: false, diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts index 7fc0a52a65ff..2462322c7954 100644 --- a/packages/tailwindcss/src/important.test.ts +++ b/packages/tailwindcss/src/important.test.ts @@ -34,3 +34,31 @@ test('Utilities can be wrapped in a selector', async () => { " `) }) + +test('Utilities can be marked with important', async () => { + // This is the v4 equivalent of `important: true` from v3 + let input = css` + @import 'tailwindcss/utilities' important; + ` + + let compiler = await compile(input, { + loadStylesheet: async (id: string, base: string) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + ".underline { + text-decoration-line: underline!important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d5ade032179d..353ca36c00a8 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -247,6 +247,18 @@ async function parseCss( return WalkAction.Skip } + // Drop instances of `@media important` + // + // We support `@import "tailwindcss" important` to mark all declarations + // in generated utilities as `!important`. + if (node.selector.startsWith('@media important')) { + important = true + + replaceWith(node.nodes) + + return WalkAction.Skip + } + if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return let [themeOptions, themePrefix] = parseThemeOptions(node.selector) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index ee7da069ef18..8c70aa3e1d2a 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -120,3 +120,33 @@ test('Utilities show when nested in a selector in intellisense', async () => { ] `) }) + +test('Utilities, when marked as important, show as important in intellisense', async () => { + let input = css` + @import 'tailwindcss/utilities' important; + ` + + let design = await __unstable__loadDesignSystem(input, { + loadStylesheet: async (_, base) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline!important; + } + ", + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + ", + ] + `) +}) From 61705881666a1f7590c59e3912f90e7988bbd9ea Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 17 Sep 2024 12:57:31 -0400 Subject: [PATCH 03/15] Merge `important` key from configs and plugins --- packages/tailwindcss/src/compat/config/resolve-config.ts | 5 +++++ packages/tailwindcss/src/compat/config/types.ts | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 0c20bf7c335c..72c2b6ca3a6c 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -28,6 +28,7 @@ interface ResolutionContext { let minimal: ResolvedConfig = { prefix: '', + important: false, darkMode: null, theme: {}, plugins: [], @@ -64,6 +65,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv if ('prefix' in config && config.prefix !== undefined) { ctx.result.prefix = config.prefix ?? '' } + + if ('important' in config && config.important !== undefined) { + ctx.result.important = config.important ?? false + } } // Merge themes diff --git a/packages/tailwindcss/src/compat/config/types.ts b/packages/tailwindcss/src/compat/config/types.ts index 4b6e072b58d2..21d68d8d0eb8 100644 --- a/packages/tailwindcss/src/compat/config/types.ts +++ b/packages/tailwindcss/src/compat/config/types.ts @@ -78,3 +78,12 @@ export interface UserConfig { export interface ResolvedConfig { prefix: string } + +// `important` support +export interface UserConfig { + important?: boolean | string +} + +export interface ResolvedConfig { + important: boolean | string +} From 2ff0e34ad1761250865641fdf13fa1ec06e5447a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 23 Sep 2024 18:21:47 -0400 Subject: [PATCH 04/15] Add support for `important` key from configs --- .../src/compat/apply-compat-hooks.ts | 8 ++ .../tailwindcss/src/compat/config.test.ts | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 1cf3cc8102ff..d9de533fe08c 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -229,6 +229,14 @@ export async function applyCompatibilityHooks({ designSystem.theme.prefix = resolvedConfig.prefix } + // If an important strategy has already been set in CSS don't override it + if (!designSystem.important && resolvedConfig.important) { + designSystem.important = + typeof resolvedConfig.important === 'string' + ? `${resolvedConfig.important} &` + : resolvedConfig.important + } + // Replace `resolveThemeValue` with a version that is backwards compatible // with dot-notation but also aware of any JS theme configurations registered // by plugins or JS config files. This is significantly slower than just diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 4725a8f2dd4b..1812156284bb 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1371,3 +1371,79 @@ test('a prefix must be letters only', async () => { `[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`, ) }) + +test('important: `#app`', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: '#app' }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + ".custom { + #app & { + color: red; + } + } + .underline { + #app & { + text-decoration-line: underline; + } + } + .hover\\:line-through { + #app & { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + } + " + `) +}) + +test('important: true', async () => { + let input = css` + @tailwind utilities; + @config "./config.js"; + + @utility custom { + color: red; + } + ` + + let compiler = await compile(input, { + loadModule: async (_, base) => ({ + base, + module: { important: true }, + }), + }) + + expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` + ".custom { + color: red!important; + } + .underline { + text-decoration-line: underline!important; + } + .hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + " + `) +}) From 3871ea1753995c011a23b10f2604d0f9b4ed9718 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 23 Sep 2024 18:25:57 -0400 Subject: [PATCH 05/15] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26610bd8d60f..ed174632bf10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501)) +- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) +- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) ### Fixed From 5b0beaaa05abe3dc6d006aea4fc64a5cf0a7d885 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 25 Sep 2024 12:52:21 -0400 Subject: [PATCH 06/15] Split `important` into two properties in the design system --- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 11 ++++++----- packages/tailwindcss/src/compile.ts | 6 +++--- packages/tailwindcss/src/design-system.ts | 11 ++++++----- packages/tailwindcss/src/index.ts | 9 +++++++-- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index d9de533fe08c..4aed848b81a3 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -230,11 +230,12 @@ export async function applyCompatibilityHooks({ } // If an important strategy has already been set in CSS don't override it - if (!designSystem.important && resolvedConfig.important) { - designSystem.important = - typeof resolvedConfig.important === 'string' - ? `${resolvedConfig.important} &` - : resolvedConfig.important + if (!designSystem.important && resolvedConfig.important === true) { + designSystem.important = true + } + + if (!designSystem.wrappingSelector && typeof resolvedConfig.important === 'string') { + designSystem.wrappingSelector = `${resolvedConfig.important} &` } // Replace `resolveThemeValue` with a version that is backwards compatible diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 1f5a1d1abff8..319276338bac 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -135,7 +135,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem for (let nodes of asts) { let propertySort = getPropertySort(nodes) - if (candidate.important || designSystem.important === true) { + if (candidate.important || designSystem.important) { applyImportant(nodes) } @@ -154,8 +154,8 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem if (result === null) return [] } - if (typeof designSystem.important === 'string') { - node.nodes = [rule(designSystem.important, node.nodes)] + if (designSystem.wrappingSelector) { + node.nodes = [rule(designSystem.wrappingSelector, node.nodes)] } rules.push({ diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index f95349600437..ca2cdf3cde4e 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -13,7 +13,11 @@ export type DesignSystem = { utilities: Utilities variants: Variants - important: string | boolean + // Whether to mark utility declarations as !important + important: boolean + + // Whether to wrap utility declarations in a selector + wrappingSelector: string | null getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] @@ -47,11 +51,8 @@ export function buildDesignSystem(theme: Theme): DesignSystem { utilities, variants, - // How to mark important utilities - // - wrap with a selector (any string) - // - add an !important (true) - // - do nothing (false) important: false, + wrappingSelector: null, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 353ca36c00a8..dd5338dcdc4d 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -76,7 +76,8 @@ async function parseCss( await substituteAtImports(ast, base, loadStylesheet) // Find all `@theme` declarations - let important: string | true | null = null + let important: boolean | null = null + let wrappingSelector: string | null = null let theme = new Theme() let customVariants: ((designSystem: DesignSystem) => void)[] = [] let customUtilities: ((designSystem: DesignSystem) => void)[] = [] @@ -240,7 +241,7 @@ async function parseCss( if (node.selector.startsWith('@media selector(')) { let themeParams = node.selector.slice(16, -1) - important = `${themeParams} &` + wrappingSelector = `${themeParams} &` replaceWith(node.nodes) @@ -315,6 +316,10 @@ async function parseCss( designSystem.important = important } + if (wrappingSelector) { + designSystem.wrappingSelector = wrappingSelector + } + // Apply hooks from backwards compatibility layer. This function takes a lot // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards From 44f5ed0269315e6eec692688ca119c999ffde340 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 25 Sep 2024 13:01:17 -0400 Subject: [PATCH 07/15] Allow selector() and important to coexist on an import --- packages/tailwindcss/src/important.test.ts | 32 ++++++++++++++ packages/tailwindcss/src/index.ts | 49 ++++++++++++++-------- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts index 2462322c7954..b1315bcf3470 100644 --- a/packages/tailwindcss/src/important.test.ts +++ b/packages/tailwindcss/src/important.test.ts @@ -62,3 +62,35 @@ test('Utilities can be marked with important', async () => { " `) }) + +test('Utilities can be wrapped with a selector and marked as important', async () => { + // This is the v4 equivalent of `important: true` from v3 + let input = css` + @import 'tailwindcss/utilities' selector(#app) important; + ` + + let compiler = await compile(input, { + loadStylesheet: async (id: string, base: string) => ({ + base, + content: '@tailwind utilities;', + }), + }) + + expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + ".underline { + #app & { + text-decoration-line: underline!important; + } + } + .hover\\:line-through { + #app & { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index dd5338dcdc4d..1cbff37d3286 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -234,28 +234,41 @@ async function parseCss( return WalkAction.Skip } - // Drop instances of `@media selector(…)` - // - // We support `@import "tailwindcss" selector(…)` as a way to - // nest utilities under a custom selector. - if (node.selector.startsWith('@media selector(')) { - let themeParams = node.selector.slice(16, -1) + if (node.selector.startsWith('@media')) { + let features = segment(node.selector.slice(6), ' ') + let shouldReplace = true + + for (let i = 0; i < features.length; i++) { + let part = features[i] + + // Drop instances of `@media important` + // + // We support `@import "tailwindcss" important` to mark all declarations + // in generated utilities as `!important`. + if (part === 'important') { + important = true + shouldReplace = true + features[i] = '' + } - wrappingSelector = `${themeParams} &` + // Drop instances of `@media selector(…)` + // + // We support `@import "tailwindcss" selector(…)` as a way to + // nest utilities under a custom selector. + else if (part.startsWith('selector(') && part.endsWith(')')) { + wrappingSelector = `${part.slice(9, -1)} &` + shouldReplace = true + features[i] = '' + } + } - replaceWith(node.nodes) + let remaining = features.filter(Boolean).join(' ') - return WalkAction.Skip - } - - // Drop instances of `@media important` - // - // We support `@import "tailwindcss" important` to mark all declarations - // in generated utilities as `!important`. - if (node.selector.startsWith('@media important')) { - important = true + node.selector = `@media ${remaining}` - replaceWith(node.nodes) + if (remaining.trim() === '' && shouldReplace) { + replaceWith(node.nodes) + } return WalkAction.Skip } From 427be39548422a3a5200852422b4f07368890682 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 09:47:32 -0400 Subject: [PATCH 08/15] Switch to wrapping `@tailwind utilities` --- .../src/compat/apply-compat-hooks.ts | 20 +++++++-- .../tailwindcss/src/compat/config.test.ts | 12 ++---- packages/tailwindcss/src/design-system.ts | 4 -- packages/tailwindcss/src/important.test.ts | 43 ++++++++----------- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 4aed848b81a3..355341c21f9a 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -1,4 +1,4 @@ -import { toCss, walk, type AstNode } from '../ast' +import { rule, toCss, walk, WalkAction, type AstNode } from '../ast' import type { DesignSystem } from '../design-system' import type { Theme, ThemeKey } from '../theme' import { withAlpha } from '../utilities' @@ -234,8 +234,22 @@ export async function applyCompatibilityHooks({ designSystem.important = true } - if (!designSystem.wrappingSelector && typeof resolvedConfig.important === 'string') { - designSystem.wrappingSelector = `${resolvedConfig.important} &` + if (typeof resolvedConfig.important === 'string') { + let wrappingSelector = resolvedConfig.important + + walk(ast, (node, { replaceWith, parent }) => { + if (node.kind !== 'rule') return + if (node.selector !== '@tailwind utilities') return + + // The AST node was already manually wrapped so there's nothing to do + if (parent?.kind === 'rule' && parent.selector === wrappingSelector) { + return WalkAction.Stop + } + + replaceWith(rule(wrappingSelector, [node])) + + return WalkAction.Stop + }) } // Replace `resolveThemeValue` with a version that is backwards compatible diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 1812156284bb..a756fc4d8a1f 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1390,18 +1390,14 @@ test('important: `#app`', async () => { }) expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(` - ".custom { - #app & { + "#app { + .custom { color: red; } - } - .underline { - #app & { + .underline { text-decoration-line: underline; } - } - .hover\\:line-through { - #app & { + .hover\\:line-through { &:hover { @media (hover: hover) { text-decoration-line: line-through; diff --git a/packages/tailwindcss/src/design-system.ts b/packages/tailwindcss/src/design-system.ts index ca2cdf3cde4e..a9b863557441 100644 --- a/packages/tailwindcss/src/design-system.ts +++ b/packages/tailwindcss/src/design-system.ts @@ -16,9 +16,6 @@ export type DesignSystem = { // Whether to mark utility declarations as !important important: boolean - // Whether to wrap utility declarations in a selector - wrappingSelector: string | null - getClassOrder(classes: string[]): [string, bigint | null][] getClassList(): ClassEntry[] getVariants(): VariantEntry[] @@ -52,7 +49,6 @@ export function buildDesignSystem(theme: Theme): DesignSystem { variants, important: false, - wrappingSelector: null, candidatesToCss(classes: string[]) { let result: (string | null)[] = [] diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts index b1315bcf3470..fe7debf20f8e 100644 --- a/packages/tailwindcss/src/important.test.ts +++ b/packages/tailwindcss/src/important.test.ts @@ -6,24 +6,19 @@ const css = String.raw test('Utilities can be wrapped in a selector', async () => { // This is the v4 equivalent of `important: "#app"` from v3 let input = css` - @import 'tailwindcss/utilities' selector(#app); + #app { + @tailwind utilities; + } ` - let compiler = await compile(input, { - loadStylesheet: async (id: string, base: string) => ({ - base, - content: '@tailwind utilities;', - }), - }) + let compiler = await compile(input) expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` - ".underline { - #app & { + "#app { + .underline { text-decoration-line: underline; } - } - .hover\\:line-through { - #app & { + .hover\\:line-through { &:hover { @media (hover: hover) { text-decoration-line: line-through; @@ -64,26 +59,24 @@ test('Utilities can be marked with important', async () => { }) test('Utilities can be wrapped with a selector and marked as important', async () => { - // This is the v4 equivalent of `important: true` from v3 + // This does not have a direct equivalent in v3 but works as a consequence of + // the new APIs let input = css` - @import 'tailwindcss/utilities' selector(#app) important; + @media important { + #app { + @tailwind utilities; + } + } ` - let compiler = await compile(input, { - loadStylesheet: async (id: string, base: string) => ({ - base, - content: '@tailwind utilities;', - }), - }) + let compiler = await compile(input) expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` - ".underline { - #app & { + "#app { + .underline { text-decoration-line: underline!important; } - } - .hover\\:line-through { - #app & { + .hover\\:line-through { &:hover { @media (hover: hover) { text-decoration-line: line-through!important; From 50e9f99dd8319656ae61fe9b67906a35c9d0fc8a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 09:49:07 -0400 Subject: [PATCH 09/15] Update tests --- packages/tailwindcss/src/intellisense.test.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 8c70aa3e1d2a..055e5efaa854 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -87,9 +87,10 @@ test('The variant `has-force` does not crash', () => { expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`) }) -test('Utilities show when nested in a selector in intellisense', async () => { +test('Utilities do not show wrapping selector in intellisense', async () => { let input = css` - @import 'tailwindcss/utilities' selector(#app); + @import 'tailwindcss/utilities'; + @config './config.js'; ` let design = await __unstable__loadDesignSystem(input, { @@ -97,22 +98,24 @@ test('Utilities show when nested in a selector in intellisense', async () => { base, content: '@tailwind utilities;', }), + loadModule: async () => ({ + base: '', + module: { + important: '#app', + }, + }), }) expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` [ ".underline { - #app & { - text-decoration-line: underline; - } + text-decoration-line: underline; } ", ".hover\\:line-through { - #app & { - &:hover { - @media (hover: hover) { - text-decoration-line: line-through; - } + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; } } } From 399d877c27f950074aa29606cc4c2b22acf6f034 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 09:53:03 -0400 Subject: [PATCH 10/15] wip --- packages/tailwindcss/src/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 1cbff37d3286..83c3cf2f99b6 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -250,16 +250,6 @@ async function parseCss( shouldReplace = true features[i] = '' } - - // Drop instances of `@media selector(…)` - // - // We support `@import "tailwindcss" selector(…)` as a way to - // nest utilities under a custom selector. - else if (part.startsWith('selector(') && part.endsWith(')')) { - wrappingSelector = `${part.slice(9, -1)} &` - shouldReplace = true - features[i] = '' - } } let remaining = features.filter(Boolean).join(' ') From d973d6d8d69bf10b70253728d1e043897df6a331 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 09:58:58 -0400 Subject: [PATCH 11/15] wip --- packages/tailwindcss/src/intellisense.test.ts | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index dd5bcd15104a..063359a23b35 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -93,7 +93,20 @@ test('Can produce CSS per candidate using `candidatesToCss`', () => { expect( design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]']), - ).toMatchInlineSnapshot() + ).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline; + } + ", + null, + null, + ".bg-\\[\\#000\\] { + background-color: #000; + } + ", + ] + `) }) test('Utilities do not show wrapping selector in intellisense', async () => { @@ -115,7 +128,22 @@ test('Utilities do not show wrapping selector in intellisense', async () => { }), }) - expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot() + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline; + } + ", + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through; + } + } + } + ", + ] + `) }) test('Utilities, when marked as important, show as important in intellisense', async () => { @@ -130,5 +158,20 @@ test('Utilities, when marked as important, show as important in intellisense', a }), }) - expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot() + expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(` + [ + ".underline { + text-decoration-line: underline!important; + } + ", + ".hover\\:line-through { + &:hover { + @media (hover: hover) { + text-decoration-line: line-through!important; + } + } + } + ", + ] + `) }) From 8ffa8f7631f02dc63579c1caae4a9fe2d2b2f53e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 10:02:16 -0400 Subject: [PATCH 12/15] wip --- packages/tailwindcss/src/compile.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 80e51bd1e97f..f7d9c5125fa8 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -159,10 +159,6 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem if (result === null) return [] } - if (designSystem.wrappingSelector) { - node.nodes = [rule(designSystem.wrappingSelector, node.nodes)] - } - rules.push({ node, propertySort, From 049c16f3c88a5bc73cecafe6bc93a1971c9f766f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 10:03:24 -0400 Subject: [PATCH 13/15] wip --- packages/tailwindcss/src/intellisense.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 063359a23b35..c61d2295119a 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -91,9 +91,8 @@ test('Can produce CSS per candidate using `candidatesToCss`', () => { let design = loadDesignSystem() design.invalidCandidates = new Set(['bg-[#fff]']) - expect( - design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]']), - ).toMatchInlineSnapshot(` + expect(design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]'])) + .toMatchInlineSnapshot(` [ ".underline { text-decoration-line: underline; From 5a5c7231ca5addcd184e27c52f6d932818fdea83 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 10:03:27 -0400 Subject: [PATCH 14/15] wip --- packages/tailwindcss/src/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d4a48c5dc09a..2d247b3aa5b8 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -77,7 +77,6 @@ async function parseCss( // Find all `@theme` declarations let important: boolean | null = null - let wrappingSelector: string | null = null let theme = new Theme() let customVariants: ((designSystem: DesignSystem) => void)[] = [] let customUtilities: ((designSystem: DesignSystem) => void)[] = [] @@ -323,10 +322,6 @@ async function parseCss( designSystem.important = important } - if (wrappingSelector) { - designSystem.wrappingSelector = wrappingSelector - } - // Apply hooks from backwards compatibility layer. This function takes a lot // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards From e5b3b864dae5a23ac45b3166ae51c2bba5e8e716 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 3 Oct 2024 10:03:51 -0400 Subject: [PATCH 15/15] tweak changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4eec8517d0..3154c5fe3860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501)) -- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) -- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) - Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553)) - Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556)) - Add `color-scheme` utilities ([#14567](https://github.com/tailwindlabs/tailwindcss/pull/14567)) +- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) +- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448)) - _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514)) - _Experimental_: Migrate `@apply` utilities with the template codemods ([#14574](https://github.com/tailwindlabs/tailwindcss/pull/14574)) - _Experimental_: Add template codemods for migrating variant order ([#14524](https://github.com/tailwindlabs/tailwindcss/pull/14524]))