@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.
Files changed (45) hide show
  1. package/dist/bin/rango.js +27 -2
  2. package/dist/vite/index.js +147 -30
  3. package/package.json +1 -1
  4. package/skills/breadcrumbs/SKILL.md +1 -1
  5. package/skills/cache-guide/SKILL.md +1 -0
  6. package/skills/caching/SKILL.md +1 -1
  7. package/skills/migrate-nextjs/SKILL.md +15 -0
  8. package/skills/migrate-react-router/SKILL.md +15 -2
  9. package/skills/ppr/SKILL.md +426 -0
  10. package/skills/rango/SKILL.md +28 -25
  11. package/skills/route/SKILL.md +43 -0
  12. package/src/build/route-trie.ts +35 -7
  13. package/src/cache/cf/cf-cache-store.ts +155 -0
  14. package/src/cache/index.ts +6 -0
  15. package/src/cache/memory-segment-store.ts +57 -1
  16. package/src/cache/shell-cache.ts +386 -0
  17. package/src/cache/types.ts +58 -0
  18. package/src/cache/vercel/vercel-cache-store.ts +159 -5
  19. package/src/index.rsc.ts +5 -0
  20. package/src/index.ts +17 -0
  21. package/src/router/middleware.ts +14 -5
  22. package/src/router/parse-pattern.ts +115 -0
  23. package/src/router/pattern-matching.ts +53 -64
  24. package/src/router/segment-resolution/fresh.ts +12 -1
  25. package/src/router/segment-resolution/loader-cache.ts +14 -0
  26. package/src/router/segment-resolution/loader-mask.ts +44 -0
  27. package/src/router/substitute-pattern-params.ts +54 -35
  28. package/src/router/trie-matching.ts +19 -11
  29. package/src/router/url-params.ts +13 -0
  30. package/src/rsc/full-payload.ts +70 -0
  31. package/src/rsc/rsc-rendering.ts +105 -51
  32. package/src/rsc/shell-capture.ts +439 -0
  33. package/src/rsc/types.ts +26 -0
  34. package/src/server/cookie-store.ts +45 -0
  35. package/src/server/live.ts +130 -0
  36. package/src/server/request-context.ts +49 -0
  37. package/src/ssr/index.tsx +377 -180
  38. package/src/ssr/ssr-root.tsx +228 -0
  39. package/src/testing/render-route.tsx +7 -9
  40. package/src/types/route-config.ts +19 -7
  41. package/src/urls/type-extraction.ts +43 -18
  42. package/src/vite/discovery/discovery-errors.ts +61 -0
  43. package/src/vite/plugins/virtual-entries.ts +27 -2
  44. package/src/vite/router-discovery.ts +69 -15
  45. 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
+ }
@@ -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 { MatchResult } from "../types.js";
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 { resolvedHandleStream } from "../handles/deferred-resolution.js";
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
- // ssr-render-html metric + rango.ssr span from one boundary. render:total is
272
- // recorded by the observePhase wrapper around this function.
273
- const htmlStream = await observePhase(PHASES.ssr, () =>
274
- ssrModule.renderHTML(rscStream, {
275
- nonce,
276
- streamMode,
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 createResponseWithMergedHeaders(htmlStream, {
281
- headers: { "content-type": "text/html;charset=utf-8" },
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
  }