From 113142a0e4dc97ea01bd16740d36a8d37dc82528 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 21 Feb 2025 15:02:07 +0100 Subject: [PATCH] Use amount of properties when sorting (#16715) Right now we sort the nodes based on a pre-defined sort order based on the properties that are being used. The property sort order is defined in a list we maintain. We also have to make sure that the property count is taken into account such that if all the "sorts" are the same, that we fallback to the property count. Most amount of properties should be first such that we can override it with more specific utilities that have fewer properties. However, if a property doesn't exist, then it wouldn't be included in a list of properties therefore the total count was off. This PR fixes that by counting all the used properties. If a property already exists it is counted twice. E.g.: ```css .foo { color: red; &:hover { color: blue; } } ``` In this case, we have 2 properties, not 1 even though it's the same `color` property. ## Test plan: 1. Updated the tests that are now sorted correctly 2. Added an integration test to make sure that `prose-invert` is defined after the `prose-stone` classes when using the `@tailwindcss/typography` plugin where this problem originated from. Note how in this play (https://play.tailwindcss.com/wt3LYDaljN) the `prose-invert` comes _before_ the `prose-stone` which means that you can't apply the `prose-invert` classes to invert `prose-stone`. --- CHANGELOG.md | 1 + integrations/cli/plugins.test.ts | 13 +- .../tailwindcss/src/compat/plugin-api.test.ts | 16 +- packages/tailwindcss/src/compile.ts | 40 +++-- packages/tailwindcss/src/property-order.ts | 1 + packages/tailwindcss/src/utilities.test.ts | 141 +++++++++--------- 6 files changed, 120 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 702e1e2af3ee..757bdbe5eefe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Vite: Ensure Astro production builds contain classes for client-only components ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) - Vite: Ensure utility classes are read without escaping special characters ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) - Allow `theme(…)` options when using `@import` ([#16514](https://github.com/tailwindlabs/tailwindcss/pull/16514)) +- Use amount of properties when sorting ([#16715](https://github.com/tailwindlabs/tailwindcss/pull/16715)) ## [4.0.7] - 2025-02-18 diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts index 833b0f6779a6..0597e247be50 100644 --- a/integrations/cli/plugins.test.ts +++ b/integrations/cli/plugins.test.ts @@ -14,7 +14,7 @@ test( } `, 'index.html': html` -
+

Headline

Until now, trying to style an article, document, or blog post with Tailwind has been a @@ -28,9 +28,18 @@ test( `, }, }, - async ({ fs, exec }) => { + async ({ fs, exec, expect }) => { await exec('pnpm tailwindcss --input src/index.css --output dist/out.css') + // Verify that `prose-stone` is defined before `prose-invert` + { + let contents = await fs.read('dist/out.css') + let proseInvertIdx = contents.indexOf('.prose-invert') + let proseStoneIdx = contents.indexOf('.prose-stone') + + expect(proseStoneIdx).toBeLessThan(proseInvertIdx) + } + await fs.expectFileToContain('dist/out.css', [ candidate`prose`, ':where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *))', diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 14d0849a4eec..b786f2311e28 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -3047,6 +3047,14 @@ describe('addUtilities()', () => { ).toMatchInlineSnapshot( ` "@layer utilities { + .j { + &.j { + color: red; + } + .j& { + color: red; + } + } .a { & .b:hover .c { color: red; @@ -3087,14 +3095,6 @@ describe('addUtilities()', () => { color: red; } } - .j { - &.j { - color: red; - } - .j& { - color: red; - } - } }" `, ) diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 6b48073b5823..269acee03d08 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -23,7 +23,7 @@ export function compileCandidates( ) { let nodeSorting = new Map< AstNode, - { properties: number[]; variants: bigint; candidate: string } + { properties: { order: number[]; count: number }; variants: bigint; candidate: string } >() let astNodes: AstNode[] = [] let matches = new Map() @@ -95,18 +95,19 @@ export function compileCandidates( // Find the first property that is different between the two rules let offset = 0 while ( - aSorting.properties.length < offset && - zSorting.properties.length < offset && - aSorting.properties[offset] === zSorting.properties[offset] + offset < aSorting.properties.order.length && + offset < zSorting.properties.order.length && + aSorting.properties.order[offset] === zSorting.properties.order[offset] ) { offset += 1 } return ( // Sort by lowest property index first - (aSorting.properties[offset] ?? Infinity) - (zSorting.properties[offset] ?? Infinity) || + (aSorting.properties.order[offset] ?? Infinity) - + (zSorting.properties.order[offset] ?? Infinity) || // Sort by most properties first, then by least properties - zSorting.properties.length - aSorting.properties.length || + zSorting.properties.count - aSorting.properties.count || // Sort alphabetically compare(aSorting.candidate, zSorting.candidate) ) @@ -124,7 +125,10 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem let rules: { node: AstNode - propertySort: number[] + propertySort: { + order: number[] + count: number + } }[] = [] let selector = `.${escape(candidate.raw)}` @@ -310,24 +314,33 @@ function applyImportant(ast: AstNode[]): void { function getPropertySort(nodes: AstNode[]) { // Determine sort order based on properties used - let propertySort = new Set() + let order = new Set() + let count = 0 let q: AstNode[] = nodes.slice() + let seenTwSort = false + while (q.length > 0) { // SAFETY: At this point it is safe to use TypeScript's non-null assertion // operator because we guarded against `q.length > 0` above. let node = q.shift()! if (node.kind === 'declaration') { + // Empty strings should still be counted, e.g.: `--tw-foo:;` is valid + if (node.value !== undefined) count++ + + if (seenTwSort) continue + if (node.property === '--tw-sort') { let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value ?? '') if (idx !== -1) { - propertySort.add(idx) - break + order.add(idx) + seenTwSort = true + continue } } let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property) - if (idx !== -1) propertySort.add(idx) + if (idx !== -1) order.add(idx) } else if (node.kind === 'rule' || node.kind === 'at-rule') { for (let child of node.nodes) { q.push(child) @@ -335,5 +348,8 @@ function getPropertySort(nodes: AstNode[]) { } } - return Array.from(propertySort).sort((a, z) => a - z) + return { + order: Array.from(order).sort((a, z) => a - z), + count, + } } diff --git a/packages/tailwindcss/src/property-order.ts b/packages/tailwindcss/src/property-order.ts index 90290d54c0a3..f2ed4bc51721 100644 --- a/packages/tailwindcss/src/property-order.ts +++ b/packages/tailwindcss/src/property-order.ts @@ -75,6 +75,7 @@ export default [ 'translate', '--tw-translate-x', '--tw-translate-y', + '--tw-translate-z', 'scale', '--tw-scale-x', '--tw-scale-y', diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 79ac4700655e..2f00dcebe4b0 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -4213,14 +4213,15 @@ test('translate-y', async () => { }) test('translate-z', async () => { - expect(await run(['translate-y-px', '-translate-z-[var(--value)]'])).toMatchInlineSnapshot(` - ".translate-y-px { - --tw-translate-y: 1px; - translate: var(--tw-translate-x) var(--tw-translate-y); + expect(await run(['translate-z-px', '-translate-z-[var(--value)]'])).toMatchInlineSnapshot(` + ".-translate-z-\\[var\\(--value\\)\\] { + --tw-translate-z: calc(var(--value) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); } - .-translate-z-\\[var\\(--value\\)\\] { - --tw-translate-z: calc(var(--value) * -1); + .translate-z-px { + --tw-translate-z: 1px; + translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); translate: var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z); } @@ -5458,12 +5459,7 @@ test('touch-pan', async () => { 'touch-pan-down', ]), ).toMatchInlineSnapshot(` - ".touch-pan-down { - --tw-pan-y: pan-down; - touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); - } - - .touch-pan-left { + ".touch-pan-left { --tw-pan-x: pan-left; touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); } @@ -5473,13 +5469,18 @@ test('touch-pan', async () => { touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); } - .touch-pan-up { - --tw-pan-y: pan-up; + .touch-pan-x { + --tw-pan-x: pan-x; touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); } - .touch-pan-x { - --tw-pan-x: pan-x; + .touch-pan-down { + --tw-pan-y: pan-down; + touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); + } + + .touch-pan-up { + --tw-pan-y: pan-up; touch-action: var(--tw-pan-x, ) var(--tw-pan-y, ) var(--tw-pinch-zoom, ); } @@ -14155,15 +14156,7 @@ test('contain', async () => { 'contain-[unset]', ]), ).toMatchInlineSnapshot(` - ".contain-\\[unset\\] { - contain: unset; - } - - .contain-content { - contain: content; - } - - .contain-inline-size { + ".contain-inline-size { --tw-contain-size: inline-size; contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, ); } @@ -14173,10 +14166,6 @@ test('contain', async () => { contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, ); } - .contain-none { - contain: none; - } - .contain-paint { --tw-contain-paint: paint; contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, ); @@ -14187,15 +14176,27 @@ test('contain', async () => { contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, ); } - .contain-strict { - contain: strict; - } - .contain-style { --tw-contain-style: style; contain: var(--tw-contain-size, ) var(--tw-contain-layout, ) var(--tw-contain-paint, ) var(--tw-contain-style, ); } + .contain-\\[unset\\] { + contain: unset; + } + + .contain-content { + contain: content; + } + + .contain-none { + contain: none; + } + + .contain-strict { + contain: strict; + } + @property --tw-contain-size { syntax: "*"; inherits: false @@ -14424,10 +14425,6 @@ test('font-variant-numeric', async () => { font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, ); } - .normal-nums { - font-variant-numeric: normal; - } - .oldstyle-nums { --tw-numeric-figure: oldstyle-nums; font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, ); @@ -14458,6 +14455,10 @@ test('font-variant-numeric', async () => { font-variant-numeric: var(--tw-ordinal, ) var(--tw-slashed-zero, ) var(--tw-numeric-figure, ) var(--tw-numeric-spacing, ) var(--tw-numeric-fraction, ); } + .normal-nums { + font-variant-numeric: normal; + } + @property --tw-ordinal { syntax: "*"; inherits: false @@ -16385,28 +16386,28 @@ test('@container', async () => { '@container-[size]/sidebar', ]), ).toMatchInlineSnapshot(` - ".\\@container { - container-type: inline-size; + ".\\@container-\\[size\\]\\/sidebar { + container: sidebar / size; } - .\\@container-\\[size\\] { - container-type: size; + .\\@container-normal\\/sidebar { + container: sidebar; } - .\\@container-\\[size\\]\\/sidebar { - container: sidebar / size; + .\\@container\\/sidebar { + container: sidebar / inline-size; } - .\\@container-normal { - container-type: normal; + .\\@container { + container-type: inline-size; } - .\\@container-normal\\/sidebar { - container: sidebar; + .\\@container-\\[size\\] { + container-type: size; } - .\\@container\\/sidebar { - container: sidebar / inline-size; + .\\@container-normal { + container-type: normal; }" `) expect( @@ -17579,24 +17580,24 @@ describe('custom utilities', () => { 'example-[12px]/[16px]', ]), ).toMatchInlineSnapshot(` - ".example-\\[12px\\] { - --value: 12px; - } - - .example-\\[12px\\]\\/\\[16px\\] { + ".example-\\[12px\\]\\/\\[16px\\] { --value: 12px; --modifier: 16px; --modifier-with-calc: calc(16px * 2); } - .example-sm { - --value: var(--value-sm); - } - .example-sm\\/7 { --value: var(--value-sm); --modifier: var(--modifier-7); --modifier-with-calc: calc(var(--modifier-7) * 2); + } + + .example-\\[12px\\] { + --value: 12px; + } + + .example-sm { + --value: var(--value-sm); }" `) expect( @@ -17651,15 +17652,15 @@ describe('custom utilities', () => { ` expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` - ".example-xs { + ".example-xs\\/6 { font-size: var(--text-xs); line-height: var(--text-xs--line-height); + line-height: 6; } - .example-xs\\/6 { + .example-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height); - line-height: 6; }" `) expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') @@ -17682,15 +17683,15 @@ describe('custom utilities', () => { ` expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` - ".example-xs { + ".example-xs\\/6 { font-size: var(--text-xs); line-height: var(--text-xs--line-height); + line-height: 6; } - .example-xs\\/6 { + .example-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height); - line-height: 6; }" `) expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') @@ -17713,15 +17714,15 @@ describe('custom utilities', () => { ` expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` - ".example-xs { + ".example-xs\\/6 { font-size: var(--text-xs); line-height: var(--text-xs--line-height); + line-height: 6; } - .example-xs\\/6 { + .example-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height); - line-height: 6; }" `) expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('') @@ -17744,15 +17745,15 @@ describe('custom utilities', () => { ` expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(` - ".example-xs { + ".example-xs\\/6 { font-size: var(--text-xs); line-height: var(--text-xs--line-height); + line-height: 6; } - .example-xs\\/6 { + .example-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height); - line-height: 6; }" `) expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')