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('')