Skip to content

Commit e3a9d5f

Browse files
Don’t move unknown pseudo-elements to the end of selectors (#10962)
* Don’t move `::deep` pseudo element to end of selector when using `@apply` * Update changelog * Move pseudo-elements in two passes * Rewrite pseudo-element relocation logic * Update test `::test` is an unknown pseudo element and therefore may be actionable _and_ nestable * Add tests * Simplify tests * Simplify * run tests on CI multiple times This works around the timeouts/flakeyness of GitHub Actions * Update formatting * Add comment * Mark webkit peusdo elements as terminal * update comment * only execute the `global-setup` once * Simplify NO SORT FN YAY * Use typedefs * Update changelog * Update changelog * update again --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent 467a39e commit e3a9d5f

16 files changed

+232
-157
lines changed

.github/workflows/ci-stable.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ jobs:
5050
run: npm run build
5151

5252
- name: Test
53-
run: npm run test
53+
run: |
54+
npm run test || \
55+
npm run test || \
56+
npm run test || exit 1
5457
5558
- name: Lint
5659
run: npm run style

.github/workflows/ci.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ jobs:
6666
run: npx turbo run build --filter=//
6767

6868
- name: Test
69-
run: npx turbo run test --filter=//
69+
run: |
70+
npx turbo run test --filter=// || \
71+
npx turbo run test --filter=// || \
72+
npx turbo run test --filter=// || exit 1
7073
7174
- name: Lint
7275
run: npx turbo run style --filter=//

.github/workflows/integration-tests-oxide.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,7 @@ jobs:
7878
run: npx turbo run build --filter=//
7979

8080
- name: Test ${{ matrix.integration }}
81-
run: npx turbo run test --filter=./integrations/${{ matrix.integration }}
81+
run: |
82+
npx turbo run test --filter=./integrations/${{ matrix.integration }} || \
83+
npx turbo run test --filter=./integrations/${{ matrix.integration }} || \
84+
npx turbo run test --filter=./integrations/${{ matrix.integration }} || exit 1

.github/workflows/integration-tests-stable.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ jobs:
7777
run: npm run build
7878

7979
- name: Test ${{ matrix.integration }}
80-
run: npm run test --prefix ./integrations/${{ matrix.integration }}
80+
run: |
81+
npm run test --prefix ./integrations/${{ matrix.integration }} || \
82+
npm run test --prefix ./integrations/${{ matrix.integration }} || \
83+
npm run test --prefix ./integrations/${{ matrix.integration }} || exit 1

.github/workflows/prepare-release.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ jobs:
6565
working-directory: standalone-cli
6666

6767
- name: Test
68-
run: npm test
68+
run: |
69+
npm test || \
70+
npm test || \
71+
npm test || exit 1
6972
working-directory: standalone-cli
7073

7174
- name: Release

.github/workflows/release-insiders-oxide.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,10 @@ jobs:
387387
run: npm run build
388388

389389
- name: Test
390-
run: npm test
390+
run: |
391+
npm test || \
392+
npm test || \
393+
npm test || exit 1
391394
392395
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
393396
run: npm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version

.github/workflows/release-insiders-stable.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ jobs:
4444
run: npm run build
4545

4646
- name: Test
47-
run: npm run test
47+
run: |
48+
npm run test || \
49+
npm run test || \
50+
npm run test || exit 1
4851
4952
- name: Resolve version
5053
id: vars

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12-
- Don’t move `::ng-deep` pseudo-element to end of selector when using `@apply` ([#10943](https://github.com/tailwindlabs/tailwindcss/pull/10943))
12+
- Don’t move unknown pseudo-elements to the end of selectors ([#10943](https://github.com/tailwindlabs/tailwindcss/pull/10943), [#10962](https://github.com/tailwindlabs/tailwindcss/pull/10962))
1313

1414
## [3.3.1] - 2023-03-30
1515

jest/global-setup.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
let { execSync } = require('child_process')
22

3+
let state = { ran: false }
34
module.exports = function () {
5+
if (state.ran) return
46
execSync('npm run build:rust', { stdio: 'ignore' })
57
execSync('npm run generate:plugin-list', { stdio: 'ignore' })
8+
state.ran = true
69
}

src/lib/expandApplyAtRules.js

+2-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import parser from 'postcss-selector-parser'
44
import { resolveMatches } from './generateRules'
55
import escapeClassName from '../util/escapeClassName'
66
import { applyImportantSelector } from '../util/applyImportantSelector'
7-
import { collectPseudoElements, sortSelector } from '../util/formatVariantSelector.js'
7+
import { movePseudos } from '../util/pseudoElements'
88

99
/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
1010

@@ -566,13 +566,7 @@ function processApply(root, context, localCache) {
566566

567567
// Move pseudo elements to the end of the selector (if necessary)
568568
let selector = parser().astSync(rule.selector)
569-
selector.each((sel) => {
570-
let [pseudoElements] = collectPseudoElements(sel)
571-
if (pseudoElements.length > 0) {
572-
sel.nodes.push(...pseudoElements.sort(sortSelector))
573-
}
574-
})
575-
569+
selector.each((sel) => movePseudos(sel))
576570
rule.selector = selector.toString()
577571
})
578572
}

src/util/applyImportantSelector.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import parser from 'postcss-selector-parser'
2-
import { collectPseudoElements, sortSelector } from './formatVariantSelector.js'
2+
import { movePseudos } from './pseudoElements'
33

44
export function applyImportantSelector(selector, important) {
55
let sel = parser().astSync(selector)
@@ -20,10 +20,7 @@ export function applyImportantSelector(selector, important) {
2020
]
2121
}
2222

23-
let [pseudoElements] = collectPseudoElements(sel)
24-
if (pseudoElements.length > 0) {
25-
sel.nodes.push(...pseudoElements.sort(sortSelector))
26-
}
23+
movePseudos(sel)
2724
})
2825

2926
return `${important} ${sel.toString()}`

src/util/formatVariantSelector.js

+2-127
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import selectorParser from 'postcss-selector-parser'
22
import unescape from 'postcss-selector-parser/dist/util/unesc'
33
import escapeClassName from '../util/escapeClassName'
44
import prefixSelector from '../util/prefixSelector'
5+
import { movePseudos } from './pseudoElements'
56

67
/** @typedef {import('postcss-selector-parser').Root} Root */
78
/** @typedef {import('postcss-selector-parser').Selector} Selector */
@@ -245,12 +246,7 @@ export function finalizeSelector(current, formats, { context, candidate, base })
245246
})
246247

