The usesSignals test that decides whether a <script client> block gets wrapped in
stx.mount() is a hand-maintained regex alternation. It has drifted from the runtime
globals list, so a client block built out of the newer reactive composables is classified as
"not using signals", skips mounting, and falls through to the legacy IIFE path with no
reactivity.
This is the same drift class that #1804 just eliminated for STX_AUTO_IMPORTS — by deriving
it from STX_RUNTIME_GLOBALS instead of maintaining a literal. The header comment at
client-script.ts:306-327 documents that decision. This regex, ~1000 lines further down in
the same file, was left hand-maintained.
The code
packages/stx/src/client-script.ts:1332:
const usesSignals = /\b(state|derived|effect|ref|reactive|computed|watch|watchEffect|useLocalStorage|useSessionStorage|useRef|useEventListener|useFetch|useDebounce|useDebouncedValue|useThrottle|useInterval|useTimeout|useToggle|useCounter|useClickOutside|useFocus|useAsync|useColorMode|useDark|useWebSocket|useRoute|useSearchParams|onMount|onDestroy|onMounted|onBeforeUnmount|onUnmounted|batch|untrack|peek)\s*(?:<[^>]*>)?\s*\(/.test(scriptContent)When usesSignals is false the block skips the stx.mount() wrapper
(client-script.ts:1353-1354). The fallback at 1358-1367 only rescues it if a top-level
declaration name happens to also appear in the template.
The gap
36 names in the regex; 71 in STX_RUNTIME_GLOBALS. 35 runtime globals are missing:
defineEmits, defineExpose, definePageMeta, defineProps, defineSlots, defineStore,
goBack, goForward, inject, isDerived, isSignal, navigate, nextTick, onBeforeMount,
provide, registerStoresClient, setRouteParams, useCookie, useHead, useId,
useMediaQuery, useMutation, useOptimistic, usePreferredContrast, usePreferredDark,
usePreferredLight, usePreferredReducedMotion, useQuery, useReactiveProp,
useScrollLock, useSeoMeta, useSlots, useStore, watchMultiple, withDefaultsSome of those are compile-time only (defineProps, defineEmits, defineSlots,
defineExpose, withDefaults, definePageMeta) and are reasonably excluded. But the
reactive ones are not:
useStore, defineStore, useQuery, useMutation, useOptimistic, useCookie,
useMediaQuery, useScrollLock, usePreferredDark, usePreferredLight,
usePreferredContrast, usePreferredReducedMotion, useReactiveProp, watchMultiple
Every one of those returns or drives a signal, and every one of them fails the test.
Repro
A client block that is entirely reasonable and entirely broken:
<script client>
const { data, loading } = useQuery('/api/issues')
</script>
<main>
<p :if="loading()">Loading…</p>
<ul :else>
<li :for="issue in data()">{{ issue.title }}</li>
</ul>
</main>useQuery is not in the alternation, no other listed name appears, so usesSignals is
false, stx.mount() is skipped, and the bindings never hydrate. The page renders the
loading branch forever. Same for a block whose only call is useStore('session').
Adding a single unrelated state(0) makes it work, which is a confusing thing to discover.
Why it is easy to miss
It is latent in any app whose client blocks also call state()/effect() — which is most
of them today, since the newer composables are the ones apps are being encouraged to move
to. It surfaces exactly when someone follows that advice and writes a block that is
purely useQuery/useStore. The failure mode is a page stuck on its skeleton with no
console error, which is a hard thing to trace back to a regex in the compiler.
Fix
Derive it, the way STX_AUTO_IMPORTS now is. Something like a
STX_REACTIVE_GLOBALS subset exported from runtime-globals.ts — the compile-time-only
names are a small, stable, explicitly-listed exclusion — with the regex built from it at
module load. That way adding a composable to the runtime cannot silently fail to mount.
A test asserting that every name in STX_RUNTIME_GLOBALS, minus the declared compile-time
set, is matched by usesSignals would pin it permanently.