Summary
stx has a Laravel-style view-composer layer, and it is wired into the render path: runComposers(filePath, context) is called for every template. But the two functions that register a composer — composer() and composerPattern() — are not exported from the package entry. packages/stx/src/index.ts has no export * from './view-composers', and neither name appears in dist/index.js or dist/index.d.ts. There is no way for an application to put anything into the registry, so runComposers iterates an empty map on every render.
The practical effect is that stx has no usable loader/controller layer, and data access has nowhere to live except inside the view.
Impact (real app)
analyticshq's dashboard is the whole data layer inside a template file:
resources/views/dashboard.stx:220-504—loadData(): a dynamicimport('@stacksjs/database'), a hand-written?→$nPostgres placeholder rewriter (:228), server-side auth from a cookie (:238-245), and ~25 raw SQL queries.resources/views/dashboard.stx:545-549— the timeout, raced by hand, because there is no framework-owned boundary:
withawait Promise.race([ loadData(), new Promise((_, reject) => setTimeout(() => reject(new Error('db timeout')), 5000)), ])catch { live = false }at:550-553as the only error boundary. One slow query out of 25 drops the entire dashboard to an empty state, because there is no per-section streaming boundary.resources/views/dashboard.stx:505-544— a 34-line injection protocol we invented for a host that does not exist:// A host server may pre-fetch the data (e.g. when rendering in a context that // can't reach the DB module) and inject it as `preloaded`; otherwise the view // queries PostgreSQL itself. if (typeof preloaded !== 'undefined' && preloaded) { kpis = preloaded.kpis ?? kpis series = preloaded.series ?? [] … // repeated for 30 fields }grep -rn 'preloaded' resources/ app/ routes/ scripts/finds it only insidedashboard.stx. There is no producer, because there is no supported way to write one.
And none of the 500 lines is checked — types/dashboard.d.ts:9-13:
tsc cannot see inside .stx, so these annotate the page for a human and an editor and are verified by nothing.
composer('dashboard', ctx => { ctx.preloaded = … }) is exactly the missing producer. It cannot be written.
Evidence in stx
The layer exists — packages/stx/src/view-composers.ts:
export function composer(view: string, callback: ViewComposerCallback): void // :126
export function composerPattern(pattern: string | RegExp, callback: ViewComposerCallback): void // :160
export async function runComposers(filePath: string, context: Record<string, any>): Promise<void> // :181It runs — packages/stx/src/process.ts:50, :1204-1212, :722:
import { runComposers } from './view-composers' // :50
…
async function runViewComposersForTemplate(filePath, context) { // :1204
await safeExecuteAsync(() => runComposers(filePath, context), …) // :1206
}
…
await runViewComposersForTemplate(filePath, context) // :722It cannot be registered:
$ grep -n "view-composers" packages/stx/src/index.ts
(no match)
$ grep -rn "view-composers" packages/stx/src/*.ts
packages/stx/src/process.ts:50:import { runComposers } from './view-composers'
$ grep -o "composer[A-Za-z]*" packages/stx/dist/index.js | sort -u
(empty)
$ grep -o "composer[A-Za-z]*" packages/stx/dist/index.d.ts | sort -u
(empty)So runComposers executes on every template render against a registry nothing can populate. This is the same shape as #1823 (stx-plugin registers only ./src/ui, so four components were exported but unrenderable) — capability present, entry point missing.
Prior art
- Laravel:
View::composer('dashboard', DashboardComposer::class)— which is precisely whatview-composers.tsmodels. - Remix:
loader()returning typed data to the route component. - Next.js:
getServerSideProps/ RSC. - Nuxt:
defineEventHandler+useAsyncDatawith<Suspense>boundaries. - Rails: controller actions.
Ask
- Add
export * from './view-composers'topackages/stx/src/index.tssocomposer/composerPattern/clearComposersship. This alone makes the loader layer usable. - Document the composer as the supported place for route data, with an example that moves a
<script server>data block into one. - Give the composer a typed contract — let it declare the shape it contributes to the context so a view has something checkable, since tsc cannot see inside
.stx. - Add a framework-owned timeout/fallback per composer, and a per-section streaming boundary, so one slow query degrades one panel instead of the whole page (
<Suspense>already exists in the runtime —signals.ts:1199-1206).
Related: #1823 (exported-but-unregistered components), #1746 (streaming SSR + islands).