@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.b30bbf02

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 (112) hide show
  1. package/README.md +112 -17
  2. package/dist/vite/index.js +1338 -462
  3. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  4. package/package.json +7 -5
  5. package/skills/breadcrumbs/SKILL.md +3 -1
  6. package/skills/handler-use/SKILL.md +362 -0
  7. package/skills/hooks/SKILL.md +33 -20
  8. package/skills/intercept/SKILL.md +20 -0
  9. package/skills/layout/SKILL.md +22 -0
  10. package/skills/links/SKILL.md +90 -16
  11. package/skills/loader/SKILL.md +70 -3
  12. package/skills/middleware/SKILL.md +34 -3
  13. package/skills/migrate-nextjs/SKILL.md +562 -0
  14. package/skills/migrate-react-router/SKILL.md +769 -0
  15. package/skills/parallel/SKILL.md +66 -0
  16. package/skills/rango/SKILL.md +25 -22
  17. package/skills/response-routes/SKILL.md +8 -0
  18. package/skills/route/SKILL.md +24 -0
  19. package/skills/server-actions/SKILL.md +739 -0
  20. package/skills/streams-and-websockets/SKILL.md +283 -0
  21. package/skills/typesafety/SKILL.md +3 -1
  22. package/src/browser/app-shell.ts +52 -0
  23. package/src/browser/event-controller.ts +44 -4
  24. package/src/browser/navigation-bridge.ts +71 -5
  25. package/src/browser/navigation-client.ts +64 -13
  26. package/src/browser/navigation-store.ts +25 -1
  27. package/src/browser/partial-update.ts +34 -3
  28. package/src/browser/prefetch/cache.ts +129 -21
  29. package/src/browser/prefetch/fetch.ts +148 -16
  30. package/src/browser/prefetch/queue.ts +36 -5
  31. package/src/browser/rango-state.ts +53 -13
  32. package/src/browser/react/Link.tsx +30 -2
  33. package/src/browser/react/NavigationProvider.tsx +70 -18
  34. package/src/browser/react/filter-segment-order.ts +51 -7
  35. package/src/browser/react/use-navigation.ts +22 -2
  36. package/src/browser/react/use-params.ts +11 -1
  37. package/src/browser/react/use-router.ts +8 -1
  38. package/src/browser/react/use-segments.ts +11 -8
  39. package/src/browser/rsc-router.tsx +34 -6
  40. package/src/browser/segment-reconciler.ts +36 -14
  41. package/src/browser/types.ts +19 -0
  42. package/src/build/route-trie.ts +50 -24
  43. package/src/cache/cf/cf-cache-store.ts +5 -7
  44. package/src/client.tsx +82 -174
  45. package/src/index.rsc.ts +3 -0
  46. package/src/index.ts +40 -9
  47. package/src/outlet-context.ts +1 -1
  48. package/src/response-utils.ts +28 -0
  49. package/src/reverse.ts +7 -3
  50. package/src/route-definition/dsl-helpers.ts +175 -23
  51. package/src/route-definition/helpers-types.ts +63 -14
  52. package/src/route-definition/resolve-handler-use.ts +6 -0
  53. package/src/route-types.ts +7 -0
  54. package/src/router/handler-context.ts +24 -4
  55. package/src/router/lazy-includes.ts +6 -6
  56. package/src/router/loader-resolution.ts +3 -0
  57. package/src/router/manifest.ts +22 -13
  58. package/src/router/match-api.ts +4 -3
  59. package/src/router/match-handlers.ts +1 -0
  60. package/src/router/match-result.ts +21 -2
  61. package/src/router/middleware-types.ts +2 -22
  62. package/src/router/middleware.ts +54 -7
  63. package/src/router/pattern-matching.ts +87 -17
  64. package/src/router/revalidation.ts +15 -1
  65. package/src/router/segment-resolution/fresh.ts +8 -0
  66. package/src/router/segment-resolution/revalidation.ts +128 -100
  67. package/src/router/trie-matching.ts +18 -13
  68. package/src/router/url-params.ts +49 -0
  69. package/src/router.ts +1 -2
  70. package/src/rsc/handler.ts +8 -4
  71. package/src/rsc/helpers.ts +69 -41
  72. package/src/rsc/progressive-enhancement.ts +4 -0
  73. package/src/rsc/response-route-handler.ts +14 -1
  74. package/src/rsc/rsc-rendering.ts +10 -0
  75. package/src/rsc/server-action.ts +4 -0
  76. package/src/rsc/types.ts +6 -0
  77. package/src/segment-content-promise.ts +67 -0
  78. package/src/segment-loader-promise.ts +122 -0
  79. package/src/segment-system.tsx +11 -61
  80. package/src/server/context.ts +26 -3
  81. package/src/server/request-context.ts +10 -42
  82. package/src/ssr/index.tsx +5 -1
  83. package/src/types/handler-context.ts +12 -39
  84. package/src/types/loader-types.ts +5 -6
  85. package/src/types/request-scope.ts +126 -0
  86. package/src/types/route-entry.ts +11 -0
  87. package/src/types/segments.ts +17 -1
  88. package/src/urls/include-helper.ts +24 -14
  89. package/src/urls/path-helper-types.ts +30 -4
  90. package/src/urls/response-types.ts +2 -10
  91. package/src/vite/debug.ts +184 -0
  92. package/src/vite/discovery/discover-routers.ts +31 -3
  93. package/src/vite/discovery/gate-state.ts +171 -0
  94. package/src/vite/discovery/prerender-collection.ts +48 -1
  95. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  96. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  97. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  98. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  99. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  100. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  101. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  102. package/src/vite/plugins/expose-action-id.ts +52 -28
  103. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  104. package/src/vite/plugins/expose-internal-ids.ts +516 -486
  105. package/src/vite/plugins/performance-tracks.ts +17 -9
  106. package/src/vite/plugins/use-cache-transform.ts +56 -43
  107. package/src/vite/plugins/version-injector.ts +37 -11
  108. package/src/vite/rango.ts +49 -14
  109. package/src/vite/router-discovery.ts +558 -53
  110. package/src/vite/utils/banner.ts +1 -1
  111. package/src/vite/utils/package-resolution.ts +41 -1
  112. package/src/vite/utils/prerender-utils.ts +20 -6
