diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index f56cd765e8d7..52b2dd4c6ad5 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -2480,7 +2480,9 @@ test( @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { ::placeholder { color: currentcolor; - @supports (color: color-mix(in lab, red, red)) { + } + @supports (color: color-mix(in lab, red, red)) { + ::placeholder { color: color-mix(in oklab, currentcolor 50%, transparent); } } @@ -2523,7 +2525,9 @@ test( @layer utilities { .bg-red-500\\/50 { background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { + } + @supports (color: color-mix(in lab, red, red)) { + .bg-red-500\\/50 { background-color: color-mix(in oklab, var(--color-red-500) 50%, transparent); } } diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 4c936a2a23af..1bb23c1681a8 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -1137,7 +1137,7 @@ test( -
Hello, world!
+
Hello, world!
`, 'src/index.css': css` @@ -1149,16 +1149,16 @@ test( async ({ exec, expect, fs }) => { await exec('pnpm vite build') - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - // Should not be minified when optimize is disabled - let content = await fs.read(filename) - expect(content).toContain('.hover\\:flex {') - expect(content).toContain('&:hover {') - expect(content).toContain('@media (hover: hover) {') - expect(content).toContain('display: flex;') + expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-.css')) + .toMatchInlineSnapshot(` + " + --- dist/assets/index-.css --- + .focus\\:text-\\[black\\]:focus { + color: black; + } + " + `) }, ) @@ -1192,7 +1192,7 @@ test( -
Hello, world!
+
Hello, world!
`, 'src/index.css': css` @@ -1204,15 +1204,17 @@ test( async ({ exec, expect, fs }) => { await exec('pnpm vite build') - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] - - // Should be optimized but not minified - let content = await fs.read(filename) - expect(content).toContain('@media (hover: hover) {') - expect(content).toContain('.hover\\:flex:hover {') - expect(content).toContain('display: flex;') + expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-.css')) + .toMatchInlineSnapshot(` + " + --- dist/assets/index-.css --- + @media (hover: hover) { + .hover\\:text-\\[black\\]:hover { + color: #000; + } + } + " + `) }, ) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 24f31f7bb5b5..c791d9c8b25a 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,26 +1,36 @@ -import { expect, it } from 'vitest' +import { describe, expect, it, test } from 'vitest' +import * as SelectorParser from '../src/selector-parser' import { atRule, + cloneAstNode, context, cssContext, decl, + DROPPABLE_IF_EMPTY_AT_RULES, + handleNesting, + HOISTABLE_AT_RULES, optimizeAst, + optimizeSelector, + rule, styleRule, toCss, type AstNode, } from './ast' import * as CSS from './css-parser' import { buildDesignSystem } from './design-system' +import { SourceLocation } from './source-maps/source' +import { pretty } from './test-utils/run' import { Theme } from './theme' +import { segment } from './utils/segment' import { walk, WalkAction } from './walk' const css = String.raw const defaultDesignSystem = buildDesignSystem(new Theme()) it('should pretty print an AST', () => { - expect(toCss(optimizeAst(CSS.parse('.foo{color:red;&:hover{color:blue;}}'), defaultDesignSystem))) - .toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(CSS.parse('.foo{color:red;&:hover{color:blue;}}')))).toMatchInlineSnapshot(` + " + .foo { color: red; &:hover { color: blue; @@ -68,47 +78,18 @@ it('allows the placement of context nodes', () => { expect(blueContext).toEqual({ context: 'a' }) expect(greenContext).toEqual({ context: 'b' }) - expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` + " + .foo { color: red; } .bar { color: blue; - .baz { - color: green; - } - } - " - `) -}) - -it('should stop walking when returning `WalkAction.Stop`', () => { - let ast = [ - styleRule('.foo', [styleRule('.nested', [styleRule('.bail', [decl('color', 'red')])])]), - styleRule('.bar'), - styleRule('.baz'), - styleRule('.qux'), - ] - - let seen = new Set() - - walk(ast, (node) => { - if (node.kind === 'rule') { - seen.add(node.selector) - } - - if (node.kind === 'rule' && node.selector === '.bail') { - return WalkAction.Stop } - }) - - // We do not want to see `.bar`, `.baz`, or `.qux` because we bailed early - expect(seen).toMatchInlineSnapshot(` - Set { - ".foo", - ".nested", - ".bail", + .bar .baz { + color: green; } + " `) }) @@ -154,13 +135,15 @@ it('should not emit empty rules once optimized', () => { /* Exceptions: */ @charset "UTF-8"; @layer foo, bar, baz; + @layer foo, bar, baz; /* Will be deduped */ @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); `) - expect(toCss(ast)).toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(ast))).toMatchInlineSnapshot(` + " + .foo { } .foo { .bar { @@ -187,14 +170,16 @@ it('should not emit empty rules once optimized', () => { } @charset "UTF-8"; @layer foo, bar, baz; + @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @import url('https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap'); " `) - expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` - "@charset "UTF-8"; + expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` + " + @charset "UTF-8"; @layer foo, bar, baz; @custom-media --modern (color), (hover); @namespace 'http://www.w3.org/1999/xhtml'; @@ -235,8 +220,9 @@ it('should not emit exact duplicate declarations in the same rule', () => { } `) - expect(toCss(ast)).toMatchInlineSnapshot(` - ".foo { + expect(pretty(toCss(ast))).toMatchInlineSnapshot(` + " + .foo { color: red; .bar { color: green; @@ -267,23 +253,26 @@ it('should not emit exact duplicate declarations in the same rule', () => { " `) - expect(toCss(optimizeAst(ast, defaultDesignSystem))).toMatchInlineSnapshot(` - ".foo { - .bar { - color: blue; - color: green; - } + expect(pretty(toCss(optimizeAst(ast, defaultDesignSystem)))).toMatchInlineSnapshot(` + " + .foo { color: red; } + .foo .bar { + color: blue; + color: green; + } .foo { color: green; color: blue; color: red; background: blue; - .bar { - color: blue; - color: green; - } + } + .foo .bar { + color: blue; + color: green; + } + .foo { caret-color: orange; } " @@ -307,8 +296,9 @@ it('should not emit color-mix() fallbacks inside @keyframes', () => { let design = buildDesignSystem(theme) - expect(toCss(optimizeAst(ast, design))).toMatchInlineSnapshot(` - "@keyframes my-animation { + expect(pretty(toCss(optimizeAst(ast, design)))).toMatchInlineSnapshot(` + " + @keyframes my-animation { 0% { color: color-mix(in oklab, var(--color-emerald-600) 0%, transparent); } @@ -320,303 +310,1294 @@ it('should not emit color-mix() fallbacks inside @keyframes', () => { `) }) -it('should only visit children once when calling `replaceWith` with single element array', () => { - let visited = new Set() +describe('optimization', () => { + function optimize(input: string) { + let ast = CSS.parse(input) - let ast: AstNode[] = [ - atRule('@media', '', [styleRule('.foo', [decl('color', 'blue')])]), - styleRule('.bar', [decl('color', 'blue')]), - ] + let cssOracle = pretty(toCss(handleNestingOracle(ast.map(cloneAstNode)))) + let cssOptimized = pretty(toCss(handleNesting(ast.map(cloneAstNode)))) - walk(ast, (node) => { - if (visited.has(node)) { - throw new Error('Visited node twice') - } - visited.add(node) + // Ensure the results matches the slower oracle version + expect(cssOptimized).toEqual(cssOracle) - if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes) - }) -}) + return cssOptimized + } -it('should only visit children once when calling `replaceWith` with multi-element array', () => { - let visited = new Set() + // See: https://drafts.csswg.org/css-nesting-1/ + describe('CSS Nesting Module Level 1', () => { + it('uses the descendant combinator by default', async () => { + expect( + optimize(css` + .a { + element { + --x: 1; + } + .class { + --x: 2; + } + #id { + --x: 3; + } + :pseudo-class { + --x: 4; + } + ::pseudo-element { + --x: 5; + } + [attribute] { + --x: 6; + } + * { + --x: 7; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a element { + --x: 1; + } + .a .class { + --x: 2; + } + .a #id { + --x: 3; + } + .a :pseudo-class { + --x: 4; + } + .a ::pseudo-element { + --x: 5; + } + .a [attribute] { + --x: 6; + } + .a * { + --x: 7; + } + " + `) + }) - let ast: AstNode[] = [ - atRule('@media', '', [ - context({}, [ - styleRule('.foo', [decl('color', 'red')]), - styleRule('.baz', [decl('color', 'blue')]), - ]), - ]), - styleRule('.bar', [decl('color', 'green')]), - ] + it('shoud be possible to change the combinator', async () => { + expect( + optimize(css` + .a { + + .b { + --x: 1; + } + > .c { + --x: 2; + } + ~ .d { + --x: 3; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a + .b { + --x: 1; + } + .a > .c { + --x: 2; + } + .a ~ .d { + --x: 3; + } + " + `) + }) - walk(ast, (node) => { - let key = id(node) - if (visited.has(key)) { - throw new Error('Visited node twice') - } - visited.add(key) + it('should replace the first rule, that contains `&` with `:scope`', async () => { + expect( + optimize(css` + /* Standalone */ + & { + --x: 1; + } - if (node.kind === 'at-rule') return WalkAction.Replace(node.nodes) - }) + /* In an at-rule */ + @supports (--y: 1) { + & { + --x: 2; + } + } - expect(visited).toMatchInlineSnapshot(` - Set { - "@media ", - "", - ".foo", - "color: red", - ".baz", - "color: blue", - ".bar", - "color: green", - } - `) -}) + /* With :is(…) */ + :is(&) { + --x: 3; + } -it('should never visit children when calling `replaceWith` with `WalkAction.Skip`', () => { - let visited = new Set() + /* In an at-rule, with :is(…) */ + @supports (--y: 2) { + :is(&) { + --x: 4; + } + } - let inner = styleRule('.foo', [decl('color', 'blue')]) + /* With multiple selectors */ + &, + .a { + --x: 5; + } - let ast: AstNode[] = [atRule('@media', '', [inner]), styleRule('.bar', [decl('color', 'blue')])] + /* With multiple selectors + :is(…) */ + :is(&), + .b { + --x: 6; + } - walk(ast, (node) => { - visited.add(node) + /* With multiple selectors in an at-rule */ + @supports (--y: 3) { + &, + .c { + --x: 7; + } + } - if (node.kind === 'at-rule') { - return WalkAction.ReplaceSkip(node.nodes) - } - }) + /* With multiple selectors in an at-rule + :is(…) */ + @supports (--y: 4) { + :is(&), + .d { + --x: 8; + } + } + `), + ).toMatchInlineSnapshot(` + " + :scope { + --x: 1; + } + @supports (--y: 1) { + :scope { + --x: 2; + } + } + :scope { + --x: 3; + } + @supports (--y: 2) { + :scope { + --x: 4; + } + } + :scope, .a { + --x: 5; + } + :scope, .b { + --x: 6; + } + @supports (--y: 3) { + :scope, .c { + --x: 7; + } + } + @supports (--y: 4) { + :scope, .d { + --x: 8; + } + } + " + `) + }) - expect(visited).not.toContain(inner) - expect(visited).toMatchInlineSnapshot(` - Set { - { - "kind": "at-rule", - "name": "@media", - "nodes": [ - { - "kind": "rule", - "nodes": [ - { - "important": false, - "kind": "declaration", - "property": "color", - "value": "blue", - }, - ], - "selector": ".foo", - }, - ], - "params": "", - }, - { - "kind": "rule", - "nodes": [ - { - "important": false, - "kind": "declaration", - "property": "color", - "value": "blue", - }, - ], - "selector": ".bar", - }, - { - "important": false, - "kind": "declaration", - "property": "color", - "value": "blue", - }, - } - `) + it('should be possible to use `&` to explicitly match the parent', async () => { + expect( + optimize(css` + .a { + & + .b { + --x: 1; + } + & > .c { + --x: 2; + } + & ~ .d { + --x: 3; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a + .b { + --x: 1; + } + .a > .c { + --x: 2; + } + .a ~ .d { + --x: 3; + } + " + `) + }) + + it('should be possible to use `&` in a different location', async () => { + expect( + optimize(css` + .a { + .b & { + --x: 1; + } + .c + & { + --x: 2; + } + .d > & { + --x: 3; + } + .e ~ & { + --x: 4; + } + } + `), + ).toMatchInlineSnapshot(` + " + .b .a { + --x: 1; + } + .c + .a { + --x: 2; + } + .d > .a { + --x: 3; + } + .e ~ .a { + --x: 4; + } + " + `) + }) + + it('should be possible to use `&` on its own', async () => { + expect( + optimize(css` + .a { + & { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a { + --x: 1; + } + " + `) + }) + + it('should be possible to use `&` nested in `:is(…)`', async () => { + expect( + optimize(css` + .a { + :is(&) { + --x: 1; + } + :is(:is(:is(&))) { + --x: 2; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a { + --x: 1; + --x: 2; + } + " + `) + }) + + it('should be possible to handle nesting with a parent selector list', async () => { + expect( + optimize(css` + .a, + .b { + .c, + .d & { + --x: 1; + &:hover { + --x: 2; + } + } + } + `), + ).toMatchInlineSnapshot(` + " + :is(.a, .b) .c, .d :is(.a, .b) { + --x: 1; + } + :is(:is(.a, .b) .c, .d :is(.a, .b)):hover { + --x: 2; + } + " + `) + }) + + it('should not replace `\&`', () => { + expect( + optimize(css` + .a { + .b-\& { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a .b-\\& { + --x: 1; + } + " + `) + }) + + it('should not replace `&` as part of a string', () => { + expect( + optimize(css` + .a { + [data-b='c&d'] { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a [data-b='c&d'] { + --x: 1; + } + " + `) + }) + + it.each([ + ['element', '&element', 'element:is(element)'], + ['element', 'element&', 'element:is(element)'], + ['element', '&.class', 'element.class'], + ['element', '.class&', 'element.class'], + ['element', '&#id', 'element#id'], + ['element', '#id&', 'element#id'], + ['element', '&:pseudo-class', 'element:pseudo-class'], + ['element', ':pseudo-class&', 'element:pseudo-class'], + ['element', '&::pseudo-element', 'element::pseudo-element'], + ['element', '::pseudo-element&', 'element::pseudo-element'], + ['element', '&:pseudo-fn()', 'element:pseudo-fn()'], + ['element', ':pseudo-fn()&', 'element:pseudo-fn()'], + ['element', '&[attribute]', 'element[attribute]'], + ['element', '[attribute]&', 'element[attribute]'], + ['element', '&*', 'element:is(*)'], + ['element', '*&', 'element:is(*)'], + + ['.class', '&element', 'element.class'], + ['.class', 'element&', 'element.class'], + ['.class', '&.class', '.class.class'], + ['.class', '.class&', '.class.class'], + ['.class', '&#id', '.class#id'], + ['.class', '#id&', '#id.class'], + ['.class', '&:pseudo-class', '.class:pseudo-class'], + ['.class', ':pseudo-class&', ':pseudo-class.class'], + ['.class', '&::pseudo-element', '.class::pseudo-element'], + ['.class', '::pseudo-element&', '::pseudo-element.class'], + ['.class', '&:pseudo-fn()', '.class:pseudo-fn()'], + ['.class', ':pseudo-fn()&', ':pseudo-fn().class'], + ['.class', '&[attribute]', '.class[attribute]'], + ['.class', '[attribute]&', '[attribute].class'], + ['.class', '&*', '*.class'], + ['.class', '*&', '*.class'], + + ['#id', '&element', 'element#id'], + ['#id', 'element&', 'element#id'], + ['#id', '&.class', '#id.class'], + ['#id', '.class&', '.class#id'], + ['#id', '&#id', '#id#id'], + ['#id', '#id&', '#id#id'], + ['#id', '&:pseudo-class', '#id:pseudo-class'], + ['#id', ':pseudo-class&', ':pseudo-class#id'], + ['#id', '&::pseudo-element', '#id::pseudo-element'], + ['#id', '::pseudo-element&', '::pseudo-element#id'], + ['#id', '&:pseudo-fn()', '#id:pseudo-fn()'], + ['#id', ':pseudo-fn()&', ':pseudo-fn()#id'], + ['#id', '&[attribute]', '#id[attribute]'], + ['#id', '[attribute]&', '[attribute]#id'], + ['#id', '&*', '*#id'], + ['#id', '*&', '*#id'], + + [':pseudo-class', '&element', 'element:pseudo-class'], + [':pseudo-class', 'element&', 'element:pseudo-class'], + [':pseudo-class', '&.class', ':pseudo-class.class'], + [':pseudo-class', '.class&', '.class:pseudo-class'], + [':pseudo-class', '&#id', ':pseudo-class#id'], + [':pseudo-class', '#id&', '#id:pseudo-class'], + [':pseudo-class', '&:pseudo-class', ':pseudo-class:pseudo-class'], + [':pseudo-class', ':pseudo-class&', ':pseudo-class:pseudo-class'], + [':pseudo-class', '&::pseudo-element', ':pseudo-class::pseudo-element'], + [':pseudo-class', '::pseudo-element&', '::pseudo-element:pseudo-class'], + [':pseudo-class', '&:pseudo-fn()', ':pseudo-class:pseudo-fn()'], + [':pseudo-class', ':pseudo-fn()&', ':pseudo-fn():pseudo-class'], + [':pseudo-class', '&[attribute]', ':pseudo-class[attribute]'], + [':pseudo-class', '[attribute]&', '[attribute]:pseudo-class'], + [':pseudo-class', '&*', '*:pseudo-class'], + [':pseudo-class', '*&', '*:pseudo-class'], + + ['::pseudo-element', '&element', 'element::pseudo-element'], + ['::pseudo-element', 'element&', 'element::pseudo-element'], + ['::pseudo-element', '&.class', '::pseudo-element.class'], + ['::pseudo-element', '.class&', '.class::pseudo-element'], + ['::pseudo-element', '&#id', '::pseudo-element#id'], + ['::pseudo-element', '#id&', '#id::pseudo-element'], + ['::pseudo-element', '&:pseudo-class', '::pseudo-element:pseudo-class'], + ['::pseudo-element', ':pseudo-class&', ':pseudo-class::pseudo-element'], + ['::pseudo-element', '&::pseudo-element', '::pseudo-element::pseudo-element'], + ['::pseudo-element', '::pseudo-element&', '::pseudo-element::pseudo-element'], + ['::pseudo-element', '&:pseudo-fn()', '::pseudo-element:pseudo-fn()'], + ['::pseudo-element', ':pseudo-fn()&', ':pseudo-fn()::pseudo-element'], + ['::pseudo-element', '&[attribute]', '::pseudo-element[attribute]'], + ['::pseudo-element', '[attribute]&', '[attribute]::pseudo-element'], + ['::pseudo-element', '&*', '*::pseudo-element'], + ['::pseudo-element', '*&', '*::pseudo-element'], + + [':pseudo-fn()', '&element', 'element:pseudo-fn()'], + [':pseudo-fn()', 'element&', 'element:pseudo-fn()'], + [':pseudo-fn()', '&.class', ':pseudo-fn().class'], + [':pseudo-fn()', '.class&', '.class:pseudo-fn()'], + [':pseudo-fn()', '&#id', ':pseudo-fn()#id'], + [':pseudo-fn()', '#id&', '#id:pseudo-fn()'], + [':pseudo-fn()', '&:pseudo-class', ':pseudo-fn():pseudo-class'], + [':pseudo-fn()', ':pseudo-class&', ':pseudo-class:pseudo-fn()'], + [':pseudo-fn()', '&::pseudo-element', ':pseudo-fn()::pseudo-element'], + [':pseudo-fn()', '::pseudo-element&', '::pseudo-element:pseudo-fn()'], + [':pseudo-fn()', '&:pseudo-fn()', ':pseudo-fn():pseudo-fn()'], + [':pseudo-fn()', ':pseudo-fn()&', ':pseudo-fn():pseudo-fn()'], + [':pseudo-fn()', '&[attribute]', ':pseudo-fn()[attribute]'], + [':pseudo-fn()', '[attribute]&', '[attribute]:pseudo-fn()'], + [':pseudo-fn()', '&*', '*:pseudo-fn()'], + [':pseudo-fn()', '*&', '*:pseudo-fn()'], + + ['[attribute]', '&element', 'element[attribute]'], + ['[attribute]', 'element&', 'element[attribute]'], + ['[attribute]', '&.class', '[attribute].class'], + ['[attribute]', '.class&', '.class[attribute]'], + ['[attribute]', '&#id', '[attribute]#id'], + ['[attribute]', '#id&', '#id[attribute]'], + ['[attribute]', '&:pseudo-class', '[attribute]:pseudo-class'], + ['[attribute]', ':pseudo-class&', ':pseudo-class[attribute]'], + ['[attribute]', '&::pseudo-element', '[attribute]::pseudo-element'], + ['[attribute]', '::pseudo-element&', '::pseudo-element[attribute]'], + ['[attribute]', '&:pseudo-fn()', '[attribute]:pseudo-fn()'], + ['[attribute]', ':pseudo-fn()&', ':pseudo-fn()[attribute]'], + ['[attribute]', '&[attribute]', '[attribute][attribute]'], + ['[attribute]', '[attribute]&', '[attribute][attribute]'], + ['[attribute]', '&*', '*[attribute]'], + ['[attribute]', '*&', '*[attribute]'], + + ['*', '&element', 'element:is(*)'], + ['*', 'element&', 'element:is(*)'], + ['*', '&.class', '*.class'], + ['*', '.class&', '*.class'], + ['*', '&#id', '*#id'], + ['*', '#id&', '*#id'], + ['*', '&:pseudo-class', '*:pseudo-class'], + ['*', ':pseudo-class&', '*:pseudo-class'], + ['*', '&::pseudo-element', '*::pseudo-element'], + ['*', '::pseudo-element&', '*::pseudo-element'], + ['*', '&:pseudo-fn()', '*:pseudo-fn()'], + ['*', ':pseudo-fn()&', '*:pseudo-fn()'], + ['*', '&[attribute]', '*[attribute]'], + ['*', '[attribute]&', '*[attribute]'], + ['*', '&*', '*:is(*)'], + ['*', '*&', '*:is(*)'], + + ['&', '&element', 'element:scope'], + ['&', 'element&', 'element:scope'], + ['&', '&.class', ':scope.class'], + ['&', '.class&', '.class:scope'], + ['&', '&#id', ':scope#id'], + ['&', '#id&', '#id:scope'], + ['&', '&:pseudo-class', ':scope:pseudo-class'], + ['&', ':pseudo-class&', ':pseudo-class:scope'], + ['&', '&::pseudo-element', ':scope::pseudo-element'], + ['&', '::pseudo-element&', '::pseudo-element:scope'], + ['&', '&:pseudo-fn()', ':scope:pseudo-fn()'], + ['&', ':pseudo-fn()&', ':pseudo-fn():scope'], + ['&', '&[attribute]', ':scope[attribute]'], + ['&', '[attribute]&', '[attribute]:scope'], + ['&', '&*', '*:scope'], + ['&', '*&', '*:scope'], + ])(`'%s { %s }' → '%s' (%#)`, async (root, nested, expected) => { + let optimized = optimize(toCss([rule(root, [rule(nested, [decl('--x', '0')])])])) + let ast = CSS.parse(optimized) + + let count = 0 + walk(ast, () => void count++) + + // 1 rule, 1 declaration + expect(count).toBe(2) + + if (ast[0].kind !== 'rule') throw new Error('expected a rule') + expect(ast[0].selector).toEqual(expected) + }) + + test('multiple selectors in the list are relative to the parent', async () => { + expect( + optimize(css` + .a, + .b { + --x: 1; + + .c { + --x: 2; + } + &.d { + --x: 3; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a, .b { + --x: 1; + } + :is(.a, .b) + .c { + --x: 2; + } + :is(.a, .b).d { + --x: 3; + } + " + `) + }) + + it('should be possible to use `&` multiple times', async () => { + expect( + optimize(css` + .a { + & .b & .c & .d { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a .b .a .c .a .d { + --x: 1; + } + " + `) + }) + + it('should be possible to use `&` multiple times in a row', async () => { + expect( + optimize(css` + .a { + &&& { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + .a.a.a { + --x: 1; + } + " + `) + }) + + it('should be possible to use `&` inside a selector', async () => { + expect( + optimize(css` + .a { + :not(&) { + --x: 1; + } + } + `), + ).toMatchInlineSnapshot(` + " + :not(.a) { + --x: 1; + } + " + `) + }) + + it('should be possible to use deeply nested CSS', async () => { + expect( + optimize(css` + .a, + .b { + --x: 1; + + .c & { + --x: 2; + + &:hover, + &:focus { + --x: 3; + .d { + --x: 4; + } + } + } + } + `), + ).toMatchInlineSnapshot(` + " + .a, .b { + --x: 1; + } + .c :is(.a, .b) { + --x: 2; + } + :is(.c :is(.a, .b)):hover, :is(.c :is(.a, .b)):focus { + --x: 3; + } + :is(:is(.c :is(.a, .b)):hover, :is(.c :is(.a, .b)):focus) .d { + --x: 4; + } + " + `) + }) + + it('should properly split rules to guarantee specificity', async () => { + expect( + optimize(css` + .a { + --before: 1; + &:hover { + --inside: 1; + } + --after: 1; + } + `), + ).toMatchInlineSnapshot(` + " + .a { + --before: 1; + } + .a:hover { + --inside: 1; + } + .a { + --after: 1; + } + " + `) + }) + + it('should hoist at-rules', async () => { + expect( + optimize(css` + @layer utilities { + .a, + .b { + @media (print) { + --x: 1; + .c { + @media (min-width: 123px) { + --x: 2; + } + } + } + } + .d { + @media (print) { + @media (min-width: 123px) { + --x: 3; + } + } + } + } + @property --foo { + syntax: '*'; + } + @layer utilities { + .e { + @media (print) { + --x: 4; + } + } + } + `), + ).toMatchInlineSnapshot(` + " + @layer utilities { + @media (print) { + .a, .b { + --x: 1; + } + @media (min-width: 123px) { + :is(.a, .b) .c { + --x: 2; + } + .d { + --x: 3; + } + } + } + } + @property --foo { + syntax: '*'; + } + @layer utilities { + @media (print) { + .e { + --x: 4; + } + } + } + " + `) + }) + + it('should leave `@property` and `@apply` alone', async () => { + expect( + optimize(css` + .foo { + .bar { + @apply text-red-500 hover:text-red-600; + } + } + + .baz { + @property --tw-content { + syntax: '*'; + initial-value: ''; + inherits: false; + } + + @property --tw-border-spacing-x { + syntax: ''; + inherits: false; + initial-value: 0; + } + } + `), + ).toMatchInlineSnapshot(` + " + .foo .bar { + @apply text-red-500 hover:text-red-600; + } + .baz { + @property --tw-content { + syntax: '*'; + initial-value: ''; + inherits: false; + } + @property --tw-border-spacing-x { + syntax: ''; + inherits: false; + initial-value: 0; + } + } + " + `) + }) + }) }) -it('should skip the correct number of children based on the replaced children nodes', () => { +// A simple step-by-step but slow implementation of CSS nesting used as an +// oracle to test faster and more efficient solutions against. +export function handleNestingOracle(ast: AstNode[]): AstNode[] { + // Remove empty nodes + // + // Inside-out such that a node containing another node that is empty, also gets + // cleaned up when walking up the tree. + // + // For at-rules, we only want to get rid of at-rules like `@supports` and + // `@media` that we know are safe to remove when they are empty. + // + // Known at-rules that are not safe to delete: `@charset`, `@layer`, + // `@namespace`, `@custom-media`, `@apply`, … + // + // ```css + // .foo {} + // @media (min-width: 123px) { + // .bar {} + // } + // @layer base; + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // @layer base; + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] - walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { + walk(ast, { + exit(node) { + if (!('nodes' in node)) return + if (node.nodes.length > 0) return + if (node.kind === 'at-rule' && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) return + return WalkAction.ReplaceSkip([]) - } + }, }) - - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) } + // A rule with an `&` selector should be converted to a `:scope` if that rule + // has no parent rule. Parent at-rules don't count since they don't contain + // selectors. + // + // ```css + // & { + // color: red; + // } + // :is(&) { + // color: blue; + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // :scope { + // color: red; + // } + // :scope { + // color: blue; + // } + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - return WalkAction.Replace([]) - } - }) + if (node.kind !== 'rule') return + + let ast = SelectorParser.parse(node.selector) - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) + walk(ast, (node) => { + // Nested in `:is(…)`, unwrap + while (node.kind === 'function' && node.value === ':is' && node.nodes.length === 1) { + node = node.nodes[0] + } + + // Just `&`, replace with `:scope` + if (node.kind === 'selector' && node.value === '&') { + return WalkAction.ReplaceSkip(SelectorParser.selector(':scope')) + } + }) + + node.selector = SelectorParser.toCss(ast) + + return WalkAction.Skip + }) } + // Remove intermediate nodes with an `&` selector, or `&` nested inside + // `:is(…)` n-levels deep, and replace them by its `nodes`. + // + // ```css + // .foo { + // & { + // color: red; + // } + // :is(&) { + // color: green; + // } + // :is(:is(&)) { + // color: blue; + // & { + // color: black + // } + // } + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .foo { + // color: red; + // color: green; + // color: blue; + // color: black; + // } + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - return WalkAction.ReplaceSkip([decl('--index', '2.1')]) + if (node.kind !== 'rule') return + + let ast = SelectorParser.parse(node.selector) + + while ( + ast.length === 1 && + ast[0].kind === 'function' && + ast[0].value === ':is' && + ast[0].nodes.length === 1 + ) { + ast = ast[0].nodes + } + + // Just `&`, replace by its `nodes` + if (ast.length === 1 && ast[0].kind === 'selector' && ast[0].value === '&') { + return WalkAction.Replace(node.nodes) } }) + } + + // Split into groups, this allows us to reduce the problem space, and only + // have to think about linear nesting because there will only ever be a single + // rule or at-rule at each level. + // + // ```css + // .foo { + // --x: 1; + // .bar { + // --x: 2; + // } + // --x: 3; + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .foo { + // --x: 1; + // } + // .foo { + // .bar { + // --x: 2; + // } + // } + // .foo { + // --x: 3; + // } + // ``` + { + walk(ast, { + exit(node) { + if (!('nodes' in node)) return + + let last: AstNode | null = null + let groups: AstNode[][] = [] + for (let child of node.nodes) { + if (last === null || 'nodes' in child) { + groups.push([child]) + } else { + if ('nodes' in last) { + groups.push([child]) + } else { + groups[groups.length - 1].push(child) + } + } + last = child + } - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) + if (groups.length <= 1) { + return + } + + node.nodes = [] + return WalkAction.Replace( + groups.map((nodes) => Object.assign(cloneAstNode(node), { nodes })), + ) + }, + }) } + // Hoist at-rules to the top in the order they were seen in + // + // ```css + // .foo { + // @media (print) { + // @supports (display: grid) { + // color: red; + // } + // } + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // @media (print) { + // @supports (display: grid) { + // .foo { + // color: red; + // } + // } + // } + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] - walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - return WalkAction.Replace([decl('--index', '2.1')]) + for (let [idx, node] of ast.entries()) { + if (!('nodes' in node)) continue + + let nodes: AstNode[] = [node] + let atRules: [ + name: string, + params: string, + src: SourceLocation | undefined, + dst: SourceLocation | undefined, + ][] = [] + walk(nodes, (node) => { + if (node.kind !== 'at-rule') return + if (node.nodes.length <= 0) return + if (!HOISTABLE_AT_RULES.has(node.name)) return + + // Track the at-rule + atRules.unshift([node.name, node.params, node.src, node.dst]) + + // Replace the at-rule by its nodes + return WalkAction.Replace(node.nodes) + }) + + // No at-rules found + if (atRules.length <= 0) continue + + // Wrap `node` in the at-rules + { + let root: AstNode | null = null + for (let [name, params, src, dst] of atRules) { + root = atRule(name, params, root ? [root] : nodes) + if (src || dst) Object.assign(root, { src, dst }) + } + if (root) ast[idx] = root } + } + } + + // Insert explicit `&`, when one was not used + // + // ```css + // .foo { + // .a, .b:is(&) { + // --x: 1; + // } + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .foo { + // & .a, .b:is(&) { + // --x: 1; + // } + // } + // ``` + { + walk(ast, (node, ctx) => { + if (node.kind !== 'rule') return + if (!ctx.path().some((node) => node.kind === 'rule')) return // Only inject `&` when it's not the first rule + + node.selector = segment(node.selector, ',') + .map((selector) => { + selector = selector.trim() + + let hasAmpersand = false + walk(SelectorParser.parse(selector.trim()), (node) => { + if (node.kind === 'selector' && node.value === '&') { + hasAmpersand = true + return WalkAction.Stop + } + }) + + return hasAmpersand ? selector : `& ${selector}` + }) + .join(', ') }) + } + + // Flatten selectors with `:is(…)` semantics + // + // ```css + // .foo, .bar { + // &:hover { + // --x: 1; + // } + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // :is(.foo, .bar):hover { + // --x: 1; + // } + // ``` + { + walk(ast, { + exit(node, ctx) { + if (node.kind !== 'rule') return + if (ctx.parent?.kind !== 'rule') return + + let parentAst = SelectorParser.parse(`:is(${ctx.parent.selector})`) - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 2.1", - "--index: 3", - "--index: 4", - ] - `) + // Wrap parent selector in `:is(…)` + let ast = SelectorParser.parse(node.selector) + walk(ast, (node) => { + if (node.kind === 'selector' && node.value === '&') { + return WalkAction.ReplaceSkip(parentAst.map(SelectorParser.cloneAstNode)) + } + }) + ctx.parent.selector = SelectorParser.toCss(ast) + + // Override the parent's nodes with our nodes now that we merged the + // selectors together. + ctx.parent.nodes = node.nodes + }, + }) } + // Optimize the selector by unwrapping unnecessary `:is(…)` + // + // ```css + // .a:is(.b) { + // color: red; + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .a.b { + // color: red; + // } + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - return WalkAction.ReplaceSkip([decl('--index', '2.1'), decl('--index', '2.2')]) - } + if (node.kind !== 'rule') return + node.selector = optimizeSelector(node.selector) }) - - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 3", - "--index: 4", - ] - `) } + // Merge adjacent at-rules + // + // ```css + // @media (print) { + // .a, .b { + // --x: 1; + // } + // } + // + // @media (print) { + // @media (min-width: 123px) { + // :is(.a, .b) .c { + // --x: 2; + // } + // } + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // @media (print) { + // .a, .b { + // --x: 1; + // } + // + // @media (min-width: 123px) { + // :is(.a, .b) .c { + // --x: 2; + // } + // } + // } + // ``` { - let ast = [ - decl('--index', '0'), - decl('--index', '1'), - decl('--index', '2'), - decl('--index', '3'), - decl('--index', '4'), - ] - let visited: string[] = [] - walk(ast, (node) => { - visited.push(id(node)) - if (node.kind === 'declaration' && node.value === '2') { - return WalkAction.Replace([decl('--index', '2.1'), decl('--index', '2.2')]) - } + walk(ast, { + enter(node, ctx) { + if (node.kind !== 'at-rule') return + + let next = ctx.siblings[ctx.index + 1] + + if (!next) return + if (next.kind !== 'at-rule') return + if (next.name !== node.name) return + if (next.params !== node.params) return + + // Move our nodes over + next.nodes = node.nodes.concat(next.nodes) + + // We merge everything into the last at-rule, but from a CSS perspective + // this should look as-if we merged it into the first one. + next.src = node.src + next.dst = node.dst + + return WalkAction.Replace([]) + }, }) + } - expect(visited).toMatchInlineSnapshot(` - [ - "--index: 0", - "--index: 1", - "--index: 2", - "--index: 2.1", - "--index: 2.2", - "--index: 3", - "--index: 4", - ] - `) + // Merge adjacent rules with the same selector + // + // ```css + // .a { + // --x: 1; + // } + // .a { + // --y: 1; + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .a { + // --x: 1; + // --y: 1; + // } + // ``` + { + walk(ast, { + enter(node, ctx) { + if (node.kind !== 'rule') return + + let next = ctx.siblings[ctx.index + 1] + + if (!next) return + if (next.kind !== 'rule') return + if (next.selector !== node.selector) return + + // Move our nodes over + next.nodes = node.nodes.concat(next.nodes) + + // We merge everything into the last at-rule, but from a CSS perspective + // this should look as-if we merged it into the first one. + next.src = node.src + next.dst = node.dst + + return WalkAction.Replace([]) + }, + }) } -}) -function id(node: AstNode) { - switch (node.kind) { - case 'at-rule': - return `${node.name} ${node.params}` - case 'rule': - return node.selector - case 'context': - return '' - case 'at-root': - return '' - case 'declaration': - return `${node.property}: ${node.value}` - case 'comment': - return `// ${node.value}` - default: - node satisfies never - throw new Error('Unknown node kind') + // Remove declarations that occur later with the same property / value / + // important information. A potential future improvement could be getting rid + // of overrides, but often a fallback value is used this way. + // + // ```css + // .foo { + // --x: 1; + // --x: 2; + // --x: 1; + // } + // ``` + // + // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + // + // ```css + // .foo { + // --x: 2; + // --x: 1; + // } + // ``` + { + walk(ast, (node, ctx) => { + if (node.kind !== 'declaration') return + + for (let i = ctx.index + 1; i < ctx.siblings.length; i++) { + let next = ctx.siblings[i] + if ( + next.kind === 'declaration' && + next.property === node.property && + next.value === node.value && + next.important === node.important + ) { + return WalkAction.Replace([]) + } + } + }) } + + return ast } diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index ce841f3776e1..93584120cd47 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,9 +1,11 @@ import { Polyfills } from '.' +import * as SelectorParser from '../src/selector-parser' import { parseAtRule } from './css-parser' import type { DesignSystem } from './design-system' import type { Source, SourceLocation } from './source-maps/source' import { Theme, ThemeOptions } from './theme' import { DefaultMap } from './utils/default-map' +import { segment } from './utils/segment' import { extractUsedVariables } from './utils/variables' import * as ValueParser from './value-parser' import { walk, WalkAction, type VisitContext } from './walk' @@ -286,9 +288,9 @@ export function optimizeAst( // found in the theme config. if ( polyfills & Polyfills.ColorMix && - node.value.includes('color-mix(') && !context.supportsColorMix && - !context.keyframes + !context.keyframes && + node.value.includes('color-mix(') ) { colorMixDeclarations.get(parent).add(node) } @@ -304,37 +306,7 @@ export function optimizeAst( transform(child, nodes, context, depth + 1) } - // Keep the last decl when there are exact duplicates. Keeping the *first* one might - // not be correct when given nested rules where a rule sits between declarations. - let seen: Record = {} - let toRemove = new Set() - - // Keep track of all nodes that produce a given declaration - for (let child of nodes) { - if (child.kind !== 'declaration') continue - - let key = `${child.property}:${child.value}:${child.important}` - seen[key] ??= [] - seen[key].push(child) - } - - // And remove all but the last of each - for (let key in seen) { - for (let i = 0; i < seen[key].length - 1; ++i) { - toRemove.add(seen[key][i]) - } - } - - if (toRemove.size > 0) { - nodes = nodes.filter((node) => !toRemove.has(node)) - } - - if (nodes.length === 0) return - - // Rules with `&` as the selector should be flattened - if (node.selector === '&') { - parent.push(...nodes) - } else { + if (nodes.length > 0) { parent.push({ ...node, nodes }) } } @@ -679,7 +651,463 @@ export function optimizeAst( } } - return newAst + return handleNesting(newAst) +} + +export function handleNesting(ast: AstNode[]): AstNode[] { + // Track `rule` selectors as we go + let selectorStack: [ + selector: string, + src: SourceLocation | undefined, + dst: SourceLocation | undefined, + ][] = [] + + // Track `at-rule` information as we go. Tracking this separately from the + // selector stack for rules such that we can hoist this above all the rules. + let atRuleStack: [ + name: string, + params: string, + src: SourceLocation | undefined, + dst: SourceLocation | undefined, + ][] = [] + + // The current "nodes" we can push to + let nodes = null as AstNode[] | null + + // Optimization: Track the declaration properties we've seen in the current + // nodes. + let seenDeclarationProperties = new Set() + + // Track nodes lists where we want to dedupe declarations + let dedupeDeclarationsInNodes = new Set() + + // The final, new AST + let result: AstNode[] = [] + + // Track whether we should skip a node in the `exit` phase + let skipExit = new Set() + + walk(ast, { + enter(node) { + switch (node.kind) { + case 'rule': { + nodes = null // Start a new level + + // First time we see a rule + if (selectorStack.length === 0) { + // A rule with a selector containing `&` should replace the `&` with + // `:scope` if there is no parent rule. + // + // Note: there could be false positives when the `&` is escaped or + // part of a string inside an attribute selector. But the + // SelectorParser will take care of that. + if (node.selector.includes('&')) { + let ast = SelectorParser.parse(node.selector) + let changed = false + + walk(ast, (node) => { + if (node.kind === 'selector' && node.value === '&') { + changed = true + node.value = ':scope' + } + }) + + if (changed) { + selectorStack.push([SelectorParser.toCss(ast), node.src, node.dst]) + } else { + selectorStack.push([node.selector, node.src, node.dst]) + } + } + + // No nesting markers, track as-is + else { + selectorStack.push([node.selector, node.src, node.dst]) + } + } + + // Nested rule, ensure `&` is present in each selector. Then track the + // selector. + else { + // A rule with just `&` can be replaced by its children. Let's + // ignore this node and keep walking its children. + if (node.selector === '&') { + skipExit.add(node) + return + } + + // `&` is using `:is(…)` semantics + let parentSelector = `:is(${selectorStack[selectorStack.length - 1][0]})` + let selector = segment(node.selector, ',') + .map((selector) => { + // Slow path: we need to replace the `&` with the parent + // selector. A simple `replaceAll(…)` won't work because a `&` + // could be escaped, or could be part of an attribute selector. + // + // Much safer to parse the selector and replace the `&` that way + if (selector.includes('&')) { + let ast = SelectorParser.parse(selector) + let changed = false + walk(ast, (node) => { + if (node.kind === 'selector' && node.value === '&') { + changed = true + node.value = parentSelector + } + }) + + // It could be that `&` was not found as an actual selector, + // in that case we still have to prepend the parent selector. + return changed ? SelectorParser.toCss(ast) : `${parentSelector} ${selector}` + } + + // Fast path: we know there isn't a `&` so we can prepend the + // parent selector immediately. + else { + return `${parentSelector} ${selector}` + } + }) + .join(', ') + selectorStack.push([selector, node.src, node.dst]) + } + break + } + + case 'at-rule': { + nodes = null // Start a new level + + // `@layer` is hoistable, but when it's empty then we have to make + // sure that we still emit it because this might influence the layer + // order. We can't just get grid of it. + if (node.nodes.length === 0 && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) { + emit(node) + skipExit.add(node) + return WalkAction.Skip + } + + // Hoist at-rules + else if (HOISTABLE_AT_RULES.has(node.name)) { + atRuleStack.push([node.name, node.params, node.src, node.dst]) + } + + // If we can't hoist them, emit them immediately as-is + else { + emit(node) + skipExit.add(node) + return WalkAction.Skip + } + break + } + + case 'declaration': + case 'comment': { + emit(node) + break + } + + case 'context': + case 'at-root': + break + + default: + node satisfies never + break + } + }, + exit(node) { + if (skipExit.delete(node)) return + + switch (node.kind) { + case 'rule': { + nodes = null + selectorStack.pop() + break + } + + case 'at-rule': { + nodes = null + atRuleStack.pop() + break + } + + case 'declaration': + case 'comment': + case 'context': + case 'at-root': + break + + default: + node satisfies never + break + } + }, + }) + + // Dedupe declarations that we've already seen before if they match the + // `property`, `value` and `important` information. + { + for (let nodes of dedupeDeclarationsInNodes) { + let seen = new Set() + for (let i = nodes.length - 1; i >= 0; --i) { + let node = nodes[i] + if (node.kind !== 'declaration') continue + + let id = `${node.property}\0${node.value}\0${node.important}` + + if (seen.has(id)) nodes.splice(i, 1) + else seen.add(id) + } + } + } + + return result + + function emit(node: AstNode) { + // Existing nodes are available, emit into those nodes + if (nodes) { + // Optimization: track used declarations in the current node. + if (node.kind === 'declaration') { + if (seenDeclarationProperties.has(node.property)) { + dedupeDeclarationsInNodes.add(nodes) + } else { + seenDeclarationProperties.add(node.property) + } + } + + nodes.push(node) + return + } + + // Nothing available, setup a fresh node + { + // There are no parent rules or at-rules available, which means taht we + // can emit the node as-is. + if (selectorStack.length === 0 && atRuleStack.length === 0) { + let target = result + let lastNode = target[target.length - 1] + + // Optimization: when the current and last node are the same, ignore the + // new node entirely otherwise we will get unnecessary duplicate + // results. + // + // We only care about at-rules with no body because some of them (such + // as `@charset` or `@layer`) need to be emitted. A normal rule that's + // empty doesn't need to be emitted. + if ( + lastNode && + lastNode.kind === 'at-rule' && + node.kind === 'at-rule' && + lastNode.name === node.name && + lastNode.params === node.params + ) { + return + } + + result.push(node) + return + } + + // Track the new "parent" nodes + { + nodes = [node] + + // Clear out seen declarations from the previous work in progress nodes + seenDeclarationProperties.clear() + + // Track new declaration + if (node.kind === 'declaration') { + seenDeclarationProperties.add(node.property) + } + } + + // Track the new root node that we build up to store in the final AST + let root = null as AstNode | null + + let target = result + let atRuleOffset = 0 + + // Optimization: merge adjacent at-rules + // + // Figure out whether we can push our new node into a previous node that + // was already emitted. + // + // We have to make sure that the order stays the same, so therefore we + // only ever have to look at the last node that was emitted. + { + let lastNode = target[target.length - 1] + if (lastNode && lastNode.kind === 'at-rule') { + for (let i = 0; i < atRuleStack.length; i++) { + let atRule = atRuleStack[i] + if (lastNode.kind !== 'at-rule') break + if (lastNode.name !== atRule[0]) break + if (lastNode.params !== atRule[1]) break + + atRuleOffset++ + target = lastNode.nodes + lastNode = lastNode.nodes[lastNode.nodes.length - 1] + } + } + } + + // Build up the rule + if (selectorStack.length > 0) { + let [lastSelector, src, dst] = selectorStack[selectorStack.length - 1] + let selector = optimizeSelector(lastSelector) + + // Optimization: merge adjacent rules with the same selector + // + // Figure out whether we can push into an existing rule. + // + // If we have some at-rules that we have to keep into account, then we + // definitely can't. + if (atRuleStack.length - atRuleOffset <= 0) { + let lastNode = target[target.length - 1] + if (lastNode && lastNode.kind === 'rule' && lastNode.selector === selector) { + lastNode.nodes.push(...nodes) + + // Ensure that our current nodes points to the nodes of the + // `lastNode`, otherwise we will lose information. + nodes = lastNode.nodes + + // We appended a group that could contain declarations already in the + // existing rule, so let the final dedupe pass handle it once. + // + // Note: we could loop over _all_ previous nodes to figure out if we + // really want to dedupe this. But this could result in a bunch of + // duplicate work if we have `n` nodes that we want to merge + // together. + dedupeDeclarationsInNodes.add(nodes) + + // We know that we don't have to handle any more at-rules, so we can + // bail early since we just merged the nodes with the same selector. + return + } + } + + // Can't push into existing node, create a new node + root = rule(selector, nodes) + if (src || dst) Object.assign(root, { src, dst }) + } + + // Wrap in at-rules, if we can push into an existing node then we can + // ignore `offset` amount of nodes since the `root`/`nodes` will already + // point to a nested node. + for (let i = atRuleStack.length - 1; i >= atRuleOffset; --i) { + let [name, params, src, dst] = atRuleStack[i] + + root = atRule(name, params, root ? [root] : nodes) + if (src || dst) Object.assign(root, { src, dst }) + } + + // Track the root node in the AST + if (root) { + target.push(root) + } + + // We didn't build up any new root, so we can move our node directly into + // the target. This can happen when we emit a node that is not a + // declaration or a comment. + else { + target.push(...nodes) + } + } + } +} + +// A set of at-rules that can be hoisted to the top without any repercussions. +// Typically at-rules that rely on the environment, not parent information and +// contain other rules/declarations. +export const HOISTABLE_AT_RULES = new Set([ + '@container', + '@layer', + '@media', + '@page', + '@starting-style', + '@supports', + '@view-transition', +]) + +// As set of at-rules that can be dropped if they don't contain any nodes. We +// don't have the distinction between an at-rule with no body, or an at-rule +// with a body that is empty right now. +export const DROPPABLE_IF_EMPTY_AT_RULES = new Set([ + '@container', + '@media', + '@page', + '@starting-style', + '@supports', + '@view-transition', +]) + +// An `element` and a `*` can only appear once, and if they do, they have +// to be first. If multiple exist, an `:is(…)` should be used. +// +// - `div*` is invalid, must be `div:is(*)` +// - `**` is invalid, must be `*:is(*)` +// - `.classdiv` is invalid, must be `div.class` +// +function mustBeFirst(node: SelectorParser.SelectorAstNode | undefined) { + return node?.kind === 'selector' && (node.value === '*' || /^[a-zA-Z-]+$/.test(node.value)) +} + +let optimizedSelectorCache = new DefaultMap((selector) => { + let ast = SelectorParser.parse(selector) + + let changed = false + walk(ast, { + exit(node, ctx) { + if (node.kind === 'compound') { + // Swap nodes in a compound selector if one of the nodes has to be first + let idx = node.nodes.findIndex((child) => mustBeFirst(child)) + if (idx >= 0) { + // Optimization: Already in the correct spot, nothing to do here + if (idx === ctx.index) return + + changed = true + node.nodes.unshift(...node.nodes.splice(idx, 1)) + } + return + } + + // Unwrap `:is(…)` + if (node.kind !== 'function') return + if (node.value !== ':is') return + if (node.nodes.length !== 1) return + + let current = node.nodes[0] + if (current.kind !== 'selector' && current.kind !== 'function') return + + if (ctx.parent?.kind !== 'compound') { + changed = true + return WalkAction.Replace(current) + } + + let existing = ctx.siblings.find((sibling) => sibling !== node && mustBeFirst(sibling)) + if (!existing) { + changed = true + return WalkAction.Replace(current) + } + + if (!mustBeFirst(current)) { + changed = true + return WalkAction.Replace(current) + } + + if ( + existing.kind === 'selector' && + existing.value === '*' && + current.kind === 'selector' && + current.value !== '*' + ) { + changed = true + ctx.siblings[ctx.siblings.indexOf(existing)] = SelectorParser.fun(':is', [existing]) + return WalkAction.Replace(current) + } + }, + }) + + return changed ? SelectorParser.toCss(ast) : selector +}) +export function optimizeSelector(selector: string): string { + return optimizedSelectorCache.get(selector) } export function toCss(ast: AstNode[], track?: boolean) { diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index aaba93c4e26a..8780b9358ce6 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1297,22 +1297,22 @@ test('utilities must be prefixed', async () => { // Prefixed utilities are generated expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options)) .toMatchInlineSnapshot(` - " - .tw\\:custom { - color: red; - } + " + .tw\\:custom { + color: red; + } - .tw\\:underline { - text-decoration-line: underline; - } + .tw\\:underline { + text-decoration-line: underline; + } - @media (hover: hover) { - .tw\\:hover\\:line-through:hover { - text-decoration-line: line-through; + @media (hover: hover) { + .tw\\:hover\\:line-through:hover { + text-decoration-line: line-through; + } } - } - " - `) + " + `) // Non-prefixed utilities are ignored expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('') @@ -1459,7 +1459,7 @@ test('important: `#app`', async () => { } @media (hover: hover) { - #app .hover\\:line-through:hover { + :is(#app .hover\\:line-through):hover { text-decoration-line: line-through; } } @@ -1534,18 +1534,18 @@ test('blocklisted candidates are not generated', async () => { // underline will as will md:bg-white expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options)) .toMatchInlineSnapshot(` - " - .underline { - text-decoration-line: underline; - } + " + .underline { + text-decoration-line: underline; + } - @media (min-width: 48rem) { - .md\\:bg-white { - background-color: var(--color-white, #fff); + @media (min-width: 48rem) { + .md\\:bg-white { + background-color: var(--color-white, #fff); + } } - } - " - `) + " + `) }) test('blocklisted candidates cannot be used with `@apply`', async () => { diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index c1d9cb57a06d..ac853982de20 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -2737,7 +2737,7 @@ describe('matchVariant', () => { ).toMatchInlineSnapshot(` " @layer utilities { - .foo-known\\:flex:is(known), .foo-\\[test\\]\\:flex:is(test) { + known.foo-known\\:flex, test.foo-\\[test\\]\\:flex { display: flex; } } @@ -2775,7 +2775,7 @@ describe('matchVariant', () => { ).toMatchInlineSnapshot(` " @layer utilities { - .foo-string\\:flex:is(some string), .foo-\\[test\\]\\:flex:is(test) { + .foo-string\\:flex:is(some string), test.foo-\\[test\\]\\:flex { display: flex; } } @@ -3204,7 +3204,7 @@ describe('addUtilities()', () => { ).toMatchInlineSnapshot(` " @layer utilities { - .j.j, .j.j, .a .b:hover .c, .a .b:hover .c, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .e .bar:not(.f):has(.g), .h ~ .i, .h ~ .i { + .j.j, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .h ~ .i { color: red; } } diff --git a/packages/tailwindcss/src/important.test.ts b/packages/tailwindcss/src/important.test.ts index a3c9447fca71..677b1548141f 100644 --- a/packages/tailwindcss/src/important.test.ts +++ b/packages/tailwindcss/src/important.test.ts @@ -21,7 +21,7 @@ test('Utilities can be wrapped in a selector', async () => { } @media (hover: hover) { - #app .hover\\:line-through:hover { + :is(#app .hover\\:line-through):hover { text-decoration-line: line-through; } } @@ -81,7 +81,7 @@ test('Utilities can be wrapped with a selector and marked as important', async ( } @media (hover: hover) { - #app .hover\\:line-through:hover { + :is(#app .hover\\:line-through):hover { text-decoration-line: line-through !important; } } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 2a72336fb738..74282c0058ae 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1027,7 +1027,7 @@ describe('variant stacking', () => { expect(await run(['[&_p]:hover:flex'])).toMatchInlineSnapshot(` " @media (hover: hover) { - .\\[\\&_p\\]\\:hover\\:flex p:hover { + :is(.\\[\\&_p\\]\\:hover\\:flex p):hover { display: flex; } } @@ -1063,7 +1063,7 @@ describe('variant stacking', () => { } @media (hover: hover) { - .before\\:hover\\:flex:before:hover { + :is():hover { display: flex; } @@ -4449,7 +4449,7 @@ describe('@custom-variant', () => { --has-before: 1; } - .custom-before\\:underline:before:hover, .custom-before\\:underline:before:focus { + :is():hover, :is():focus { text-decoration-line: underline; } } @@ -4938,7 +4938,7 @@ describe('@custom-variant', () => { ), ).toMatchInlineSnapshot(` " - .a\\:flex .a, .b\\:flex .b .a .a-inside-b, .a\\:b\\:flex .a .b .a .a-inside-b, .b\\:a\\:flex .b .a .a-inside-b .a { + .a\\:flex .a, :is(:is(.b\\:flex .b) .a) .a-inside-b, :is(:is(:is(.a\\:b\\:flex .a) .b) .a) .a-inside-b, :is(:is(:is(.b\\:a\\:flex .b) .a) .a-inside-b) .a { display: flex; } " diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index 17e11f4feec6..5f76939e2e97 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -213,11 +213,9 @@ test('Utilities do not show wrapping selector in intellisense', async () => { text-decoration-line: underline; } ", - ".hover\\:line-through { - &:hover { - @media (hover: hover) { - text-decoration-line: line-through; - } + "@media (hover: hover) { + .hover\\:line-through:hover { + text-decoration-line: line-through; } } ", @@ -244,11 +242,9 @@ test('Utilities, when marked as important, show as important in intellisense', a text-decoration-line: underline !important; } ", - ".hover\\:line-through { - &:hover { - @media (hover: hover) { - text-decoration-line: line-through !important; - } + "@media (hover: hover) { + .hover\\:line-through:hover { + text-decoration-line: line-through !important; } } ", diff --git a/packages/tailwindcss/src/selector-parser.ts b/packages/tailwindcss/src/selector-parser.ts index fcc78b2e90f7..ea0eb88c8952 100644 --- a/packages/tailwindcss/src/selector-parser.ts +++ b/packages/tailwindcss/src/selector-parser.ts @@ -70,7 +70,7 @@ function compound(nodes: SelectorAstNode[]): SelectorCompoundNode { } } -function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode { +export function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode { return { kind: 'function', value, @@ -85,7 +85,7 @@ function list(nodes: SelectorAstNode[]): SelectorListNode { } } -function selector(value: string): SelectorNode { +export function selector(value: string): SelectorNode { return { kind: 'selector', value, @@ -99,6 +99,37 @@ function value(value: string): SelectorValueNode { } } +export function cloneAstNode(node: T): T { + switch (node.kind) { + case 'combinator': + case 'selector': + case 'value': + return { + kind: node.kind, + value: node.value, + } as T + + case 'complex': + case 'compound': + case 'list': + return { + kind: node.kind, + nodes: node.nodes.map(cloneAstNode), + } as T + + case 'function': + return { + kind: node.kind, + value: node.value, + nodes: node.nodes.map(cloneAstNode), + } satisfies SelectorFunctionNode as T + + default: + node satisfies never + throw new Error(`Unknown node kind: ${(node as any).kind}`) + } +} + export function toCss(ast: SelectorAstNode[], minify = false) { let css = '' for (let node of ast) { diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts index 25399e843898..fa8dfd9528f3 100644 --- a/packages/tailwindcss/src/source-maps/source-map.test.ts +++ b/packages/tailwindcss/src/source-maps/source-map.test.ts @@ -462,115 +462,118 @@ test('source maps trace back to @import location', async ({ expect }) => { ^^^^^^^^^^^^^^^^^^^ EQ @ 119:6-25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 294:4-61 | 295 } | 296 } - 120 @supports (color: color-mix(in lab, red, red)) { | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 120:6-53 | - 121 color: color-mix(in oklab, currentcolor 50%, transparent); | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 121:8-65 | - 122 } | - 123 } | - 124 } | - 125 textarea { | 302 textarea { - ^^^^^^^^^ ER @ 125:2-11 | ^^^^^^^^^ ER @ 302:0-9 - 126 resize: vertical; | 303 resize: vertical; - ^^^^^^^^^^^^^^^^ ES @ 126:4-20 | ^^^^^^^^^^^^^^^^ ES @ 303:2-18 + 120 } | + 121 @supports (color: color-mix(in lab, red, red)) { | + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 121:4-51 | + 122 ::placeholder { | + ^^^^^^^^^^^^^^ EP @ 122:6-20 | + 123 color: color-mix(in oklab, currentcolor 50%, transparent); | + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 123:8-65 | + 124 } | + 125 } | + 126 } | + 127 textarea { | 302 textarea { + ^^^^^^^^^ ER @ 127:2-11 | ^^^^^^^^^ ER @ 302:0-9 + 128 resize: vertical; | 303 resize: vertical; + ^^^^^^^^^^^^^^^^ ES @ 128:4-20 | ^^^^^^^^^^^^^^^^ ES @ 303:2-18 | 304 } - 127 } | - 128 ::-webkit-search-decoration { | 310 ::-webkit-search-decoration { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 128:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 310:0-28 - 129 -webkit-appearance: none; | 311 -webkit-appearance: none; - ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 129:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 311:2-26 + 129 } | + 130 ::-webkit-search-decoration { | 310 ::-webkit-search-decoration { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 130:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 310:0-28 + 131 -webkit-appearance: none; | 311 -webkit-appearance: none; + ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 131:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 311:2-26 | 312 } - 130 } | - 131 ::-webkit-date-and-time-value { | 319 ::-webkit-date-and-time-value { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 131:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 319:0-30 - 132 min-height: 1lh; | 320 min-height: 1lh; /* 1 */ - ^^^^^^^^^^^^^^^ EW @ 132:4-19 | ^^^^^^^^^^^^^^^ EW @ 320:2-17 - 133 text-align: inherit; | 321 text-align: inherit; /* 2 */ - ^^^^^^^^^^^^^^^^^^^ EX @ 133:4-23 | ^^^^^^^^^^^^^^^^^^^ EX @ 321:2-21 + 132 } | + 133 ::-webkit-date-and-time-value { | 319 ::-webkit-date-and-time-value { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 133:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 319:0-30 + 134 min-height: 1lh; | 320 min-height: 1lh; /* 1 */ + ^^^^^^^^^^^^^^^ EW @ 134:4-19 | ^^^^^^^^^^^^^^^ EW @ 320:2-17 + 135 text-align: inherit; | 321 text-align: inherit; /* 2 */ + ^^^^^^^^^^^^^^^^^^^ EX @ 135:4-23 | ^^^^^^^^^^^^^^^^^^^ EX @ 321:2-21 | 322 } - 134 } | - 135 ::-webkit-datetime-edit { | 328 ::-webkit-datetime-edit { - ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 135:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 328:0-24 - 136 display: inline-flex; | 329 display: inline-flex; - ^^^^^^^^^^^^^^^^^^^^ EZ @ 136:4-24 | ^^^^^^^^^^^^^^^^^^^^ EZ @ 329:2-22 + 136 } | + 137 ::-webkit-datetime-edit { | 328 ::-webkit-datetime-edit { + ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 137:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 328:0-24 + 138 display: inline-flex; | 329 display: inline-flex; + ^^^^^^^^^^^^^^^^^^^^ EZ @ 138:4-24 | ^^^^^^^^^^^^^^^^^^^^ EZ @ 329:2-22 | 330 } - 137 } | - 138 ::-webkit-datetime-edit-fields-wrapper { | 336 ::-webkit-datetime-edit-fields-wrapper { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 138:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 336:0-39 - 139 padding: 0; | 337 padding: 0; - ^^^^^^^^^^ FB @ 139:4-14 | ^^^^^^^^^^ FB @ 337:2-12 + 139 } | + 140 ::-webkit-datetime-edit-fields-wrapper { | 336 ::-webkit-datetime-edit-fields-wrapper { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 140:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 336:0-39 + 141 padding: 0; | 337 padding: 0; + ^^^^^^^^^^ FB @ 141:4-14 | ^^^^^^^^^^ FB @ 337:2-12 | 338 } - 140 } | - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 340 ::-webkit-datetime-edit, - ^ FC @ 141:2 | ^ FC @ 340:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 341 ::-webkit-datetime-edit-year-field, - ^ FD @ 141:2 | ^ FD @ 341:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 342 ::-webkit-datetime-edit-month-field, - ^ FE @ 141:2 | ^ FE @ 342:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 343 ::-webkit-datetime-edit-day-field, - ^ FF @ 141:2 | ^ FF @ 343:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 344 ::-webkit-datetime-edit-hour-field, - ^ FG @ 141:2 | ^ FG @ 344:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit-minute-field, - ^ FH @ 141:2 | ^ FH @ 345:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-second-field, - ^ FI @ 141:2 | ^ FI @ 346:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-millisecond-field, - ^ FJ @ 141:2 | ^ FJ @ 347:0 - 141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-meridiem-field { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FK @ 141:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FK @ 348:0-39 - 142 padding-block: 0; | 349 padding-block: 0; - ^^^^^^^^^^^^^^^^ FL @ 142:4-20 | ^^^^^^^^^^^^^^^^ FL @ 349:2-18 + 142 } | + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 340 ::-webkit-datetime-edit, + ^ FC @ 143:2 | ^ FC @ 340:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 341 ::-webkit-datetime-edit-year-field, + ^ FD @ 143:2 | ^ FD @ 341:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 342 ::-webkit-datetime-edit-month-field, + ^ FE @ 143:2 | ^ FE @ 342:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 343 ::-webkit-datetime-edit-day-field, + ^ FF @ 143:2 | ^ FF @ 343:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 344 ::-webkit-datetime-edit-hour-field, + ^ FG @ 143:2 | ^ FG @ 344:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit-minute-field, + ^ FH @ 143:2 | ^ FH @ 345:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-second-field, + ^ FI @ 143:2 | ^ FI @ 346:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-millisecond-field, + ^ FJ @ 143:2 | ^ FJ @ 347:0 + 143 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-meridiem-field { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FK @ 143:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FK @ 348:0-39 + 144 padding-block: 0; | 349 padding-block: 0; + ^^^^^^^^^^^^^^^^ FL @ 144:4-20 | ^^^^^^^^^^^^^^^^ FL @ 349:2-18 | 350 } - 143 } | - 144 ::-webkit-calendar-picker-indicator { | 356 ::-webkit-calendar-picker-indicator { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 144:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 356:0-36 - 145 line-height: 1; | 357 line-height: 1; - ^^^^^^^^^^^^^^ FN @ 145:4-18 | ^^^^^^^^^^^^^^ FN @ 357:2-16 + 145 } | + 146 ::-webkit-calendar-picker-indicator { | 356 ::-webkit-calendar-picker-indicator { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 146:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 356:0-36 + 147 line-height: 1; | 357 line-height: 1; + ^^^^^^^^^^^^^^ FN @ 147:4-18 | ^^^^^^^^^^^^^^ FN @ 357:2-16 | 358 } - 146 } | - 147 :-moz-ui-invalid { | 364 :-moz-ui-invalid { - ^^^^^^^^^^^^^^^^^ FO @ 147:2-19 | ^^^^^^^^^^^^^^^^^ FO @ 364:0-17 - 148 box-shadow: none; | 365 box-shadow: none; - ^^^^^^^^^^^^^^^^ FP @ 148:4-20 | ^^^^^^^^^^^^^^^^ FP @ 365:2-18 + 148 } | + 149 :-moz-ui-invalid { | 364 :-moz-ui-invalid { + ^^^^^^^^^^^^^^^^^ FO @ 149:2-19 | ^^^^^^^^^^^^^^^^^ FO @ 364:0-17 + 150 box-shadow: none; | 365 box-shadow: none; + ^^^^^^^^^^^^^^^^ FP @ 150:4-20 | ^^^^^^^^^^^^^^^^ FP @ 365:2-18 | 366 } - 149 } | - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 372 button, - ^ FQ @ 150:2 | ^ FQ @ 372:0 - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 373 input:where([type='button'], [type='reset'], [type='submit']), - ^ FR @ 150:2 | ^ FR @ 373:0 - 150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 374 ::file-selector-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FS @ 150:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FS @ 374:0-23 - 151 appearance: button; | 375 appearance: button; - ^^^^^^^^^^^^^^^^^^ FT @ 151:4-22 | ^^^^^^^^^^^^^^^^^^ FT @ 375:2-20 + 151 } | + 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 372 button, + ^ FQ @ 152:2 | ^ FQ @ 372:0 + 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 373 input:where([type='button'], [type='reset'], [type='submit']), + ^ FR @ 152:2 | ^ FR @ 373:0 + 152 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 374 ::file-selector-button { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FS @ 152:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FS @ 374:0-23 + 153 appearance: button; | 375 appearance: button; + ^^^^^^^^^^^^^^^^^^ FT @ 153:4-22 | ^^^^^^^^^^^^^^^^^^ FT @ 375:2-20 | 376 } - 152 } | - 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 382 ::-webkit-inner-spin-button, - ^ FU @ 153:2 | ^ FU @ 382:0 - 153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 383 ::-webkit-outer-spin-button { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 153:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 383:0-28 - 154 height: auto; | 384 height: auto; - ^^^^^^^^^^^^ FW @ 154:4-16 | ^^^^^^^^^^^^ FW @ 384:2-14 + 154 } | + 155 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 382 ::-webkit-inner-spin-button, + ^ FU @ 155:2 | ^ FU @ 382:0 + 155 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 383 ::-webkit-outer-spin-button { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 155:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 383:0-28 + 156 height: auto; | 384 height: auto; + ^^^^^^^^^^^^ FW @ 156:4-16 | ^^^^^^^^^^^^ FW @ 384:2-14 | 385 } - 155 } | - 156 [hidden]:where(:not([hidden='until-found'])) { | 391 [hidden]:where(:not([hidden='until-found'])) { - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 156:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 391:0-45 - 157 display: none !important; | 392 display: none !important; - ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 157:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 392:2-26 + 157 } | + 158 [hidden]:where(:not([hidden='until-found'])) { | 391 [hidden]:where(:not([hidden='until-found'])) { + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 158:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 391:0-45 + 159 display: none !important; | 392 display: none !important; + ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 159:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 392:2-26 | 393 } - 158 } | - 159 } | + 160 } | + 161 } | | --- index.css --- - 160 @layer utilities; | 5 @import './utilities.css' layer(utilities); - ^^^^^^^^^^^^^^^^ FZ @ 160:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FZ @ 5:0-42 + 162 @layer utilities; | 5 @import './utilities.css' layer(utilities); + ^^^^^^^^^^^^^^^^ FZ @ 162:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FZ @ 5:0-42 | --- input.css --- - 161 .foo { | 3 .foo { - ^^^^^ GA @ 161:0-5 | ^^^^^ GA @ 3:0-5 - 162 text-decoration-line: underline; | 4 @apply underline; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GB @ 162:2-33 | ^^^^^^^^^ GB @ 4:9-18 + 163 .foo { | 3 .foo { + ^^^^^ GA @ 163:0-5 | ^^^^^ GA @ 3:0-5 + 164 text-decoration-line: underline; | 4 @apply underline; + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GB @ 164:2-33 | ^^^^^^^^^ GB @ 4:9-18 | 5 } - 163 } | - 164 | + 165 } | + 166 | " `) }) @@ -695,23 +698,26 @@ test('@apply generates source maps', async ({ expect }) => { ^^^^^^^^^^^ B @ 2:2-13 | ^^^^^^^^^^^ B @ 2:2-13 3 color: #000; | 3 @apply text-[#000] hover:text-[#f00]; ^^^^^^^^^^^ C @ 3:2-13 | ^^^^^^^^^^^ C @ 3:9-20 - 4 &:hover { | 3 @apply text-[#000] hover:text-[#f00]; - ^^^^^^^^ D @ 4:2-10 | ^^^^^^^^^^^^^^^^^ D @ 3:21-38 - 5 @media (hover: hover) { | - ^^^^^^^^^^^^^^^^^^^^^^ D @ 5:4-26 | - 6 color: #f00; | - ^^^^^^^^^^^ D @ 6:6-17 | - 7 } | + 4 } | + 5 @media (hover: hover) { | 3 @apply text-[#000] hover:text-[#f00]; + ^^^^^^^^^^^^^^^^^^^^^^ D @ 5:0-22 | ^^^^^^^^^^^^^^^^^ D @ 3:21-38 + 6 .foo:hover { | + ^^^^^^^^^^^ D @ 6:2-13 | + 7 color: #f00; | + ^^^^^^^^^^^ D @ 7:4-15 | 8 } | - 9 text-decoration-line: underline; | 4 @apply underline; - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E @ 9:2-33 | ^^^^^^^^^ E @ 4:9-18 - 10 @apply --my-mixin-1 --my-mixin-2(); | 5 @apply --my-mixin-1 --my-mixin-2(); - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 10:2-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 5:2-36 - 11 color: red; | 6 color: red; - ^^^^^^^^^^ G @ 11:2-12 | ^^^^^^^^^^ G @ 6:2-12 + 9 } | + 10 .foo { | + ^^^^^ A @ 10:0-5 | + 11 text-decoration-line: underline; | 4 @apply underline; + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E @ 11:2-33 | ^^^^^^^^^ E @ 4:9-18 + 12 @apply --my-mixin-1 --my-mixin-2(); | 5 @apply --my-mixin-1 --my-mixin-2(); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 12:2-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F @ 5:2-36 + 13 color: red; | 6 color: red; + ^^^^^^^^^^ G @ 13:2-12 | ^^^^^^^^^^ G @ 6:2-12 | 7 } - 12 } | - 13 | + 14 } | + 15 | " `) }) @@ -737,49 +743,43 @@ test('@variant generates source maps', async ({ expect }) => { expect(annotations).toMatchInlineSnapshot(` " - output.css | input.css - | - 1 .foo { | 1 .foo { - ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5 - 2 color: red; | 2 color: red; - ^^^^^^^^^^ B @ 2:2-12 | ^^^^^^^^^^ B @ 2:2-12 - 3 &[data-a] { | - | 4 @variant data-a, data-b:data-c { - 4 color: green; | 5 color: green; - ^^^^^^^^^^^^ C @ 4:4-16 | ^^^^^^^^^^^^ C @ 5:4-16 - 5 &[data-d] { | - | 7 @variant data-d, data-e:data-f { - 6 color: blue; | 8 color: blue; - ^^^^^^^^^^^ D @ 6:6-17 | ^^^^^^^^^^^ D @ 8:6-17 - | 9 } - | 10 } - | 11 } - 7 } | - 8 &[data-e] { | - 9 &[data-f] { | - 10 color: blue; | - ^^^^^^^^^^^ D @ 10:8-19 | - 11 } | - 12 } | - 13 } | - 14 &[data-b] { | - 15 &[data-c] { | - 16 color: green; | - ^^^^^^^^^^^^ C @ 16:6-18 | - 17 &[data-d] { | - 18 color: blue; | - ^^^^^^^^^^^ D @ 18:8-19 | - 19 } | - 20 &[data-e] { | - 21 &[data-f] { | - 22 color: blue; | - ^^^^^^^^^^^ D @ 22:10-21 | - 23 } | - 24 } | - 25 } | - 26 } | - 27 } | - 28 | + output.css | input.css + | + 1 .foo { | 1 .foo { + ^^^^^ A @ 1:0-5 | ^^^^^ A @ 1:0-5 + 2 color: red; | 2 color: red; + ^^^^^^^^^^ B @ 2:2-12 | ^^^^^^^^^^ B @ 2:2-12 + 3 } | + 4 .foo[data-a] { | + | 4 @variant data-a, data-b:data-c { + 5 color: green; | 5 color: green; + ^^^^^^^^^^^^ C @ 5:2-14 | ^^^^^^^^^^^^ C @ 5:4-16 + 6 } | + 7 :is(.foo[data-a])[data-... | + | 7 @variant data-d, data-e:data-f { + 8 color: blue; | 8 color: blue; + ^^^^^^^^^^^ D @ 8:2-13 | ^^^^^^^^^^^ D @ 8:6-17 + | 9 } + | 10 } + | 11 } + 9 } | + 10 :is(:is(.foo[data-a])[d... | + 11 color: blue; | + ^^^^^^^^^^^ D @ 11:2-13 | + 12 } | + 13 :is(.foo[data-b])[data-... | + 14 color: green; | + ^^^^^^^^^^^^ C @ 14:2-14 | + 15 } | + 16 :is(:is(.foo[data-b])[d... | + 17 color: blue; | + ^^^^^^^^^^^ D @ 17:2-13 | + 18 } | + 19 :is(:is(:is(.foo[data-b... | + 20 color: blue; | + ^^^^^^^^^^^ D @ 20:2-13 | + 21 } | + 22 | " `) }) @@ -865,25 +865,19 @@ test('Source locations for `addBase` point to the `@plugin` that generated them' expect(annotations).toMatchInlineSnapshot(` " - output.css | input.css - | - 1 @layer base { | 1 @plugin "./plugin.js"; - ^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21 - 2 body { | - ^^^^^ A @ 2:2-7 | - 3 color: red; | - ^^^^^^^^^^ A @ 3:4-14 | - 4 } | - 5 } | - 6 @layer base { | 2 @config "./config.js"; - ^^^^^^^^^^^^ B @ 6:0-12 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21 - 7 body { | - ^^^^^ B @ 7:2-7 | - 8 color: green; | - ^^^^^^^^^^^^ B @ 8:4-16 | - 9 } | - 10 } | - 11 | + output.css | input.css + | + 1 @layer base { | 1 @plugin "./plugin.js"; + ^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21 + 2 body { | + ^^^^^ A @ 2:2-7 | + 3 color: red; | + ^^^^^^^^^^ A @ 3:4-14 | + 4 color: green; | 2 @config "./config.js"; + ^^^^^^^^^^^^ B @ 4:4-16 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21 + 5 } | + 6 } | + 7 | " `) }) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index cefcf7d03b92..d83b12827041 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -10078,57 +10078,57 @@ test('space-y-reverse', async () => { test('divide-x', async () => { expect(await run(['divide-x', 'divide-x-4', 'divide-x-123', 'divide-x-[4px]'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-divide-x-reverse: 0; - --tw-border-style: solid; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-divide-x-reverse: 0; + --tw-border-style: solid; + } } } - } - :where(.divide-x > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(1px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-4 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-4 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-123 > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-123 > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(123px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(123px * calc(1 - var(--tw-divide-x-reverse))); + } - :where(.divide-x-\\[4px\\] > :not(:last-child)) { - --tw-divide-x-reverse: 0; - border-inline-style: var(--tw-border-style); - border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); - border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); - } + :where(.divide-x-\\[4px\\] > :not(:last-child)) { + --tw-divide-x-reverse: 0; + border-inline-style: var(--tw-border-style); + border-inline-start-width: calc(4px * var(--tw-divide-x-reverse)); + border-inline-end-width: calc(4px * calc(1 - var(--tw-divide-x-reverse))); + } - @property --tw-divide-x-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } + @property --tw-divide-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + } + " + `) expect( await run([ '-divide-x', @@ -10192,61 +10192,61 @@ test('divide-x with custom default border width', async () => { test('divide-y', async () => { expect(await run(['divide-y', 'divide-y-4', 'divide-y-123', 'divide-y-[4px]'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-divide-y-reverse: 0; - --tw-border-style: solid; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-divide-y-reverse: 0; + --tw-border-style: solid; + } } } - } - :where(.divide-y > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-4 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-4 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-123 > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(123px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-123 > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(123px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(123px * calc(1 - var(--tw-divide-y-reverse))); + } - :where(.divide-y-\\[4px\\] > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(4px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); - } + :where(.divide-y-\\[4px\\] > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(4px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(4px * calc(1 - var(--tw-divide-y-reverse))); + } - @property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; - } + @property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; + } - @property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; - } - " - `) + @property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; + } + " + `) expect( await run([ '-divide-y', @@ -11450,20 +11450,20 @@ test('scrollbar-width', async () => { test('scrollbar-gutter', async () => { expect(await run(['scrollbar-gutter-auto', 'scrollbar-gutter-stable', 'scrollbar-gutter-both'])) .toMatchInlineSnapshot(` - " - .scrollbar-gutter-auto { - scrollbar-gutter: auto; - } + " + .scrollbar-gutter-auto { + scrollbar-gutter: auto; + } - .scrollbar-gutter-both { - scrollbar-gutter: stable both-edges; - } + .scrollbar-gutter-both { + scrollbar-gutter: stable both-edges; + } - .scrollbar-gutter-stable { - scrollbar-gutter: stable; - } - " - `) + .scrollbar-gutter-stable { + scrollbar-gutter: stable; + } + " + `) expect( await run([ 'scrollbar-gutter', @@ -19253,104 +19253,104 @@ test('mask-y-to', async () => { test('mask-linear', async () => { expect(await run(['mask-linear-45', 'mask-linear-[3rad]', '-mask-linear-45'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-mask-linear: linear-gradient(#fff, #fff); - --tw-mask-radial: linear-gradient(#fff, #fff); - --tw-mask-conic: linear-gradient(#fff, #fff); - --tw-mask-linear-position: 0deg; - --tw-mask-linear-from-position: 0%; - --tw-mask-linear-to-position: 100%; - --tw-mask-linear-from-color: black; - --tw-mask-linear-to-color: transparent; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-mask-linear: linear-gradient(#fff, #fff); + --tw-mask-radial: linear-gradient(#fff, #fff); + --tw-mask-conic: linear-gradient(#fff, #fff); + --tw-mask-linear-position: 0deg; + --tw-mask-linear-from-position: 0%; + --tw-mask-linear-to-position: 100%; + --tw-mask-linear-from-color: black; + --tw-mask-linear-to-color: transparent; + } } } - } - .-mask-linear-45 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: calc(1deg * -45); - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .-mask-linear-45 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: calc(1deg * -45); + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - .mask-linear-45 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: calc(1deg * 45); - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .mask-linear-45 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: calc(1deg * 45); + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - .mask-linear-\\[3rad\\] { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); - --tw-mask-linear-position: 171.887deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .mask-linear-\\[3rad\\] { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position))); + --tw-mask-linear-position: 171.887deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - @property --tw-mask-linear { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-linear { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-radial { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-radial { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-conic { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-conic { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-linear-position { - syntax: "*"; - inherits: false; - initial-value: 0deg; - } + @property --tw-mask-linear-position { + syntax: "*"; + inherits: false; + initial-value: 0deg; + } - @property --tw-mask-linear-from-position { - syntax: "*"; - inherits: false; - initial-value: 0%; - } + @property --tw-mask-linear-from-position { + syntax: "*"; + inherits: false; + initial-value: 0%; + } - @property --tw-mask-linear-to-position { - syntax: "*"; - inherits: false; - initial-value: 100%; - } + @property --tw-mask-linear-to-position { + syntax: "*"; + inherits: false; + initial-value: 100%; + } - @property --tw-mask-linear-from-color { - syntax: "*"; - inherits: false; - initial-value: black; - } + @property --tw-mask-linear-from-color { + syntax: "*"; + inherits: false; + initial-value: black; + } - @property --tw-mask-linear-to-color { - syntax: "*"; - inherits: false; - initial-value: transparent; - } - " - `) + @property --tw-mask-linear-to-color { + syntax: "*"; + inherits: false; + initial-value: transparent; + } + " + `) expect( await run([ 'mask-linear', @@ -20608,104 +20608,104 @@ test('mask-radial-to', async () => { test('mask-conic', async () => { expect(await run(['mask-conic-45', 'mask-conic-[3rad]', '-mask-conic-45'])) .toMatchInlineSnapshot(` - " - @layer properties { - @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { - *, :before, :after, ::backdrop { - --tw-mask-linear: linear-gradient(#fff, #fff); - --tw-mask-radial: linear-gradient(#fff, #fff); - --tw-mask-conic: linear-gradient(#fff, #fff); - --tw-mask-conic-position: 0deg; - --tw-mask-conic-from-position: 0%; - --tw-mask-conic-to-position: 100%; - --tw-mask-conic-from-color: black; - --tw-mask-conic-to-color: transparent; + " + @layer properties { + @supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) { + *, :before, :after, ::backdrop { + --tw-mask-linear: linear-gradient(#fff, #fff); + --tw-mask-radial: linear-gradient(#fff, #fff); + --tw-mask-conic: linear-gradient(#fff, #fff); + --tw-mask-conic-position: 0deg; + --tw-mask-conic-from-position: 0%; + --tw-mask-conic-to-position: 100%; + --tw-mask-conic-from-color: black; + --tw-mask-conic-to-color: transparent; + } } } - } - .-mask-conic-45 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: calc(1deg * -45); - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .-mask-conic-45 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: calc(1deg * -45); + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - .mask-conic-45 { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: calc(1deg * 45); - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .mask-conic-45 { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: calc(1deg * 45); + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - .mask-conic-\\[3rad\\] { - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); - --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); - --tw-mask-conic-position: 171.887deg; - -webkit-mask-composite: source-in; - -webkit-mask-composite: source-in; - mask-composite: intersect; - } + .mask-conic-\\[3rad\\] { + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + -webkit-mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic); + --tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position))); + --tw-mask-conic-position: 171.887deg; + -webkit-mask-composite: source-in; + -webkit-mask-composite: source-in; + mask-composite: intersect; + } - @property --tw-mask-linear { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-linear { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-radial { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-radial { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-conic { - syntax: "*"; - inherits: false; - initial-value: linear-gradient(#fff, #fff); - } + @property --tw-mask-conic { + syntax: "*"; + inherits: false; + initial-value: linear-gradient(#fff, #fff); + } - @property --tw-mask-conic-position { - syntax: "*"; - inherits: false; - initial-value: 0deg; - } + @property --tw-mask-conic-position { + syntax: "*"; + inherits: false; + initial-value: 0deg; + } - @property --tw-mask-conic-from-position { - syntax: "*"; - inherits: false; - initial-value: 0%; - } + @property --tw-mask-conic-from-position { + syntax: "*"; + inherits: false; + initial-value: 0%; + } - @property --tw-mask-conic-to-position { - syntax: "*"; - inherits: false; - initial-value: 100%; - } + @property --tw-mask-conic-to-position { + syntax: "*"; + inherits: false; + initial-value: 100%; + } - @property --tw-mask-conic-from-color { - syntax: "*"; - inherits: false; - initial-value: black; - } + @property --tw-mask-conic-from-color { + syntax: "*"; + inherits: false; + initial-value: black; + } - @property --tw-mask-conic-to-color { - syntax: "*"; - inherits: false; - initial-value: transparent; - } - " - `) + @property --tw-mask-conic-to-color { + syntax: "*"; + inherits: false; + initial-value: transparent; + } + " + `) expect( await run([ 'mask-conic', @@ -29941,20 +29941,20 @@ describe('custom utilities', () => { ` expect(await run(['border--0', 'border--1', 'border--2'], input)).toMatchInlineSnapshot(` - " - .border--0 { - border-color: var(--color-border-0, #e5e7eb); - } + " + .border--0 { + border-color: var(--color-border-0, #e5e7eb); + } - .border--1 { - border-color: var(--color-border-1, #d1d5db); - } + .border--1 { + border-color: var(--color-border-1, #d1d5db); + } - .border--2 { - border-color: var(--color-border-2, #9ca3af); - } - " - `) + .border--2 { + border-color: var(--color-border-2, #9ca3af); + } + " + `) expect(await run(['border--3'], input)).toEqual('') }) @@ -30050,20 +30050,20 @@ describe('custom utilities', () => { ` expect(await run(['example-1', 'example-76', 'example-971'], input)).toMatchInlineSnapshot(` - " - .example-1 { - --resolved-value: 1; - } + " + .example-1 { + --resolved-value: 1; + } - .example-76 { - --resolved-value: 76; - } + .example-76 { + --resolved-value: 76; + } - .example-971 { - --resolved-value: 971; - } - " - `) + .example-971 { + --resolved-value: 971; + } + " + `) expect(await run(['example-foo'], input)).toEqual('') }) @@ -30387,20 +30387,20 @@ describe('custom utilities', () => { ` expect(await run(['example-a', 'example-76', 'example-[123]'], input)).toMatchInlineSnapshot(` - " - .example-76 { - --resolved-value: 76; - } + " + .example-76 { + --resolved-value: 76; + } - .example-\\[123\\] { - --resolved-value: 123; - } + .example-\\[123\\] { + --resolved-value: 123; + } - .example-a { - --resolved-value: var(--example-a, 8); - } - " - `) + .example-a { + --resolved-value: var(--example-a, 8); + } + " + `) expect(await run(['example-[#0088cc]', 'example-[1px]'], input)).toEqual('') }) diff --git a/packages/tailwindcss/src/utils/variables.ts b/packages/tailwindcss/src/utils/variables.ts index 57ac4c3a5d27..b72987565a03 100644 --- a/packages/tailwindcss/src/utils/variables.ts +++ b/packages/tailwindcss/src/utils/variables.ts @@ -1,7 +1,8 @@ import * as ValueParser from '../value-parser' import { walk, WalkAction } from '../walk' +import { DefaultMap } from './default-map' -export function extractUsedVariables(raw: string): string[] { +let extractUsedVariablesCache = new DefaultMap((raw) => { let variables: string[] = [] walk(ValueParser.parse(raw), (node) => { if (node.kind !== 'function' || node.value !== 'var') return @@ -15,4 +16,8 @@ export function extractUsedVariables(raw: string): string[] { return WalkAction.Skip }) return variables +}) + +export function extractUsedVariables(raw: string): string[] { + return extractUsedVariablesCache.get(raw) } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 305859d5b443..715d16f31675 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1935,7 +1935,7 @@ test('in', async () => { ]), ).toMatchInlineSnapshot(` " - .not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(:is(p)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(:is(p)) .in-\\[p\\]\\:flex { + .not-in-\\[\\.group\\]\\:flex:not(:where(.group) *), .not-in-\\[p\\]\\:flex:not(:where(p:is(*)) *), :where([data-visible]) .in-data-visible\\:flex, :where(.group) .in-\\[\\.group\\]\\:flex, :where(p:is(*)) .in-\\[p\\]\\:flex { display: flex; } " diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 4d4637d8f55d..d62f13c8fb4d 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -11,6 +11,7 @@ import { type Rule, type StyleRule, } from './ast' +import * as AttributeSelectorParser from './attribute-selector-parser' import { type Variant } from './candidate' import { applyVariant } from './compile' import type { DesignSystem } from './design-system' @@ -845,11 +846,15 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null if (variant.value.kind === 'arbitrary') { - ruleNode.nodes = [ - styleRule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes), - ] + let selector = `[aria-${quoteAttributeValue(variant.value.value)}]` + let parsed = AttributeSelectorParser.parse(selector) + if (parsed === null) return null + ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] } else { - ruleNode.nodes = [styleRule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)] + let selector = `[aria-${variant.value.value}="true"]` + let parsed = AttributeSelectorParser.parse(selector) + if (parsed === null) return null + ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] } }) @@ -868,9 +873,11 @@ export function createVariants(theme: Theme): Variants { variants.functional('data', (ruleNode, variant) => { if (!variant.value || variant.modifier) return null - ruleNode.nodes = [ - styleRule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes), - ] + let selector = `[data-${quoteAttributeValue(variant.value.value)}]` + let parsed = AttributeSelectorParser.parse(selector) + if (parsed === null) return null + + ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)] }) variants.functional('nth', (ruleNode, variant) => { @@ -1295,7 +1302,11 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem): } } - nodes.push(node) + if (node.selector === '&') { + nodes.push(...node.nodes) + } else { + nodes.push(node) + } } // Update the variant at-rule node, to be the `&` rule node