Skip to content

Make var()/env() declarations engine-proof at the import boundary#21

Merged
DavidBabinec merged 1 commit into
mainfrom
fix/css-substitution-fidelity
Jun 10, 2026
Merged

Make var()/env() declarations engine-proof at the import boundary#21
DavidBabinec merged 1 commit into
mainfrom
fix/css-substitution-fidelity

Conversation

@DavidBabinec

Copy link
Copy Markdown
Contributor

Summary

Follow-up from the import-fidelity work (#15/#19): the test harness (happy-dom) and production (Chromium) exposed DIFFERENT lossy views of CSS declarations whose value uses var()/env():

  • Chromium: shorthand longhands enumerate empty; authored text survives only in cssText (covered by an engine-specific recovery hack).
  • happy-dom: the declaration is destroyed at parse time — longhands report the bare var() text (1px solid gone), and cssText serialises the same mangle, so the authored declaration is unrecoverable. Tests therefore exercised a lossy path production never sees — exactly the bug class behind "all borders dropped".
  • Inline style="" harvesting had no recovery at all: style="border: 1px solid var(--x)" was silently lost in both engines.

Fix — don't let engines see substitution declarations

New @core/css-substitution leaf module: before any engine parse, every declaration whose value contains var(/env( is encoded as a marker custom property (all engines preserve custom properties verbatim — validated empirically in Chromium and happy-dom), and decoded back afterwards. Output is byte-faithful and identical across engines by construction:

  • cssToStyleRules encodes before replaceSync, decodes in parseDeclarations; the Chromium-specific recoverSubstitutionShorthands is deleted.
  • harvestInlineStyles re-encodes the authored style attribute — fixing inline shorthand+var declarations lost in both engines.
  • @keyframes / @font-face blocks pass through unencoded (keyframes are captured as raw CSS; a marker would leak into published output) — covered by tests incl. vendor-prefixed keyframes.

cssToStyleRules.ts shrank below its grandfathered size cap; ratcheted down per the gate.

Test plan

  • Substitution test file rewritten from Chromium mocks to REAL engine parses: shorthand+var byte-fidelity, @media overrides, blocked properties via var, keyframes marker-leak guard, encoder tokenizer units (strings/comments/nested separators), inline-style regression
  • Full suite: 5038 pass / 0 fail; build + lint clean

🤖 Generated with Claude Code

CSS engines disagree about what their CSSOM exposes for declarations whose
value contains a substitution function: Chromium enumerates the shorthand's
longhands with empty values (authored text survives only in cssText), while
happy-dom destroys the declaration at parse time — each longhand reports the
bare var() text and cssText serialises the same mangle, so the authored
declaration is unrecoverable. The importer's Chromium-specific cssText
recovery papered over half of this; tests exercised a different (lossy)
path than production, and inline style="" harvesting had no recovery at all
in either engine.

New @core/css-substitution module: before the engine parse, every
declaration whose value uses var()/env() is encoded as a marker custom
property (all engines preserve custom properties verbatim — validated in
Chromium and happy-dom) and decoded back after. Output is byte-faithful and
identical across engines by construction:

- cssToStyleRules encodes the stylesheet before replaceSync and decodes in
  parseDeclarations; recoverSubstitutionShorthands is deleted.
- harvestInlineStyles re-encodes the authored style attribute, fixing
  shorthand+var inline styles that were silently lost in BOTH engines.
- @Keyframes / @font-face blocks pass through unencoded (keyframes are
  captured as raw CSS; a marker would leak into published output).

cssToStyleRules shrank below its grandfathered cap; ratcheted down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DavidBabinec DavidBabinec merged commit 75edb82 into main Jun 10, 2026
6 checks passed
@DavidBabinec DavidBabinec deleted the fix/css-substitution-fidelity branch June 10, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant