@rangojs/router 0.0.0-experimental.8678bb02 → 0.0.0-experimental.87

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 (147) hide show
  1. package/README.md +126 -38
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +847 -384
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +5 -5
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/handler-use/SKILL.md +362 -0
  9. package/skills/hooks/SKILL.md +28 -20
  10. package/skills/intercept/SKILL.md +20 -0
  11. package/skills/layout/SKILL.md +22 -0
  12. package/skills/links/SKILL.md +91 -17
  13. package/skills/loader/SKILL.md +35 -2
  14. package/skills/middleware/SKILL.md +34 -3
  15. package/skills/migrate-nextjs/SKILL.md +560 -0
  16. package/skills/migrate-react-router/SKILL.md +765 -0
  17. package/skills/parallel/SKILL.md +59 -0
  18. package/skills/prerender/SKILL.md +110 -68
  19. package/skills/rango/SKILL.md +24 -22
  20. package/skills/response-routes/SKILL.md +8 -0
  21. package/skills/route/SKILL.md +24 -0
  22. package/skills/router-setup/SKILL.md +35 -0
  23. package/skills/streams-and-websockets/SKILL.md +283 -0
  24. package/skills/typesafety/SKILL.md +3 -1
  25. package/src/__internal.ts +1 -1
  26. package/src/browser/app-shell.ts +52 -0
  27. package/src/browser/app-version.ts +14 -0
  28. package/src/browser/navigation-bridge.ts +87 -6
  29. package/src/browser/navigation-client.ts +128 -77
  30. package/src/browser/navigation-store.ts +68 -9
  31. package/src/browser/partial-update.ts +60 -7
  32. package/src/browser/prefetch/cache.ts +129 -21
  33. package/src/browser/prefetch/fetch.ts +156 -18
  34. package/src/browser/prefetch/queue.ts +36 -5
  35. package/src/browser/rango-state.ts +53 -13
  36. package/src/browser/react/Link.tsx +72 -8
  37. package/src/browser/react/NavigationProvider.tsx +57 -11
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-navigation.ts +22 -2
  41. package/src/browser/react/use-params.ts +11 -1
  42. package/src/browser/react/use-router.ts +29 -9
  43. package/src/browser/rsc-router.tsx +60 -9
  44. package/src/browser/scroll-restoration.ts +10 -8
  45. package/src/browser/segment-reconciler.ts +36 -14
  46. package/src/browser/server-action-bridge.ts +8 -18
  47. package/src/browser/types.ts +33 -5
  48. package/src/build/generate-manifest.ts +6 -6
  49. package/src/build/generate-route-types.ts +3 -0
  50. package/src/build/route-trie.ts +50 -24
  51. package/src/build/route-types/include-resolution.ts +8 -1
  52. package/src/build/route-types/router-processing.ts +211 -72
  53. package/src/build/route-types/scan-filter.ts +8 -1
  54. package/src/cache/cf/cf-cache-store.ts +5 -7
  55. package/src/client.tsx +84 -230
  56. package/src/deps/browser.ts +0 -1
  57. package/src/handle.ts +40 -0
  58. package/src/index.rsc.ts +6 -1
  59. package/src/index.ts +49 -6
  60. package/src/outlet-context.ts +1 -1
  61. package/src/prerender/store.ts +5 -4
  62. package/src/prerender.ts +138 -77
  63. package/src/response-utils.ts +28 -0
  64. package/src/reverse.ts +27 -2
  65. package/src/route-definition/dsl-helpers.ts +210 -35
  66. package/src/route-definition/helpers-types.ts +61 -14
  67. package/src/route-definition/index.ts +3 -0
  68. package/src/route-definition/redirect.ts +9 -1
  69. package/src/route-definition/resolve-handler-use.ts +155 -0
  70. package/src/route-types.ts +18 -0
  71. package/src/router/content-negotiation.ts +100 -1
  72. package/src/router/handler-context.ts +70 -17
  73. package/src/router/intercept-resolution.ts +9 -4
  74. package/src/router/lazy-includes.ts +6 -6
  75. package/src/router/loader-resolution.ts +153 -21
  76. package/src/router/manifest.ts +22 -13
  77. package/src/router/match-api.ts +127 -192
  78. package/src/router/match-middleware/cache-lookup.ts +28 -8
  79. package/src/router/match-middleware/segment-resolution.ts +53 -0
  80. package/src/router/match-result.ts +82 -4
  81. package/src/router/middleware-types.ts +2 -28
  82. package/src/router/middleware.ts +32 -7
  83. package/src/router/navigation-snapshot.ts +182 -0
  84. package/src/router/pattern-matching.ts +60 -9
  85. package/src/router/prerender-match.ts +110 -10
  86. package/src/router/preview-match.ts +30 -102
  87. package/src/router/request-classification.ts +310 -0
  88. package/src/router/route-snapshot.ts +245 -0
  89. package/src/router/router-interfaces.ts +36 -4
  90. package/src/router/router-options.ts +37 -11
  91. package/src/router/segment-resolution/fresh.ts +70 -5
  92. package/src/router/segment-resolution/revalidation.ts +87 -9
  93. package/src/router/trie-matching.ts +10 -4
  94. package/src/router/url-params.ts +49 -0
  95. package/src/router.ts +54 -7
  96. package/src/rsc/handler.ts +478 -399
  97. package/src/rsc/helpers.ts +69 -41
  98. package/src/rsc/loader-fetch.ts +18 -3
  99. package/src/rsc/manifest-init.ts +5 -1
  100. package/src/rsc/progressive-enhancement.ts +14 -3
  101. package/src/rsc/response-route-handler.ts +14 -1
  102. package/src/rsc/rsc-rendering.ts +15 -2
  103. package/src/rsc/server-action.ts +10 -2
  104. package/src/rsc/ssr-setup.ts +2 -2
  105. package/src/rsc/types.ts +6 -4
  106. package/src/segment-content-promise.ts +67 -0
  107. package/src/segment-loader-promise.ts +122 -0
  108. package/src/segment-system.tsx +11 -61
  109. package/src/server/context.ts +65 -5
  110. package/src/server/handle-store.ts +19 -0
  111. package/src/server/loader-registry.ts +9 -8
  112. package/src/server/request-context.ts +142 -55
  113. package/src/ssr/index.tsx +3 -0
  114. package/src/static-handler.ts +18 -6
  115. package/src/types/cache-types.ts +4 -4
  116. package/src/types/handler-context.ts +17 -43
  117. package/src/types/loader-types.ts +37 -11
  118. package/src/types/request-scope.ts +126 -0
  119. package/src/types/route-entry.ts +12 -1
  120. package/src/types/segments.ts +1 -1
  121. package/src/urls/include-helper.ts +24 -14
  122. package/src/urls/path-helper-types.ts +39 -6
  123. package/src/urls/path-helper.ts +47 -12
  124. package/src/urls/pattern-types.ts +12 -0
  125. package/src/urls/response-types.ts +18 -16
  126. package/src/use-loader.tsx +77 -5
  127. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  128. package/src/vite/discovery/discover-routers.ts +5 -1
  129. package/src/vite/discovery/prerender-collection.ts +128 -74
  130. package/src/vite/discovery/state.ts +13 -4
  131. package/src/vite/index.ts +4 -0
  132. package/src/vite/plugin-types.ts +60 -5
  133. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  134. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  135. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  136. package/src/vite/plugins/expose-id-utils.ts +12 -0
  137. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  138. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  139. package/src/vite/plugins/performance-tracks.ts +64 -206
  140. package/src/vite/plugins/refresh-cmd.ts +88 -26
  141. package/src/vite/rango.ts +40 -18
  142. package/src/vite/router-discovery.ts +237 -37
  143. package/src/vite/utils/banner.ts +1 -1
  144. package/src/vite/utils/package-resolution.ts +1 -1
  145. package/src/vite/utils/prerender-utils.ts +37 -5
  146. package/src/vite/utils/shared-utils.ts +3 -2
  147. package/src/browser/debug-channel.ts +0 -93