@@ -37,6 +37,8 @@ import { track, type MetricsStore } from "./context.js";
37
37
  import { getFetchableLoader } from "./fetchable-loader-store.js";
38
38
  import type { SegmentCacheStore } from "../cache/types.js";
39
39
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
40
+ import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
41
+ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
40
42
  import { THEME_COOKIE } from "../theme/constants.js";
41
43
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
42
44
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
@@ -58,22 +60,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
58
60
  export interface RequestContext<
59
61
  TEnv = DefaultEnv,
60
62
  TParams = Record<string, string>,
61
- > {
62
- /** Platform bindings (Cloudflare env, etc.) */
63
- env: TEnv;
64
- /** Original HTTP request */
65
- request: Request;
66
- /** Parsed URL (with internal `_rsc*` params stripped) */
67
- url: URL;
68
- /**
69
- * The original request URL with all parameters intact, including
70
- * internal `_rsc*` transport params.
71
- */
72
- originalUrl: URL;
73
- /** URL pathname */
74
- pathname: string;
75
- /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
76
- searchParams: URLSearchParams;
63
+ > extends RequestScope<TEnv> {
77
64
  /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
78
65
  _variables: Record<string, any>;
79
66
  /** Get a variable set by middleware */
@@ -159,20 +146,6 @@ export interface RequestContext<
159
146
  import("../cache/profile-registry.js").CacheProfile
160
147
  >;
161
148
 
162
- /**
163
- * Schedule work to run after the response is sent.
164
- * On Cloudflare Workers, uses ctx.waitUntil().
165
- * On Node.js, runs as fire-and-forget.
166
- *
167
- * @example
168
- * ```typescript
169
- * ctx.waitUntil(async () => {
170
- * await cacheStore.set(key, data, ttl);
171
- * });
172
- * ```
173
- */
174
- waitUntil(fn: () => Promise<void>): void;
175
-
176
149
  /**
177
150
  * Register a callback to run when the response is created.
178
151
  * Callbacks are sync and receive the response. They can:
@@ -498,13 +471,7 @@ export function requireRequestContext<
498
471
  return getRequestContext<TEnv>();
499
472
  }
500
473
 
501
- /**
502
- * Cloudflare Workers ExecutionContext (subset we need)
503
- */
504
- export interface ExecutionContext {
505
- waitUntil(promise: Promise<any>): void;
506
- passThroughOnException(): void;
507
- }
474
+ export type { ExecutionContext };
508
475
 
509
476
  /**
510
477
  * Options for creating a request context
@@ -768,16 +735,14 @@ export function createRequestContext<TEnv>(
768
735
 
769
736
  waitUntil(fn: () => Promise<void>): void {
770
737
  if (executionContext?.waitUntil) {
771
- // Cloudflare Workers: use native waitUntil
772
738
  executionContext.waitUntil(fn());
773
739
  } else {
774
- // Node.js / dev: fire-and-forget with error logging
775
- fn().catch((err) =>
776
- console.error("[waitUntil] Background task failed:", err),
777
- );
740
+ fireAndForgetWaitUntil(fn);
778
741
  }
779
742
  },
780
743
 
744
+ executionContext,
745
+
781
746
  _onResponseCallbacks: [],
782
747
 
783
748
  onResponse(callback: (response: Response) => Response): void {
@@ -1043,7 +1008,10 @@ export function createUseFunction<TEnv>(
1043
1008
  search: (ctx as any).search ?? {},
1044
1009
  pathname: ctx.pathname,
1045
1010
  url: ctx.url,
1011
+ originalUrl: ctx.originalUrl,
1046
1012
  env: ctx.env as any,
1013
+ waitUntil: ctx.waitUntil.bind(ctx),
1014
+ executionContext: ctx.executionContext,
1047
1015
  get: ctx.get as any,
1048
1016
  use: (<TDep, TDepParams = any>(
1049
1017
  dep: LoaderDefinition<TDep, TDepParams>,
package/src/ssr/index.tsx CHANGED
@@ -162,9 +162,13 @@ function createSsrEventController(opts: {
162
162
  }): EventController {
163
163
  const location = new URL(opts.pathname, "http://localhost");
164
164
  let params = opts.params ?? {};
165
+ const rawMatched = opts.matched ?? [];
165
166
  const handleState = {
166
167
  data: opts.handleData ?? {},
167
- segmentOrder: filterSegmentOrder(opts.matched ?? []),
168
+ segmentOrder: filterSegmentOrder(rawMatched),
169
+ routeSegmentIds: rawMatched.filter(
170
+ (id) => !id.includes(".@") && !/D\d+\./.test(id),
171
+ ),
168
172
  };
169
173
  const state: DerivedNavigationState = {
170
174
  state: "idle",
@@ -20,6 +20,7 @@ import type {
20
20
  } from "./route-config.js";
21
21
  import type { LoaderDefinition } from "./loader-types.js";
22
22
  import type { UseItems, HandlerUseItem } from "../route-types.js";
23
+ import type { RequestScope } from "./request-scope.js";
23
24
 
24
25
  // Re-export MiddlewareFn for internal/advanced use
25
26
  export type { MiddlewareFn } from "../router/middleware.js";
@@ -195,7 +196,7 @@ export type HandlerContext<
195
196
  TEnv = DefaultEnv,
196
197
  TSearch extends SearchSchema = {},
197
198
  TRouteMap = never,
198
- > = {
199
+ > = RequestScope<TEnv> & {
199
200
  /**
200
201
  * Route parameters extracted from the URL pattern.
201
202
  * Type-safe when using Handler<"/path/:param"> or Handler<{ param: string }>.
@@ -215,44 +216,11 @@ export type HandlerContext<
215
216
  * changing build semantics (e.g., skip expensive operations in dev).
216
217
  */
217
218
  dev: boolean;
218
- /**
219
- * The original incoming Request object (transport URL intact).
220
- * Use `ctx.url` / `ctx.searchParams` for application logic — those have
221
- * internal `_rsc*` params stripped. `ctx.request` preserves the raw URL
222
- * for cases where you need original headers, method, or body.
223
- */
224
- request: Request;
225
- /**
226
- * Query parameters from the URL (system params like `_rsc*` are filtered).
227
- * Always a standard URLSearchParams instance.
228
- */
229
- searchParams: URLSearchParams;
230
219
  /**
231
220
  * Typed search parameters parsed from URL query string via the route's
232
221
  * search schema. Empty object when no schema is defined.
233
222
  */
234
223
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
235
- /**
236
- * The pathname portion of the request URL.
237
- */
238
- pathname: string;
239
- /**
240
- * The full URL object (with internal `_rsc*` params stripped).
241
- * Use this for application logic — routing, link generation, display.
242
- */
243
- url: URL;
244
- /**
245
- * The original request URL with all parameters intact, including
246
- * internal `_rsc*` transport params. Use `ctx.url` for application
247
- * logic — this is only needed for advanced cases like debugging
248
- * or custom cache keying.
249
- */
250
- originalUrl: URL;
251
- /**
252
- * Platform bindings (DB, KV, secrets, etc.).
253
- * Access resources like `ctx.env.DB`, `ctx.env.KV`.
254
- */
255
- env: TEnv;
256
224
  /**
257
225
  * Type-safe getter for middleware variables.
258
226
  * Preferred way to read middleware-injected variables.
@@ -503,13 +471,16 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
503
471
  * **Return Types:**
504
472
  * - `boolean` - Hard decision: immediately returns this value (short-circuits)
505
473
  * - `{ defaultShouldRevalidate: boolean }` - Soft decision: updates suggestion for next revalidator
474
+ * - `void` / `null` / `undefined` - Defer to the current suggestion (no opinion); the
475
+ * loop continues to the next revalidator without changing the running default
506
476
  *
507
477
  * **Execution Flow:**
508
478
  * 1. Start with built-in `defaultShouldRevalidate` (true if params changed)
509
479
  * 2. Execute global revalidators first, then route-specific
510
480
  * 3. Hard decision (boolean): stop immediately and use that value
511
481
  * 4. Soft decision (object): update suggestion and continue to next revalidator
512
- * 5. If all return soft decisions: use the final suggestion
482
+ * 5. Defer (`void` / `null` / `undefined`): leave suggestion unchanged and continue
483
+ * 6. If no hard decision was returned: use the final running suggestion
513
484
  *
514
485
  * @param args.currentParams - Previous route params (generic by default, can be narrowed)
515
486
  * @param args.currentUrl - Previous URL
@@ -521,7 +492,8 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
521
492
  * @param args.formData - Form data from action (future support)
522
493
  * @param args.formMethod - HTTP method from action (future support)
523
494
  *
524
- * @returns Hard decision (boolean) or soft suggestion (object)
495
+ * @returns Hard decision (boolean), soft suggestion (object), or defer
496
+ * (`void` / `null` / `undefined`) to keep the running suggestion as-is.
525
497
  *
526
498
  * @example
527
499
  * ```typescript
@@ -546,8 +518,9 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
546
518
  * a segment (layout, route, parallel slot, or loader) should be re-rendered.
547
519
  *
548
520
  * Return `true` to re-render, `false` to skip (keep client's current version),
549
- * or `{ defaultShouldRevalidate: boolean }` to override the default for
550
- * downstream segments.
521
+ * `{ defaultShouldRevalidate: boolean }` to update the running suggestion for
522
+ * downstream revalidators, or nothing (`void` / `null` / `undefined`) to defer
523
+ * to the current suggestion without changing it.
551
524
  *
552
525
  * @example
553
526
  * ```ts
@@ -647,7 +620,7 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
647
620
  * action that may have mutated backend state.
648
621
  */
649
622
  stale?: boolean;
650
- }) => boolean | { defaultShouldRevalidate: boolean };
623
+ }) => boolean | { defaultShouldRevalidate: boolean } | null | void;
651
624
 
