Skip to content

fix: keep runtime sheet intact when the CSSOM rejects a rule#123

Merged
dbanksdesign merged 1 commit into
mainfrom
fix/runtime-injection-property-fallback
Jul 6, 2026
Merged

fix: keep runtime sheet intact when the CSSOM rejects a rule#123
dbanksdesign merged 1 commit into
mainfrom
fix/runtime-injection-property-fallback

Conversation

@dbanksdesign

Copy link
Copy Markdown
Collaborator

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):

  1. Invalid @property emission. registerAtPropertyRule writes initial-value: ${value} verbatim. Per CSS Properties & Values Level 1, an @property initial value must be computationally independentvar() / env() references are not allowed, so CSSStyleSheet.insertRule throws SyntaxError. Every c.vars() recipe var or registered property whose default is a token reference (var(--…)) hits this.

  2. Destructive text fallback. When insertRule throws, flush() / writeAllRulesToStyleElement appended 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 via insertRule. 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 @property rules.

jsdom does not validate @property, which is why unit tests never caught this.

Fix

  1. 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 omit initial-value. Type checking is lost for those properties, but inherits (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.

  2. 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 — never insertRule — 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 for var() / env(); :root default assignment unaffected.
  • sheet.test.ts: patched CSSStyleSheet.prototype.insertRule to 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 verify passes (43/43 turbo tasks; 438 typestyles tests). Changeset included (patch).

🤖 Generated with Claude Code

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>
@dbanksdesign dbanksdesign merged commit f641e3f into main Jul 6, 2026
7 checks passed
@dbanksdesign dbanksdesign deleted the fix/runtime-injection-property-fallback branch July 6, 2026 05:06
@github-actions github-actions Bot mentioned this pull request Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant