ReviewOS

stacks/stx

stx typecheck does not see composablesDir exports — an app's own composables report as undefined names

#1934
Closed glennmichael123 opened this 14 days ago · 1 comment
14 days ago

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 UiOptions

resources/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 const entries for each export of each composable module before checking, or
  • emit that as a real .d.ts into 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.

14 days ago

Follow-up that should make this cheap to fix: the framework already generates exactly the declarations the checker needs — it just does not read them.

Adding a composable regenerates storage/framework/auto-imports/globals.ts:

declare global {
  const toggleDark: typeof autoImports.toggleDark
  const isDark: typeof autoImports.isDark
  const useSessionToken: typeof autoImports.useSessionToken
  const SESSION_MAX_AGE: typeof autoImports.SESSION_MAX_AGE
}

and auto-imports/functions.ts re-exports each one from its source module. So tsc and the editor both resolve these names correctly; only stx typecheck reports them undefined, because its virtual program is seeded from stx.d.ts plus whatever --lib files the caller passes, and that generated file is not among them.

That narrows the fix a lot — the checker could include <root>/storage/framework/auto-imports/globals.ts when it exists, rather than synthesising anything new. The hand-written mirror in my --lib file then becomes unnecessary, which also removes the drift risk I flagged above.

Sign in to comment on this issue.