652
625
  // MiddlewareFn is imported from "../router/middleware.js" and re-exported
653
626
 
@@ -3,11 +3,13 @@ import type { Handle } from "../handle.js";
3
3
  import type { MiddlewareFn } from "../router/middleware.js";
4
4
  import type { ScopedReverseFunction } from "../reverse.js";
5
5
  import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
6
+ import type { UseItems, LoaderUseItem } from "../route-types.js";
6
7
  import type {
7
8
  DefaultEnv,
8
9
  DefaultReverseRouteMap,
9
10
  DefaultVars,
10
11
  } from "./global-namespace.js";
12
+ import type { RequestScope } from "./request-scope.js";
11
13
 
12
14
  /**
13
15
  * Context passed to loader functions during execution
@@ -39,7 +41,7 @@ export type LoaderContext<
39
41
  TEnv = DefaultEnv,
40
42
  TBody = unknown,
41
43
  TSearch extends SearchSchema = {},
42
- > = {
44
+ > = RequestScope<TEnv> & {
43
45
  params: TParams;
44
46
  /**
45
47
  * Route params extracted from the URL pattern match (server-side only).
@@ -48,12 +50,7 @@ export type LoaderContext<
48
50
  * resource scoping.
49
51
  */
50
52
  routeParams: Record<string, string>;
