feat: framework entry factories collapse page handler and API route setup to one-liners#135
Conversation
Ship createNextPage/createReactRouterPage/createTanStackPageOptions and toNextRouteHandlers/toReactRouterHandlers/toTanStackHandlers in the per-framework entry points from #127, so each catch-all page and API route file collapses to a few lines. Factories own SSR prefetch, dehydration (including failed queries), loader-before-meta ordering, and 404 handling; hand-written glue keeps working. Updates all three codegen templates and rewrites the two installation doc steps with the manual versions kept in advanced accordions. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1c564b7. Configure here.
There was a problem hiding this comment.
Security Review
This PR extracts boilerplate from three framework templates into reusable library factories (createNextPage, createReactRouterPage, createTanStackPageOptions, plus handler wrappers). The structural change is sound, but moving the code from user-editable scaffolding into sealed library code hardens two pre-existing weaknesses into non-overridable behaviour.
MEDIUM — Hardcoded ErrorBoundary exposes raw error text to end users
File: packages/stack/src/react-router/page.tsx, line 101
The old scaffolded template produced editable user code. Developers could (and were expected to) replace <pre>{String(error)}</pre> with appropriate production error handling. That option is now gone: createReactRouterPage returns a fixed ErrorBoundary that renders String(error) directly in the DOM, with no override hook in CreateReactRouterPageOptions.
If a route loader (running on the server) throws an error carrying sensitive context — a database error message with a connection string fragment, a downstream HTTP 401 body, an internal file path, or a structured error object serialized via String() — that string is rendered verbatim inside a <pre> tag and delivered to the browser. React JSX escapes the HTML entities, so this is not XSS, but it is an uncontrolled information-disclosure surface visible to any unauthenticated visitor who triggers an error.
Remediation: Add an ErrorBoundary?: ComponentType<{ error: unknown }> (or equivalent) option to CreateReactRouterPageOptions, and use it if supplied. The built-in fallback can remain, but consumers must have a way to replace it in production.
LOW — stackDehydrateOptions serialises error objects into the SSR HTML payload
File: packages/stack/src/shared/entry-factories.ts, lines 50–54
The previous scaffold templates called dehydrate(queryClient) with the default options, which excludes failed queries. stackDehydrateOptions deliberately includes them:
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'error',A dehydrated error query carries the raw Error object (message, and in some serialisers the stack trace). The React Query dehydrate serialiser calls JSON.stringify on the state, which strips non-enumerable properties from Error but does preserve message. On Node.js runtimes, error messages from database drivers, ORMs, or downstream services routinely include table names, SQL snippets, or internal service URLs. That string is then embedded in a <script> tag in the HTML response and sent to every browser that loads the page.
This behaviour is new and cannot be opted out of without abandoning the factory entirely.
Remediation (short-term): Accept an optional dehydrateOptions?: DehydrateOptions in the factory options so callers can override or disable error-state dehydration. Document the information-disclosure trade-off.
Remediation (longer-term): Sanitise the error before dehydrating — e.g. replace the raw error with a safe string such as new Error('Server error') if query.state.status === 'error' — so the deduplication benefit is retained without leaking the original error message.
No finding — normalizePath and .. segments
normalizePath does not strip .. segments, but the resulting path is used only as a key to router.getRoute() (a client-side pattern-matching lookup), not as a file system path. A ..-carrying string would simply produce no route match and fall through to the 404 path. No exploit surface identified.
No finding — Link wrapper href spread
All three *LinkWrapper components spread href to their underlying framework Link (Next.js, React Router, TanStack). Next.js Link sanitises javascript: URIs. The href values in these wrappers originate from plugin route definitions in library code, not from user-supplied input, so unvalidated-href XSS is not a realistic attack path here.
Summary: Two findings, both stemming from the same root cause — code that was previously user-editable scaffolding is now sealed library code without escape hatches. Adding ErrorBoundary and dehydrateOptions override options to the factory APIs would close both gaps without changing the default behaviour for consumers who do not specify them.
Sent by Cursor Automation: Find vulnerabilities
|
|
||
| function ErrorBoundary() { | ||
| const error = useRouteError(); | ||
| return <pre>{String(error)}</pre>; |
There was a problem hiding this comment.
[MEDIUM] Hardcoded raw error display with no override
This ErrorBoundary was previously in user-editable scaffolded code; it is now sealed inside the library. String(error) can expose server-side error details (DB messages, internal paths, downstream API responses) to any browser visitor. There is no way for a consumer to replace this with a safe production error UI without abandoning the factory entirely.
Suggested fix: accept an optional ErrorBoundary?: ComponentType<{ error: unknown }> in CreateReactRouterPageOptions and fall back to this implementation only when the consumer does not provide one.
| * how the consumer configured their QueryClient. | ||
| */ | ||
| export const stackDehydrateOptions: DehydrateOptions = { | ||
| shouldDehydrateQuery: (query) => |
There was a problem hiding this comment.
[LOW] Error-state dehydration leaks server error messages into SSR HTML
Including failed queries in the dehydrated state embeds the raw error object (at minimum its message) in a <script> tag sent to every browser. Database drivers, ORMs, and downstream service clients frequently embed sensitive details in error messages (table names, SQL fragments, internal hostnames). This is new behaviour relative to the old scaffold template, which used the default dehydrate(queryClient) without error-state inclusion.
Suggested fix: accept an optional dehydrateOptions?: DehydrateOptions in the factory options, or sanitise the error before dehydrating (e.g. replace query.state.error with a generic Error('Server error') before serialisation).
| /** Rendered when no route matches. Defaults to a "Route not found" div. */ | ||
| NotFound?: ComponentType; | ||
| /** Wraps the rendered page (inside the `HydrationBoundary`). */ | ||
| wrapPage?: (page: ReactNode) => ReactNode; |
There was a problem hiding this comment.
[MEDIUM – related] CreateReactRouterPageOptions has no ErrorBoundary escape hatch. Since the ErrorBoundary returned by createReactRouterPage is now the only option a consumer gets, and it renders raw error strings in production, an override prop is needed here alongside NotFound and wrapPage.
React Router's build-time route-exports plugin cannot strip destructured
exports from route modules ("Cannot remove destructured export \"loader\""),
so assign loader/action individually from toReactRouterHandlers().
Co-authored-by: Cursor <cursoragent@cursor.com>
- generateMetadata now calls notFound() for unknown paths, matching Page - createReactRouterPage accepts an ErrorBoundary override so consumers can ship a production-safe error UI without abandoning the factory - both page factories accept dehydrateOptions to override the default failed-query dehydration (e.g. to sanitize error payloads) Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the review feedback in 94f5db7:
|



Closes #130
Summary
@btst/stack/next,@btst/stack/react-router,@btst/stack/tanstack):createNextPage({ getStackClient, getQueryClient })→{ Page, generateMetadata };toNextRouteHandlers(handler)→{ GET, POST, PUT, PATCH, DELETE }createReactRouterPage({ getStackClient, getQueryClient })→{ loader, meta, Component, ErrorBoundary };toReactRouterHandlers(handler)→{ loader, action }createTanStackPageOptions({ getStackClient })→ route options to spread intocreateFileRoute("/pages/$")(...);toTanStackHandlers(handler)→server.handlersmaproute.loader(), React Query dehydration including failed queries (so hydrated/errored queries don't refetch on the client), loader-before-meta ordering, and 404 via the framework mechanism. Escape hatches:notFound/NotFound,wrapPage,getQueryClient; hand-written glue remains fully supported (non-breaking, purely additive).router.tsx("use client"preset from Top-level router adapter on StackProvider (framework presets for Link/navigate/refresh/Image) #127, moved verbatim),page.tsx(no directive, socreateNextPagecan return an async server component),handlers.ts, and a plain re-exportindex.tsx. Noexports/typesVersions/build.config.tschanges needed — import paths are unchanged.packages/cli/src/templates/*/pages-route.tsx.hbs,api-route.ts.hbs) plus the tanstack E2E overlay to the factory one-liners.docs/content/docs/installation.mdx; the manual versions are preserved in "Advanced" accordions.packages/stack/src/__tests__/entry-factories.test.tsx) covering handler delegation, loader/meta ordering, failed-query dehydration, and 404 paths.Test plan
pnpm build && pnpm typecheck && pnpm lintpasspnpm test(stack: 247, cli: 100) and knip passMade with Cursor