Enable optimize universal defaults by default (#5635)

* enabled `optimizeUniversalDefaults` by default

This PR is done in a way so that the default is set to `true`, but you
can still disable it if it causes issues. In this case we do appreciate
an issue in that case 😅.

* update tests to use optimized universal selector

* update integration tests

* add dedicated tests for the optimized universal selector

* improve minimumImpactSelector algorithm

I think I cracked the algorithm, but I will probably need another pair
of eyes on the subject.

The current implementation works like this:

Prerequisites:

- The selector should already have been parsed using the selectorParser
  from 'postcss-selector-parser'.

Algorithm:

1. Remove all of the pseudo classes from the list of nodes.
  1.1. We do want to keep pseudo elements (E.g.: `::before`, `::first-line`, ...)
  1.2. We do want to keep pseudo classes that contain nodes (E.g.:
    `:not(...)`)
2. Reverse the list of nodes.
  This will make it easier to search from the end to the start. For
  example `.group:hover .group-hover` should result in `.group-hover`
  not `.group`.
  2.1. Find the index of the best match (class, id, attribute), and
    convert the node if required. (E.g.: `span#app` -> `#app` => `[id="app"]`)
  2.2. Remove the rest of the selector that is not important anymore
  2.3. Re-join the left-over nodes together

* update tests using new algorithm

* also look for `tag` types

* take `tag` into account

* simplify logic

* add test to prove `rest.reverse()` in first case is required

In case we don't find a match (idx === -1), we use `rest.reverse()`.
However, it looks like you can just use `nodes` instead.
This is not entirely true, because the `rest` variable will contain only
the nodes that are not pseudo elements.

`*:hover` would result in `*:hover` instead of just `*`

* replace all nodes after > with a single universal selector
This commit is contained in:
Robin Malfait 2021-10-06 17:45:26 +02:00 • committed by GitHub
parent 7b94feaef3
commit b661614265
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 298 additions and 107 deletions

View file

@ -21,10 +21,9 @@ describe('Build command', () => {
// `-i` is omitted, therefore the default `@tailwind base; @tailwind
// components; @tailwind utilities` is used. However `preflight` is
// disabled. I still want to verify that the `base` got included.
expect(contents).toContain('*')
expect(contents).toContain('::before')
expect(contents).toContain('::after')
expect(contents).toContain('--tw-shadow')
expect(contents).toContain('--tw-ring-offset-shadow: 0 0 #0000')
expect(contents).toContain('--tw-ring-shadow: 0 0 #0000')
expect(contents).toContain('--tw-shadow: 0 0 #0000')
// Verify `utilities` output is correct
expect(contents).toIncludeCss(

View file

@ -1,18 +1,24 @@
import chalk from 'chalk'
import log from './util/log'
const featureFlags = {
let defaults = {
optimizeUniversalDefaults: true,
}
let featureFlags = {
future: [],
experimental: ['optimizeUniversalDefaults'],
}
export function flagEnabled(config, flag) {
if (featureFlags.future.includes(flag)) {
return config.future === 'all' || (config?.future?.[flag] ?? false)
return config.future === 'all' || (config?.future?.[flag] ?? defaults[flag] ?? false)
}
if (featureFlags.experimental.includes(flag)) {
return config.experimental === 'all' || (config?.experimental?.[flag] ?? false)
return (
config.experimental === 'all' || (config?.experimental?.[flag] ?? defaults[flag] ?? false)
)
}
return false
@ -34,7 +40,7 @@ export function issueFlagNotices(config) {
}
if (experimentalFlagsEnabled(config).length > 0) {
const changes = experimentalFlagsEnabled(config)
let changes = experimentalFlagsEnabled(config)
.map((s) => chalk.yellow(s))
.join(', ')

View file

@ -2,60 +2,56 @@ import postcss from 'postcss'
import selectorParser from 'postcss-selector-parser'
import { flagEnabled } from '../featureFlags'
function isPseudoElement(n) {
if (n.type !== 'pseudo') {
return false
}
return (
n.value.startsWith('::') ||
[':before', ':after', ':first-line', ':first-letter'].includes(n.value)
)
let getNode = {
id(node) {
return selectorParser.attribute({
attribute: 'id',
operator: '=',
value: node.value,
quoteMark: '"',
})
},
}
function minimumImpactSelector(nodes) {
let rest = nodes
// Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden]))
.filter((n) => n.type === 'pseudo' || n.type === 'combinator')
// Remove leading pseudo's (:hover, :focus, ...)
.filter((n, idx, all) => {
// Keep pseudo elements
if (isPseudoElement(n)) return true
.filter((node) => {
// Keep non-pseudo nodes
if (node.type !== 'pseudo') return true
if (idx === 0 && n.type === 'pseudo') return false
if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false
// Keep pseudo nodes that have subnodes
// E.g.: `:not()` contains subnodes inside the parentheses
if (node.nodes.length > 0) return true
return true
// Keep pseudo `elements`
// This implicitly means that we ignore pseudo `classes`
return (
node.value.startsWith('::') ||
[':before', ':after', ':first-line', ':first-letter'].includes(node.value)
)
})
.reverse()
let [bestNode] = nodes
let searchFor = new Set(['tag', 'class', 'id', 'attribute'])
for (let [type, getNode = (n) => n] of [
['class'],
[
'id',
(n) =>
selectorParser.attribute({
attribute: 'id',
operator: '=',
value: n.value,
quoteMark: '"',
}),
],
['attribute'],
]) {
let match = nodes.find((n) => n.type === type)
let splitPointIdx = rest.findIndex((n) => searchFor.has(n.type))
if (splitPointIdx === -1) return rest.reverse().join('').trim()
if (match) {
bestNode = getNode(match)
break
}
let node = rest[splitPointIdx]
let bestNode = getNode[node.type] ? getNode[node.type](node) : node
rest = rest.slice(0, splitPointIdx)
let combinatorIdx = rest.findIndex((n) => n.type === 'combinator' && n.value === '>')
if (combinatorIdx !== -1) {
rest.splice(0, combinatorIdx)
rest.unshift(selectorParser.universal())
}
return [bestNode, ...rest].join('').trim()
return [bestNode, ...rest.reverse()].join('').trim()
}
let elementSelectorParser = selectorParser((selectors) => {
export let elementSelectorParser = selectorParser((selectors) => {
return selectors.map((s) => {
let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
return minimumImpactSelector(nodes)

View file

@ -1,6 +1,12 @@
*,
::before,
::after {
.translate-x-5,
.-translate-x-4,
.translate-y-6,
.-translate-x-3,
.rotate-3,
.skew-y-12,
.skew-x-12,
.scale-95,
.transform {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -11,12 +17,36 @@
--tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.snap-x {
--tw-scroll-snap-strictness: proximity;
}
.divide-x-2 > *,
.divide-y-4 > *,
.divide-x-0 > *,
.divide-y-0 > *,
.border,
.border-2,
.border-x-4,
.border-y-4,
.border-t,
.border-b-4 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.shadow,
.shadow-md,
.shadow-lg {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
}
.ring,
.ring-4 {
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
@ -24,6 +54,18 @@
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
}
.blur-md,
.brightness-150,
.contrast-50,
.drop-shadow-md,
.grayscale,
.hue-rotate-60,
.invert,
.saturate-200,
.sepia,
.filter {
--tw-blur: var(--tw-empty, /*!*/ /*!*/);
--tw-brightness: var(--tw-empty, /*!*/ /*!*/);
--tw-contrast: var(--tw-empty, /*!*/ /*!*/);
@ -35,6 +77,18 @@
--tw-drop-shadow: var(--tw-empty, /*!*/ /*!*/);
--tw-filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale)
var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.backdrop-blur-lg,
.backdrop-brightness-50,
.backdrop-contrast-0,
.backdrop-grayscale,
.backdrop-hue-rotate-90,
.backdrop-invert,
.backdrop-opacity-75,
.backdrop-saturate-150,
.backdrop-sepia,
.backdrop-filter {
--tw-backdrop-blur: var(--tw-empty, /*!*/ /*!*/);
--tw-backdrop-brightness: var(--tw-empty, /*!*/ /*!*/);
--tw-backdrop-contrast: var(--tw-empty, /*!*/ /*!*/);
@ -351,10 +405,10 @@
.columns-md {
columns: 28rem;
}
.break-before-page {
.break-before-page {
break-before: page;
}
.break-inside-avoid-column {
.break-inside-avoid-column {
break-inside: avoid-column;
}
.break-after-auto {

View file

@ -113,7 +113,7 @@ test('experimental universal selector improvements (child selectors: divide-y)',
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.divide-y > :not([hidden]) ~ :not([hidden]) {
.divide-y > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -145,7 +145,7 @@ test('experimental universal selector improvements (hover:divide-y)', () => {
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.hover\\:divide-y > :not([hidden]) ~ :not([hidden]) {
.hover\\:divide-y > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -178,7 +178,7 @@ test('experimental universal selector improvements (#app important)', () => {
return run(input, config).then((result) => {
expect(result.css).toMatchCss(css`
.divide-y > :not([hidden]) ~ :not([hidden]) {
.divide-y > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}

View file

@ -126,9 +126,9 @@
}
}
}
*,
::before,
::after {
.scale-50,
.transform,
.hover\:scale-75 {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -139,9 +139,18 @@
--tw-transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y))
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.shadow-sm,
.shadow-md,
.hover\:shadow-lg,
.md\:shadow-sm {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
}
.focus\:ring-2 {
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;

View file

@ -41,9 +41,7 @@ it('should be possible to matchComponents', () => {
return run('@tailwind base; @tailwind components; @tailwind utilities', config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.shadow {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;

View file

@ -0,0 +1,29 @@
import { elementSelectorParser } from '../src/lib/resolveDefaultsAtRules'
it.each`
before | after
${'*'} | ${'*'}
${'*:hover'} | ${'*'}
${'* > *'} | ${'* > *'}
${'.foo'} | ${'.foo'}
${'.foo:hover'} | ${'.foo'}
${'.foo:focus:hover'} | ${'.foo'}
${'li:first-child'} | ${'li'}
${'li:before'} | ${'li:before'}
${'li::before'} | ${'li::before'}
${'#app .foo'} | ${'.foo'}
${'#app'} | ${'[id=app]'}
${'#app.other'} | ${'.other'}
${'input[type="text"]'} | ${'[type="text"]'}
${'input[type="text"].foo'} | ${'.foo'}
${'.group .group\\:foo'} | ${'.group\\:foo'}
${'.group:hover .group-hover\\:foo'} | ${'.group-hover\\:foo'}
${'.owl > * + *'} | ${'.owl > *'}
${'.owl > :not([hidden]) + :not([hidden])'} | ${'.owl > *'}
${'.group:hover .group-hover\\:owl > :not([hidden]) + :not([hidden])'} | ${'.group-hover\\:owl > *'}
${'.peer:first-child ~ .peer-first\\:shadow-md'} | ${'.peer-first\\:shadow-md'}
${'.whats ~ .next > span:hover'} | ${'span'}
${'.foo .bar ~ .baz > .next > span > article:hover'} | ${'article'}
`('should generate "$after" from "$before"', ({ before, after }) => {
expect(elementSelectorParser.transformSync(before).join(', ')).toEqual(after)
})

View file

@ -8,9 +8,7 @@ it('should add the divide styles for divide-y and a default border color', () =>
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
*,
::before,
::after {
.divide-y > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -32,9 +30,7 @@ it('should add the divide styles for divide-x and a default border color', () =>
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
*,
::before,
::after {
.divide-x > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -56,9 +52,7 @@ it('should add the divide styles for divide-y-reverse and a default border color
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
*,
::before,
::after {
.divide-y-reverse > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -78,9 +72,7 @@ it('should add the divide styles for divide-x-reverse and a default border color
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
*,
::before,
::after {
.divide-x-reverse > * {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -100,9 +92,8 @@ it('should only inject the base styles once if we use divide and border at the s
return run('@tailwind base; @tailwind utilities;', config).then((result) => {
expect(result.css).toMatchCss(css`
*,
::before,
::after {
.divide-y > *,
.border-r {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}

View file

@ -14,9 +14,9 @@ test('basic utilities', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.rotate-3,
.skew-y-6,
.scale-x-110 {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -61,9 +61,9 @@ test('with pseudo-class variants', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.hover\\:scale-x-110,
.focus\\:rotate-3,
.hover\\:focus\\:skew-y-6 {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -106,9 +106,8 @@ test('with pseudo-element variants', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.before\\:scale-x-110::before,
.after\\:rotate-3::after {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -149,9 +148,8 @@ test('with multi-class variants', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.group-hover\\:scale-x-110,
.peer-focus\\:rotate-3 {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -192,9 +190,8 @@ test('with multi-class pseudo-element variants', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.group-hover\\:before\\:scale-x-110::before,
.peer-focus\\:after\\:rotate-3::after {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -241,9 +238,8 @@ test('with multi-class pseudo-element and pseudo-class variants', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.group-hover\\:hover\\:before\\:scale-x-110::before,
.peer-focus\\:focus\\:after\\:rotate-3::after {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -327,9 +323,17 @@ test('with apply', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.foo,
.bar::before,
.baz::before,
span,
.media-queries,
.a,
.b,
.c,
.a::before,
.b::after,
.recursive {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
@ -480,9 +484,8 @@ test('with borders', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.border,
.md\\:border-2 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
@ -517,12 +520,14 @@ test('with shadows', async () => {
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
*,
::before,
::after {
.shadow,
.md\\:shadow-xl {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
}
.ring-1 {
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;

View file

@ -1,6 +1,110 @@
*,
::before,
::after {
.first\:shadow-md,
.last\:shadow-md,
.only\:shadow-md,
.odd\:shadow-md:nth-child(odd),
.even\:shadow-md:nth-child(even),
.first-of-type\:shadow-md,
.last-of-type\:shadow-md,
.only-of-type\:shadow-md,
.visited\:shadow-md,
.target\:shadow-md,
.default\:shadow-md,
.checked\:shadow-md,
.indeterminate\:shadow-md,
.placeholder-shown\:shadow-md,
.autofill\:shadow-md,
.required\:shadow-md,
.valid\:shadow-md,
.invalid\:shadow-md,
.in-range\:shadow-md,
.out-of-range\:shadow-md,
.read-only\:shadow-md,
.empty\:shadow-md,
.focus-within\:shadow-md,
.hover\:shadow-md,
.focus\:shadow-md,
.focus\:hover\:shadow-md,
.focus-visible\:shadow-md,
.active\:shadow-md,
.disabled\:shadow-md,
.group-first\:shadow-md,
.group-last\:shadow-md,
.group-only\:shadow-md,
.group-odd\:shadow-md,
.group-even\:shadow-md,
.group-first-of-type\:shadow-md,
.group-last-of-type\:shadow-md,
.group-only-of-type\:shadow-md,
.group-visited\:shadow-md,
.group-target\:shadow-md,
.group-default\:shadow-md,
.group-checked\:shadow-md,
.group-indeterminate\:shadow-md,
.group-placeholder-shown\:shadow-md,
.group-autofill\:shadow-md,
.group-required\:shadow-md,
.group-valid\:shadow-md,
.group-invalid\:shadow-md,
.group-in-range\:shadow-md,
.group-out-of-range\:shadow-md,
.group-read-only\:shadow-md,
.group-empty\:shadow-md,
.group-focus-within\:shadow-md,
.group-hover\:shadow-md,
.group-focus\:shadow-md,
.group-focus\:group-hover\:shadow-md,
.group-focus-visible\:shadow-md,
.group-active\:shadow-md,
.group-disabled\:shadow-md,
.group-disabled\:group-focus\:group-hover\:shadow-md,
.group-disabled\:group-focus\:group-hover\:first\:shadow-md,
.peer-first\:shadow-md,
.peer-last\:shadow-md,
.peer-only\:shadow-md,
.peer-odd\:shadow-md,
.peer-even\:shadow-md,
.peer-first-of-type\:shadow-md,
.peer-last-of-type\:shadow-md,
.peer-only-of-type\:shadow-md,
.peer-visited\:shadow-md,
.peer-target\:shadow-md,
.peer-default\:shadow-md,
.peer-checked\:shadow-md,
.peer-indeterminate\:shadow-md,
.peer-placeholder-shown\:shadow-md,
.peer-autofill\:shadow-md,
.peer-required\:shadow-md,
.peer-valid\:shadow-md,
.peer-invalid\:shadow-md,
.peer-in-range\:shadow-md,
.peer-out-of-range\:shadow-md,
.peer-read-only\:shadow-md,
.peer-empty\:shadow-md,
.peer-focus-within\:shadow-md,
.peer-hover\:shadow-md,
.peer-focus\:shadow-md,
.peer-focus\:peer-hover\:shadow-md,
.peer-focus-visible\:shadow-md,
.peer-active\:shadow-md,
.peer-disabled\:shadow-md,
.peer-disabled\:peer-focus\:peer-hover\:shadow-md,
.peer-disabled\:peer-focus\:peer-hover\:first\:shadow-md,
.ltr\:shadow-md,
.rtl\:shadow-md,
.motion-safe\:shadow-md,
.motion-reduce\:shadow-md,
.dark\:shadow-md,
.dark\:group-disabled\:group-focus\:group-hover\:shadow-md,
.dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md,
.sm\:shadow-md,
.sm\:active\:shadow-md,
.md\:shadow-md,
.md\:group-focus\:shadow-md,
.lg\:shadow-md,
.lg\:dark\:shadow-md,
.xl\:shadow-md,
.\32xl\:shadow-md,
.\32xl\:dark\:motion-safe\:focus-within\:shadow-md {
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;