@@ -5,8 +5,8 @@
5
5
  * during cache key generation (before middleware runs).
6
6
  *
7
7
  * Note: While the full RequestContext is passed, middleware-set variables
8
- * (ctx.var, ctx.get()) may not be populated yet since cache lookup
9
- * happens before middleware execution.
8
+ * read via `ctx.get()` may not be populated yet since cache lookup happens
9
+ * before middleware execution.
10
10
  */
11
11
  export type { RequestContext as CacheContext } from "../server/request-context.js";
12
12
 
@@ -101,7 +101,7 @@ export interface CacheOptions<TEnv = unknown> {
101
101
  * Return false to skip cache for this request (always fetch fresh).
102
102
  *
103
103
  * Has access to full RequestContext including env, request, params, cookies, etc.
104
- * Note: Middleware-set variables (ctx.var) may not be populated yet.
104
+ * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
105
105
  *
106
106
  * @example
107
107
  * ```typescript
@@ -123,7 +123,7 @@ export interface CacheOptions<TEnv = unknown> {
123
123
  * Bypasses default key generation AND store's keyGenerator.
124
124
  *
125
125
  * Has access to full RequestContext including env, request, params, cookies, etc.
126
- * Note: Middleware-set variables (ctx.var) may not be populated yet.
126
+ * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
127
127
  *
128
128
  * @example
129
129
  * ```typescript
@@ -19,6 +19,8 @@ import type {
19
19
  ResolvedRouteMap,
20
20
  } from "./route-config.js";
21
21
  import type { LoaderDefinition } from "./loader-types.js";
22
+ import type { UseItems, HandlerUseItem } from "../route-types.js";
23
+ import type { RequestScope } from "./request-scope.js";
22
24
 
23
25
  // Re-export MiddlewareFn for internal/advanced use
24
26
  export type { MiddlewareFn } from "../router/middleware.js";
@@ -135,7 +137,7 @@ export type Handler<
135
137
  | Record<string, any> = {},
136
138
  TRouteMap extends {} = DefaultHandlerRouteMap,
137
139
  TEnv = DefaultEnv,
138
- > = (
140
+ > = ((
139
141
  ctx: HandlerContext<
140
142
  T extends `.${infer Local}`
141
143
  ? Local extends keyof TRouteMap
@@ -160,7 +162,10 @@ export type Handler<
160
162
  : ExtractSearchFromEntry<DefaultHandlerRouteMap, T>,
161
163
  TRouteMap extends DefaultHandlerRouteMap ? never : TRouteMap
162
164
  >,
163
- ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
165
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>) & {
166
+ /** Composable default DSL items merged when the handler is mounted. */
167
+ use?: () => UseItems<HandlerUseItem>;
168
+ };
164
169
 
165
170
  /**
166
171
  * Context passed to handlers (Hono-inspired type-safe context)
@@ -170,7 +175,7 @@ export type Handler<
170
175
  * - Cleaned route URL (`url`, `searchParams`, `pathname` — no `_rsc*` params)
171
176
  * - Original request (`request` — raw transport URL, headers, method, body)
172
177
  * - Platform bindings (env.DB, env.KV, env.SECRETS)
173
- * - Middleware variables (var.user, var.permissions)
178
+ * - Middleware variables (`get("user")`, `get("permissions")`)
174
179
  * - Getter/setter for variables (get('user'), set('user', ...))
175
180
  *
176
181
  * @example
@@ -178,8 +183,7 @@ export type Handler<
178
183
  * const handler = (ctx: HandlerContext<{ slug: string }, AppEnv>) => {
179
184
  * ctx.params.slug // Route param (string)
180
185
  * ctx.env.DB // Binding (D1Database)
181
- * ctx.var.user // Variable (User | undefined)
182
- * ctx.get('user') // Alternative getter
186
+ * ctx.get('user') // Variable (User | undefined)
183
187
  * ctx.set('user', {...}) // Setter
184
188
  * ctx.url // Clean URL (no _rsc* params)
185
189
  * ctx.searchParams // Clean params (no _rsc* params)
@@ -192,7 +196,7 @@ export type HandlerContext<
192
196
  TEnv = DefaultEnv,
193
197
  TSearch extends SearchSchema = {},
194
198
  TRouteMap = never,
195
- > = {
199
+ > = RequestScope<TEnv> & {
196
200
  /**
197
201
  * Route parameters extracted from the URL pattern.
198
202
  * Type-safe when using Handler<"/path/:param"> or Handler<{ param: string }>.
@@ -207,51 +211,19 @@ export type HandlerContext<
207
211
  */
208
212
  build: boolean;
209
213
  /**
210
- * The original incoming Request object (transport URL intact).
211
- * Use `ctx.url` / `ctx.searchParams` for application logic those have
212
- * internal `_rsc*` params stripped. `ctx.request` preserves the raw URL
213
- * for cases where you need original headers, method, or body.
214
- */
215
- request: Request;
216
- /**
217
- * Query parameters from the URL (system params like `_rsc*` are filtered).
218
- * Always a standard URLSearchParams instance.
214
+ * True when running in Vite dev mode, false during production build or
215
+ * live request rendering. Use this to branch on runtime mode without
216
+ * changing build semantics (e.g., skip expensive operations in dev).
219
217
  */
220
- searchParams: URLSearchParams;
218
+ dev: boolean;
221
219
  /**
222
220
  * Typed search parameters parsed from URL query string via the route's
223
221
  * search schema. Empty object when no schema is defined.
224
222
  */
225
223
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
226
- /**
227
- * The pathname portion of the request URL.
228
- */
229
- pathname: string;
230
- /**
231
- * The full URL object (with internal `_rsc*` params stripped).
232
- * Use this for application logic — routing, link generation, display.
233
- */
234
- url: URL;
235
- /**
236
- * The original request URL with all parameters intact, including
237
- * internal `_rsc*` transport params. Use `ctx.url` for application
238
- * logic — this is only needed for advanced cases like debugging
239
- * or custom cache keying.
240
- */
241
- originalUrl: URL;
242
- /**
243
- * Platform bindings (DB, KV, secrets, etc.).
244
- * Access resources like `ctx.env.DB`, `ctx.env.KV`.
245
- */
246
- env: TEnv;
247
- /**
248
- * Middleware-injected variables.
249
- * Access values like `ctx.var.user`, `ctx.var.permissions`.
250
- */
251
- var: DefaultVars;
252
224
  /**
253
225
  * Type-safe getter for middleware variables.
254
- * Alternative to `ctx.var.key` with better autocomplete.
226
+ * Preferred way to read middleware-injected variables.
255
227
  *
256
228
  * @example
257
229
  * ```typescript
@@ -448,6 +420,8 @@ export type InternalHandlerContext<
448
420
  > = HandlerContext<TParams, TEnv, TSearch> & {
449
421
  /** @internal Stub response for collecting headers/cookies. */
450
422
  res: Response;
423
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
424
+ _variables: Record<string, any>;
451
425
  /** Prerender-only control flow helper, attached when the runtime context supports it. */
452
426
  passthrough?: () => unknown;
453
427
  /** Current segment ID for handle data attribution. */
@@ -1,12 +1,15 @@
1
1
  import type { ContextVar } from "../context-var.js";
2
+ import type { Handle } from "../handle.js";
2
3
  import type { MiddlewareFn } from "../router/middleware.js";
3
4
  import type { ScopedReverseFunction } from "../reverse.js";
4
5
  import type { SearchSchema, ResolveSearchSchema } from "../search-params.js";
6
+ import type { UseItems, LoaderUseItem } from "../route-types.js";
5
7
  import type {
6
8
  DefaultEnv,
7
9
  DefaultReverseRouteMap,
8
10
  DefaultVars,
9
11
  } from "./global-namespace.js";
12
+ import type { RequestScope } from "./request-scope.js";
10
13
 
11
14
  /**
12
15
  * Context passed to loader functions during execution
@@ -38,7 +41,7 @@ export type LoaderContext<
38
41
  TEnv = DefaultEnv,
39
42
  TBody = unknown,
40
43
  TSearch extends SearchSchema = {},
41
- > = {
44
+ > = RequestScope<TEnv> & {
42
45
  params: TParams;
43
46
  /**
44
47
  * Route params extracted from the URL pattern match (server-side only).
@@ -47,22 +50,43 @@ export type LoaderContext<
47
50
  * resource scoping.
48
51
  */
49
52
  routeParams: Record<string, string>;
50
- request: Request;
51
- searchParams: URLSearchParams;
52
53
  search: {} extends TSearch ? {} : ResolveSearchSchema<TSearch>;
53
- pathname: string;
54
- url: URL;
55
- env: TEnv;
56
- var: DefaultVars;
57
54
  get: {
58
55
  <T>(contextVar: ContextVar<T>): T | undefined;
59
56
  } & (<K extends keyof DefaultVars>(key: K) => DefaultVars[K]);
60
57
  /**
61
- * Access another loader's data (returns promise since loaders run in parallel)
58
+ * Access another loader's data, or read handle data after rendered().
59
+ *
60
+ * For loaders: returns a promise (loaders run in parallel).
61
+ * For handles: returns collected data (only after `await ctx.rendered()`).
62
+ */
63
+ use: {
64
+ <T, TLoaderParams = any>(
65
+ loader: LoaderDefinition<T, TLoaderParams>,
66
+ ): Promise<T>;
67
+ <TData, TAccumulated = TData[]>(
68
+ handle: Handle<TData, TAccumulated>,
69
+ ): TAccumulated;
70
+ };
71
+ /**
72
+ * **Experimental.** Wait for all non-loader segments to settle.
73
+ *
74
+ * After the returned promise resolves, handle data is available via
75
+ * `ctx.use(handle)`. Only supported in DSL loaders on non-streaming
76
+ * trees (no `loading()`). Throws if called from a handler-invoked
77
+ * loader or when the tree uses streaming.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const PricesLoader = createLoader(async (ctx) => {
82
+ * "use server";
83
+ * await ctx.rendered();
84
+ * const products = ctx.use(Products); // reads handle data
85
+ * return pricing.getLive(products.map(p => p.id));
86
+ * });
87
+ * ```
62
88
  */
63
- use: <T, TLoaderParams = any>(
64
- loader: LoaderDefinition<T, TLoaderParams>,
65
- ) => Promise<T>;
89
+ rendered: () => Promise<void>;
66
90
  /**
67
91
  * HTTP method (GET, POST, PUT, PATCH, DELETE)
68
92
  * Available when loader is called via load({ method: "POST", ... })
@@ -180,4 +204,6 @@ export type LoaderDefinition<
180
204
  __brand: "loader";
181
205
  $$id: string; // Injected by Vite plugin (exposeInternalIds) - unique identifier
182
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>;
183
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
  /**
@@ -69,7 +80,7 @@ export interface RouteEntry<TEnv = any> {
69
80
  prerenderRouteKeys?: Set<string>;
70
81
 
71
82
  /**
72
- * Route keys in this entry that use `{ passthrough: true }`.
83
+ * Route keys in this entry that are wrapped with `Passthrough()`.
73
84
  * Used by the non-trie match path to set the `pt` flag.
74
85
  */
75
86
  passthroughRouteKeys?: Set<string>;
@@ -50,12 +50,12 @@ export interface ResolvedSegment {
50
50
  parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
51
51
  // Loader-specific fields
52
52
  loaderId?: string; // For loaders: the loader $$id identifier
53
+ _inherited?: boolean; // For inherited loaders: dedup marker for buildMatchResult
53
54
  loaderData?: any; // For loaders: the resolved data from loader execution
54
55
  parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
55
56
  // Intercept loader fields (for streaming loader data in parallel segments)
56
57
  loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
57
58
  loaderIds?: string[]; // IDs ($$id) of loaders for this segment
58
- parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
59
59
  // Error-specific fields
60
60
  error?: ErrorInfo; // For error segments: the error information
61
61
  // NotFound-specific fields
@@ -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
  };
@@ -37,7 +37,10 @@ import type {
37
37
  UseItems,
38
38
  } from "../route-types.js";
39
39
  import type { SearchSchema } from "../search-params.js";
40
- import type { PrerenderHandlerDefinition } from "../prerender.js";
40
+ import type {
41
+ PrerenderHandlerDefinition,
42
+ PassthroughHandlerDefinition,
43
+ } from "../prerender.js";
41
44
  import type { StaticHandlerDefinition } from "../static-handler.js";
42
45
  import type { InterceptWhenFn } from "../server/context";
43
46
  import type {
@@ -70,6 +73,7 @@ export type PathFn<TEnv> = <
70
73
  ctx: HandlerContext<TParams, TEnv, TSearch>,
71
74
  ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>)
72
75
  | PrerenderHandlerDefinition<TParams>
76
+ | PassthroughHandlerDefinition<TParams, TEnv>
73
77
  | StaticHandlerDefinition<TParams>,
74
78
  optionsOrUse?: PathOptions<TName, TSearch> | (() => UseItems<RouteUseItem>),
75
79
  use?: () => UseItems<RouteUseItem>,
@@ -229,12 +233,27 @@ export type PathHelpers<TEnv> = {
229
233
  include: IncludeFn<TEnv>;
230
234
 
231
235
  /**
232
- * 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()`.
233
243
  */
234
244
  parallel: <
235
245
  TSlots extends Record<
236
246
  `@${string}`,
237
- 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
+ }
238
257
  >,
239
258
  >(
240
259
  slots: TSlots,
@@ -260,9 +279,20 @@ export type PathHelpers<TEnv> = {
260
279
  ) => InterceptItem;
261
280
 
262
281
  /**
263
- * Attach middleware to the current route/layout
282
+ * Attach middleware to the current route/layout, or wrap child segments
264
283
  */
265
- 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
+ };
266
296
 
267
297
  /**
268
298
  * Control when a segment should revalidate during navigation
@@ -280,7 +310,10 @@ export type PathHelpers<TEnv> = {
280
310
  /**
281
311
  * Attach a loading component to the current route/layout
282
312
  */
283
- loading: (component: ReactNode, options?: { ssr?: boolean }) => LoadingItem;
313
+ loading: (
314
+ component: ReactNode | (() => ReactNode),
315
+ options?: { ssr?: boolean },
316
+ ) => LoadingItem;
284
317
 
285
318
  /**
286
319
  * Attach an error boundary to catch errors in this segment
@@ -12,10 +12,11 @@ import {
12
12
  getNamePrefix,
13
13
  getRootScoped,
14
14
  } from "../server/context";
15
- import { invariant } from "../errors";
15
+ import { invariant, DataNotFoundError } from "../errors";
16
16
  import { validateUserRouteName } from "../route-name.js";
17
17
  import {
18
18
  isPrerenderHandler,
19
+ isPassthroughHandler,
19
20
  type PrerenderHandlerDefinition,
20
21
  } from "../prerender.js";
21
22
  import {
@@ -34,6 +35,10 @@ import type {
34
35
  JsonResponsePathFn,
35
36
  TextResponsePathFn,
36
37
  } from "./path-helper-types.js";
38
+ import {
39
+ resolveHandlerUse,
40
+ mergeHandlerUse,
41
+ } from "../route-definition/resolve-handler-use.js";
37
42
 
38
43
  /**
39
44
  * Check if a value is a valid use item
@@ -142,6 +147,12 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
142
147
  use = maybeUse;
143
148
  }
144
149
 
150
+ // Merge handler.use() defaults with explicit use()
151
+ // Response routes (path.json, path.text, etc.) only allow middleware + cache
152
+ const handlerUseFn = resolveHandlerUse(handler);
153
+ const mountSite = resolveResponseType(options) ? "response" : "path";
154
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, mountSite);
155
+
145
156
  // Get prefixes from context (set by include())
146
157
  const urlPrefix = getUrlPrefix();
147
158
  const namePrefix = getNamePrefix();
@@ -176,14 +187,31 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
176
187
  }
177
188
 
178
189
  // Ensure handler is always a function (wrap ReactNode or extract from prerender/static def)
190
+ // For prerender stubs (production builds where handler code is evicted),
191
+ // handler.handler is undefined — provide a notFound fallback so requests
192
+ // for non-prerendered params get 404 instead of "handler is not a function".
179
193
  const wrappedHandler: Handler<any, any, TEnv> =
180
194
  typeof handler === "function"
181
195
  ? (handler as Handler<any, any, TEnv>)
182
- : isPrerenderHandler(handler)
183
- ? (handler.handler as Handler<any, any, TEnv>)
184
- : isStaticHandler(handler)
185
- ? (handler.handler as Handler<any, any, TEnv>)
186
- : () => handler;
196
+ : isPassthroughHandler(handler)
197
+ ? typeof handler.prerenderDef.handler === "function"
198
+ ? (handler.prerenderDef.handler as Handler<any, any, TEnv>)
199
+ : () => {
200
+ throw new DataNotFoundError(
201
+ "No prerender data found for this route",
202
+ );
203
+ }
204
+ : isPrerenderHandler(handler)
205
+ ? typeof handler.handler === "function"
206
+ ? (handler.handler as Handler<any, any, TEnv>)
207
+ : () => {
208
+ throw new DataNotFoundError(
209
+ "No prerender data found for this route",
210
+ );
211
+ }
212
+ : isStaticHandler(handler)
213
+ ? (handler.handler as Handler<any, any, TEnv>)
214
+ : () => handler;
187
215
 
188
216
  const entry = {
189
217
  id: namespace,
@@ -203,12 +231,19 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
203
231
  intercept: [],
204
232
  loader: [],
205
233
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
206
- ...(isPrerenderHandler(handler)
234
+ ...(isPassthroughHandler(handler)
207
235
  ? {
208
236
  isPrerender: true as const,
209
- prerenderDef: handler as PrerenderHandlerDefinition,
237
+ prerenderDef: handler.prerenderDef as PrerenderHandlerDefinition,
238
+ isPassthrough: true as const,
239
+ liveHandler: handler.liveHandler as Handler<any, any, TEnv>,
210
240
  }
211
- : {}),
241
+ : isPrerenderHandler(handler)
242
+ ? {
243
+ isPrerender: true as const,
244
+ prerenderDef: handler as PrerenderHandlerDefinition,
245
+ }
246
+ : {}),
212
247
  ...(isStaticHandler(handler)
213
248
  ? {
214
249
  isStaticPrerender: true as const,
@@ -264,9 +299,9 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
264
299
  registerSearchSchema(routeName, options.search);
265
300
  }
266
301
 
267
- // Run use callback if provided
268
- if (use && typeof use === "function") {
269
- const result = store.run(namespace, entry, use)?.flat(3);
302
+ // Run merged use callback (handler.use defaults + explicit use) if present
303
+ if (mergedUse) {
304
+ const result = store.run(namespace, entry, mergedUse)?.flat(3);
270
305
  invariant(
271
306
  Array.isArray(result) && result.every((item) => isValidUseItem(item)),
272
307
  `path() use() callback must return an array of use items [${namespace}]`,