Skip to content

fix(svelte2tsx): don't treat <style>-like text in a script comment as a real style tag - #3079

Merged
jasonlyu123 merged 4 commits into
sveltejs:masterfrom
Socialpranker:fix/3071-style-tag-in-script-comment
Aug 18, 2026
Merged

fix(svelte2tsx): don't treat <style>-like text in a script comment as a real style tag#3079
jasonlyu123 merged 4 commits into
sveltejs:masterfrom
Socialpranker:fix/3071-style-tag-in-script-comment

Conversation

@Socialpranker

Copy link
Copy Markdown
Contributor

Fixes #3071

Problem

svelte-check (and the VS Code extension, same engine) reports a false element_unclosed: <script> was left open for a file whose <script> is correctly closed. The trigger is the literal token <style> appearing inside the <script> (e.g. in a // comment) when the file also contains a real <style> block. svelte/compiler's compile() accepts the exact same source without error, so this is a svelte2tsx false positive.

Minimal reproduction:

<script lang="ts">
  // see the <style> block
</script>
<style>p{color:red}</style>

Root cause

In packages/svelte2tsx/src/utils/htmlxparser.ts, findVerbatimElements uses a non-greedy styleRegex that can start matching at a <style> substring inside the script and run past the real </script> up to the next </style>.

The existing insideScript guard was meant to drop such false style matches, but it only skipped matches that are strictly nested inside a script tag (tag.start < style.start && tag.end > style.end). In the issue's case the false style match ends after the script tag, so the guard didn't fire. blankVerbatimContent then blanked out the real </script> along with the fake style content, and the compiler correctly complained about an unclosed <script> in the now-broken text.

Fix

Relax the guard to also skip a style match that merely starts inside a script tag:

- (tag) => tag.start < styleTag.start && tag.end > styleTag.end
+ (tag) => tag.start < styleTag.start && styleTag.start < tag.end

Test plan

  • Added test/svelte2tsx/samples/style-tag-in-script-comment-with-real-style covering the exact reproduction (a <style> token in a script comment plus a real separate <style> block). It fails before the fix (<script> must have a closing tag) and passes after, emitting correct TSX with both the script comment and the real <style> preserved.
  • Full svelte2tsx sample suite passes (371 tests), including the neighbouring style-in-script (a <style>...</style> string literal inside a script with no real style block — still correctly treated as inside-script) and ts-style-and-script (a normal script followed by a separate style block — unaffected).
  • pnpm lint (Prettier) passes.

…as a real style tag

svelte-check reported a false `element_unclosed: <script> was left open`
for a file whose `<script>` is correctly closed, when the literal token
`<style>` appears inside the script (e.g. in a comment) *and* the file also
contains a real `<style>` block. svelte/compiler accepts the same source.

Root cause: the non-greedy `styleRegex` in `findVerbatimElements` can start
matching at a `<style>` substring inside the script and run past the real
`</script>` up to the next `</style>`. The existing `insideScript` guard only
skipped style matches *strictly nested* in a script tag, so this partially
overlapping match slipped through and `blankVerbatimContent` then blanked out
the real `</script>`, making the compiler see an unclosed script.

Relax the guard to also skip a style match that merely *starts* inside a
script tag.

Fixes sveltejs#3071

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c2c8430

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
svelte2tsx Patch
svelte-check Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jasonlyu123

Copy link
Copy Markdown
Member

This won't work. The actual style tag should be removed. So the proper fix isn't to filter it out, but to properly recognise the actual start tag.

…ing the false match

The previous approach relaxed the insideScript guard to drop a style match that
starts inside a script. That removed the false match but also dropped the real
`<style>` block, because the non-greedy style regex had already swallowed
everything from the `<style>` token in the script comment up to the real
`</style>` into a single match.

Instead, find the script containers first, blank them out (newlines preserved so
character offsets stay put), then run the style regex on that masked text. The
style regex can no longer see a `<style>` token sitting inside a script, so it
matches the actual start tag.

This covers the script-to-style direction from sveltejs#3071. The mirror case (a
`<script>` token inside a `<style>` comment) has the same shape and is left as a
follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Socialpranker

Copy link
Copy Markdown
Contributor Author

You're right, filtering it out dropped the real <style> block along with the false match. I reworked it: find the script containers first, blank them out (newlines kept so offsets don't shift), then run the style regex on that. Now the style match can only land on the actual start tag, never a <style> token sitting inside a script comment. Pushed the new version with a recalculated snapshot; the full svelte2tsx suite is green.

One thing I hit while doing this: the mirror case breaks the same way on main today, where a <script> token inside a <style> comment makes the script regex run past the real </style>. My change only covers the script-to-style direction from this issue. Want me to handle the symmetric case here too, or keep this PR scoped to #3071 and file the other one separately?

@Socialpranker

Copy link
Copy Markdown
Contributor Author

@jasonlyu123 friendly ping — I reworked this on 11 July after your feedback, and it's been quiet since.

You were right that filtering the match out dropped the real <style> block too. The current version blanks out the script containers first (keeping newlines so offsets don't shift), then runs the style regex on that, so a match inside a script comment can't win over the actual start tag.

No rush — let me know if the approach still isn't what you had in mind and I'll adjust.

@jasonlyu123

Copy link
Copy Markdown
Member

Want me to handle the symmetric case here too, or keep this PR scoped to #3071 and file the other one separately?

Yeah. It would be better to fix the script tag in the script tag case as well. You can try merging extractTag('script') and extractTag('style') into one loop.

Merge extractTag('script') and extractTag('style') into one loop that
searches both tags at each position and keeps the earlier match, then
resumes past its end.

This drops the script-container masking added earlier and fixes the
mirror case as well: a `<script>` token inside a `<style>` comment no
longer makes the script regex run past the real `</style>`, so the
actual script element is still recognised.
@Socialpranker

Copy link
Copy Markdown
Contributor Author

Merged the two into a single pass in e045a91.

findNextVerbatimElement() now runs both regexes from the same offset and keeps whichever matches earlier, then the loop resumes past that element's end. Since the element that actually opens first always wins, a <script>/<style> token sitting inside the other kind of tag can't be picked up as a start tag — which drops the script-masking step I had added and covers the mirror case at the same time.

Before this, the symmetric case failed outright:

<style>
  /* see the <script> block */
</style>
<script lang="ts">
  let a = 1;
</script>

The script regex started at the <script> inside the CSS comment and ran to the real </script>, swallowing the closing </style> and the real start tag with it, so the parse failed with <script> must have a closing tag. Now the real script element is recognised and its content ends up in the output.

Added script-tag-in-style-comment-with-real-script as the counterpart fixture. Full svelte2tsx suite is green (372), prettier clean. Also checked the perf side since both regexes now run per position — a 276 KB file with a few thousand cross-mentions in both blocks parses in ~56 ms, and a genuinely unclosed <script> still reports the error rather than being masked.

@Socialpranker

Copy link
Copy Markdown
Contributor Author

@jasonlyu123 friendly ping — this has been approved and green since July 30th, just didn't want it to get lost. Let me know if you want anything else touched before merging.

@jasonlyu123
jasonlyu123 merged commit b9f3faa into sveltejs:master Aug 18, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 18, 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

3 participants