Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 11 additions & 2 deletions integrations/cli/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test(
}
`,
'index.html': html`
<div className="prose">
<div className="prose prose-stone prose-invert">
<h1>Headline</h1>
<p>
Until now, trying to style an article, document, or blog post with Tailwind has been a
Expand All @@ -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"] *))',
Expand Down
16 changes: 8 additions & 8 deletions packages/tailwindcss/src/compat/plugin-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3047,6 +3047,14 @@ describe('addUtilities()', () => {
).toMatchInlineSnapshot(
`
"@layer utilities {
.j {
&.j {
color: red;
}
.j& {
color: red;
}
}
.a {
& .b:hover .c {
color: red;
Expand Down Expand Up @@ -3087,14 +3095,6 @@ describe('addUtilities()', () => {
color: red;
}
}
.j {
&.j {
color: red;
}
.j& {
color: red;
}
}
}"
`,
)
Expand Down
48 changes: 31 additions & 17 deletions packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Candidate[]>()
Expand Down Expand Up @@ -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]
aSorting.properties.order.length < offset &&
zSorting.properties.order.length < offset &&
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)
)
Expand All @@ -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)}`
Expand Down Expand Up @@ -310,30 +314,40 @@ function applyImportant(ast: AstNode[]): void {

function getPropertySort(nodes: AstNode[]) {
// Determine sort order based on properties used
let propertySort = new Set<number>()
let order = new Set<number>()
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') {
if (node.property === '--tw-sort') {
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value ?? '')
if (idx !== -1) {
propertySort.add(idx)
break
count++

if (!seenTwSort) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were using a break before once we noticed a --tw-sort, but now we want to make sure we count all properties. So once we notice a --tw-sort we don't want to collect new properties in the order set anymore, but we still want to keep walking to count other properties.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we invert this condition so we reduce the level of nesting here?

Suggested change
if (!seenTwSort) {
if (teenTwSort) continue;

if (node.property === '--tw-sort') {
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value ?? '')
if (idx !== -1) {
order.add(idx)
seenTwSort = true
}
}
}

let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property)
if (idx !== -1) propertySort.add(idx)
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property)
if (idx !== -1) order.add(idx)
}
} else if (node.kind === 'rule' || node.kind === 'at-rule') {
for (let child of node.nodes) {
q.push(child)
}
}
}

return Array.from(propertySort).sort((a, z) => a - z)
return {
order: Array.from(order).sort((a, z) => a - z),
count,
}
}
Loading