Summary
Three separate client bundles each assign window.stx wholesale rather than merging into it. Whichever runs last silently erases the other two. The result is that 16 of the 82 names in STX_AUTO_IMPORTS are undefined at runtime — the compiler destructures them from window.stx for you, so you get undefined is not a function at the call site with nothing pointing at the cause.
The four assignment sites:
| File | Line | Statement | Merges? |
|---|---|---|---|
src/reactivity.ts | 995 | window.stx = { ref, reactive, computed, watch, … } | ✗ overwrite |
src/state-management.ts | 1221 | window.stx = s; | ✗ overwrite |
src/signals.ts | 5458 | window.stx = { hydrate, state, derived, effect, … } | ✗ overwrite |
src/client-helpers.ts | 14 | window.stx = window.stx || {} | ✓ merge |
Only client-helpers.ts does it correctly — and it loses anyway, because it merges first and is then overwritten by one of the other three.
Proof it is clobbering, not absence
This is the measurement that distinguishes the two. On a rendered page, onBeforeMount is present in an inline script — its bundle loaded and ran — yet it is not on window.stx:
rendered : signals(hydrate)=function reactivity(onBeforeMount)=undefined
statemgmt(createStore)=undefined helpers(formatCurrency)=undefined
js requested : runtime.js, router.js
inline blocks : 9
any inline defining onBeforeMount: true <- the bundle IS on the pagesignals.ts is the winner (hydrate survives). reactivity.ts, state-management.ts and client-helpers.ts are all erased.
Reproduction
Any <script client> block:
<script client>
const r = state('')
r.set(['hydrate', 'onBeforeMount', 'createStore', 'formatCurrency']
.map(n => n + '=' + typeof window.stx?.[n]).join(' '))
</script>
<main><p>{{ r() }}</p></main>→ hydrate=function onBeforeMount=undefined createStore=undefined formatCurrency=undefined
The 16 dead names
Enumerated by parsing STX_AUTO_IMPORTS out of client-script.ts:308 and testing each against window.stx in a live page:
promised by auto-import : 82
NOT provided at runtime : 16
isDerived, useMeta, watchMultiple, onBeforeMount, onBeforeUpdate, onUpdated,
inject, getCurrentInstance, onErrorCaptured, useSlots, useAttrs,
createStore, action, createSelector, h, FragmentThey fail for two different reasons, and the fix differs:
Clobbered — defined in a bundle that loads and is then overwritten:
| Name | Owner |
|---|---|
onBeforeMount, onBeforeUpdate, onUpdated, watchMultiple, getCurrentInstance | reactivity.ts |
createStore, createSelector | state-management.ts |
Never exposed to the client at all — in STX_AUTO_IMPORTS but absent from every client window.stx literal:
| Name | Where it lives |
|---|---|
isDerived | signals.ts imports it from signals-api for server use only |
useMeta | composables/use-meta.ts — server-only module tree |
h | jsx-runtime.ts — not bundled to the client |
inject, onErrorCaptured, useSlots, useAttrs, action, Fragment | no implementation found anywhere |
The sharpest one
provide works and inject does not.
provide=function inject=undefinedprovide is implemented twice (reactivity.ts and signals.ts), so it survives on the winner. inject is implemented nowhere despite being auto-imported. Half of a provide/inject pair is worse than neither — the first call succeeds, so the failure surfaces only in the consumer.
Note provide is also window[name] = value (reactivity.ts:990), i.e. a global write with no scoping, which is a separate concern.
Why this is hard to debug downstream
Because these names are auto-imported, an app author never writes an import for them. onBeforeMount(() => …) reads as a supported API, matches the documented Vue-compat surface, and fails at runtime as undefined is not a function with no indication that the name was destructured off an object that a later bundle replaced.
Suggested fix
- Make every site merge:
window.stx = Object.assign(window.stx || {}, { … }). That alone revives the 7 clobbered names. - Derive
STX_AUTO_IMPORTSfrom what the runtime actually exposes, or add a build-time assertion that every entry resolves. The list is hand-maintained inclient-script.ts:308and has drifted from three separate runtime bundles with nothing checking agreement. - Either implement
inject/useSlots/useAttrs/onErrorCaptured/action/Fragmentor remove them from the auto-import list. Promising a name that cannot resolve is worse than not offering it.
Point 2 is the durable one — 1 and 3 fix today's drift, 2 stops it recurring.
Found while migrating a production stx app off vanilla JS onto the framework's own primitives. Related: #1803, #1797, #1795.