diff --git a/.changeset/http-transport-cli-redirects.md b/.changeset/http-transport-cli-redirects.md index 2bf25b6..61d53a7 100644 --- a/.changeset/http-transport-cli-redirects.md +++ b/.changeset/http-transport-cli-redirects.md @@ -19,7 +19,7 @@ Prerender anything over HTTP, from the command line, with redirects the host can **Query-string pages.** `keepQuery: true` renders `/posts?page=2` apart from `/posts` (parameters sorted for dedupe), following its links and capturing its data, but writes it only when its seed entry names a `filename` — a static host serves a path the same for every query. Off by default; CLI `--keep-query`. -**Router seeding, from the server.** New `prerender-crawler/routers` (no Node imports — for application server code): `tanstackRouterPages(router)` and `solidRouterPages(router | routes, { base? })` list a router's static pages from the instance the app built for the request (leaves and indexes, no params or splats), and `announcePages(request, headers, paths)` puts them on the response's hint header when the request is the crawler's. `@solidjs/prerender` gains `announceRoutes(Router)` — one line in the app root, reading the ambient request event; a no-op in the browser. The Vite plugin, the CLI against a module, and the CLI against a running server all seed from the same header. +**Router seeding, from the server.** New `prerender-crawler/routers` (no Node imports — for application server code): `tanstackRouterPages(router)` and `solidRouterPages(router | routes, { base? })` list a router's static pages from the instance the app built for the request (leaves and indexes, no params or splats), and `announcePages(request, headers, paths)` puts them on the response's hint header when the request is the crawler's. `@solidjs/prerender` gains `announceRoutes(router)` — one line in the app root (Solid Router) or the request setup (TanStack Router); it takes either router by shape, reads the ambient request event, and is a no-op in the browser. The Vite plugin, the CLI against a module, and the CLI against a running server all seed from the same header. **Removed:** `fileRoutePages`, `staticRoutePaths`, and the Vite plugin's `fileRoutes` option — build-time seeding that walked a `filesystem-routing` directory and re-derived its path rules. The server's own router is the source of truth for which pages exist, and the header works for crawls that never see the project's disk (the CLI against a running server). `filesystem-routing` is no longer a peer dependency. diff --git a/packages/solid/README.md b/packages/solid/README.md index b9c5dc7..44efb41 100644 --- a/packages/solid/README.md +++ b/packages/solid/README.md @@ -61,6 +61,14 @@ export default function App() { On the server, when the request is the crawler's, the router's static pages go on the response's hint header and the crawl seeds every one of them. A visitor's response is untouched; in the browser it is a no-op. Dynamic routes (`/posts/:id`) are not announced — only a render knows their values; the crawl finds them by their links. Options: `header` (a custom crawl `hintHeader`), `base`. +It takes either router Solid apps use. With TanStack Router the instance is built per request, so the call goes where the router is — the `start.setup` hook: + +```ts +// src/setup.tsx +const router = createAppRouter(queryClient, history); +announceRoutes(router); +``` + ## `serverFunctions(options?)` The integration has two jobs. diff --git a/packages/solid/src/client.ts b/packages/solid/src/client.ts index 7349510..5aa5b04 100644 --- a/packages/solid/src/client.ts +++ b/packages/solid/src/client.ts @@ -26,8 +26,14 @@ import type { AnnounceRoutesOptions, PrerenderedFunction } from "./shared.ts"; export { staticArtifactPath, staticCallKey } from "./shared.ts"; export type { AnnounceRoutesOptions, PrerenderedFunction } from "./shared.ts"; +/** A router `announceRoutes` can read — see the server half. */ +export type AnnounceableRouter = unknown; + /** The client half of `announceRoutes`: there is no request here. Always false. */ -export function announceRoutes(_router: unknown, _options?: AnnounceRoutesOptions): boolean { +export function announceRoutes( + _router: AnnounceableRouter, + _options?: AnnounceRoutesOptions +): boolean { return false; } diff --git a/packages/solid/src/server.ts b/packages/solid/src/server.ts index 498e900..a5344af 100644 --- a/packages/solid/src/server.ts +++ b/packages/solid/src/server.ts @@ -14,8 +14,12 @@ import { } from "@solidjs/web/server-functions/server"; import { getRequestEvent } from "@solidjs/web"; import type { RequestEvent, ResponseStub } from "@solidjs/web"; -import { announcePages, solidRouterPages } from "prerender-crawler/routers"; -import type { SolidRouteLike, SolidRouterLike } from "prerender-crawler/routers"; +import { announcePages, solidRouterPages, tanstackRouterPages } from "prerender-crawler/routers"; +import type { + SolidRouteLike, + SolidRouterLike, + TanStackRouterLike +} from "prerender-crawler/routers"; import { CAPTURE_SINK, PRERENDERED_META_KEY } from "./shared.ts"; import type { AnnounceRoutesOptions, CaptureSink, PrerenderedFunction } from "./shared.ts"; @@ -23,13 +27,21 @@ export { staticArtifactPath, staticCallKey } from "./shared.ts"; export type { CaptureSink, PrerenderedFunction } from "./shared.ts"; export type { AnnounceRoutesOptions } from "./shared.ts"; +/** A router `announceRoutes` can read: Solid Router (instance or tree) or a TanStack Router instance. */ +export type AnnounceableRouter = + SolidRouterLike | SolidRouteLike | readonly SolidRouteLike[] | TanStackRouterLike; + /** * Tells a prerender crawl which pages this app's router has — the server - * half. Called during a server render (the app root is the natural place), - * it reads the ambient request: when the request is the crawler's, the - * router's static pages go on the response's hint header and the crawl - * seeds every one of them, linked or not. A visitor's request is untouched; - * on the client this is a no-op. Returns whether it announced. + * half. Called during a server render or request setup, it reads the + * ambient request: when the request is the crawler's, the router's static + * pages go on the response's hint header and the crawl seeds every one of + * them, linked or not. A visitor's request is untouched; on the client + * this is a no-op. Returns whether it announced. + * + * Takes either router Solid apps use — a Solid Router `createRouter` + * instance (or its route-definition tree, with `base`) or a TanStack + * Router instance — and tells them apart by shape. * * ```tsx * import { announceRoutes } from "@solidjs/prerender"; @@ -41,20 +53,23 @@ export type { AnnounceRoutesOptions } from "./shared.ts"; * } * ``` * - * Dynamic routes (`/posts/:id`) are not announced — only a render knows - * their values; the crawl finds them by their links. + * Dynamic routes (`/posts/:id`, `/posts/$id`) are not announced — only a + * render knows their values; the crawl finds them by their links. */ -export function announceRoutes( - router: SolidRouterLike | SolidRouteLike | readonly SolidRouteLike[], - options: AnnounceRoutesOptions = {} -): boolean { +export function announceRoutes(router: AnnounceableRouter, options: AnnounceRoutesOptions = {}) { const event = getRequestEvent() as (RequestEvent & { response?: ResponseStub }) | undefined; if (!event?.response || event.response.committed) return false; if (!event.request.headers.has(options.header ?? "x-prerender")) return false; - const pages = solidRouterPages(router, { base: options.base }); + const pages = isTanStackRouter(router) + ? tanstackRouterPages(router) + : solidRouterPages(router, { base: options.base }); return announcePages(event.request, event.response.headers, pages, { header: options.header }); } +// a TanStack instance carries its path index; nothing of Solid Router's does +const isTanStackRouter = (router: AnnounceableRouter): router is TanStackRouterLike => + typeof router === "object" && router !== null && "routesByPath" in router; + const SERVER_FUNCTION_METADATA = Symbol.for("solid.ServerFunctionMetadata"); /** diff --git a/packages/solid/test/announce.test.ts b/packages/solid/test/announce.test.ts index 313f973..994a480 100644 --- a/packages/solid/test/announce.test.ts +++ b/packages/solid/test/announce.test.ts @@ -50,6 +50,21 @@ describe("announceRoutes", () => { }); }); + it("reads a TanStack Router instance by shape", () => { + const tanstack = { + routesByPath: { + "/": { fullPath: "/" }, + "/users": { fullPath: "/users/", children: [{}] }, + "/users/$id": { fullPath: "/users/$id" } + } + }; + const request = new Request("http://localhost/", { headers: { "x-prerender": "1" } }); + withEvent(request, event => { + expect(announceRoutes(tanstack)).toBe(true); + expect(event.response.headers.get("x-prerender")!.split(",").sort()).toEqual(["/", "/users"]); + }); + }); + it("is a no-op on the client", () => { expect(announceClient(Router)).toBe(false); });