@rangojs/router 0.0.0-experimental.61 → 0.0.0-experimental.63
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.
- package/README.md +61 -8
- package/dist/bin/rango.js +2 -1
- package/dist/vite/index.js +144 -62
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +14 -15
- package/skills/prerender/SKILL.md +110 -68
- package/src/__internal.ts +1 -1
- package/src/build/generate-manifest.ts +3 -6
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +8 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +2 -0
- package/src/route-definition/dsl-helpers.ts +37 -18
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/handler-context.ts +22 -5
- package/src/router/match-api.ts +2 -8
- package/src/router/match-middleware/cache-lookup.ts +2 -6
- package/src/router/prerender-match.ts +104 -8
- package/src/router/router-interfaces.ts +4 -0
- package/src/router/segment-resolution/fresh.ts +7 -2
- package/src/router/segment-resolution/revalidation.ts +10 -5
- package/src/router.ts +9 -1
- package/src/server/context.ts +5 -1
- package/src/static-handler.ts +18 -6
- package/src/types/handler-context.ts +12 -2
- package/src/types/route-entry.ts +1 -1
- package/src/urls/path-helper-types.ts +5 -1
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/response-types.ts +16 -6
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/prerender-collection.ts +14 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/rango.ts +2 -1
- package/src/vite/router-discovery.ts +153 -34
- package/src/vite/utils/prerender-utils.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.63",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -132,15 +132,6 @@
|
|
|
132
132
|
"access": "public",
|
|
133
133
|
"tag": "experimental"
|
|
134
134
|
},
|
|
135
|
-
"scripts": {
|
|
136
|
-
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
|
-
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit",
|
|
139
|
-
"test": "playwright test",
|
|
140
|
-
"test:ui": "playwright test --ui",
|
|
141
|
-
"test:unit": "vitest run",
|
|
142
|
-
"test:unit:watch": "vitest"
|
|
143
|
-
},
|
|
144
135
|
"dependencies": {
|
|
145
136
|
"@vitejs/plugin-rsc": "^0.5.19",
|
|
146
137
|
"magic-string": "^0.30.17",
|
|
@@ -150,12 +141,12 @@
|
|
|
150
141
|
"devDependencies": {
|
|
151
142
|
"@playwright/test": "^1.49.1",
|
|
152
143
|
"@types/node": "^24.10.1",
|
|
153
|
-
"@types/react": "
|
|
154
|
-
"@types/react-dom": "
|
|
144
|
+
"@types/react": "^19.2.7",
|
|
145
|
+
"@types/react-dom": "^19.2.3",
|
|
155
146
|
"esbuild": "^0.27.0",
|
|
156
147
|
"jiti": "^2.6.1",
|
|
157
|
-
"react": "
|
|
158
|
-
"react-dom": "
|
|
148
|
+
"react": "^19.2.4",
|
|
149
|
+
"react-dom": "^19.2.4",
|
|
159
150
|
"tinyexec": "^0.3.2",
|
|
160
151
|
"typescript": "^5.3.0",
|
|
161
152
|
"vitest": "^4.0.0"
|
|
@@ -173,5 +164,13 @@
|
|
|
173
164
|
"vite": {
|
|
174
165
|
"optional": true
|
|
175
166
|
}
|
|
167
|
+
},
|
|
168
|
+
"scripts": {
|
|
169
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
170
|
+
"typecheck": "tsc --noEmit",
|
|
171
|
+
"test": "playwright test",
|
|
172
|
+
"test:ui": "playwright test --ui",
|
|
173
|
+
"test:unit": "vitest run",
|
|
174
|
+
"test:unit:watch": "vitest"
|
|
176
175
|
}
|
|
177
|
-
}
|
|
176
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prerender
|
|
3
|
-
description: Pre-render route segments at build time with Prerender and
|
|
3
|
+
description: Pre-render route segments at build time with Prerender and Passthrough live fallback
|
|
4
4
|
argument-hint: [passthrough]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -54,8 +54,14 @@ path("/blog/:slug", BlogPost, { name: "blog.post" })
|
|
|
54
54
|
|
|
55
55
|
### With Passthrough (live fallback for unknown params)
|
|
56
56
|
|
|
57
|
+
Wrap a `Prerender` definition with `Passthrough()` to add a separate live handler
|
|
58
|
+
for unknown params at runtime. The build handler runs at build time, the live
|
|
59
|
+
handler runs at request time.
|
|
60
|
+
|
|
57
61
|
```typescript
|
|
58
|
-
|
|
62
|
+
import { Prerender, Passthrough } from "@rangojs/router";
|
|
63
|
+
|
|
64
|
+
export const ProductPageDef = Prerender(
|
|
59
65
|
async () => {
|
|
60
66
|
const top = await db.query("SELECT id FROM products WHERE featured");
|
|
61
67
|
return top.map(p => ({ id: p.id }));
|
|
@@ -64,32 +70,41 @@ export const ProductPage = Prerender(
|
|
|
64
70
|
const product = await db.query("SELECT * FROM products WHERE id = ?", ctx.params.id);
|
|
65
71
|
return <Product data={product} />;
|
|
66
72
|
},
|
|
67
|
-
{
|
|
73
|
+
{ concurrency: 4 }
|
|
68
74
|
);
|
|
75
|
+
|
|
76
|
+
// In route definition:
|
|
77
|
+
path("/products/:id", Passthrough(ProductPageDef, async (ctx) => {
|
|
78
|
+
const product = await ctx.env.DB.query("SELECT * FROM products WHERE id = ?", ctx.params.id);
|
|
79
|
+
return <Product data={product} />;
|
|
80
|
+
}), { name: "product" })
|
|
69
81
|
```
|
|
70
82
|
|
|
71
|
-
## Passthrough
|
|
83
|
+
## Passthrough Wrapper
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
`Passthrough(prerenderDef, liveHandler)` wraps a `Prerender` definition with a
|
|
86
|
+
separate handler for runtime fallback. The build and live handlers are separate
|
|
87
|
+
functions — no `ctx.build` branching needed.
|
|
74
88
|
|
|
75
|
-
| |
|
|
76
|
-
| ------------------- | --------------------------------------- |
|
|
77
|
-
| Known params | Served from pre-rendered Flight payload | Served from pre-rendered Flight payload
|
|
78
|
-
| Unknown params | Handler evicted, no live fallback |
|
|
79
|
-
| `ctx.passthrough()` | Throws (not
|
|
80
|
-
| Bundle size |
|
|
81
|
-
| `revalidate()` | Not allowed (handler gone) | Allowed (handler can re-render)
|
|
82
|
-
| `loading()` | Ignored (segments fully resolved) | Works for live fallback renders
|
|
89
|
+
| | Plain `Prerender` (no wrapper) | `Passthrough(def, liveHandler)` |
|
|
90
|
+
| ------------------- | --------------------------------------- | ---------------------------------------- |
|
|
91
|
+
| Known params | Served from pre-rendered Flight payload | Served from pre-rendered Flight payload |
|
|
92
|
+
| Unknown params | Handler evicted, no live fallback | Live handler runs at request time |
|
|
93
|
+
| `ctx.passthrough()` | Throws (not on Passthrough route) | Skips artifact, defers to live handler |
|
|
94
|
+
| Bundle size | Build handler code + imports removed | Build handler evicted, live handler kept |
|
|
95
|
+
| `revalidate()` | Not allowed (handler gone) | Allowed (live handler can re-render) |
|
|
96
|
+
| `loading()` | Ignored (segments fully resolved) | Works for live fallback renders |
|
|
83
97
|
|
|
84
|
-
### When to use
|
|
98
|
+
### When to use Passthrough
|
|
85
99
|
|
|
86
|
-
Use `
|
|
100
|
+
Use `Passthrough()` when:
|
|
87
101
|
|
|
88
102
|
- The route has a large or open-ended param space (e.g., user profiles, product pages)
|
|
89
103
|
- You want to pre-render popular/known params for speed but still serve unknown params live
|
|
90
104
|
- You need `revalidate()` on the route
|
|
105
|
+
- The live handler needs runtime bindings (e.g., `ctx.env.DB`)
|
|
91
106
|
|
|
92
|
-
Use
|
|
107
|
+
Use plain `Prerender` (no wrapper) when:
|
|
93
108
|
|
|
94
109
|
- All possible params are known at build time (e.g., markdown files, config-driven pages)
|
|
95
110
|
- You want maximum bundle size reduction (handler code + node:fs imports removed)
|
|
@@ -103,6 +118,7 @@ Handlers receive `BuildContext` at build time, a subset of the runtime `HandlerC
|
|
|
103
118
|
interface BuildContext<TParams> {
|
|
104
119
|
params: TParams; // From getParams
|
|
105
120
|
build: true; // Always true at build time
|
|
121
|
+
dev: boolean; // true in Vite dev mode, false during production build
|
|
106
122
|
use: <T>(handle: Handle<T>) => (data: T) => void; // Push handle data
|
|
107
123
|
url: URL; // Synthetic URL from pattern + params
|
|
108
124
|
pathname: string; // Pathname from synthetic URL
|
|
@@ -115,8 +131,9 @@ interface BuildContext<TParams> {
|
|
|
115
131
|
params?: Record<string, string>,
|
|
116
132
|
search?: Record<string, unknown>,
|
|
117
133
|
): string; // URL generation
|
|
118
|
-
passthrough(): PrerenderPassthroughResult; // Skip local artifact (
|
|
119
|
-
//
|
|
134
|
+
passthrough(): PrerenderPassthroughResult; // Skip local artifact (Passthrough routes only)
|
|
135
|
+
env: DefaultEnv; // Available when buildEnv is configured in rango() (throws otherwise)
|
|
136
|
+
// NOT available: request, headers, cookies (always throw)
|
|
120
137
|
}
|
|
121
138
|
```
|
|
122
139
|
|
|
@@ -183,18 +200,17 @@ In production builds, `Prerender` exports are replaced with stubs:
|
|
|
183
200
|
// Original
|
|
184
201
|
export const BlogPost = Prerender(getParams, handler);
|
|
185
202
|
|
|
186
|
-
// Stubbed (
|
|
203
|
+
// Stubbed (all Prerender handlers are evicted)
|
|
187
204
|
export const BlogPost = {
|
|
188
205
|
__brand: "prerenderHandler",
|
|
189
206
|
$$id: "abc123#BlogPost",
|
|
190
207
|
};
|
|
191
208
|
```
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
|
|
210
|
+
All Prerender handlers are evicted in production. The live handler for
|
|
211
|
+
`Passthrough()` routes lives in the urls module and is not evicted.
|
|
195
212
|
|
|
196
|
-
In client and SSR environments, ALL prerender handlers are always stubbed
|
|
197
|
-
(passthrough only affects the RSC server bundle).
|
|
213
|
+
In client and SSR environments, ALL prerender handlers are always stubbed.
|
|
198
214
|
|
|
199
215
|
## Sub-use Semantics
|
|
200
216
|
|
|
@@ -231,15 +247,15 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
|
|
|
231
247
|
| DSL item | Behavior with Prerender |
|
|
232
248
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
233
249
|
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
|
|
234
|
-
| `revalidate()` | Not allowed without
|
|
250
|
+
| `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
|
|
235
251
|
| `cache()` | Orthogonal -- use on parent layouts and loaders. |
|
|
236
252
|
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
|
|
237
253
|
| `parallel()` | Parallel slots inside path are pre-rendered. |
|
|
238
254
|
| `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
|
|
239
|
-
| `loading()` | Ignored without
|
|
255
|
+
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
|
|
240
256
|
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when()` conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
|
|
241
257
|
|
|
242
|
-
When
|
|
258
|
+
When Passthrough revalidation is enabled, remember that revalidation is
|
|
243
259
|
still partial: opting a child segment into revalidation does not
|
|
244
260
|
implicitly re-run outer prerender-derived handlers/layouts.
|
|
245
261
|
|
|
@@ -304,12 +320,19 @@ export const BlogPost = Prerender(
|
|
|
304
320
|
}
|
|
305
321
|
return <PostPage slug={ctx.params.slug} />;
|
|
306
322
|
},
|
|
307
|
-
{ passthrough: true },
|
|
308
323
|
);
|
|
324
|
+
|
|
325
|
+
// Wrap with Passthrough to serve skipped params live at runtime
|
|
326
|
+
export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
|
|
327
|
+
if (ctx.params.slug === "draft") {
|
|
328
|
+
throw new Skip("Draft articles are not pre-rendered");
|
|
329
|
+
}
|
|
330
|
+
return <PostPage slug={ctx.params.slug} />;
|
|
331
|
+
});
|
|
309
332
|
```
|
|
310
333
|
|
|
311
|
-
Skipped entries are excluded from the build output. With `
|
|
312
|
-
the handler
|
|
334
|
+
Skipped entries are excluded from the build output. With `Passthrough()`,
|
|
335
|
+
the live handler serves skipped params at request time.
|
|
313
336
|
|
|
314
337
|
`Skip` also works in `Static` handlers:
|
|
315
338
|
|
|
@@ -326,7 +349,7 @@ export const TocSidebar = Static(() => {
|
|
|
326
349
|
| Handler outcome | Effect |
|
|
327
350
|
| --------------------------- | ----------------------------------------------------- |
|
|
328
351
|
| JSX / `null` | Normal prerender entry, log OK |
|
|
329
|
-
| `return ctx.passthrough()` | Skip entry, log PASS, continue (
|
|
352
|
+
| `return ctx.passthrough()` | Skip entry, log PASS, continue (Passthrough routes) |
|
|
330
353
|
| `throw new Skip("reason")` | Skip entry, log SKIP, continue with remaining entries |
|
|
331
354
|
| `throw new Error("reason")` | Log FAIL, stop ALL pre-rendering, fail the build |
|
|
332
355
|
|
|
@@ -366,56 +389,59 @@ falls back according to normal dev/runtime behavior.
|
|
|
366
389
|
|
|
367
390
|
## Per-Param Passthrough with ctx.passthrough()
|
|
368
391
|
|
|
369
|
-
On
|
|
370
|
-
to skip writing a local prerender artifact for a specific
|
|
371
|
-
runtime, the missing entry falls through to the live handler.
|
|
392
|
+
On routes wrapped with `Passthrough()`, the build handler can return
|
|
393
|
+
`ctx.passthrough()` to skip writing a local prerender artifact for a specific
|
|
394
|
+
param set. At runtime, the missing entry falls through to the live handler.
|
|
372
395
|
|
|
373
396
|
```typescript
|
|
374
|
-
export const
|
|
397
|
+
export const BlogPostDef = Prerender(
|
|
375
398
|
async () => [{ slug: "a" }, { slug: "b" }, { slug: "c" }],
|
|
376
399
|
async (ctx) => {
|
|
377
400
|
const post = await getPost(ctx.params.slug);
|
|
378
401
|
if (!post) return ctx.passthrough();
|
|
379
402
|
return <article>{post.content}</article>;
|
|
380
403
|
},
|
|
381
|
-
{ passthrough: true },
|
|
382
404
|
);
|
|
405
|
+
|
|
406
|
+
export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
|
|
407
|
+
const post = await getPost(ctx.params.slug);
|
|
408
|
+
return <article>{post.content}</article>;
|
|
409
|
+
});
|
|
383
410
|
```
|
|
384
411
|
|
|
385
412
|
### Semantics
|
|
386
413
|
|
|
387
|
-
- JSX or `null` from the handler produces a normal prerender entry.
|
|
414
|
+
- JSX or `null` from the build handler produces a normal prerender entry.
|
|
388
415
|
- `ctx.passthrough()` returns a sentinel that signals "no local artifact".
|
|
389
416
|
The build skips the manifest entry for that param set.
|
|
390
|
-
- `ctx.passthrough()` on a
|
|
417
|
+
- `ctx.passthrough()` on a route not wrapped with `Passthrough()` throws.
|
|
391
418
|
- `ctx.passthrough()` at runtime (`ctx.build === false`) also throws.
|
|
392
419
|
It is a build-time-only control flow.
|
|
393
|
-
- `getParams()` still enumerates the param set; the handler decides
|
|
394
|
-
whether to produce an artifact or defer to
|
|
420
|
+
- `getParams()` still enumerates the param set; the build handler decides
|
|
421
|
+
per-param whether to produce an artifact or defer to the live handler.
|
|
395
422
|
|
|
396
423
|
### Difference from Skip
|
|
397
424
|
|
|
398
|
-
| Mechanism | Effect on build | Runtime behavior
|
|
399
|
-
| ------------------- | ---------------------- |
|
|
400
|
-
| `throw new Skip()` | Skips entry, logs SKIP | No artifact, no live fallback unless
|
|
401
|
-
| `ctx.passthrough()` | Skips entry, logs PASS | Always defers to live handler (requires
|
|
425
|
+
| Mechanism | Effect on build | Runtime behavior |
|
|
426
|
+
| ------------------- | ---------------------- | ------------------------------------------------------ |
|
|
427
|
+
| `throw new Skip()` | Skips entry, logs SKIP | No artifact, no live fallback unless Passthrough route |
|
|
428
|
+
| `ctx.passthrough()` | Skips entry, logs PASS | Always defers to live handler (requires Passthrough) |
|
|
402
429
|
|
|
403
|
-
Use `ctx.passthrough()` when you want the handler to run
|
|
430
|
+
Use `ctx.passthrough()` when you want the live handler to run at request time
|
|
404
431
|
for specific params. Use `Skip` when you want to exclude params entirely.
|
|
405
432
|
|
|
406
433
|
### Use case: Remote storage
|
|
407
434
|
|
|
408
435
|
`ctx.passthrough()` enables a pattern where build-time data is stored in a
|
|
409
|
-
remote KV store instead of the local prerender manifest. The handler
|
|
436
|
+
remote KV store instead of the local prerender manifest. The build handler
|
|
410
437
|
pre-computes data during `getParams`, pushes it to KV, then calls
|
|
411
438
|
`ctx.passthrough()` so the local build skips the artifact. At runtime,
|
|
412
|
-
the live handler reads from KV:
|
|
439
|
+
the Passthrough live handler reads from KV:
|
|
413
440
|
|
|
414
441
|
```typescript
|
|
415
|
-
export const
|
|
442
|
+
export const ProductDef = Prerender(
|
|
416
443
|
async () => {
|
|
417
444
|
const products = await db.getFeaturedProducts();
|
|
418
|
-
// Pre-compute and store in remote KV during build
|
|
419
445
|
for (const p of products) {
|
|
420
446
|
await kv.put(`product:${p.id}`, await renderProduct(p));
|
|
421
447
|
}
|
|
@@ -423,14 +449,16 @@ export const Product = Prerender(
|
|
|
423
449
|
},
|
|
424
450
|
async (ctx) => {
|
|
425
451
|
// At build time: skip local artifact, data is in KV
|
|
426
|
-
|
|
427
|
-
// At runtime: read from KV
|
|
428
|
-
const cached = await kv.get(`product:${ctx.params.id}`);
|
|
429
|
-
if (cached) return cached;
|
|
430
|
-
return <Product data={await db.getProduct(ctx.params.id)} />;
|
|
452
|
+
return ctx.passthrough();
|
|
431
453
|
},
|
|
432
|
-
{ passthrough: true },
|
|
433
454
|
);
|
|
455
|
+
|
|
456
|
+
export const Product = Passthrough(ProductDef, async (ctx) => {
|
|
457
|
+
// At runtime: read from KV, fall back to DB
|
|
458
|
+
const cached = await kv.get(`product:${ctx.params.id}`);
|
|
459
|
+
if (cached) return cached;
|
|
460
|
+
return <Product data={await ctx.env.DB.getProduct(ctx.params.id)} />;
|
|
461
|
+
});
|
|
434
462
|
```
|
|
435
463
|
|
|
436
464
|
### Build logs
|
|
@@ -458,8 +486,8 @@ Flight payload. They do not update at request time.
|
|
|
458
486
|
### Server actions work normally
|
|
459
487
|
|
|
460
488
|
Actions do not re-render the B segment. The pre-rendered handler output stays
|
|
461
|
-
frozen. Loaders are live and can be revalidated by actions. With `
|
|
462
|
-
and `revalidate()`, the handler
|
|
489
|
+
frozen. Loaders are live and can be revalidated by actions. With `Passthrough()`
|
|
490
|
+
and `revalidate()`, the live handler can re-render.
|
|
463
491
|
|
|
464
492
|
### Empty getParams
|
|
465
493
|
|
|
@@ -470,21 +498,21 @@ If `getParams` returns an empty array, no Flight payloads are written. No error.
|
|
|
470
498
|
Routes using `Prerender` must have a `name` in path options.
|
|
471
499
|
The name is used as the storage key for Flight payloads.
|
|
472
500
|
|
|
473
|
-
### No revalidate without
|
|
501
|
+
### No revalidate without Passthrough
|
|
474
502
|
|
|
475
|
-
Using `revalidate()`
|
|
503
|
+
Using `revalidate()` without `Passthrough()` produces a build-time warning.
|
|
476
504
|
The handler is evicted -- there is nothing to re-render.
|
|
477
505
|
|
|
478
|
-
### loading() is ignored without
|
|
506
|
+
### loading() is ignored without Passthrough
|
|
479
507
|
|
|
480
508
|
Pre-rendered segments are fully resolved at build time and never suspend.
|
|
481
|
-
With `
|
|
509
|
+
With `Passthrough()`, `loading()` works for live fallback renders.
|
|
482
510
|
|
|
483
511
|
## Complete Example
|
|
484
512
|
|
|
485
513
|
```typescript
|
|
486
514
|
// pages/guides-handler.tsx
|
|
487
|
-
import { Prerender } from "@rangojs/router";
|
|
515
|
+
import { Prerender, Passthrough } from "@rangojs/router";
|
|
488
516
|
import { Link } from "@rangojs/router/client";
|
|
489
517
|
import { href } from "../router.js";
|
|
490
518
|
|
|
@@ -493,7 +521,7 @@ const knownGuides: Record<string, string> = {
|
|
|
493
521
|
caching: "Caching Guide",
|
|
494
522
|
};
|
|
495
523
|
|
|
496
|
-
export const
|
|
524
|
+
export const GuidesDetailDef = Prerender<{ slug: string }>(
|
|
497
525
|
async () => Object.keys(knownGuides).map((slug) => ({ slug })),
|
|
498
526
|
async (ctx) => {
|
|
499
527
|
const title = knownGuides[ctx.params.slug] ?? `Guide: ${ctx.params.slug}`;
|
|
@@ -509,9 +537,23 @@ export const GuidesDetail = Prerender<{ slug: string }>(
|
|
|
509
537
|
</div>
|
|
510
538
|
);
|
|
511
539
|
},
|
|
512
|
-
{ passthrough: true },
|
|
513
540
|
);
|
|
514
541
|
|
|
542
|
+
export const GuidesDetail = Passthrough(GuidesDetailDef, async (ctx) => {
|
|
543
|
+
const title = knownGuides[ctx.params.slug] ?? `Guide: ${ctx.params.slug}`;
|
|
544
|
+
return (
|
|
545
|
+
<div>
|
|
546
|
+
<h1>{title}</h1>
|
|
547
|
+
<p>Slug: {ctx.params.slug}</p>
|
|
548
|
+
<nav>
|
|
549
|
+
<Link to={href("guides.detail", { slug: "routing" })}>Routing</Link>
|
|
550
|
+
{" | "}
|
|
551
|
+
<Link to={href("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
|
|
552
|
+
</nav>
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
515
557
|
// pages/guides.tsx
|
|
516
558
|
import { urls } from "@rangojs/router";
|
|
517
559
|
import { GuidesDetail } from "./guides-handler.js";
|
|
@@ -588,12 +630,12 @@ Loaders run fresh at request time for both variants.
|
|
|
588
630
|
Pre-rendered routes set flags on the route trie leaf at build time:
|
|
589
631
|
|
|
590
632
|
- `pr: true` -- route has pre-rendered B segment data
|
|
591
|
-
- `pt: true` --
|
|
633
|
+
- `pt: true` -- route wrapped with `Passthrough()` (live handler available)
|
|
592
634
|
|
|
593
635
|
At runtime, the cache-lookup middleware uses these flags:
|
|
594
636
|
|
|
595
637
|
- `pr + hit` -- serve pre-rendered Flight payload
|
|
596
|
-
- `pr + pt + miss` -- fall through to live handler
|
|
638
|
+
- `pr + pt + miss` -- fall through to Passthrough live handler
|
|
597
639
|
- `pr + miss` (no pt) -- fall through (handler stubbed, no live render)
|
|
598
640
|
|
|
599
641
|
## Contributor Checklist
|
|
@@ -603,7 +645,7 @@ Before changing prerender behavior, read these docs and run these tests.
|
|
|
603
645
|
### Docs to re-read
|
|
604
646
|
|
|
605
647
|
- [Prerender API design](../../docs/prerender-api-design.md) -- canonical
|
|
606
|
-
architecture: build-time flow, runtime flow, storage,
|
|
648
|
+
architecture: build-time flow, runtime flow, storage, Passthrough, intercept
|
|
607
649
|
- [Execution model](../../docs/internal/execution-model.md) -- handler-first
|
|
608
650
|
ordering, middleware scope, context visibility rules
|
|
609
651
|
- [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
|
|
@@ -612,7 +654,7 @@ Before changing prerender behavior, read these docs and run these tests.
|
|
|
612
654
|
### Tests to run
|
|
613
655
|
|
|
614
656
|
```bash
|
|
615
|
-
# Core prerender e2e (
|
|
657
|
+
# Core prerender e2e (Passthrough, eviction, loaders, sub-use, intercept)
|
|
616
658
|
pnpm --filter @rangojs/router exec playwright test prerender
|
|
617
659
|
|
|
618
660
|
# Prerender-specific unit test
|
|
@@ -632,7 +674,7 @@ pnpm --filter @rangojs/router exec playwright test handler-first
|
|
|
632
674
|
`/__rsc_prerender` endpoint tests and node.js dev-server fallback.
|
|
633
675
|
- Log-based assertions (build output lines, debug cache logs) are inherently
|
|
634
676
|
dev/build-only and do not need a production counterpart.
|
|
635
|
-
- Behavioral assertions (rendered content, loader freshness,
|
|
677
|
+
- Behavioral assertions (rendered content, loader freshness, Passthrough
|
|
636
678
|
fallback, intercept variant selection) must work in the production build.
|
|
637
679
|
|
|
638
680
|
## Maintenance References
|
package/src/__internal.ts
CHANGED
|
@@ -225,7 +225,7 @@ export type {
|
|
|
225
225
|
* @internal
|
|
226
226
|
* Type guard for prerender handler definitions.
|
|
227
227
|
*/
|
|
228
|
-
export { isPrerenderHandler } from "./prerender.js";
|
|
228
|
+
export { isPrerenderHandler, isPassthroughHandler } from "./prerender.js";
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
231
|
* @internal
|
|
@@ -45,7 +45,7 @@ export interface GeneratedManifest {
|
|
|
45
45
|
routeTrailingSlash?: Record<string, string>;
|
|
46
46
|
/** Route names using Prerender (for dev-mode Node.js delegation) */
|
|
47
47
|
prerenderRoutes?: string[];
|
|
48
|
-
/** Route names with
|
|
48
|
+
/** Route names wrapped with Passthrough() (live handler for runtime fallback) */
|
|
49
49
|
passthroughRoutes?: string[];
|
|
50
50
|
/** Route name → response type for non-RSC routes */
|
|
51
51
|
responseTypeRoutes?: Record<string, string>;
|
|
@@ -150,10 +150,7 @@ function buildPrefixTreeNode(
|
|
|
150
150
|
if (prerenderDefs && entry.prerenderDef) {
|
|
151
151
|
prerenderDefs[name] = entry.prerenderDef;
|
|
152
152
|
}
|
|
153
|
-
if (
|
|
154
|
-
passthroughRoutes &&
|
|
155
|
-
entry.prerenderDef?.options?.passthrough === true
|
|
156
|
-
) {
|
|
153
|
+
if (passthroughRoutes && entry.isPassthrough === true) {
|
|
157
154
|
passthroughRoutes.push(name);
|
|
158
155
|
}
|
|
159
156
|
}
|
|
@@ -350,7 +347,7 @@ export function generateManifestFull<TEnv>(
|
|
|
350
347
|
if (entry.prerenderDef) {
|
|
351
348
|
prerenderDefs[name] = entry.prerenderDef;
|
|
352
349
|
}
|
|
353
|
-
if (entry.
|
|
350
|
+
if (entry.isPassthrough === true) {
|
|
354
351
|
passthroughRoutes.push(name);
|
|
355
352
|
}
|
|
356
353
|
}
|
|
@@ -61,7 +61,14 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
61
61
|
for (const entry of entries) {
|
|
62
62
|
const fullPath = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
entry.name === "node_modules" ||
|
|
66
|
+
entry.name.startsWith(".") ||
|
|
67
|
+
entry.name === "dist" ||
|
|
68
|
+
entry.name === "build" ||
|
|
69
|
+
entry.name === "coverage"
|
|
70
|
+
)
|
|
71
|
+
continue;
|
|
65
72
|
results.push(...findTsFiles(fullPath, filter));
|
|
66
73
|
} else if (
|
|
67
74
|
(entry.name.endsWith(".ts") ||
|
package/src/index.rsc.ts
CHANGED
|
@@ -100,6 +100,7 @@ export type {
|
|
|
100
100
|
LayoutUseItem,
|
|
101
101
|
AllUseItems,
|
|
102
102
|
UseItems,
|
|
103
|
+
HandlerUseItem,
|
|
103
104
|
} from "./route-types.js";
|
|
104
105
|
|
|
105
106
|
// Handle API
|
|
@@ -114,8 +115,9 @@ export { nonce } from "./rsc/nonce.js";
|
|
|
114
115
|
// Pre-render handler API
|
|
115
116
|
export {
|
|
116
117
|
Prerender,
|
|
118
|
+
Passthrough,
|
|
117
119
|
type PrerenderHandlerDefinition,
|
|
118
|
-
type
|
|
120
|
+
type PassthroughHandlerDefinition,
|
|
119
121
|
type PrerenderOptions,
|
|
120
122
|
type BuildContext,
|
|
121
123
|
type StaticBuildContext,
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ export type {
|
|
|
88
88
|
LayoutUseItem,
|
|
89
89
|
AllUseItems,
|
|
90
90
|
UseItems,
|
|
91
|
+
HandlerUseItem,
|
|
91
92
|
} from "./route-types.js";
|
|
92
93
|
|
|
93
94
|
// Response route types (usable in both server and client contexts)
|
|
@@ -152,6 +153,13 @@ export function Prerender(): never {
|
|
|
152
153
|
throw serverOnlyStubError("Prerender");
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Error-throwing stub for server-only `Passthrough` function.
|
|
158
|
+
*/
|
|
159
|
+
export function Passthrough(): never {
|
|
160
|
+
throw serverOnlyStubError("Passthrough");
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
/**
|
|
156
164
|
* Error-throwing stub for server-only `Static` function.
|
|
157
165
|
*/
|
package/src/prerender/store.ts
CHANGED
|
@@ -121,10 +121,11 @@ export function createPrerenderStore(): PrerenderStore | null {
|
|
|
121
121
|
if (!mod) return null;
|
|
122
122
|
const specifier = mod.default[key];
|
|
123
123
|
if (!specifier) return null;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
// Let asset load errors propagate — a missing/corrupted artifact
|
|
125
|
+
// for a key that exists in the manifest is a build/deploy error
|
|
126
|
+
// and should surface as a 500, not be silently swallowed as null
|
|
127
|
+
// (which the handler stub would misreport as a 404).
|
|
128
|
+
return mod.loadPrerenderAsset(specifier).then((asset) => asset.default);
|
|
128
129
|
});
|
|
129
130
|
cache.set(key, promise);
|
|
130
131
|
return promise;
|