@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,8 @@ argument-hint: [hook-name]
6
6
 
7
7
  # Client-Side React Hooks
8
8
 
9
- All hooks are imported from `@rangojs/router` or `@rangojs/router/client`.
9
+ Import the hooks and components in this skill from `@rangojs/router/client`.
10
+ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
10
11
 
11
12
  ## Navigation Hooks
12
13
 
@@ -63,7 +64,7 @@ Access current URL path and matched route segments:
63
64
 
64
65
  ```tsx
65
66
  "use client";
66
- import { useSegments } from "@rangojs/router";
67
+ import { useSegments } from "@rangojs/router/client";
67
68
 
68
69
  function Breadcrumbs() {
69
70
  const { path, segmentIds, location } = useSegments();
@@ -107,7 +108,7 @@ Access loader data (strict - data guaranteed):
107
108
 
108
109
  ```tsx
109
110
  "use client";
110
- import { useLoader } from "@rangojs/router";
111
+ import { useLoader } from "@rangojs/router/client";
111
112
  import { ProductLoader } from "../loaders/product";
112
113
 
113
114
  function ProductPrice() {
@@ -143,7 +144,7 @@ Access loader with on-demand fetching (flexible):
143
144
 
144
145
  ```tsx
145
146
  "use client";
146
- import { useFetchLoader } from "@rangojs/router";
147
+ import { useFetchLoader } from "@rangojs/router/client";
147
148
  import { SearchLoader } from "../loaders/search";
148
149
 
149
150
  function SearchResults() {
@@ -197,7 +198,7 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
197
198
 
198
199
  ```tsx
199
200
  "use client";
200
- import { useFetchLoader } from "@rangojs/router";
201
+ import { useFetchLoader } from "@rangojs/router/client";
201
202
  import { FileUploadLoader } from "../loaders/upload";
202
203
 
203
204
  function FileUploader() {
@@ -244,7 +245,7 @@ Get all loader data in current context:
244
245
 
245
246
  ```tsx
246
247
  "use client";
247
- import { useLoaderData } from "@rangojs/router";
248
+ import { useLoaderData } from "@rangojs/router/client";
248
249
 
249
250
  function DebugPanel() {
250
251
  const allData = useLoaderData();
@@ -262,7 +263,7 @@ Access accumulated handle data from route segments:
262
263
 
263
264
  ```tsx
264
265
  "use client";
265
- import { useHandle } from "@rangojs/router";
266
+ import { useHandle } from "@rangojs/router/client";
266
267
  import { Breadcrumbs } from "../handles/breadcrumbs";
267
268
 
268
269
  function BreadcrumbNav() {
@@ -324,7 +325,7 @@ Track state of server action invocations:
324
325
 
325
326
  ```tsx
326
327
  "use client";
327
- import { useAction } from "@rangojs/router";
328
+ import { useAction } from "@rangojs/router/client";
328
329
  import { addToCart } from "../actions/cart";
329
330
 
330
331
  function AddToCartButton({ productId }: { productId: string }) {
@@ -359,7 +360,7 @@ Read type-safe state from history:
359
360
 
360
361
  ```tsx
361
362
  "use client";
362
- import { useLocationState, createLocationState } from "@rangojs/router";
363
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
363
364
 
364
365
  // Define typed state (all export patterns supported)
365
366
  // Keys are auto-injected by the Vite plugin -- no manual key needed.
@@ -509,7 +510,7 @@ Manually control client-side navigation cache:
509
510
 
510
511
  ```tsx
511
512
  "use client";
512
- import { useClientCache } from "@rangojs/router";
513
+ import { useClientCache } from "@rangojs/router/client";
513
514
 
514
515
  function SaveButton() {
515
516
  const { clear } = useClientCache();
@@ -537,7 +538,7 @@ function SaveButton() {
537
538
  Render child content in layouts:
538
539
 
539
540
  ```tsx
540
- import { Outlet, ParallelOutlet } from "@rangojs/router";
541
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
541
542
 
542
543
  function DashboardLayout({ children }: { children?: React.ReactNode }) {
543
544
  return (
@@ -558,7 +559,7 @@ Access outlet content programmatically:
558
559
 
559
560
  ```tsx
560
561
  "use client";
561
- import { useOutlet } from "@rangojs/router";
562
+ import { useOutlet } from "@rangojs/router/client";
562
563
 
563
564
  function ConditionalLayout() {
564
565
  const outlet = useOutlet();
@@ -0,0 +1,218 @@
1
+ ---
2
+ name: host-router
3
+ description: Multi-app host routing with domain/subdomain patterns
4
+ argument-hint:
5
+ ---
6
+
7
+ # Host Router
8
+
9
+ Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
10
+
11
+ ## Import
12
+
13
+ ```typescript
14
+ import { createHostRouter, defineHosts } from "@rangojs/router/host";
15
+ ```
16
+
17
+ ## Basic Setup
18
+
19
+ ```typescript
20
+ // host-router.ts
21
+ import { createHostRouter } from "@rangojs/router/host";
22
+
23
+ const router = createHostRouter();
24
+
25
+ router.host(["."]).map(() => import("./apps/main"));
26
+ router.host(["admin.*"]).map(() => import("./apps/admin"));
27
+ router.host(["api.*"]).map(() => import("./apps/api"));
28
+
29
+ export default {
30
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
31
+ return router.match(request, { env, ctx });
32
+ },
33
+ };
34
+ ```
35
+
36
+ Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
37
+
38
+ ## Pattern Syntax
39
+
40
+ | Pattern | Matches |
41
+ | ----------------- | ---------------------------------------------- |
42
+ | `.` or `*` | Any apex domain (`example.com`) |
43
+ | `**` | Any domain (apex + all subdomains) |
44
+ | `*.` | Any single-level subdomain (`www.example.com`) |
45
+ | `**. ` | Any multi-level subdomain (`a.b.example.com`) |
46
+ | `example.com` | Exact domain |
47
+ | `*.com` | Any apex `.com` domain |
48
+ | `*.example.com` | Single subdomain of `example.com` |
49
+ | `**.example.com` | Any depth subdomain of `example.com` |
50
+ | `admin.*` | `admin` subdomain of any apex domain |
51
+ | `admin.**` | `admin` subdomain of any domain |
52
+ | `admin.` | `admin` subdomain of any apex (no wildcard) |
53
+ | `example.com/api` | Domain + path prefix (prefix match) |
54
+
55
+ Patterns are tested in registration order. First match wins.
56
+
57
+ ## `defineHosts` for Type Safety
58
+
59
+ ```typescript
60
+ import { defineHosts } from "@rangojs/router/host";
61
+
62
+ const hosts = defineHosts({
63
+ admin: "admin.*",
64
+ api: "api.*",
65
+ app: [".", "www.*"],
66
+ });
67
+
68
+ router.host(hosts.admin).map(() => import("./apps/admin"));
69
+ router.host(hosts.app).map(() => import("./apps/main"));
70
+ ```
71
+
72
+ Returns a frozen object — keys are autocompleted by TypeScript.
73
+
74
+ ## Middleware
75
+
76
+ Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
77
+
78
+ ```typescript
79
+ const router = createHostRouter();
80
+
81
+ // Global — runs for all routes
82
+ router.use(async (request, input, next) => {
83
+ console.log(`[${new Date().toISOString()}] ${request.url}`);
84
+ return next();
85
+ });
86
+
87
+ // Per-route
88
+ router
89
+ .host(["admin.*"])
90
+ .use(requireAuth)
91
+ .map(() => import("./apps/admin"));
92
+ ```
93
+
94
+ Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
95
+
96
+ Calling `next()` more than once throws.
97
+
98
+ ## Fallback Handler
99
+
100
+ Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
101
+
102
+ ```typescript
103
+ const router = createHostRouter({
104
+ hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
105
+ });
106
+
107
+ // Called when cookie override fails (not for general unmatched hosts)
108
+ router.fallback().map((request) => {
109
+ return new Response("Invalid host override", { status: 400 });
110
+ });
111
+ ```
112
+
113
+ For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
114
+
115
+ ```typescript
116
+ import { NoRouteMatchError } from "@rangojs/router/host";
117
+
118
+ export default {
119
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
120
+ try {
121
+ return await router.match(request, { env, ctx });
122
+ } catch (err) {
123
+ if (err instanceof NoRouteMatchError) {
124
+ return new Response("Not Found", { status: 404 });
125
+ }
126
+ throw err;
127
+ }
128
+ },
129
+ };
130
+ ```
131
+
132
+ ## Cookie-Based Host Override
133
+
134
+ For development: route requests to a different app based on a cookie value, allowing developers to test different host routes from a single domain.
135
+
136
+ ```typescript
137
+ const router = createHostRouter({
138
+ hostOverride: {
139
+ cookieName: "x-dev-host",
140
+ allowedHosts: ["localhost", "**.dev.example.com"],
141
+ validate: (request, cookieValue, input) => {
142
+ // Optional custom validation — return the effective hostname
143
+ return cookieValue;
144
+ },
145
+ },
146
+ });
147
+ ```
148
+
149
+ When a request arrives:
150
+
151
+ 1. If no cookie → use actual hostname
152
+ 2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
153
+ 3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
154
+
155
+ Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
156
+
157
+ ## Debug Mode
158
+
159
+ ```typescript
160
+ const router = createHostRouter({ debug: true });
161
+ ```
162
+
163
+ Logs pattern matching, route registration, and cookie override decisions to console.
164
+
165
+ ## Testing
166
+
167
+ ```typescript
168
+ import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
169
+
170
+ // Test pattern matching
171
+ testPattern("admin.*", "admin.example.com"); // true
172
+ testPattern([".", "www.*"], "example.com"); // true
173
+
174
+ // Create requests for integration tests
175
+ const request = createTestRequest({
176
+ host: "admin.example.com",
177
+ path: "/dashboard",
178
+ cookies: { "x-dev-host": "api.example.com" },
179
+ });
180
+
181
+ // Test which route would match (without executing)
182
+ router.test("admin.example.com"); // { pattern, handler } | null
183
+ ```
184
+
185
+ ## Error Types
186
+
187
+ All errors extend `HostRouterError`:
188
+
189
+ | Error | When |
190
+ | ----------------------------- | ------------------------------------------- |
191
+ | `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
192
+ | `HostOverrideNotAllowedError` | Cookie override from disallowed host |
193
+ | `InvalidHostnameError` | Cookie value isn't a valid hostname |
194
+ | `HostValidationError` | Custom `validate` function threw |
195
+ | `NoRouteMatchError` | No host pattern matched the request |
196
+ | `InvalidHandlerError` | Handler is not a function |
197
+
198
+ See the fallback section above for a `NoRouteMatchError` catch example.
199
+
200
+ ## Nesting Host Routers
201
+
202
+ A lazy handler can resolve to another `HostRouter`:
203
+
204
+ ```typescript
205
+ // apps/regional.ts
206
+ import { createHostRouter } from "@rangojs/router/host";
207
+
208
+ const regional = createHostRouter();
209
+ regional.host(["us.*"]).map(() => import("./regions/us"));
210
+ regional.host(["eu.*"]).map(() => import("./regions/eu"));
211
+
212
+ export default regional;
213
+ ```
214
+
215
+ ```typescript
216
+ // host-router.ts
217
+ router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
218
+ ```
@@ -68,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
68
68
  ### In Server Components
69
69
 
70
70
  ```typescript
71
- import { useLoader } from "@rangojs/router";
71
+ import { useLoader } from "@rangojs/router/client";
72
72
  import { ProductLoader } from "./loaders/product";
73
73
 
74
74
  async function ProductPage() {
@@ -539,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
539
539
  ]);
540
540
 
541
541
  // pages/product.tsx
542
- import { useLoader } from "@rangojs/router";
542
+ import { useLoader } from "@rangojs/router/client";
543
543
  import { ProductLoader, CartLoader } from "./loaders/shop";
544
544
 
545
545
  async function ProductPage() {
@@ -521,10 +521,10 @@ export const guidesPatterns = urls(({ path }) => [
521
521
  ]);
522
522
 
523
523
  // urls.tsx
524
- import { urls, include } from "@rangojs/router";
524
+ import { urls } from "@rangojs/router";
525
525
  import { guidesPatterns } from "./pages/guides.js";
526
526
 
527
- export const urlpatterns = urls(({ path }) => [
527
+ export const urlpatterns = urls(({ path, include }) => [
528
528
  path("/", HomePage, { name: "home" }),
529
529
  include("/guides", guidesPatterns, { name: "guides" }),
530
530
  ]);
@@ -32,7 +32,6 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
32
32
  | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
33
33
  | `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
34
34
  | `/fonts` | Load web fonts with preload hints |
35
- | `/testing` | Unit test route trees with `buildRouteTree()` |
36
35
 
37
36
  ## Quick Start
38
37
 
@@ -297,10 +297,10 @@ export const shopPatterns = urls(({ path, layout }) => [
297
297
  ]);
298
298
 
299
299
  // src/urls.tsx
300
- import { urls, include } from "@rangojs/router";
300
+ import { urls } from "@rangojs/router";
301
301
  import { shopPatterns } from "./urls/shop";
302
302
 
303
- export const urlpatterns = urls(({ path }) => [
303
+ export const urlpatterns = urls(({ path, include }) => [
304
304
  path("/", HomePage, { name: "home" }),
305
305
  include("/shop", shopPatterns, { name: "shop" }),
306
306
  ]);
@@ -334,7 +334,7 @@ export const ProductLoader = createLoader(async (ctx) => {
334
334
  });
335
335
 
336
336
  // In server component - type is inferred
337
- import { useLoader } from "@rangojs/router";
337
+ import { useLoader } from "@rangojs/router/client";
338
338
 
339
339
  async function ProductPage() {
340
340
  const product = await useLoader(ProductLoader);
package/src/host/index.ts CHANGED
@@ -25,9 +25,6 @@
25
25
  // Core router
26
26
  export { createHostRouter } from "./router.js";
27
27
 
28
- // Host router registry for build-time discovery
29
- export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
30
-
31
28
  // Utilities
32
29
  export { defineHosts } from "./utils.js";
33
30
 
package/src/index.ts CHANGED
@@ -115,25 +115,32 @@ export type {
115
115
  // Middleware context types
116
116
  export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
117
117
 
118
+ function serverOnlyStubError(name: string): Error {
119
+ return new Error(
120
+ `${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
121
+ `For client hooks and components, import from "@rangojs/router/client".`,
122
+ );
123
+ }
124
+
118
125
  /**
119
126
  * Error-throwing stub for server-only `urls` function.
120
127
  */
121
128
  export function urls(): never {
122
- throw new Error("urls() is server-only and requires RSC context.");
129
+ throw serverOnlyStubError("urls");
123
130
  }
124
131
 
125
132
  /**
126
133
  * Error-throwing stub for server-only `createRouter` function.
127
134
  */
128
135
  export function createRouter(): never {
129
- throw new Error("createRouter() is server-only and requires RSC context.");
136
+ throw serverOnlyStubError("createRouter");
130
137
  }
131
138
 
132
139
  /**
133
140
  * Error-throwing stub for server-only `redirect` function.
134
141
  */
135
142
  export function redirect(): never {
136
- throw new Error("redirect() is server-only and requires RSC context.");
143
+ throw serverOnlyStubError("redirect");
137
144
  }
138
145
 
139
146
  // Handle API (universal - works on both server and client)
@@ -149,102 +156,94 @@ export { nonce } from "./rsc/nonce.js";
149
156
  * Error-throwing stub for server-only `Prerender` function.
150
157
  */
151
158
  export function Prerender(): never {
152
- throw new Error("Prerender() is server-only and requires RSC context.");
159
+ throw serverOnlyStubError("Prerender");
153
160
  }
154
161
 
155
162
  /**
156
163
  * Error-throwing stub for server-only `Static` function.
157
164
  */
158
165
  export function Static(): never {
159
- throw new Error("Static() is server-only and requires RSC context.");
166
+ throw serverOnlyStubError("Static");
160
167
  }
161
168
 
162
169
  /**
163
170
  * Error-throwing stub for server-only `getRequestContext` function.
164
171
  */
165
172
  export function getRequestContext(): never {
166
- throw new Error(
167
- "getRequestContext() is server-only and requires RSC context.",
168
- );
173
+ throw serverOnlyStubError("getRequestContext");
169
174
  }
170
175
 
171
176
  /**
172
177
  * Error-throwing stub for server-only `cookies` function.
173
178
  */
174
179
  export function cookies(): never {
175
- throw new Error("cookies() is server-only and requires RSC context.");
180
+ throw serverOnlyStubError("cookies");
176
181
  }
177
182
 
178
183
  /**
179
184
  * Error-throwing stub for server-only `headers` function.
180
185
  */
181
186
  export function headers(): never {
182
- throw new Error("headers() is server-only and requires RSC context.");
187
+ throw serverOnlyStubError("headers");
183
188
  }
184
189
 
185
190
  /**
186
191
  * Error-throwing stub for server-only `createReverse` function.
187
192
  */
188
193
  export function createReverse(): never {
189
- throw new Error("createReverse() is server-only and requires RSC context.");
194
+ throw serverOnlyStubError("createReverse");
190
195
  }
191
196
 
192
197
  /**
193
198
  * Error-throwing stub for server-only `enableMatchDebug` function.
194
199
  */
195
200
  export function enableMatchDebug(): never {
196
- throw new Error(
197
- "enableMatchDebug() is server-only and requires RSC context.",
198
- );
201
+ throw serverOnlyStubError("enableMatchDebug");
199
202
  }
200
203
 
201
204
  /**
202
205
  * Error-throwing stub for server-only `getMatchDebugStats` function.
203
206
  */
204
207
  export function getMatchDebugStats(): never {
205
- throw new Error(
206
- "getMatchDebugStats() is server-only and requires RSC context.",
207
- );
208
+ throw serverOnlyStubError("getMatchDebugStats");
208
209
  }
209
210
 
210
211
  // Error-throwing stubs for server-only route helpers
211
212
  export function layout(): never {
212
- throw new Error("layout() is server-only and requires RSC context.");
213
+ throw serverOnlyStubError("layout");
213
214
  }
214
215
  export function cache(): never {
215
- throw new Error("cache() is server-only and requires RSC context.");
216
+ throw serverOnlyStubError("cache");
216
217
  }
217
218
  export function middleware(): never {
218
- throw new Error("middleware() is server-only and requires RSC context.");
219
+ throw serverOnlyStubError("middleware");
219
220
  }
220
221
  export function revalidate(): never {
221
- throw new Error("revalidate() is server-only and requires RSC context.");
222
+ throw serverOnlyStubError("revalidate");
222
223
  }
223
224
  export function loader(): never {
224
- throw new Error("loader() is server-only and requires RSC context.");
225
+ throw serverOnlyStubError("loader");
225
226
  }
226
227
  export function loading(): never {
227
- throw new Error("loading() is server-only and requires RSC context.");
228
+ throw serverOnlyStubError("loading");
228
229
  }
229
230
  export function parallel(): never {
230
- throw new Error("parallel() is server-only and requires RSC context.");
231
+ throw serverOnlyStubError("parallel");
231
232
  }
232
233
  export function intercept(): never {
233
- throw new Error("intercept() is server-only and requires RSC context.");
234
+ throw serverOnlyStubError("intercept");
234
235
  }
235
236
  export function when(): never {
236
- throw new Error("when() is server-only and requires RSC context.");
237
+ throw serverOnlyStubError("when");
237
238
  }
238
239
  export function errorBoundary(): never {
239
- throw new Error("errorBoundary() is server-only and requires RSC context.");
240
+ throw serverOnlyStubError("errorBoundary");
240
241
  }
241
242
  export function notFoundBoundary(): never {
242
- throw new Error(
243
- "notFoundBoundary() is server-only and requires RSC context.",
244
- );
243
+ throw serverOnlyStubError("notFoundBoundary");
245
244
  }
246
245
  export function transition(): never {
247
- throw new Error("transition() is server-only and requires RSC context.");
246
+ throw serverOnlyStubError("transition");
248
247
  }
249
248
 
250
249
  // Request context type (safe for client)
@@ -2,9 +2,10 @@
2
2
  * Prerender Store
3
3
  *
4
4
  * Reads pre-rendered segment data from the worker bundle at build time.
5
- * The data is stored as globalThis.__PRERENDER_MANIFEST, a map of
6
- * "<routeName>/<paramHash>" to dynamic import functions that resolve
7
- * individual prerender entry modules.
5
+ * The manifest module is lazily loaded via globalThis.__loadPrerenderManifestModule,
6
+ * a function injected into the RSC entry that returns the manifest module
7
+ * containing a key-to-specifier map and a `loadPrerenderAsset` function
8
+ * that anchors import() resolution relative to the manifest file.
8
9
  */
9
10
 
10
11
  import type {
@@ -34,11 +35,20 @@ export interface StaticStore {
34
35
  get(handlerId: string): Promise<StaticEntry | null>;
35
36
  }
36
37
 
38
+ interface PrerenderManifestModule {
39
+ default: Record<string, string>;
40
+ loadPrerenderAsset: (
41
+ specifier: string,
42
+ ) => Promise<{ default: PrerenderEntry }>;
43
+ }
44
+
37
45
  declare global {
38
- // Injected by closeBundle post-processing: map of key -> () => import("./assets/__pr-*.js")
46
+ // Injected by closeBundle post-processing: lazy loader for the prerender
47
+ // manifest module. The module exports a key→specifier map and a
48
+ // loadPrerenderAsset function that anchors import() relative to the manifest.
39
49
  // eslint-disable-next-line no-var
40
- var __PRERENDER_MANIFEST:
41
- | Record<string, () => Promise<{ default: PrerenderEntry }>>
50
+ var __loadPrerenderManifestModule:
51
+ | (() => Promise<PrerenderManifestModule>)
42
52
  | undefined;
43
53
  // Injected by closeBundle post-processing: map of handlerId -> () => import("./assets/__st-*.js")
44
54
  // Asset default export is either a string (no handles) or { encoded, handles } object.
@@ -78,17 +88,28 @@ export function createDevPrerenderStore(devUrl: string): PrerenderStore {
78
88
  /**
79
89
  * Create a prerender store.
80
90
  * Dev mode: on-demand fetch from Vite dev server (node:fs works there).
81
- * Production: backed by globalThis.__PRERENDER_MANIFEST injected at build time.
91
+ * Production: backed by globalThis.__loadPrerenderManifestModule which lazily
92
+ * loads the manifest module on first access.
82
93
  * Returns null if no prerender data is available.
83
94
  */
84
95
  export function createPrerenderStore(): PrerenderStore | null {
85
96
  if (globalThis.__PRERENDER_DEV_URL) {
86
97
  return createDevPrerenderStore(globalThis.__PRERENDER_DEV_URL);
87
98
  }
88
- const manifest = globalThis.__PRERENDER_MANIFEST;
89
- if (!manifest || Object.keys(manifest).length === 0) return null;
99
+ if (!globalThis.__loadPrerenderManifestModule) return null;
90
100
 
91
101
  const cache = new Map<string, Promise<PrerenderEntry | null>>();
102
+ let manifestModulePromise: Promise<PrerenderManifestModule | null> | null =
103
+ null;
104
+
105
+ function loadManifestModule(): Promise<PrerenderManifestModule | null> {
106
+ if (!manifestModulePromise) {
107
+ manifestModulePromise = globalThis.__loadPrerenderManifestModule!().catch(
108
+ () => null,
109
+ );
110
+ }
111
+ return manifestModulePromise;
112
+ }
92
113
 
93
114
  return {
94
115
  get(routeName: string, paramHash: string): Promise<PrerenderEntry | null> {
@@ -96,18 +117,38 @@ export function createPrerenderStore(): PrerenderStore | null {
96
117
  const cached = cache.get(key);
97
118
  if (cached) return cached;
98
119
 
99
- const loader = manifest[key];
100
- if (!loader) return Promise.resolve(null);
101
-
102
- const promise = loader()
103
- .then((mod) => mod.default)
104
- .catch(() => null);
120
+ const promise = loadManifestModule().then((mod) => {
121
+ if (!mod) return null;
122
+ const specifier = mod.default[key];
123
+ if (!specifier) return null;
124
+ return mod
125
+ .loadPrerenderAsset(specifier)
126
+ .then((asset) => asset.default)
127
+ .catch(() => null);
128
+ });
105
129
  cache.set(key, promise);
106
130
  return promise;
107
131
  },
108
132
  };
109
133
  }
110
134
 
135
+ /**
136
+ * Load the prerender manifest index for test introspection.
137
+ * Returns the key→specifier map or null if unavailable.
138
+ */
139
+ export async function loadPrerenderManifestIndex(): Promise<Record<
140
+ string,
141
+ string
142
+ > | null> {
143
+ if (!globalThis.__loadPrerenderManifestModule) return null;
144
+ try {
145
+ const mod = await globalThis.__loadPrerenderManifestModule();
146
+ return mod.default;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+
111
152
  /**
112
153
  * Create a static segment store.
113
154
  * Production only: backed by globalThis.__STATIC_MANIFEST injected at build time.
@@ -1,6 +1,3 @@
1
- // Route definition
2
- export { route, type RouteDefinitionResult } from "./route-function.js";
3
-
4
1
  // Type definitions
5
2
  export type { RouteHelpers } from "./helpers-types.js";
6
3
  export type {