stx typecheck reports an app's own composables as undefined names. The runtime injects every export from composablesDir as a bare global — the checker only knows the framework's globals from stx.d.ts — so calling one is a hard error even though it works at runtime and in the static build.
Found on 0.2.176.
Reproduction
config/ui.ts points composablesDir at a real directory:
export default {
root: '.',
pagesDir: 'resources/views',
composablesDir: 'resources/functions',
} satisfies UiOptionsresources/functions/session.ts:
export const SESSION_MAX_AGE = 30 * 24 * 60 * 60
export function useSessionToken(expiresIn?: number | string | null) {
const seconds = Number(expiresIn)
return useCookie('auth-token', { maxAge: seconds > 0 ? seconds : SESSION_MAX_AGE })
}Any view:
<script client>
const authToken = useSessionToken()
</script>settings.stx:122:23 error TS2552 Cannot find name 'useSessionToken'.
Did you mean 'useSessionStorage'?It genuinely works everywhere else
Not a config mistake — the same build the checker rejects ships the composable correctly. From dist/settings.html:
function useSessionToken(expiresIn) { … }
try { __c["useSessionToken"] = useSessionToken; } catch (e) {}
…,"SESSION_MAX_AGE","useSessionToken"];
const authToken = useSessionToken();Verified in Chromium too: a probe composable returning a sentinel string rendered that string through a :text binding. So the runtime, the bundler and the SSG all agree the name is in scope; only the typechecker disagrees.
The near-miss suggestion makes it worse in practice — useSessionStorage is a real framework global with a superficially similar name, so the error reads like a typo rather than a missing-scope problem.
Why it matters
composablesDir is the framework's own answer to "where does shared client logic live", and rule 6.3-style guidance actively pushes shared state into it. But adopting it costs you the typechecker for every call site, which is a strong disincentive against the recommended structure. The workaround is to restate each export in a --lib file:
type SessionModule = typeof import('./resources/functions/session')
declare const useSessionToken: SessionModule['useSessionToken']
declare const SESSION_MAX_AGE: SessionModule['SESSION_MAX_AGE']That works — it is what I shipped — but it is a hand-maintained mirror of a directory the framework already enumerates, and it drifts exactly the way stx.d.ts has been drifting from signals.ts (#1932 is the sixth instance).
Suggested
The checker already resolves composablesDir well enough to bundle it; the same list could seed the virtual program's global scope. Either:
- synthesise ambient
declare constentries for each export of each composable module before checking, or - emit that as a real
.d.tsinto the build dir and include it automatically, which would also give editors the same names without per-project boilerplate.
Related: #1932 (stx.d.ts vs signals.ts drift). Both are the same underlying shape — the checker's model of what is in scope is maintained separately from what actually is.