ReviewOS

stacks/stx

A <style> tag named in a script comment is read as a real style block, hoisting the rest of the component into the page as text

#2005
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Found while documenting #1982 (a mustache inside a component's style block is never interpolated). Writing the explanation in a comment broke the component.

Reproduction

<script server>
// A component <style> block is extracted before expressions run.
export const label = 'hello'
</script>

<p>{{ label }}</p>

<style>
p { color: red; }
</style>

Rendered through processDirectives as <Probe />:

<p>hello</p>

<style> block is extracted before expressions run.
export const label = 'hello'
</script>

<p></p>

<style>
p { color: red; }
</style>

The component renders correctly and then the server script's source, the raw </script>, and a second uninterpolated copy of the template are printed into the page as visible text.

Cause

packages/stx/src/utils.ts:1456 looks for the component's stylesheet in the raw source:

const styleMatch = componentContent.match(/<style\b([^>]*)>([\s\S]*?)<\/style>/i)

componentContent still contains <script server>. So <style> inside the comment is the match's start, [\s\S]*?</style> runs to the real closing tag at the end of the file, and the whole span — comment remainder, the rest of the server script, the template, the actual stylesheet — becomes "the style block". That block is preserved verbatim and re-emitted, which is where the leaked text comes from.

The subsequent removal from the template is not affected, because scripts are stripped from templateContent before styles are.

Severity

Silent and disproportionate: no throw, no warning, no build failure. The trigger is a comment, so nothing about the change that causes it looks like it touches rendering. Any component whose script mentions <style> — or <style scoped>, or a string containing one — ships its own source to the browser.

Same shape as the .replace('</body>', …) class of bug already called out in CLAUDE.md note 24: a tag-shaped substring inside script content treated as an element.

Fix

Search with the scripts stashed out, using the module's existing element-position-aware helper (already imported in that file):

const styleSearchSource = stashScriptElements(componentContent).output
const styleMatch = styleSearchSource.match(/<style\b([^>]*)>([\s\S]*?)<\/style>/i)

Verified: the reproduction above renders clean afterwards, and the real stylesheet is still extracted and preserved.

Filed with a fix and a regression test on the way.

Sign in to comment on this issue.