Follow-up to #1819 / #1820. Those fixed two hand-maintained "does this template use signals"
tests by deriving them from STX_RUNTIME_GLOBALS. There is a third copy, in
events.ts, which was not covered — and the path it falls through to cannot resolve a named
handler from a page-level client block, so the handler throws ReferenceError on click.
Net effect after the #1820 fix: a page whose client block uses useStore (or any reactive
API other than state/derived/effect) now correctly gets the signals runtime, and then
has all of its @click handlers stripped and rebound to code that cannot find them. The
symptom moved from TypeError: …reading 'mount' to ReferenceError: toggleTheme is not
defined.
1. The third gate
packages/stx/src/events.ts:598:
// When signals are used, skip event processing - the signals runtime handles events
const usesSignals = /\b(?:state|derived|effect)\s*(?:<[^<>()]*>)?\s*\(/.test(template)
|| /@(?:model|show|for|if)\s*=/.test(template)
|| /data-stx(?:-auto)?(?![-\w])/.test(template)
if (usesSignals) {
return template
}Three names out of 71. When it returns early, @click="toggleTheme" survives into the HTML
and the signals runtime binds it declaratively — which works. When it does not, the template
goes down the stripping path and the attribute is replaced by an imperative binding.
Two pages from the same app, same framework build:
| client block | @ attr in HTML | __stx_evt_ | result | |
|---|---|---|---|---|
/login | state('') | @submit="handleSubmit" survives | 0 | works |
/projects | useStore('theme') only | stripped | 4 | ReferenceError |
The only difference is which reactive API the block happens to call.
2. The path it falls through to cannot resolve the handler
client-script.ts:960-970 (and the parallel block at 1010-1020) emits:
var $el = document.getElementById('__stx_evt_0')
if ($el) $el.addEventListener('click', function($event) {
var scope = $el.closest('[data-stx-scope]');
if (scope && scope.__stx_execute) {
scope.__stx_execute('toggleTheme', $event, $el);
}
else {
toggleTheme // bare identifier
}
})Both branches fail for a page-level <script client>:
[data-stx-scope]is not emitted for it. Grepping the emitters, that attribute is produced only byincludes.ts:372(for@includepartials) and by runtime scope remapping (signals.ts:3065, 3159, 3172). A page client block registers on<body data-stx="__stx_setup_…">instead — a different attribute. Measured on the page above:document.querySelectorAll('[data-stx-scope]').length === 0, while[data-stx]matches 1 (the body).__stx_executeis assigned in exactly one place —reactive.ts:644, on an x-data scope element.document.body.__stx_executeisundefined.- So the
elsebranch runs, and it is a bare identifier.toggleThemeis defined inside thefunction __stx_setup_…()closure the client block compiles to, not as a global, so evaluating the name throws.
ReferenceError: toggleTheme is not defined
at HTMLButtonElement.<anonymous> (http://localhost:3100/projects:203:7)The else branch also just evaluates the identifier rather than calling it — even if the
name resolved, toggleTheme on its own is a no-op expression, not an invocation. For the
@click="doThing()" call form that is fine; for the @click="doThing" reference form it
would silently do nothing.
Repro
<script client>
const themeStore = useStore('theme')
function toggleTheme(): void { themeStore.toggle() }
</script>
<button @click="toggleTheme">Toggle</button>Click the button: ReferenceError: toggleTheme is not defined.
Change the block's first line to const x = state(0) and it works — because that trips the
regex in (1) and the event is never stripped.
Suggested direction
For (1): derive it from STX_RUNTIME_GLOBALS like #1819 and #1820 now do. This is the third
instance of the same drift, which suggests the shared helper those two fixes introduced
should simply be reused here rather than a fourth list maintained.
For (2): the imperative fallback is worth a second look regardless of (1), since it is
reachable whenever a template genuinely has no signals. Either it should look for the
attribute that page-level client scopes actually carry ([data-stx]) and call through
window.__stx_latestSetup, or — probably better — the setup closure should expose its
named handlers on the scope element the same way reactive.ts does, so both binding
systems resolve handlers through one mechanism instead of two that disagree.
A test that clicks a @click="namedHandler" button on a page whose client block uses only
useStore would cover both halves. The existing tests presumably all use state(), which
is exactly why this survived.