fix: keep runtime sheet intact when the CSSOM rejects a rule#123
Merged
Conversation
Two injection bugs compounded to render consumer apps completely unstyled in real browsers: 1. @Property rules registered with a var()/env() initial value are invalid per CSS Properties & Values (initial-value must be computationally independent), so CSSStyleSheet.insertRule throws. Such registrations now degrade to the universal "*" syntax without an initial-value, keeping the inherits behavior; defaults still cascade via :root/base assignments. 2. The insertRule text fallback appended text nodes to the main managed <style> element, which re-parses the element from its text content and discards every rule previously inserted through insertRule — one rejected rule wiped the whole sheet. Rejected rules now go to a dedicated <style id="typestyles-fallback"> element that only ever holds text. Found integrating var-ui's component-breadth work, where every c.vars() recipe registers token-referencing @Property rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two runtime-injection bugs compound to render consumer apps completely unstyled in real browsers (confirmed in Chrome while integrating var-ui's component-breadth work):
Invalid
@propertyemission.registerAtPropertyRulewritesinitial-value: ${value}verbatim. Per CSS Properties & Values Level 1, an@propertyinitial value must be computationally independent —var()/env()references are not allowed, soCSSStyleSheet.insertRulethrowsSyntaxError. Everyc.vars()recipe var or registered property whose default is a token reference (var(--…)) hits this.Destructive text fallback. When
insertRulethrows,flush()/writeAllRulesToStyleElementappended the rejected rule as a text node to the same<style>element that holds the insertRule-built sheet. Mutating a style element's text makes the browser re-parse it from text content, discarding every rule previously added viainsertRule. Net effect: one rejected rule wipes the entire runtime sheet. Observed end state in Chrome:document.getElementById('typestyles').sheet.cssRules.length === 0, page fully unstyled, while the element's text held only the rejected@propertyrules.jsdom does not validate
@property, which is why unit tests never caught this.Fix
When a registered property's initial value contains
var(/env(, degrade to the universal syntax —@property --x { syntax: "*"; inherits: <inherits>; }— which is the only form allowed to omitinitial-value. Type checking is lost for those properties, butinherits(the load-bearing behavior) survives, and the default still reaches the cascade through the existing:root/ base-style assignments. Computationally independent values keep the typed rule exactly as before.Route all text fallbacks (flush, doc-swap rewrite, layer-order preamble) to a dedicated
<style id="typestyles-fallback">element placed immediately after the main element. The fallback element only ever holds text — neverinsertRule— so re-parsing it on append cannot lose rules. It mirrors the main element's detach/reconnect lifecycle.Tests
registered-property.test.ts(new): typed rule preserved for literals; universal degradation forvar()/env();:rootdefault assignment unaffected.sheet.test.ts: patchedCSSStyleSheet.prototype.insertRuleto reject marked rules — asserts previously inserted CSSOM rules survive, rejected rules land in the fallback element, the main element gains no text nodes, the doc-swap rewrite path stays non-destructive, and the fallback sits right after the main element.pnpm verifypasses (43/43 turbo tasks; 438 typestyles tests). Changeset included (patch).🤖 Generated with Claude Code