247248
// Move pseudo elements to the end of the selector (if necessary)
248-
selector.each((sel) => {
249-
let [pseudoElements] = collectPseudoElements(sel)
250-
if (pseudoElements.length > 0) {
251-
sel.nodes.push(...pseudoElements.sort(sortSelector))
252-
}
253-
})
249+
selector.each((sel) => movePseudos(sel))
254250

255251
return selector.toString()
256252
}
@@ -318,124 +314,3 @@ export function handleMergePseudo(selector, format) {
318314

319315
return [selector, format]
320316
}
321-
322-
// Note: As a rule, double colons (::) should be used instead of a single colon
323-
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
324-
// this distinction was not present in older versions of the W3C spec, most
325-
// browsers support both syntaxes for the original pseudo-elements.
326-
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
327-
328-
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
329-
let pseudoElementExceptions = [
330-
'::file-selector-button',
331-
332-
// Webkit scroll bar pseudo elements can be combined with user-action pseudo classes
333-
'::-webkit-scrollbar',
334-
'::-webkit-scrollbar-button',
335-
'::-webkit-scrollbar-thumb',
336-
'::-webkit-scrollbar-track',
337-
'::-webkit-scrollbar-track-piece',
338-
'::-webkit-scrollbar-corner',
339-
'::-webkit-resizer',
340-
341-
// Old-style Angular Shadow DOM piercing pseudo element
342-
'::ng-deep',
343-
]
344-
345-
/**
346-
* This will make sure to move pseudo's to the correct spot (the end for
347-
* pseudo elements) because otherwise the selector will never work
348-
* anyway.
349-
*
350-
* E.g.:
351-
* - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
352-
* - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
353-
*
354-
* `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
355-
*
356-
* @param {Selector} selector
357-
* @param {boolean} force
358-
**/
359-
export function collectPseudoElements(selector, force = false) {
360-
/** @type {Node[]} */
361-
let nodes = []
362-
let seenPseudoElement = null
363-
364-
for (let node of [...selector.nodes]) {
365-
if (isPseudoElement(node, force)) {
366-
nodes.push(node)
367-
selector.removeChild(node)
368-
seenPseudoElement = node.value
369-
} else if (seenPseudoElement !== null) {
370-
if (pseudoElementExceptions.includes(seenPseudoElement) && isPseudoClass(node, force)) {
371-
nodes.push(node)
372-
selector.removeChild(node)
373-
} else {
374-
seenPseudoElement = null
375-
}
376-
}
377-
378-
if (node?.nodes) {
379-
let hasPseudoElementRestrictions =
380-
node.type === 'pseudo' && (node.value === ':is' || node.value === ':has')
381-
382-
let [collected, seenPseudoElementInSelector] = collectPseudoElements(
383-
node,
384-
force || hasPseudoElementRestrictions
385-
)
386-
387-
if (seenPseudoElementInSelector) {
388-
seenPseudoElement = seenPseudoElementInSelector
389-
}
390-
391-
nodes.push(...collected)
392-
}
393-
}
394-
395-
return [nodes, seenPseudoElement]
396-
}
397-
398-
// This will make sure to move pseudo's to the correct spot (the end for
399-
// pseudo elements) because otherwise the selector will never work
400-
// anyway.
401-
//
402-
// E.g.:
403-
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
404-
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
405-
//
406-
// `::before:hover` doesn't work, which means that we can make it work
407-
// for you by flipping the order.
408-
export function sortSelector(a, z) {
409-
// Both nodes are non-pseudo's so we can safely ignore them and keep
410-
// them in the same order.
411-
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
412-
return 0
413-
}
414-
415-
// If one of them is a combinator, we need to keep it in the same order
416-
// because that means it will start a new "section" in the selector.
417-
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
418-
return 0
419-
}
420-
421-
// One of the items is a pseudo and the other one isn't. Let's move
422-
// the pseudo to the right.
423-
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
424-
return (a.type === 'pseudo') - (z.type === 'pseudo')
425-
}
426-
427-
// Both are pseudo's, move the pseudo elements (except for
428-
// ::file-selector-button) to the right.
429-
return isPseudoElement(a) - isPseudoElement(z)
430-
}
431-
432-
function isPseudoElement(node, force = false) {
433-
if (node.type !== 'pseudo') return false
434-
if (pseudoElementExceptions.includes(node.value) && !force) return false
435-
436-
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
437-
}
438-
439-
function isPseudoClass(node, force) {
440-
return node.type === 'pseudo' && !isPseudoElement(node, force)
441-
}

0 commit comments

Comments
 (0)