Skip to content

Commit 991bfc6

Browse files
committed
Fix(Runtime): Rule insertion and deletion handling
1 parent e01da56 commit 991bfc6

File tree

4 files changed

+88
-89
lines changed

4 files changed

+88
-89
lines changed

packages/runtime/e2e/variables.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ test('expects the variable output', async ({ page }) => {
128128
return globalThis.cssRuntime.text
129129
})
130130
expect(text).not.toMatch(/:root\{[^}]*--first:17 17 17[^}]*\}/)
131-
expect(text).not.toMatch(/\.dark\{[^}]*--first:34 34 34[^}]*\}/)
131+
expect(text).not.toMatch(/\.dark\{[^}]*--first:29 28 29[^}]*\}/)
132132
expect(text).not.toMatch(/\.light, :root\{[^}]*--first:51 51 51[^}]*\}/)
133133

134134
text = await page.evaluate(async () => {

packages/runtime/src/core.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MasterCSS, config as defaultConfig, Rule } from '@master/css'
1+
import { MasterCSS, config as defaultConfig, Rule, VariableRule, AnimationRule } from '@master/css'
22
import { type Config } from '@master/css'
33
import registerGlobal from './register-global'
44
import { HydrateResult } from './types'
@@ -88,7 +88,7 @@ export default class CSSRuntime extends MasterCSS {
8888
this.style.id = 'master'
8989
this.container.append(this.style)
9090
this.style.sheet!.insertRule(this.layerStatementRule.text)
91-
this.layerStatementRule.nodes[0].native = this.style!.sheet!.cssRules.item(0) as CSSLayerStatementRule
91+
this.layerStatementRule.native = this.style!.sheet!.cssRules.item(0) as CSSLayerStatementRule
9292
for (const eachConnectedName of connectedNames) {
9393
this.add(eachConnectedName)
9494
}
@@ -230,37 +230,49 @@ export default class CSSRuntime extends MasterCSS {
230230
const eachCSSLayerRule = eachNativeCSSRule as CSSLayerBlockRule
231231
if ((eachNativeCSSRule as CSSLayerBlockRule).name === 'theme') {
232232
this.themeLayer.native = eachCSSLayerRule
233-
let variableRule: Rule | undefined
234-
let lastVariableName: string | undefined
235-
for (let j = 0; j < eachCSSLayerRule.cssRules.length; j++) {
236-
const cssRule = eachCSSLayerRule.cssRules[j]
233+
let variableRule: VariableRule | undefined
234+
const unresolvedCSSRules = new Map<string, CSSRule>()
235+
for (const rule of eachCSSLayerRule.cssRules) {
236+
// trim() for fix the firefox bug that the cssText ends with \n\n
237+
unresolvedCSSRules.set(rule.cssText.trim(), rule)
238+
}
239+
for (const cssRule of eachCSSLayerRule.cssRules) {
240+
if (!unresolvedCSSRules.has(cssRule.cssText)) continue
237241
const variableCSSRule = (cssRule.constructor.name === 'CSSMediaRule'
238242
? (cssRule as CSSMediaRule).cssRules[0]
239243
: cssRule) as CSSStyleRule
240244
const variableName = variableCSSRule.style[0].slice(2)
241-
if (variableName !== lastVariableName) {
242-
lastVariableName = variableName
243-
variableRule = new Rule(variableName)
244-
this.themeLayer.rules.push(variableRule)
245-
this.themeLayer.tokenCounts.set(variableRule.name, 0)
246-
}
247-
variableRule?.nodes.push({
248-
native: cssRule,
249-
text: cssRule.cssText
245+
const variable = this.variables.get(variableName)
246+
if (!variable) continue
247+
variableRule = new VariableRule(variableName, variable, this)
248+
this.themeLayer.rules.push(variableRule)
249+
this.themeLayer.tokenCounts.set(variableRule.name, 0)
250+
variableRule.nodes.forEach((node) => {
251+
const checkRuleIndex = checkSheet.insertRule(node.text)
252+
const checkNodeNativeRule = checkSheet.cssRules.item(checkRuleIndex)
253+
if (checkNodeNativeRule) {
254+
const checkNodeNativeRuleText = checkNodeNativeRule.cssText.trim()
255+
const match = unresolvedCSSRules.get(checkNodeNativeRuleText)
256+
if (match) {
257+
node.native = match
258+
unresolvedCSSRules.delete(checkNodeNativeRuleText)
259+
return
260+
}
261+
}
250262
})
251263
}
252264
if (this.themeLayer.rules.length) this.rules.push(this.themeLayer)
253265
} else {
254266
cssLayerRules.push(eachCSSLayerRule)
255267
}
256268
} else if (eachNativeCSSRule.constructor.name === 'CSSLayerStatementRule') {
257-
this.layerStatementRule.nodes[0].native = eachNativeCSSRule as CSSLayerStatementRule
269+
this.layerStatementRule.native = eachNativeCSSRule as CSSLayerStatementRule
258270
} else if (eachNativeCSSRule.constructor.name === 'CSSKeyframesRule') {
259-
const keyframsRule = eachNativeCSSRule as CSSKeyframesRule
260-
const animationRule = new Rule(keyframsRule.name, [{
261-
native: keyframsRule,
262-
text: keyframsRule.cssText
263-
}])
271+
const nativeKeyframsRule = eachNativeCSSRule as CSSKeyframesRule
272+
const keyframes = this.animations.get(nativeKeyframsRule.name)
273+
if (!keyframes) continue
274+
const animationRule = new AnimationRule(nativeKeyframsRule.name, keyframes, this)
275+
animationRule.native = nativeKeyframsRule as unknown as CSSKeyframeRule
264276
this.animationsNonLayer.rules.push(animationRule)
265277
this.rules.push(animationRule)
266278
this.animationsNonLayer.tokenCounts.set(animationRule.name, 0)
@@ -306,26 +318,24 @@ export default class CSSRuntime extends MasterCSS {
306318
layer.insertVariables(createdRule)
307319
layer.insertAnimations(createdRule)
308320
result.allSyntaxRules.push(createdRule)
309-
createdRule.nodes.forEach((node) => {
310-
try {
311-
const checkRuleIndex = checkSheet.insertRule(node.text)
312-
const checkNodeNativeRule = checkSheet.cssRules.item(checkRuleIndex)
313-
if (checkNodeNativeRule) {
314-
const checkNodeNativeRuleText = checkNodeNativeRule.cssText.trim()
315-
const match = unresolvedCSSRules.get(checkNodeNativeRuleText)
316-
if (match) {
317-
node.native = match
318-
unresolvedCSSRules.delete(checkNodeNativeRuleText)
319-
return
320-
}
321-
}
322-
console.error(`Cannot retrieve CSS rule for \`${node.text}\`. (${layer.name}) (https://rc.css.master.co/messages/hydration-errors)`)
323-
} catch (error) {
324-
if (process.env.NODE_ENV === 'development') {
325-
console.debug(`Cannot insert CSS rule for \`${node.text}\`. (${layer.name}) (https://rc.css.master.co/messages/hydration-errors)`)
321+
try {
322+
const checkRuleIndex = checkSheet.insertRule(createdRule.text)
323+
const checkNodeNativeRule = checkSheet.cssRules.item(checkRuleIndex)
324+
if (checkNodeNativeRule) {
325+
const checkNodeNativeRuleText = checkNodeNativeRule.cssText.trim()
326+
const match = unresolvedCSSRules.get(checkNodeNativeRuleText)
327+
if (match) {
328+
createdRule.native = match
329+
unresolvedCSSRules.delete(checkNodeNativeRuleText)
330+
continue
326331
}
327332
}
328-
})
333+
console.error(`Cannot retrieve CSS rule for \`${createdRule.text}\`. (${layer.name}) (https://rc.css.master.co/messages/hydration-errors)`)
334+
} catch (error) {
335+
if (process.env.NODE_ENV === 'development') {
336+
console.debug(`Cannot insert CSS rule for \`${createdRule.text}\`. (${layer.name}) (https://rc.css.master.co/messages/hydration-errors)`)
337+
}
338+
}
329339
}
330340
} else {
331341
console.error(`Cannot recognize \`${eachNativeLayerRule.cssText}\`. (${layer.name}) (https://rc.css.master.co/messages/hydration-errors)`)

packages/runtime/src/debuggers/class-count.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ export default function registerClassCountDebugger() {
2424
const eachCount = cssRuntime.classCounts.get(className)
2525
const eachActualCount = actualClassCounts[className]
2626
if (eachCount !== eachActualCount) {
27-
log.error(`Class count mismatch for '${className}' (expected ${eachActualCount}) (received ${eachCount})`)
27+
log.error(`Class count mismatch for \`${className}\` (expected ${eachActualCount}) (received ${eachCount})`)
2828
errored = true
2929
}
3030
}
3131

3232
cssRuntime.classCounts.forEach((eachCount, className) => {
3333
if (!Object.prototype.hasOwnProperty.call(actualClassCounts, className)) {
34-
log.error(`Class count mismatch for '${className}' (expected ${0}) (received ${eachCount})`)
34+
log.error(`Class count mismatch for \`${className}\` (expected ${0}) (received ${eachCount})`)
3535
errored = true
3636
}
3737
})

packages/runtime/src/layer.ts

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Layer, Rule } from '@master/css'
1+
import { Layer, Rule, VariableRule } from '@master/css'
22
import findNativeCSSRuleIndex from 'shared/utils/find-native-css-rule-index'
33
import CSSRuntime from './core'
44

@@ -14,7 +14,7 @@ export default class RuntimeLayer extends Layer {
1414
super(name, cssRuntime)
1515
}
1616

17-
override attach() {
17+
attach() {
1818
super.attach()
1919
const nativeSheet = this.cssRuntime.style?.sheet
2020
if (nativeSheet && !this.native?.parentStyleSheet) {
@@ -23,48 +23,32 @@ export default class RuntimeLayer extends Layer {
2323
}
2424
}
2525

26-
insert(rule: Rule, index = this.rules.length) {
26+
insert(rule: Rule | VariableRule, index = this.rules.length) {
2727
const insertedIndex = super.insert(rule, index)
28-
if (insertedIndex === undefined) return
29-
if (this.native) {
30-
let cssRuleIndex = 0
31-
const lastCssRule = (function getLastCssRule(layer: Layer, index: number) {
32-
let lastCssRule: any
33-
const previouRule = layer.rules[index]
34-
if (previouRule && 'nodes' in previouRule) {
35-
if (!previouRule.nodes.length)
36-
return getLastCssRule(layer, index - 1)
37-
const lastNativeRule = previouRule.nodes[previouRule.nodes.length - 1]
38-
lastCssRule = lastNativeRule.native
39-
}
40-
return lastCssRule
41-
})(this, insertedIndex as number - 1)
42-
if (lastCssRule) {
43-
for (let i = 0; i < this.native.cssRules.length; i++) {
44-
if (this.native.cssRules[i] === lastCssRule) {
45-
cssRuleIndex = i + 1
46-
break
47-
}
48-
}
49-
}
50-
51-
for (let i = 0; i < rule.nodes.length;) {
52-
const node = rule.nodes[i]
53-
try {
54-
const insertedIndex = this.native.insertRule(node.text, cssRuleIndex)
55-
node.native = this.native.cssRules.item(insertedIndex) as CSSRule
56-
cssRuleIndex++
57-
i++
58-
} catch (error) {
59-
console.error(error, node)
60-
rule.nodes.splice(i, 1)
61-
}
28+
if (insertedIndex === undefined || !this.native) return
29+
const insertRuleSafely = (text: string, position: number) => {
30+
try {
31+
const insertedIndex = this.native!.insertRule(text, position)
32+
return this.native!.cssRules.item(insertedIndex) as CSSRule
33+
} catch (error) {
34+
console.error(error, rule)
35+
return
6236
}
6337
}
38+
if ('nodes' in rule) {
39+
let currentIndex = insertedIndex
40+
rule.nodes.forEach((node) => {
41+
node.native = insertRuleSafely(node.text, currentIndex)
42+
if (node.native) currentIndex++
43+
})
44+
} else {
45+
rule.native = insertRuleSafely(rule.text, index)
46+
}
6447
return insertedIndex
6548
}
6649

67-
override detach() {
50+
detach() {
51+
super.detach()
6852
const nativeSheet = this.cssRuntime.style?.sheet
6953
if (nativeSheet && this.native?.parentStyleSheet) {
7054
const foundIndex = findNativeCSSRuleIndex(nativeSheet.cssRules, this.native)
@@ -76,18 +60,23 @@ export default class RuntimeLayer extends Layer {
7660

7761
delete(key: string) {
7862
const deletedRule = super.delete(key)
79-
if (!deletedRule) return
80-
if (this.native?.cssRules && 'nodes' in deletedRule) {
81-
for (const node of deletedRule.nodes) {
82-
if (node.native) {
83-
const foundIndex = findNativeCSSRuleIndex(this.native.cssRules, node.native)
84-
if (foundIndex !== -1) {
85-
// todo: Firefox throw "Uncaught NS_ERROR_FAILURE". Reproduce: Add '@fade|1s @fade|2s' and remove '@fade|1s @fade|2s'
86-
this.native.deleteRule(foundIndex)
87-
}
63+
if (!deletedRule || !this.native) return
64+
const deleteRuleSafely = (rule?: CSSRule) => {
65+
if (!rule) return
66+
const foundIndex = findNativeCSSRuleIndex(this.native!.cssRules, rule)
67+
if (foundIndex !== -1) {
68+
try {
69+
this.native!.deleteRule(foundIndex)
70+
} catch (error) {
71+
console.error(error, rule)
8872
}
8973
}
9074
}
75+
if ('nodes' in deletedRule) {
76+
deletedRule.nodes.forEach((node) => deleteRuleSafely(node.native))
77+
} else {
78+
deleteRuleSafely(deletedRule.native)
79+
}
9180
return deletedRule
9281
}
9382

0 commit comments

Comments
 (0)