Fix the intersection polyfill in canonicalizeCandidates (#20562)

## Summary

The `intersection` helper in `canonicalize-candidates.ts` falls back to
a polyfill when `Set.prototype.intersection` is missing (Node.js < 22).
The polyfill copied the first set and never removed any item, so it
returned the first set instead of the intersection. The collapse step
gave different results on those Node.js versions.

The polyfill now keeps only the items of the first set that are also in
the second set. The function is exported so the polyfill path can be
tested directly.

## Test plan

Added unit tests for the polyfill path (the native method is stubbed
out).

```
cd packages/tailwindcss
pnpm vitest run src/canonicalize-candidates.test.ts
```
This commit is contained in:
Robin Malfait authored and GitHub committed 2026-10-12 17:27:19 +02:00
1 parent c56200d0c4
commit ffb47984e7
3 files changed
+49 -5

No files matched your search

+1
View File
@@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve style invalidation performance of `group-*` and `peer-*` variants ([#20513](https://github.com/tailwindlabs/tailwindcss/pull/20513))
- Ensure cached compilers don't keep `loadModule` and `loadStylesheet` callbacks alive after compilation ([#20531](https://github.com/tailwindlabs/tailwindcss/pull/20531))
- Canonicalization: keep the `&` when moving a pseudo-element into `*` or `**` (e.g. `[&_::before]:flex` → `**:[&::before]:flex`) ([#20550](https://github.com/tailwindlabs/tailwindcss/pull/20550))
- Fix `Set.prototype.intersection` polyfill in `canonicalizeCandidates(…)` on Node.js < 22, which returned the first set instead of the intersection ([#20562](https://github.com/tailwindlabs/tailwindcss/pull/20562))
## [4.3.3] - 2026-07-16
@@ -3,6 +3,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { intersection } from './canonicalize-candidates'
import { cartesian } from './cartesian'
import type { CanonicalizeOptions } from './intellisense'
import plugin from './plugin'
@@ -1666,3 +1667,45 @@ describe('regressions', () => {
).toEqual(['text-foreground/60', 'text-default-soft-hover'])
})
})
describe('intersection', () => {
// Shadow `Set.prototype.intersection` so that the polyfill is used, even on
// Node.js versions that have a native implementation.
function withoutNative<T>(set: Set<T>): Set<T> {
return Object.assign(set, { intersection: undefined })
}
test('polyfill keeps only the items that are in both sets', () => {
let a = withoutNative(new Set([1, 2, 3, 4]))
let b = new Set([3, 4, 5, 6])
expect(intersection(a, b)).toEqual(new Set([3, 4]))
expect(intersection(withoutNative(new Set(b)), a)).toEqual(new Set([3, 4]))
})
test('polyfill returns an empty set when the sets are disjoint', () => {
expect(intersection(withoutNative(new Set([1, 2])), new Set([3, 4]))).toEqual(new Set())
})
test('polyfill returns an empty set when one of the sets is empty', () => {
expect(intersection(withoutNative(new Set<number>()), new Set([1]))).toEqual(new Set())
expect(intersection(withoutNative(new Set([1])), new Set<number>())).toEqual(new Set())
})
test('polyfill does not mutate the input sets', () => {
let a = withoutNative(new Set([1, 2, 3]))
let b = new Set([2, 3, 4])
intersection(a, b)
expect(a).toEqual(new Set([1, 2, 3]))
expect(b).toEqual(new Set([2, 3, 4]))
})
test('native and polyfill implementations agree', () => {
let a = new Set(['a', 'b', 'c', 'd'])
let b = new Set(['b', 'd', 'e'])
expect(intersection(withoutNative(new Set(a)), b)).toEqual(intersection(a, b))
})
})
@@ -3055,17 +3055,17 @@ function* combinations<T>(arr: T[]): Generator<T[]> {
}
}
function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
export function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
// @ts-expect-error Set.prototype.intersection is only available in Node.js v22+
if (typeof a.intersection === 'function') return a.intersection(b)
// Polyfill for environments that do not support Set.prototype.intersection yet
if (a.size === 0 || b.size === 0) return new Set<T>()
let result = new Set<T>(a)
for (let item of b) {
if (!result.has(item)) {
result.delete(item)
let result = new Set<T>()
for (let item of a) {
if (b.has(item)) {
result.add(item)
}
}