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 5 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 @@ -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

Expand Down
8 changes: 8 additions & 0 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
"
`)
})
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 @@ -28,6 +28,7 @@ interface ResolutionContext {

let minimal: ResolvedConfig = {
prefix: '',
important: false,
darkMode: null,
theme: {},
plugins: [],
Expand Down Expand Up @@ -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
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 @@ -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
}
6 changes: 5 additions & 1 deletion packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)]
}
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

rules.push({
node,
propertySort,
Expand Down
8 changes: 8 additions & 0 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type DesignSystem = {
utilities: Utilities
variants: Variants

important: string | boolean
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
Expand Down Expand Up @@ -45,6 +47,12 @@ 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)
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
important: false,

candidatesToCss(classes: string[]) {
let result: (string | null)[] = []

Expand Down
64 changes: 64 additions & 0 deletions packages/tailwindcss/src/important.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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);
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
`

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

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;
}
}
}
"
`)
})
31 changes: 31 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: string | true | null = null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
Expand Down Expand Up @@ -232,6 +233,32 @@ 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
}

// 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)
Expand Down Expand Up @@ -284,6 +311,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
67 changes: 67 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -83,3 +86,67 @@ 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 {
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
#app & {
text-decoration-line: underline;
}
}
",
".hover\\:line-through {
#app & {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
}
",
]
`)
})

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