ReviewOS

stacks/stx

A component's <script client> is dropped from the native bundle, so its bindings silently evaluate to undefined

#2076
Closed glennmichael123 opened this 8 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
8 days ago

A component used by a native screen renders its template, but anything its own <script client> declares is undefined at runtime. No diagnostic, no runtime error — the bindings just quietly do nothing.

Measured

resources/components/PhoneMessagesButton.stx, drawn by two native screens, with three probes added to its template:

<Text :text="'hello'" />                          → "hello"
<Text :text="String(session.signedIn())" />       → "true"     (the screen's scope: fine)
<Text :text="String(typeof pmbUnread)" />         → "undefined" (the component's own script)

where the component holds:

<script client>
  const pmbUnread = derived(() => Number(session.profile()?.messagesUnread) || 0)
  const pmbLabel = derived(() => nav[session.locale()].phone.messages)
</script>

So the template is compiled and the screen's scope reaches it, but the component's script never runs. <script client lang="ts"> behaves the same.

Why it is worth a diagnostic

Both of this component's bindings were dead for weeks without anyone noticing, because the two failure modes are invisible:

  • :if="pmbUnread > 0" with pmbUnread undefined is simply false, so the element is never drawn. Nothing is logged; the screen looks fine, one feature is just missing.
  • :accessibilityLabel="pmbLabel" becomes no label at all, which only a screen-reader user finds.

Meanwhile @click="session.open('/m/messages')" kept working, because event handlers run in the screen's scope — so the component looked alive.

An "identifier not in scope" diagnostic from stx native compile would have caught both at build time; the compiler already reports unmapped and unread-event diagnostics, which is the same shape of check.

If dropping component scripts is deliberate for the native renderer, the diagnostic is still the fix: it should be an error to reference a name the compiled output cannot resolve, rather than a binding that evaluates to undefined.

Worked around by moving the component's state into the screens it is drawn in, so the template reads only the screen's scope. Measured on stx 0.2.530, Craft 0.0.187, Bun 1.4.3-canary.1.

8 days ago

Fixed on main by fdc1b2e and ac0806a. Native compilation now retains generated component client setups, indexes component-local scope roots, and runs each component setup against its own native instance. The regression test proves two instances keep independent local signal state and only the pressed instance updates. Verified with the native suite: 360 passed, 0 failed; full suite: 16,014 passed, 0 failed; CI run 38897877529 is green. This landed after stx v0.2.544 and will ship in the next patch release.

Sign in to comment on this issue.