Skip to content

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

Open
Arul1998 wants to merge 1 commit into
stenciljs:mainfrom
Arul1998:fix/escape-backticks-in-styles
Open

fix(compiler): escape backslashes before other escape sequences in styles#6773
Arul1998 wants to merge 1 commit into
stenciljs:mainfrom
Arul1998:fix/escape-backticks-in-styles

Conversation

@Arul1998

Copy link
Copy Markdown

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.

Looks good 🙏
The fix itself looks correct - I 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 byte-for-byte 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) exercise the \u000c / \u0008 / \u000b / \0 regex branches with an actual raw control-character byte. The new "preserves CSS escape sequences like icon font content" test uses the JS string '\\f101', which is a literal backslash character followed by the text f101 - not a real U+000C form-feed byte.

Suggest 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!

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