WIP
This commit is contained in:
parent
fc5782bee1
commit
ee1cd3c0d1
4 changed files with 181 additions and 32 deletions
|
|
@ -569,10 +569,6 @@ export function optimizeAst(
|
|||
|
||||
newAst = newAst.concat(atRoots)
|
||||
|
||||
// Flatten nested style-rule selectors globally so stringify produces
|
||||
// standards-compliant CSS without nesting.
|
||||
newAst = flattenAst(newAst)
|
||||
|
||||
// Fallbacks
|
||||
// Create fallback values for usages of the `color-mix(…)` function that reference variables
|
||||
// found in the theme config.
|
||||
|
|
@ -667,7 +663,11 @@ export function optimizeAst(
|
|||
...declaration,
|
||||
value: ValueParser.toCss(ast),
|
||||
}
|
||||
let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [declaration])
|
||||
let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [
|
||||
// Keep the original declaration behind a `@supports` rule and ensure
|
||||
// it remains scoped to the current selector by wrapping it in `&`.
|
||||
rule('&', [declaration]),
|
||||
])
|
||||
colorMixQuery.src = declaration.src
|
||||
parent.splice(idx, 1, fallback, colorMixQuery)
|
||||
}
|
||||
|
|
@ -734,6 +734,12 @@ export function optimizeAst(
|
|||
}
|
||||
}
|
||||
|
||||
// Flatten nested style-rule selectors globally so stringify produces
|
||||
// standards-compliant CSS without nesting. This must run after we insert
|
||||
// polyfills that depend on the original parent/child arrays created during
|
||||
// transformation, otherwise we would be mutating stale arrays.
|
||||
newAst = flattenAst(newAst)
|
||||
|
||||
return newAst
|
||||
}
|
||||
|
||||
|
|
@ -799,28 +805,36 @@ export function flattenAst(ast: AstNode[]): AstNode[] {
|
|||
? combine(parentSelectors, selfList)
|
||||
: selfList.map(normalizeSpaces)
|
||||
|
||||
let decls: AstNode[] = []
|
||||
let nested: AstNode[] = []
|
||||
for (let child of node.nodes) {
|
||||
if (child.kind === 'declaration' || child.kind === 'comment') decls.push(child)
|
||||
else nested.push(child)
|
||||
}
|
||||
|
||||
if (decls.length > 0) {
|
||||
let bufferDecls: AstNode[] = []
|
||||
let flush = () => {
|
||||
if (bufferDecls.length === 0) return
|
||||
for (let sel of current) {
|
||||
result.push({
|
||||
kind: 'rule',
|
||||
selector: normalizePseudoElements(sel),
|
||||
nodes: decls.slice(),
|
||||
nodes: bufferDecls.slice(),
|
||||
})
|
||||
}
|
||||
bufferDecls = []
|
||||
}
|
||||
|
||||
for (let child of nested) {
|
||||
for (let child of node.nodes) {
|
||||
if (child.kind === 'declaration' || child.kind === 'comment') {
|
||||
bufferDecls.push(child)
|
||||
continue
|
||||
}
|
||||
|
||||
// We hit a nested block; flush accumulated decls first to preserve order
|
||||
flush()
|
||||
|
||||
if (child.kind === 'rule') {
|
||||
result.push(...flatten([child], current))
|
||||
} else if (child.kind === 'at-rule') {
|
||||
let inner = flatten(child.nodes, current)
|
||||
// Ensure declarations inside nested at-rules remain scoped to the
|
||||
// current selector by first wrapping them in a synthetic rule and
|
||||
// then flattening. This prevents emitting invalid CSS like
|
||||
// `@media{ color:red }` without a selector.
|
||||
let inner = flatten([styleRule('&', child.nodes)], current)
|
||||
if (
|
||||
inner.length > 0 ||
|
||||
child.name === '@layer' ||
|
||||
|
|
@ -833,6 +847,9 @@ export function flattenAst(ast: AstNode[]): AstNode[] {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining decls after processing nested children
|
||||
flush()
|
||||
} else if (node.kind === 'at-rule') {
|
||||
let inner = flatten(node.nodes, parentSelectors)
|
||||
if (
|
||||
|
|
@ -871,6 +888,9 @@ export function flattenAst(ast: AstNode[]): AstNode[] {
|
|||
|
||||
function mergeRules(nodes: AstNode[]): AstNode[] {
|
||||
let result: AstNode[] = []
|
||||
// Only merge rules within contiguous segments so we don't reorder
|
||||
// content across at-rule boundaries (e.g. keep rules before/after
|
||||
// an intervening @supports or @media block separate).
|
||||
let bySelector = new Map<string, StyleRule>()
|
||||
|
||||
for (let node of nodes) {
|
||||
|
|
@ -886,6 +906,9 @@ export function flattenAst(ast: AstNode[]): AstNode[] {
|
|||
} else if (node.kind === 'at-rule') {
|
||||
let mergedChildren = mergeRules(node.nodes)
|
||||
result.push({ ...node, nodes: mergedChildren })
|
||||
// Reset the merge cache across at-rule boundaries to preserve
|
||||
// ordering of selectors before/after the block.
|
||||
bySelector = new Map()
|
||||
} else {
|
||||
result.push(node)
|
||||
}
|
||||
|
|
@ -894,7 +917,84 @@ export function flattenAst(ast: AstNode[]): AstNode[] {
|
|||
return result
|
||||
}
|
||||
|
||||
return mergeRules(flatten(ast, null))
|
||||
function canRecombine(nodes: AstNode[]): boolean {
|
||||
for (let n of nodes) {
|
||||
if (n.kind !== 'declaration' && n.kind !== 'comment') return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function fingerprint(nodes: AstNode[]): string | null {
|
||||
if (!canRecombine(nodes)) return null
|
||||
let parts: string[] = []
|
||||
for (let n of nodes) {
|
||||
if (n.kind === 'declaration') {
|
||||
parts.push(`d\u0001${n.property}\u0001${n.value ?? ''}\u0001${n.important ? '!' : ''}`)
|
||||
} else if (n.kind === 'comment') {
|
||||
parts.push(`c\u0001${n.value}`)
|
||||
}
|
||||
}
|
||||
return parts.join('\u0002')
|
||||
}
|
||||
|
||||
function recombineAdjacentRules(nodes: AstNode[]): AstNode[] {
|
||||
let out: AstNode[] = []
|
||||
for (let node of nodes) {
|
||||
if (node.kind === 'at-rule') {
|
||||
let mergedChildren = recombineAdjacentRules(node.nodes)
|
||||
|
||||
// Merge adjacent conditional at-rules with identical name/params
|
||||
// to reduce duplication like:
|
||||
// @media foo { … }
|
||||
// @media foo { … }
|
||||
// becomes:
|
||||
// @media foo { … … }
|
||||
function canMergeAtRule(name: string) {
|
||||
return name === '@media' || name === '@supports' || name === '@container'
|
||||
}
|
||||
|
||||
if (
|
||||
out.length > 0 &&
|
||||
out[out.length - 1].kind === 'at-rule' &&
|
||||
(out[out.length - 1] as AtRule).name === node.name &&
|
||||
(out[out.length - 1] as AtRule).params === node.params &&
|
||||
canMergeAtRule(node.name)
|
||||
) {
|
||||
let prev = out[out.length - 1] as AtRule
|
||||
prev.nodes = recombineAdjacentRules(prev.nodes.concat(mergedChildren))
|
||||
continue
|
||||
}
|
||||
|
||||
out.push({ ...node, nodes: mergedChildren })
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.kind !== 'rule') {
|
||||
out.push(node)
|
||||
continue
|
||||
}
|
||||
|
||||
let fp = fingerprint(node.nodes)
|
||||
if (
|
||||
fp &&
|
||||
out.length > 0 &&
|
||||
out[out.length - 1].kind === 'rule' &&
|
||||
fingerprint((out[out.length - 1] as StyleRule).nodes) === fp
|
||||
) {
|
||||
let prev = out[out.length - 1] as StyleRule
|
||||
prev.selector = `${prev.selector}, ${node.selector}`
|
||||
continue
|
||||
}
|
||||
|
||||
out.push(node)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
let flattened = flatten(ast, null)
|
||||
let merged = mergeRules(flattened)
|
||||
let recombined = recombineAdjacentRules(merged)
|
||||
return recombined
|
||||
}
|
||||
|
||||
export function toCss(ast: AstNode[], track?: boolean) {
|
||||
|
|
|
|||
|
|
@ -5996,7 +5996,10 @@ describe('CSS flattening', () => {
|
|||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
".a:hover {\n opacity: 0.5;\n}\n.a:focus {\n opacity: 0.5;\n}\n.b:hover {\n opacity: 0.5;\n}\n.b:focus {\n opacity: 0.5;\n}\n"
|
||||
".a:hover, .a:focus, .b:hover, .b:focus {
|
||||
opacity: 0.5;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -6012,7 +6015,10 @@ describe('CSS flattening', () => {
|
|||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
".a .child {\n color: blue;\n}\n.a .c2 {\n color: blue;\n}\n.b .child {\n color: blue;\n}\n.b .c2 {\n color: blue;\n}\n"
|
||||
".a .child, .a .c2, .b .child, .b .c2 {
|
||||
color: blue;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -6028,7 +6034,10 @@ describe('CSS flattening', () => {
|
|||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
".a > .b {\n color: red;\n}\n.a + .c {\n color: red;\n}\n.a ~ .d {\n color: red;\n}\n"
|
||||
".a > .b, .a + .c, .a ~ .d {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -6043,10 +6052,7 @@ describe('CSS flattening', () => {
|
|||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
".a + .a {
|
||||
color: green;
|
||||
}
|
||||
.b + .b {
|
||||
".a + .a, .b + .b {
|
||||
color: green;
|
||||
}
|
||||
"
|
||||
|
|
@ -6067,7 +6073,13 @@ describe('CSS flattening', () => {
|
|||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
".a:is(:hover, :focus) {\n opacity: 0.5;\n}\n.b:is(:hover, :focus) {\n opacity: 0.5;\n}\n.a :where(.x, .y) {\n color: red;\n}\n.b :where(.x, .y) {\n color: red;\n}\n"
|
||||
".a:is(:hover, :focus), .b:is(:hover, :focus) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.a :where(.x, .y), .b :where(.x, .y) {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -6103,14 +6115,44 @@ describe('CSS flattening', () => {
|
|||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
"@media (min-width: 768px) {
|
||||
.a:hover {
|
||||
color: red;
|
||||
}
|
||||
.b:hover {
|
||||
.a:hover, .b:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('recombines blocks nested in multiple @media rules', () => {
|
||||
let ast = CSS.parse(css`
|
||||
.x {
|
||||
@media foo {
|
||||
@media bar {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.y {
|
||||
@media foo {
|
||||
@media bar {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
let flat = flattenAst(ast)
|
||||
expect(toCssAst(flat)).toMatchInlineSnapshot(`
|
||||
"@media foo {
|
||||
@media bar {
|
||||
.x {
|
||||
color: red;
|
||||
}
|
||||
.y {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -399,7 +399,10 @@ async function parseCss(
|
|||
let nodes: AstNode[] = []
|
||||
|
||||
if (styleRuleSelectors.length > 0) {
|
||||
nodes.push(styleRule(styleRuleSelectors.join(', '), r.nodes))
|
||||
// Preserve specificity across a selector list by wrapping in :is(…)
|
||||
// so later flattening/combination doesn’t turn this into two
|
||||
// separate rules with potentially differing specificity.
|
||||
nodes.push(styleRule(`:is(${styleRuleSelectors.join(', ')})`, r.nodes))
|
||||
}
|
||||
|
||||
for (let selector of atRuleParams) {
|
||||
|
|
|
|||
|
|
@ -6361,7 +6361,9 @@ function alphaReplacedShadowProperties(
|
|||
if (requiresFallback) {
|
||||
return [
|
||||
decl(property, applyPrefix(replaceShadowColors(value, varInjector))),
|
||||
rule('@supports (color: lab(from red l a b))', [decl(property, applyPrefix(replacedValue))]),
|
||||
rule('@supports (color: lab(from red l a b))', [
|
||||
rule('&', [decl(property, applyPrefix(replacedValue))]),
|
||||
]),
|
||||
]
|
||||
} else {
|
||||
return [decl(property, applyPrefix(replacedValue))]
|
||||
|
|
@ -6412,7 +6414,9 @@ function alphaReplacedDropShadowProperties(
|
|||
.map((value) => `drop-shadow(${replaceShadowColors(value, varInjector)})`)
|
||||
.join(' '),
|
||||
),
|
||||
rule('@supports (color: lab(from red l a b))', [decl(property, prefix + replacedValue)]),
|
||||
rule('@supports (color: lab(from red l a b))', [
|
||||
rule('&', [decl(property, prefix + replacedValue)]),
|
||||
]),
|
||||
]
|
||||
} else {
|
||||
return [decl(property, prefix + replacedValue)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue