ReviewOS

stacks/stx

`{⁠{ }}` interpolation inside `<script client>` is HTML-escaped → invalid JS (breaks all @stacksjs/components interactive components)

#1757
Closed glennmichael123 opened this 14 days ago · 0 comments
14 days ago

Summary

{⁠{ expr }} interpolation inside a <script client> block is HTML-escaped, so any interpolated value containing quotes (e.g. a string or JSON.stringify(...) output) is emitted as invalid JavaScript. " becomes &quot;, which breaks the whole client bundle with Unexpected token '&' / missing ) after argument list.

This makes every @stacksjs/components component that injects a server prop into a client signal unusable — Notification, Dialog, Drawer, Tabs, Tooltip, Switch, Select, Pagination, Accordion, etc. all use this pattern (const x = state({⁠{ initialX }}), const cls = {⁠{ JSON.stringify(className) }}), and dropping any of them onto a page crashes that page's JS.

Versions: @stacksjs/stx 0.2.70, bun-plugin-stx 0.2.70, @stacksjs/components 0.2.70 (note: components@0.2.70 pins stx@0.2.70 — same monorepo — so the shipped components are broken against their own engine). Also reproduced on 0.2.72. Bun 1.3.13, macOS arm64.

Minimal reproduction

components/ReproWidget.stx:

<script server>
export const initialShow = $props.show !== false
export const className = $props.className || ''
</script>

<script client>
const isVisible = state({{ initialShow }})
const extraClassName = {{ JSON.stringify(className) }}
function toggle() { isVisible.set(!isVisible()) }
</script>

<div :if="isVisible()" role="alert">widget visible</div>

views/index.stx:

<!DOCTYPE html><html><head><title>repro</title></head>
<body>
  <ReproWidget show className="foo" />
</body></html>

Serve with bun-plugin-stx/serve and open the page.

Actual compiled client JS

const isVisible = state(true);          // OK — `true` has no special chars
const extraClassName = &quot;foo&quot;;  // BROKEN — quotes HTML-escaped

Browser: Uncaught SyntaxError: Unexpected token '&'. The entire client bundle fails to execute, so the whole page is dead (no signals, no event handlers).

Expected

Interpolation inside a <script client> (and <script server>) block must not HTML-escape. The line should compile to:

const extraClassName = "foo";

i.e. {⁠{ }} inside script context should behave like {⁠!! !!} (raw), since script content is JavaScript, not HTML text.

Impact / why it matters

  • state({⁠{ initialShow }}) happens to work only because booleans/numbers have no escapable characters; the moment a string or JSON.stringify result is injected, the bundle breaks.
  • This blocks all of @stacksjs/components' interactive components on a vanilla install.

Workaround

None from userland — the affected code is inside node_modules/@stacksjs/components. Consumers can only use purely server-rendered components (e.g. Spinner, Badge, Card).

Same {⁠{ }}-in-components family as #1748 ({⁠{ }} inside x-for renders empty in components), but this one corrupts script output rather than rendering blank.

Sign in to comment on this issue.