Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for important in v4 #14448

Merged
merged 16 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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]))
Expand Down
25 changes: 24 additions & 1 deletion packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -229,6 +229,29 @@ 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 === true) {
designSystem.important = true
}

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
})
}

for (let candidate of resolvedConfig.blocklist) {
designSystem.invalidCandidates.add(candidate)
}
Expand Down
72 changes: 72 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,78 @@ test('a prefix must be letters only', async () => {
)
})

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(`
"#app {
.custom {
color: red;
}
.underline {
text-decoration-line: underline;
}
.hover\\:line-through {
&: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;
}
}
}
"
`)
})

test('blocklisted canddiates are not generated', async () => {
let compiler = await compile(
css`
Expand Down
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ResolutionContext {
let minimal: ResolvedConfig = {
blocklist: [],
prefix: '',
important: false,
darkMode: null,
theme: {},
plugins: [],
Expand Down Expand Up @@ -69,6 +70,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
if ('blocklist' in config && config.blocklist !== undefined) {
ctx.result.blocklist = config.blocklist ?? []
}

if ('important' in config && config.important !== undefined) {
ctx.result.important = config.important ?? false
}
}

// Merge themes
Expand Down
9 changes: 9 additions & 0 deletions packages/tailwindcss/src/compat/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,12 @@ export interface UserConfig {
export interface ResolvedConfig {
blocklist: string[]
}

// `important` support
export interface UserConfig {
important?: boolean | string
}

export interface ResolvedConfig {
important: boolean | string
}
2 changes: 1 addition & 1 deletion packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,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) {
applyImportant(nodes)
}

Expand Down
4 changes: 4 additions & 0 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export type DesignSystem = {

invalidCandidates: Set<string>

// Whether to mark utility declarations as !important
important: boolean

getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
Expand Down Expand Up @@ -48,6 +51,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
variants,

invalidCandidates: new Set(),
important: false,

candidatesToCss(classes: string[]) {
let result: (string | null)[] = []
Expand Down
89 changes: 89 additions & 0 deletions packages/tailwindcss/src/important.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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`
#app {
@tailwind utilities;
}
`

let compiler = await compile(input)

expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
"#app {
.underline {
text-decoration-line: underline;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
}
"
`)
})

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;
}
}
}
"
`)
})

test('Utilities can be wrapped with a selector and marked as important', async () => {
// This does not have a direct equivalent in v3 but works as a consequence of
// the new APIs
let input = css`
@media important {
#app {
@tailwind utilities;
}
}
`

let compiler = await compile(input)

expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
"#app {
.underline {
text-decoration-line: underline!important;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through!important;
}
}
}
}
"
`)
})
34 changes: 34 additions & 0 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async function parseCss(
await substituteAtImports(ast, base, loadStylesheet)

// Find all `@theme` declarations
let important: boolean | null = null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
Expand Down Expand Up @@ -236,6 +237,35 @@ async function parseCss(
return WalkAction.Skip
}

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] = ''
}
}

let remaining = features.filter(Boolean).join(' ')

node.selector = `@media ${remaining}`

if (remaining.trim() === '' && shouldReplace) {
replaceWith(node.nodes)
}

return WalkAction.Skip
}

if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return

let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
Expand Down Expand Up @@ -288,6 +318,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
Expand Down
Loading