A page can emit window.stx.mount(...) while the signals runtime is never injected, so the
client block dies on its first line with TypeError: Cannot read properties of undefined
(reading 'mount') and every handler it defines is lost.
This is live in a real app today, on exactly one route, which is what makes it worth reporting — the gate is narrow enough that most pages accidentally pass it.
Two decisions, one of which is wrong
hasSignalsSyntax(template) (packages/stx/src/signal-processing.ts:203-229) decides
whether the runtime is needed. The client-script pass separately decides to emit
stx.mount(). Nothing reconciles them.
The gate's 24 patterns cover @if/@for/@show/@model/@bind:/@class/@style, x-data, the
core signal APIs, useId, useReactiveProp, and:
/\s:(?:if|else-if|else|for|show|text|html|model)\s*=/, // colon-form reactive directivesSo it does not match:
@click=or any event handler. The@patterns listed are directive forms; event binding is absent entirely.- Any
:attr=outside that fixed list —:data-t=,:value=,:disabled=,:class=,:href=. Arbitrary attribute binding is the common case and none of it counts. - Every reactive composable except two.
useStore,useQuery,useFetch,useMutation,useColorMode,useLocalStorage… onlyuseIdanduseReactivePropare listed, out of 71 runtime globals.
Live repro
A page whose client block uses a store and whose template uses an event handler and one bound attribute — nothing exotic:
<script client>
const themeStore = useStore('theme')
const theme = themeStore.theme
function toggleTheme(): void { themeStore.toggle() }
function logout() { … }
</script>
<html-root :data-t="theme()">
<button @click="toggleTheme">…</button>
<button @click="logout">Log out</button>
</html-root>Served output:
$ curl -s http://localhost:3100/projects | grep -c 'data-stx-runtime'
0
$ curl -s http://localhost:3100/projects | grep -c 'stx\.mount('
1<main>
<script data-stx-scoped data-stx-run="always">
window.stx.mount(function() {Browser console:
TypeError: Cannot read properties of undefined (reading 'mount')
at http://localhost:3100/projects:47:12The theme toggle and the Log out button are both dead — every function in the block is
defined inside the mount callback, so the throw on line 1 takes all of them.
Across the app's eight routes, seven inject the runtime and one does not; the one that does not is the only page whose template contains no directive from the gate's fixed list.
The data-stx-scoped safety net cannot fire
The pattern list ends with:
/data-stx-scoped/, // client scripts need the signals runtimewhich looks like it should catch exactly this case. It cannot: the gate runs at
signal-processing.ts:1565 (if (!hasSignalsSyntax(template)) return template), which is
before the client-script pass emits data-stx-scoped. At gate time the attribute does not
exist in the template.
The page above proves it empirically — its served HTML does contain
<script data-stx-scoped …>, and the runtime was still not injected. So that pattern is
dead code as ordered, and its presence makes the gate look safer than it is.
Suggested direction
Two independent things to fix, and I would do both:
Widen the gate, but not by extending the hand-written list again — that is the same drift #1804 fixed for
STX_AUTO_IMPORTSand #1819 reports for the auto-mount regex. Event handlers and arbitrary:attr=bindings are structural (/\s@[a-z]/,/\s:[a-z][\w-]*\s*=/), and the composable half should derive fromSTX_RUNTIME_GLOBALSrather than naming two of seventy-one.Make the contradiction impossible rather than unlikely. Whatever decides to emit
stx.mount()should be the same thing that decides the runtime is needed — or, failing that, emittingstx.mount()should force runtime injection. A page that mounts without a runtime is never correct, so it should not be representable. Even a guardedif (window.stx) window.stx.mount(…)would degrade to "no reactivity" instead of "every handler on the page is gone", though it would trade a loud failure for a silent one.
A test asserting that any template producing data-stx-scoped also gets the runtime tag
would pin this.