Skip to content

feat: framework entry factories collapse page handler and API route setup to one-liners#135

Merged
olliethedev merged 3 commits into
mainfrom
feat/130-entry-factories
Jul 4, 2026
Merged

feat: framework entry factories collapse page handler and API route setup to one-liners#135
olliethedev merged 3 commits into
mainfrom
feat/130-entry-factories

Conversation

@olliethedev

Copy link
Copy Markdown
Collaborator

Closes #130

Summary

  • Adds per-framework entry factories to the entry points established by Top-level router adapter on StackProvider (framework presets for Link/navigate/refresh/Image) #127 (@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 into createFileRoute("/pages/$")(...); toTanStackHandlers(handler)server.handlers map
  • Factories own the invariant plumbing once: SSR prefetch via route.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).
  • Each entry dir is split into 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, so createNextPage can return an async server component), handlers.ts, and a plain re-export index.tsx. No exports/typesVersions/build.config.ts changes needed — import paths are unchanged.
  • Updates all three codegen templates (packages/cli/src/templates/*/pages-route.tsx.hbs, api-route.ts.hbs) plus the tanstack E2E overlay to the factory one-liners.
  • Rewrites the "Create API Route" and "Set Up Page Handler" steps in docs/content/docs/installation.mdx; the manual versions are preserved in "Advanced" accordions.
  • Adds unit tests (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 lint pass
  • pnpm test (stack: 247, cli: 100) and knip pass
  • Docs site builds with the rewritten MDX
  • Codegen E2E for all three frameworks (runs in CI on this PR)

Made with Cursor

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>
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview, Comment Jul 4, 2026 1:07am
better-stack-playground Ready Ready Preview, Comment Jul 4, 2026 1:07am

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/stack/src/next/page.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities


function ErrorBoundary() {
const error = useRouteError();
return <pre>{String(error)}</pre>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
@olliethedev

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in 94f5db7:

  • Missing route metadata skips notFound (Bugbot): generateMetadata now calls the configured notFound() for unknown paths, so metadata generation and page rendering agree on 404 handling.
  • Hardcoded raw error display / no ErrorBoundary override: createReactRouterPage now accepts an optional ErrorBoundary component; the <pre>{String(error)}</pre> fallback (same as the previous scaffold template) is only used when none is provided.
  • Error-state dehydration: both page factories now accept dehydrateOptions to override the default failed-query dehydration (e.g. to sanitize error payloads). The default intentionally includes failed queries — that matches the behavior the docs previously asked consumers to configure via shouldDehydrateQuery: () => true on their QueryClient, and prevents client-side refetch/loading flashes on SSR errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Framework entry factories: collapse page handler and API route setup to one-liners

1 participant