@rangojs/router 0.0.0-experimental.139 → 0.0.0-experimental.140
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/dist/bin/rango.js +27 -2
- package/dist/vite/index.js +147 -30
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +1 -0
- package/skills/caching/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +15 -0
- package/skills/migrate-react-router/SKILL.md +15 -2
- package/skills/ppr/SKILL.md +426 -0
- package/skills/rango/SKILL.md +28 -25
- package/skills/route/SKILL.md +43 -0
- package/src/build/route-trie.ts +35 -7
- package/src/cache/cf/cf-cache-store.ts +155 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory-segment-store.ts +57 -1
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/types.ts +58 -0
- package/src/cache/vercel/vercel-cache-store.ts +159 -5
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +17 -0
- package/src/router/middleware.ts +14 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/segment-resolution/fresh.ts +12 -1
- package/src/router/segment-resolution/loader-cache.ts +14 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/rsc-rendering.ts +105 -51
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/types.ts +26 -0
- package/src/server/cookie-store.ts +45 -0
- package/src/server/live.ts +130 -0
- package/src/server/request-context.ts +49 -0
- package/src/ssr/index.tsx +377 -180
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/render-route.tsx +7 -9
- package/src/types/route-config.ts +19 -7
- package/src/urls/type-extraction.ts +43 -18
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/plugins/virtual-entries.ts +27 -2
- package/src/vite/router-discovery.ts +69 -15
- package/src/vite/utils/prerender-utils.ts +17 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full (initial-load / document) RSC payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from rsc-rendering.ts so both the foreground render AND the PPR
|
|
5
|
+
* background shell capture (shell-capture.ts) build the SAME payload shape over
|
|
6
|
+
* their own handle store. `resume` requires the captured tree to match the served
|
|
7
|
+
* tree; building an identical payload over the same replayed segments is what
|
|
8
|
+
* makes that hold. Keep this byte-identical to the normal full-render path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MatchResult } from "../types.js";
|
|
12
|
+
import type { RscPayload } from "./types.js";
|
|
13
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
14
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
15
|
+
import type { HandleStore } from "../server/handle-store.js";
|
|
16
|
+
import { gateTransitions } from "./transition-gate.js";
|
|
17
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the metadata payload for a full (non-partial) document render.
|
|
21
|
+
*
|
|
22
|
+
* @param handleStore - the store whose resolved handle stream feeds the payload;
|
|
23
|
+
* the foreground passes reqCtx's store, the background capture passes its fresh
|
|
24
|
+
* derived-context store.
|
|
25
|
+
*/
|
|
26
|
+
export function buildFullPayload(
|
|
27
|
+
m: MatchResult,
|
|
28
|
+
// env is opaque here (the payload never reads it); `any` avoids the invariance
|
|
29
|
+
// friction of HandlerContext<TEnv> vs the ambient RequestContext env at the two
|
|
30
|
+
// call sites (foreground render + background capture).
|
|
31
|
+
ctx: HandlerContext<any>,
|
|
32
|
+
url: URL,
|
|
33
|
+
reqCtx: RequestContext<any>,
|
|
34
|
+
handleStore: HandleStore,
|
|
35
|
+
): RscPayload {
|
|
36
|
+
return {
|
|
37
|
+
metadata: {
|
|
38
|
+
pathname: url.pathname,
|
|
39
|
+
routerId: ctx.router.id,
|
|
40
|
+
basename: ctx.router.basename,
|
|
41
|
+
segments: gateTransitions(m.segments, reqCtx, ctx.router.onError),
|
|
42
|
+
matched: m.matched,
|
|
43
|
+
diff: m.diff,
|
|
44
|
+
resolvedIds: m.resolvedIds,
|
|
45
|
+
params: m.params,
|
|
46
|
+
isPartial: false,
|
|
47
|
+
rootLayout: ctx.router.rootLayout,
|
|
48
|
+
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
49
|
+
// the first sync useHandle read see resolved values. Partial payloads
|
|
50
|
+
// (rsc-rendering.ts) keep streaming (handleStore.stream()).
|
|
51
|
+
handles: resolvedHandleStream(handleStore),
|
|
52
|
+
version: ctx.version,
|
|
53
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
54
|
+
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
|
55
|
+
prefetchConcurrency: ctx.router.prefetchConcurrency,
|
|
56
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
57
|
+
themeConfig: ctx.router.themeConfig,
|
|
58
|
+
// Carry warmupEnabled on the initial full-render payload so the client
|
|
59
|
+
// respects warmup:false from first load. The 404 and PE payloads already
|
|
60
|
+
// include it; without it here warmup could never be disabled on the
|
|
61
|
+
// normal full-load path (partial payloads omit it by design).
|
|
62
|
+
warmupEnabled: ctx.router.warmupEnabled,
|
|
63
|
+
// Carry strictMode on the initial full-render payload so the browser
|
|
64
|
+
// entry knows whether to wrap hydration in React.StrictMode. Partial
|
|
65
|
+
// (navigation) payloads omit it by design; StrictMode is decided once.
|
|
66
|
+
strictMode: ctx.router.strictMode,
|
|
67
|
+
initialTheme: reqCtx.theme,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { appendMetric } from "../router/metrics.js";
|
|
|
14
14
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
15
15
|
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
16
16
|
import type { RscPayload } from "./types.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type { SSRModule } from "./types.js";
|
|
18
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
18
19
|
import {
|
|
19
20
|
createResponseWithMergedHeaders,
|
|
20
21
|
createSimpleRedirectResponse,
|
|
@@ -22,7 +23,8 @@ import {
|
|
|
22
23
|
} from "./helpers.js";
|
|
23
24
|
import type { HandlerContext } from "./handler-context.js";
|
|
24
25
|
import { gateTransitions } from "./transition-gate.js";
|
|
25
|
-
import {
|
|
26
|
+
import { buildFullPayload } from "./full-payload.js";
|
|
27
|
+
import { scheduleShellCapture } from "./shell-capture.js";
|
|
26
28
|
|
|
27
29
|
export function handleRscRendering<TEnv>(
|
|
28
30
|
ctx: HandlerContext<TEnv>,
|
|
@@ -65,43 +67,6 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
65
67
|
let payload: RscPayload;
|
|
66
68
|
let hasInterceptSlots = false;
|
|
67
69
|
|
|
68
|
-
// Shared by the partial-fallback and full-render paths. The partial-success
|
|
69
|
-
// payload below is intentionally different (omits rootLayout/theme, adds slots).
|
|
70
|
-
const buildFullPayload = (m: MatchResult): RscPayload => ({
|
|
71
|
-
metadata: {
|
|
72
|
-
pathname: url.pathname,
|
|
73
|
-
routerId: ctx.router.id,
|
|
74
|
-
basename: ctx.router.basename,
|
|
75
|
-
segments: gateTransitions(m.segments, reqCtx, ctx.router.onError),
|
|
76
|
-
matched: m.matched,
|
|
77
|
-
diff: m.diff,
|
|
78
|
-
resolvedIds: m.resolvedIds,
|
|
79
|
-
params: m.params,
|
|
80
|
-
isPartial: false,
|
|
81
|
-
rootLayout: ctx.router.rootLayout,
|
|
82
|
-
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
83
|
-
// the first sync useHandle read see resolved values. Partial payloads below
|
|
84
|
-
// keep streaming (handleStore.stream()).
|
|
85
|
-
handles: resolvedHandleStream(handleStore),
|
|
86
|
-
version: ctx.version,
|
|
87
|
-
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
88
|
-
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
|
89
|
-
prefetchConcurrency: ctx.router.prefetchConcurrency,
|
|
90
|
-
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
91
|
-
themeConfig: ctx.router.themeConfig,
|
|
92
|
-
// Carry warmupEnabled on the initial full-render payload so the client
|
|
93
|
-
// respects warmup:false from first load. The 404 and PE payloads already
|
|
94
|
-
// include it; without it here warmup could never be disabled on the
|
|
95
|
-
// normal full-load path (partial payloads omit it by design).
|
|
96
|
-
warmupEnabled: ctx.router.warmupEnabled,
|
|
97
|
-
// Carry strictMode on the initial full-render payload so the browser
|
|
98
|
-
// entry knows whether to wrap hydration in React.StrictMode. Partial
|
|
99
|
-
// (navigation) payloads omit it by design; StrictMode is decided once.
|
|
100
|
-
strictMode: ctx.router.strictMode,
|
|
101
|
-
initialTheme: reqCtx.theme,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
70
|
if (isPartial) {
|
|
106
71
|
// Partial render (navigation)
|
|
107
72
|
const result = await ctx.router.matchPartial(request, { env });
|
|
@@ -118,7 +83,7 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
118
83
|
return createSimpleRedirectResponse(match.redirect);
|
|
119
84
|
}
|
|
120
85
|
|
|
121
|
-
payload = buildFullPayload(match);
|
|
86
|
+
payload = buildFullPayload(match, ctx, url, reqCtx, handleStore);
|
|
122
87
|
} else {
|
|
123
88
|
setRequestContextParams(result.params, result.routeName);
|
|
124
89
|
|
|
@@ -196,7 +161,7 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
196
161
|
{ headers: { "Content-Type": "application/json" } },
|
|
197
162
|
);
|
|
198
163
|
} else {
|
|
199
|
-
payload = buildFullPayload(match);
|
|
164
|
+
payload = buildFullPayload(match, ctx, url, reqCtx, handleStore);
|
|
200
165
|
}
|
|
201
166
|
}
|
|
202
167
|
|
|
@@ -268,16 +233,105 @@ async function handleRscRenderingInner<TEnv>(
|
|
|
268
233
|
metricsStore,
|
|
269
234
|
);
|
|
270
235
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
236
|
+
// --- Axis 2: PPR shell RESUME (see docs/design/ppr-shell-resume.md) ---
|
|
237
|
+
// The shell-cache middleware armed reqCtx._shellResume optimistically on a
|
|
238
|
+
// validated shell HIT. The render layer is the FINAL AUTHORITY: resume only on
|
|
239
|
+
// the main 200 HTML document path (we are past the isRscRequest early return, so
|
|
240
|
+
// !isPartial holds), with no per-request nonce (a frozen prelude cannot carry a
|
|
241
|
+
// fresh nonce), not under allReady buffering (which defeats streaming), and only
|
|
242
|
+
// when the SSR module actually exports the resume strategy. When we resume we
|
|
243
|
+
// MUST mark the response with x-rango-shell-resumed so the middleware prepends
|
|
244
|
+
// the cached prelude; if any guard fails we fall through to a normal renderHTML
|
|
245
|
+
// with no marker and the middleware fails open to axis 1.
|
|
246
|
+
const shellResume = reqCtx._shellResume;
|
|
247
|
+
let response: Response;
|
|
248
|
+
if (
|
|
249
|
+
shellResume &&
|
|
250
|
+
!isPartial &&
|
|
251
|
+
nonce === undefined &&
|
|
252
|
+
streamMode !== "allReady" &&
|
|
253
|
+
ssrModule.resumeShellHTML
|
|
254
|
+
) {
|
|
255
|
+
const resumedStream = await observePhase(PHASES.ssr, () =>
|
|
256
|
+
ssrModule.resumeShellHTML!(rscStream, {
|
|
257
|
+
postponed: shellResume.postponed,
|
|
258
|
+
nonce,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
response = createResponseWithMergedHeaders(resumedStream, {
|
|
262
|
+
headers: {
|
|
263
|
+
"content-type": "text/html;charset=utf-8",
|
|
264
|
+
"x-rango-shell-resumed": "1",
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
// ssr-render-html metric + rango.ssr span from one boundary. render:total is
|
|
269
|
+
// recorded by the observePhase wrapper around this function.
|
|
270
|
+
const htmlStream = await observePhase(PHASES.ssr, () =>
|
|
271
|
+
ssrModule.renderHTML(rscStream, {
|
|
272
|
+
nonce,
|
|
273
|
+
streamMode,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
response = createResponseWithMergedHeaders(htmlStream, {
|
|
277
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Axis 2: PPR shell CAPTURE (background task; see design doc) ---
|
|
282
|
+
// The middleware set reqCtx._shellCapture (the "capture wanted" descriptor)
|
|
283
|
+
// before its single next(). Capture does NOT flow through the HTTP pipeline: we
|
|
284
|
+
// schedule a background task that re-derives the shell via router.match() under
|
|
285
|
+
// its own derived context (fresh handle store, _shellCaptureRun: true), so the
|
|
286
|
+
// middleware chain never re-runs. Eligibility mirrors resume plus a servable
|
|
287
|
+
// 200-HTML gate; the descriptor is read now (still set — the middleware clears
|
|
288
|
+
// it in a finally after next() returns, which is after this synchronous point).
|
|
289
|
+
maybeScheduleShellCapture(
|
|
290
|
+
ctx,
|
|
291
|
+
request,
|
|
292
|
+
env,
|
|
293
|
+
url,
|
|
294
|
+
reqCtx,
|
|
295
|
+
ssrModule,
|
|
296
|
+
nonce,
|
|
297
|
+
streamMode,
|
|
298
|
+
isPartial,
|
|
299
|
+
response,
|
|
278
300
|
);
|
|
279
301
|
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Schedule a background PPR shell capture when the middleware requested one
|
|
307
|
+
* (`reqCtx._shellCapture` descriptor present) and this render is eligible: no
|
|
308
|
+
* per-request nonce (a frozen prelude cannot carry a fresh nonce), not under
|
|
309
|
+
* allReady buffering (which defeats streaming), the document path (never a
|
|
310
|
+
* partial), the SSR module exports the capture strategy, and the served response
|
|
311
|
+
* is a 200 HTML document (a 404/redirect/JSON is not a cacheable shell). All
|
|
312
|
+
* gating lives here so the middleware stays a thin descriptor-setter.
|
|
313
|
+
*/
|
|
314
|
+
function maybeScheduleShellCapture(
|
|
315
|
+
ctx: HandlerContext<any>,
|
|
316
|
+
request: Request,
|
|
317
|
+
env: any,
|
|
318
|
+
url: URL,
|
|
319
|
+
reqCtx: RequestContext<any>,
|
|
320
|
+
ssrModule: SSRModule,
|
|
321
|
+
nonce: string | undefined,
|
|
322
|
+
streamMode: import("../router/router-options.js").SSRStreamMode,
|
|
323
|
+
isPartial: boolean,
|
|
324
|
+
response: Response,
|
|
325
|
+
): void {
|
|
326
|
+
const descriptor = reqCtx._shellCapture;
|
|
327
|
+
if (!descriptor) return;
|
|
328
|
+
if (nonce !== undefined) return;
|
|
329
|
+
if (streamMode === "allReady") return;
|
|
330
|
+
if (isPartial) return;
|
|
331
|
+
if (!ssrModule.captureShellHTML) return;
|
|
332
|
+
if (response.status !== 200) return;
|
|
333
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/html")) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
scheduleShellCapture(ctx, request, env, url, reqCtx, ssrModule, descriptor);
|
|
283
337
|
}
|