ReviewOS

stacks/stx

{⁠{ }} interpolation emptied server-side in @include partials / components (script stripped before processExpressions)

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

Summary

interpolation that reads a signal defined in a partial's own <script client> renders empty server-side and never binds on the client. This affects @include partials and <Component> bodies — top-level pages work fine. It's the gap behind the recent :text→ migration regressions (pages migrated cleanly, partials/components went blank).

Root cause

includes.ts (~L915) strips the partial's <script> blocks out of the body and relocates them into a separate <script data-stx-scoped> (preservedScript). It then runs processExpressions on the script-stripped body (~L1107).

processExpressions gates preservation on usesSignalsInScript (expressions.ts:449). Rule #1 detects signals via state|derived|effect|ref|reactive|computed|watch|watchEffect( inside a <script> — but the script was just stripped, so rule #1 can't fire. If the body has none of the other gate triggers (:/x- + for|if|else-if|else|show|text|html|model|bind, or @if/@for/@show()), usesSignalsInScript returns false → is evaluated server-side (signal undefined → emptied) → and the marker is gone, so the re-executed scoped script has nothing to bind.

Repro

partials/Counter.stx:

<script client>
  const n = derived(() => 41 + 1)
</script>
<p>{{ n() }}</p>

pages/index.stx:

<script client>
  const ready = state(true)
</script>
@include('partials/Counter')

→ server renders <p></p> (empty); client never fills it. Replacing with :text="n()" works (it's an attribute, survives server-side, binds after the scoped script re-runs).

A body that happens to contain a structural directive (:for/:if/:show) trips a different gate rule and stays correct — which is why some component conversions (e.g. a :for table) survive and others go blank.

Proposed fix

In includes.ts, compute usesSignalsInScript(partialContent) on the full content before stripping <script> at ~L915, and thread that boolean into processExpressions as a force-preserve flag, so in a partial that had a client <script> is preserved for the runtime (same as a top-level page).

Workaround until fixed

Keep :text/x-text in @include partials and <Component> bodies whose only directives are x-class/@click/x-src/props. is only safe in top-level route pages, or in partials/components whose body already has a :for/:if/:show.

Affected (already-merged migration regressions, fixed by reverting to :text)

  • stacksjs/training#86 — confirmation.stx
  • stacksjs/bench-review#67 — AdminDashboard, AdminShell, BlogHeader, CourtHeader, PricingSection, VerifyEmail (all @include partials)

Sign in to comment on this issue.