Skip to content

fix(compiler): escape backslashes before other escape sequences in styles#6773

Merged
johnjenkins merged 2 commits into
stenciljs:mainfrom
Arul1998:fix/escape-backticks-in-styles
Jul 16, 2026
Merged

fix(compiler): escape backslashes before other escape sequences in styles#6773
johnjenkins merged 2 commits into
stenciljs:mainfrom
Arul1998:fix/escape-backticks-in-styles

Conversation

@Arul1998

Copy link
Copy Markdown
Contributor

Escaping backslashes last re-escaped the backslash added for backticks, leaving an unescaped backtick that terminated the generated template literal and failed the build. Escape backslashes first, and also escape dollar-brace so CSS content cannot be evaluated as a template literal interpolation.

Fixes #6710

What is the current behavior?

GitHub Issue Number: #6710

A component stylesheet containing a backtick, e.g.

p::before {
  content: "`";
}

fails the whole build with:

Rollup: Parse Error
src/components/my-component/my-component.css?tag=my-component&encapsulation=shadow (1:183): Unterminated string constant

When embedding CSS into the generated JS template literal, css-to-esm.ts escaped backslashes last, which re-escaped the backslash it had just added for backticks (\` became \\` ), leaving an unescaped backtick that terminates the template literal early. This is a regression — it worked in 4.29.3, before the escape chain was introduced.

${ in CSS content had the same class of problem: it was not escaped at all, so it was evaluated as a template literal interpolation at runtime.

What is the new behavior?

Backslashes are escaped first, so the escape sequences added afterwards are not re-escaped, and ${ is now escaped as well. CSS containing backticks or ${ compiles and round-trips correctly.

Behavior for everything else is unchanged, including the intentional handling of control characters (\f, \b, \v, \0) and icon-font content like content: "\f101" — the generated source for those inputs is identical to before this change.

Documentation

N/A — internal compiler fix, no API or documentation changes.

Does this introduce a breaking change?

  • Yes
  • No

Testing

  • Strengthened the existing escapes backticks in CSS content unit test: it previously only did a substring check that the broken output also satisfied; it now evaluates the generated module to prove it is valid JavaScript and round-trips the input CSS.
  • Added unit tests for ${} in CSS content and for icon-font escape sequences (\f101) round-tripping.
  • src/compiler/style/test/css-to-esm.spec.ts: 31/31 pass locally; full jest suite run locally as well.
  • End-to-end: built a minimal component project using the reproduction CSS from bug: compilation fails if stylesheet contains content: content: "`"; #6710 against this branch — the build succeeds and the emitted bundle contains the correctly escaped style:
const myComponentCss = () => `:host{display:block}p::before{content:"\`"}`;

Before this change, the same input reproduced the Rollup parse error from the issue.

Other information

N/A

…yles

Escaping backslashes last re-escaped the backslash added for backticks, leaving an unescaped backtick that terminated the generated template literal and failed the build. Escape backslashes first, and also escape dollar-brace so CSS content cannot be evaluated as a template literal interpolation.

Fixes stenciljs#6710
@Arul1998 Arul1998 requested a review from a team as a code owner July 15, 2026 20:24
@Arul1998 Arul1998 changed the title fix(compiler): escape backslashes before other escape sequences in st… fix(compiler): escape backslashes before other escape sequences in styles Jul 15, 2026

@johnjenkins johnjenkins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TYSM 🙏
The fix looks good I think - traced through the escape-order logic (including the \f/\b/\v/\0 branches); moving backslash-escaping to the front (combined with switching those four control-char replacements to double-backslash output) cancels out the reordering so control-char output stays identical to before.
The ${ escape is also safe against pre-existing backslashes in the source.

One test gap: none of the tests (old or new) actually test \u000c / \u0008 / \u000b / \0 regex branches with a raw control-character byte. The new "preserves CSS escape sequences like icon font content" test uses the JS string '\\f101' (I know that test was done by me 😅 ), which is a literal backslash character followed by the text f101 - not a real U+000C form-feed byte.

Would you mind adding one more test with a genuine raw control-character byte (e.g. an actual \u000c in input, not its escaped text form) asserted via the same evaluateEsmOutput round-trip pattern used for the other cases - tysm!

@Arul1998

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Added a test that feeds genuine raw control-character bytes (U+000C, U+0008, U+000B, U+0000) through the transform and asserts the round-tripped runtime CSS via evaluateEsmOutput, covering all four control-character branches.

@Arul1998 Arul1998 requested a review from johnjenkins July 16, 2026 09:58
@johnjenkins johnjenkins added this pull request to the merge queue Jul 16, 2026
Merged via the queue into stenciljs:main with commit 348f226 Jul 16, 2026
28 checks passed
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.

bug: compilation fails if stylesheet contains content: content: "`";

2 participants