51
- request: Request;
52
- searchParams: URLSearchParams;
53
53
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
54
- pathname: string;
55
- url: URL;
56
- env: TEnv;
57
54
  get: {
58
55
  <T>(contextVar: ContextVar<T>): T | undefined;
59
56
  } & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
@@ -207,4 +204,6 @@ export type LoaderDefinition<
207
204
  __brand: "loader";
208
205
  $$id: string; // Injected by Vite plugin (exposeInternalIds) - unique identifier
209
206
  fn?: LoaderFn<T, TParams, any>; // Optional - server-side only, stored in registry for RSC
207
+ /** Composable default DSL items merged when the loader is mounted. */
208
+ use?: () => UseItems<LoaderUseItem>;
210
209
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * RequestScope: the fields every user-facing context shares.
3
+ *
4
+ * A handler, middleware, loader, response handler, and the ALS-bound
5
+ * RequestContext are all different phases of the same request, and they
6
+ * all carry the same set of request-scoped capabilities: the raw Request,
7
+ * the parsed URL pair (`url` is cleaned of internal `_rsc*` params,
8
+ * `originalUrl` retains them), pathname/searchParams, platform bindings
9
+ * (`env`), and two escape hatches for work that outlives the response
10
+ * (`waitUntil`) or needs the raw Cloudflare runtime object
11
+ * (`executionContext`).
12
+ *
13
+ * Each public context type intersects `RequestScope<TEnv>` with its own
14
+ * phase-specific fields (e.g. `params`/`reverse` on HandlerContext,
15
+ * `headers`/`header()` on MiddlewareContext). That keeps platform surface
16
+ * in one place and lets the next runtime escape hatch we need land in
17
+ * one file instead of four.
18
+ */
19
+
20
+ import type { DefaultEnv } from "./global-namespace.js";
21
+
22
+ /**
23
+ * Minimal subset of Cloudflare Workers' ExecutionContext that the router
24
+ * uses. Defined locally so the package does not depend on
25
+ * `@cloudflare/workers-types`. Consumers that want the full type can cast.
26
+ *
27
+ * On non-Cloudflare runtimes (Node, dev server, tests), this is undefined
28
+ * — portable apps should prefer `ctx.waitUntil(...)`, which degrades
29
+ * gracefully. `ctx.executionContext` is the escape hatch for libraries
30
+ * (MCP, Durable Object routing, etc.) that type their arguments as the
31
+ * raw ExecutionContext.
32
+ */
33
+ export interface ExecutionContext {
34
+ waitUntil(promise: Promise<any>): void;
35
+ passThroughOnException(): void;
36
+ }
37
+
38
+ /**
39
+ * Fallback `waitUntil` body used when no Cloudflare `ExecutionContext`
40
+ * is available (Node, dev, tests). Runs the work fire-and-forget and
41
+ * logs errors so they don't silently swallow.
42
+ *
43
+ * Exported so every `waitUntil` call site degrades identically instead
44
+ * of inventing its own fallback policy.
45
+ */
46
+ export function fireAndForgetWaitUntil(fn: () => Promise<void>): void {
47
+ fn().catch((err) =>
48
+ console.error("[waitUntil] Background task failed:", err),
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Fields present on every user-facing request context.
54
+ *
55
+ * @template TEnv - Platform bindings type (Cloudflare env, etc.).
56
+ */
57
+ export interface RequestScope<TEnv = DefaultEnv> {
58
+ /**
59
+ * The original incoming Request object (transport URL intact).
60
+ * Use `url` / `searchParams` for application logic — those have
61
+ * internal `_rsc*` params stripped. `request` preserves the raw URL
62
+ * when you need original headers, method, or body.
63
+ */
64
+ request: Request;
65
+
66
+ /**
67
+ * The request URL with internal `_rsc*` transport params stripped.
68
+ * Use this for routing, link generation, and display.
69
+ */
70
+ url: URL;
71
+
72
+ /**
73
+ * The original request URL with all parameters intact, including
74
+ * internal `_rsc*` transport params. Use `url` for application logic
75
+ * — this is only needed for advanced cases like debugging or custom
76
+ * cache keying.
77
+ */
78
+ originalUrl: URL;
79
+
80
+ /** URL pathname (same as `url.pathname`). */
81
+ pathname: string;
82
+
83
+ /**
84
+ * Query parameters from the URL (system params like `_rsc*` are
85
+ * filtered). Always a standard `URLSearchParams` instance.
86
+ */
87
+ searchParams: URLSearchParams;
88
+
89
+ /**
90
+ * Platform bindings (DB, KV, secrets, etc.). On Cloudflare Workers
91
+ * these are the `env` object passed to the Worker's `fetch()` handler.
92
+ */
93
+ env: TEnv;
94
+
95
+ /**
96
+ * Schedule work to run after the response is sent.
97
+ * On Cloudflare Workers, delegates to `executionContext.waitUntil()`.
98
+ * On Node / dev / tests, runs as fire-and-forget with error logging.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * ctx.waitUntil(async () => {
103
+ * await cacheStore.set(key, data, ttl);
104
+ * });
105
+ * ```
106
+ */
107
+ waitUntil(fn: () => Promise<void>): void;
108
+
109
+ /**
110
+ * Raw Cloudflare Workers `ExecutionContext`, when running on a
111
+ * Cloudflare-compatible runtime. Undefined elsewhere.
112
+ *
113
+ * Escape hatch for libraries that type their arguments as
114
+ * `ExecutionContext` (MCP `fetch`, `routeAgentRequest`, etc.).
115
+ * For the common "do work after the response" case, prefer
116
+ * `ctx.waitUntil(...)` — it is platform-neutral.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * path.any("/mcp", (ctx) =>
121
+ * emailMcp.fetch(ctx.request, ctx.env, ctx.executionContext!),
122
+ * );
123
+ * ```
124
+ */
125
+ executionContext?: ExecutionContext;
126
+ }
@@ -8,10 +8,21 @@ export interface LazyIncludeContext {
8
8
  urlPrefix: string;
9
9
  namePrefix: string | undefined;
10
10
  parent: unknown; // EntryData - avoid circular import
11
+ /** Counter snapshot from pattern extraction for consistent shortCode indices */
12
+ counters?: Record<string, number>;
11
13
  cacheProfiles?: Record<
12
14
  string,
13
15
  import("../cache/profile-registry.js").CacheProfile
14
16
  >;
17
+ /** Root scope flag for dot-local reverse resolution */
18
+ rootScoped?: boolean;
19
+ /**
20
+ * Positional include scope token composed from the parent scope plus this
21
+ * include's sibling index (`${parentScope}I${idx}`). Applied to direct-
22
+ * descendant shortCodes during lazy evaluation so routes inside the
23
+ * include cannot collide with siblings declared outside it.
24
+ */
25
+ includeScope?: string;
15
26
  }
16
27
 
17
28
  /**
@@ -56,13 +56,20 @@ export interface ResolvedSegment {
56
56
  // Intercept loader fields (for streaming loader data in parallel segments)
57
57
  loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
58
58
  loaderIds?: string[]; // IDs ($$id) of loaders for this segment
59
- parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
60
59
  // Error-specific fields
61
60
  error?: ErrorInfo; // For error segments: the error information
62
61
  // NotFound-specific fields
63
62
  notFoundInfo?: NotFoundInfo; // For notFound segments: the not found information
64
63
  // Mount path from include() scope, used for MountContext.Provider wrapping
65
64
  mountPath?: string;
65
+ /**
66
+ * @internal Server-side marker: true when the segment's handler actually ran
67
+ * this request (not skipped via the revalidate cache path). Used by
68
+ * match-result.ts to populate `MatchResult.resolvedIds` for client-side
69
+ * handle-bucket cleanup. Stripped from the wire payload before serialization
70
+ * — never reaches the client.
71
+ */
72
+ _handlerRan?: boolean;
66
73
  }
67
74
 
68
75
  /**
@@ -117,6 +124,15 @@ export interface MatchResult {
117
124
  segments: ResolvedSegment[];
118
125
  matched: string[];
119
126
  diff: string[];
127
+ /**
128
+ * Every segment id whose handler actually ran on the server this request,
129
+ * including ones with `component === null` that get filtered out of
130
+ * `segments`/`diff` to avoid wasted bytes. Drives the client's handle-
131
+ * cleanup pass — a slot that re-resolves and pushes nothing must clear
132
+ * its previous handle bucket, but `diff` doesn't carry it because the
133
+ * segment payload doesn't either. A superset of `diff`.
134
+ */
135
+ resolvedIds: string[];
120
136
  /**
121
137
  * Merged route params from all matched segments
122
138
  * Available for use by the handler after route matching
@@ -4,7 +4,6 @@ import {
4
4
  runWithPrefixes,
5
5
  getUrlPrefix,
6
6
  getNamePrefix,
7
- getRootScoped,
8
7
  } from "../server/context";
9
8
  import {
10
9
  INTERNAL_INCLUDE_SCOPE_PREFIX,
@@ -149,22 +148,32 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
149
148
  });
150
149
  }
151
150
 
152
- // Snapshot parent's counters so lazy manifest generation starts
153
- // at the correct index, preventing shortCode collisions with
154
- // sibling entries (e.g., BlogLayout and ArticlesLayout under NavLayout).
155
- const capturedCounters = { ...ctx.counters };
156
-
157
- // Reserve a layout slot in the parent's counter so sibling lazy includes
158
- // produce different shortCode indices for their root layout.
159
- // Without this, consecutive include() calls capture identical counters
160
- // and their first child layouts get the same shortCode (e.g., both M0L0L0),
161
- // causing the client partial-update diff to see no changes on navigation.
151
+ // Allocate an include-scope token for this include() call. The token is
152
+ // appended to the parent's shortCode prefix whenever the include's
153
+ // direct-descendant shortCodes are generated (see getShortCode in
154
+ // context.ts), partitioning the parent's counter namespace so routes
155
+ // inside an include cannot collide with siblings declared outside it.
156
+ //
157
+ // Scopes compose: a nested include inside an outer include with scope
158
+ // "I0" allocates against the `${parent.shortCode}I0_include` counter
159
+ // and produces scope "I0I0", "I0I1", etc.
160
+ const parentScope = ctx.includeScope ?? "";
161
+ let includeScope = parentScope;
162
162
  if (capturedParent?.shortCode) {
163
- const layoutCounterKey = `${capturedParent.shortCode}_layout`;
164
- ctx.counters[layoutCounterKey] ??= 0;
165
- ctx.counters[layoutCounterKey]++;
163
+ const includeCounterKey = `${capturedParent.shortCode}${parentScope}_include`;
164
+ ctx.counters[includeCounterKey] ??= 0;
165
+ const includeIdx = ctx.counters[includeCounterKey];
166
+ ctx.counters[includeCounterKey] = includeIdx + 1;
167
+ includeScope = `${parentScope}I${includeIdx}`;
166
168
  }
167
169
 
170
+ // Snapshot parent's counters AFTER allocating the include scope so lazy
171
+ // manifest generation starts with the same counter state this include
172
+ // observed — its descendants still get fresh per-scope counters because
173
+ // they key off `${parent.shortCode}${includeScope}_*` (not shared with
174
+ // siblings outside the include).
175
+ const capturedCounters = { ...ctx.counters };
176
+
168
177
  // Compute rootScoped at capture time, mirroring the logic in runWithPrefixes.
169
178
  // This ensures lazy evaluation restores the correct scope state.
170
179
  const parentRootScoped = ctx.rootScoped;
@@ -191,6 +200,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
191
200
  counters: capturedCounters,
192
201
  cacheProfiles: ctx.cacheProfiles,
193
202
  rootScoped: capturedRootScoped,
203
+ includeScope,
194
204
  },
195
205
  } as IncludeItem;
196
206
  };
@@ -233,12 +233,27 @@ export type PathHelpers<TEnv> = {
233
233
  include: IncludeFn<TEnv>;
234
234
 
235
235
  /**
236
- * Define parallel routes that render simultaneously in named slots
236
+ * Define parallel routes that render simultaneously in named slots.
237
+ *
238
+ * A slot value can be a Handler / ReactNode / StaticHandlerDefinition
239
+ * (legacy form, broadcast use applies to every slot) or a slot descriptor
240
+ * `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
241
+ * merge order is `handler.use` → shared `use` → slot-local `use`, with
242
+ * narrowest scope winning for last-write-wins items like `loading()`.
237
243
  */
238
244
  parallel: <
239
245
  TSlots extends Record<
240
246
  `@${string}`,
241
- Handler<any, any, TEnv> | ReactNode | StaticHandlerDefinition
247
+ | Handler<any, any, TEnv>
248
+ | ReactNode
249
+ | StaticHandlerDefinition
250
+ | {
251
+ handler:
252
+ | Handler<any, any, TEnv>
253
+ | ReactNode
254
+ | StaticHandlerDefinition;
255
+ use?: () => ParallelUseItem[];
256
+ }
242
257
  >,
243
258
  >(
244
259
  slots: TSlots,
@@ -264,9 +279,20 @@ export type PathHelpers<TEnv> = {
264
279
  ) => InterceptItem;
265
280
 
266
281
  /**
267
- * Attach middleware to the current route/layout
282
+ * Attach middleware to the current route/layout, or wrap child segments
268
283
  */
269
- middleware: (...fns: MiddlewareFn<TEnv>[]) => MiddlewareItem;
284
+ middleware: {
285
+ (fn: MiddlewareFn<TEnv>): MiddlewareItem;
286
+ (
287
+ fn: MiddlewareFn<TEnv>,
288
+ children: () => UseItems<LayoutUseItem>,
289
+ ): MiddlewareItem;
290
+ (fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
291
+ (
292
+ fns: MiddlewareFn<TEnv>[],
293
+ children: () => UseItems<LayoutUseItem>,
294
+ ): MiddlewareItem;
295
+ };
270
296
 
271
297
  /**
272
298
  * Control when a segment should revalidate during navigation
@@ -5,6 +5,7 @@ import type {
5
5
  DefaultVars,
6
6
  } from "../types/global-namespace.js";
7
7
  import type { UseItems, ResponseRouteUseItem } from "../route-types.js";
8
+ import type { RequestScope } from "../types/request-scope.js";
8
9
 
9
10
  /**
10
11
  * Reverse function for response handler contexts.
@@ -93,19 +94,10 @@ export type TextResponseHandler<
93
94
  export interface ResponseHandlerContext<
94
95
  TParams = Record<string, string>,
95
96
  TEnv = any,
96
- > {
97
- request: Request;
97
+ > extends RequestScope<TEnv> {
98
98
  params: TParams;
99
99
  /** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */
100
100
  readonly _paramCheck?: (params: TParams) => TParams;
101
- /** Platform bindings (DB, KV, secrets, etc.). */
102
- env: TEnv;
103
- /** Query parameters from the URL (system params like `_rsc*` are filtered). */
104
- searchParams: URLSearchParams;
105
- /** The full URL object (with system params filtered). */
106
- url: URL;
107
- /** The pathname portion of the request URL. */
108
- pathname: string;
109
101
  reverse: ResponseReverseFunction;
110
102
  /** Read a variable set by middleware via ctx.set(key, value) or ctx.set(ContextVar, value). */
111
103
  get: {