@rangojs/router 0.0.0-experimental.61 → 0.0.0-experimental.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +61 -8
  2. package/dist/bin/rango.js +2 -1
  3. package/dist/vite/index.js +144 -62
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +14 -15
  6. package/skills/prerender/SKILL.md +110 -68
  7. package/src/__internal.ts +1 -1
  8. package/src/build/generate-manifest.ts +3 -6
  9. package/src/build/route-types/scan-filter.ts +8 -1
  10. package/src/index.rsc.ts +3 -1
  11. package/src/index.ts +8 -0
  12. package/src/prerender/store.ts +5 -4
  13. package/src/prerender.ts +138 -77
  14. package/src/reverse.ts +2 -0
  15. package/src/route-definition/dsl-helpers.ts +37 -18
  16. package/src/route-definition/index.ts +3 -0
  17. package/src/route-definition/resolve-handler-use.ts +149 -0
  18. package/src/route-types.ts +11 -0
  19. package/src/router/handler-context.ts +22 -5
  20. package/src/router/match-api.ts +2 -8
  21. package/src/router/match-middleware/cache-lookup.ts +2 -6
  22. package/src/router/prerender-match.ts +104 -8
  23. package/src/router/router-interfaces.ts +4 -0
  24. package/src/router/segment-resolution/fresh.ts +7 -2
  25. package/src/router/segment-resolution/revalidation.ts +10 -5
  26. package/src/router.ts +9 -1
  27. package/src/server/context.ts +5 -1
  28. package/src/static-handler.ts +18 -6
  29. package/src/types/handler-context.ts +12 -2
  30. package/src/types/route-entry.ts +1 -1
  31. package/src/urls/path-helper-types.ts +5 -1
  32. package/src/urls/path-helper.ts +47 -12
  33. package/src/urls/response-types.ts +16 -6
  34. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  35. package/src/vite/discovery/prerender-collection.ts +14 -1
  36. package/src/vite/discovery/state.ts +13 -4
  37. package/src/vite/index.ts +4 -0
  38. package/src/vite/plugin-types.ts +60 -5
  39. package/src/vite/rango.ts +2 -1
  40. package/src/vite/router-discovery.ts +153 -34
  41. package/src/vite/utils/prerender-utils.ts +2 -0
@@ -114,9 +114,9 @@ function createPrerenderPassthroughFn(
114
114
  }
115
115
  if (!isPassthroughRoute) {
116
116
  throw new Error(
117
- "ctx.passthrough() is only available on routes declared with " +
118
- "{ passthrough: true }. Remove the passthrough() call or add " +
119
- "{ passthrough: true } to the Prerender options.",
117
+ "ctx.passthrough() is only available on routes wrapped with " +
118
+ "Passthrough(). Remove the passthrough() call or wrap the " +
119
+ "Prerender definition with Passthrough(prerenderDef, liveHandler).",
120
120
  );
121
121
  }
122
122
  return PRERENDER_PASSTHROUGH;
@@ -195,7 +195,9 @@ export function createReverseFunction(
195
195
  // Clean up slashes only when an optional param was actually omitted,
196
196
  // so intentional trailing-slash patterns like "/blog/" are preserved.
197
197
  if (hadOmittedOptional) {
198
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
198
199
  result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
200
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
199
201
  }
200
202
  }
201
203
 
@@ -270,6 +272,7 @@ export function createHandlerContext<TEnv>(
270
272
  ctx = {
271
273
  params,
272
274
  build: false,
275
+ dev: false,
273
276
  request,
274
277
  searchParams,
275
278
  search: searchSchema ? resolvedSearchParams : {},
@@ -349,6 +352,8 @@ export function createPrerenderContext<TEnv>(
349
352
  routeName?: string,
350
353
  buildVars?: Record<string, any>,
351
354
  isPassthroughRoute?: boolean,
355
+ buildEnv?: TEnv,
356
+ devMode?: boolean,
352
357
  ): InternalHandlerContext<any, TEnv> {
353
358
  const syntheticUrl = new URL(`http://prerender${pathname}`);
354
359
  const variables = buildVars ?? {};
@@ -363,6 +368,7 @@ export function createPrerenderContext<TEnv>(
363
368
  return {
364
369
  params,
365
370
  build: true,
371
+ dev: devMode ?? false,
366
372
  get request(): Request {
367
373
  return throwUnavailable("request");
368
374
  },
@@ -372,7 +378,11 @@ export function createPrerenderContext<TEnv>(
372
378
  url: syntheticUrl,
373
379
  originalUrl: syntheticUrl,
374
380
  get env(): TEnv {
375
- return throwUnavailable("env");
381
+ if (buildEnv !== undefined) return buildEnv;
382
+ throw new Error(
383
+ "ctx.env is not available during pre-rendering. " +
384
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
385
+ );
376
386
  },
377
387
  _variables: variables,
378
388
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
@@ -420,6 +430,8 @@ export function createPrerenderContext<TEnv>(
420
430
  export function createStaticContext<TEnv>(
421
431
  routeMap: Record<string, string>,
422
432
  routeName?: string,
433
+ buildEnv?: TEnv,
434
+ devMode?: boolean,
423
435
  ): InternalHandlerContext<any, TEnv> {
424
436
  const variables: Record<string, any> = {};
425
437
 
@@ -435,6 +447,7 @@ export function createStaticContext<TEnv>(
435
447
  return throwUnavailable("params");
436
448
  },
437
449
  build: true,
450
+ dev: devMode ?? false,
438
451
  get request(): Request {
439
452
  return throwUnavailable("request");
440
453
  },
@@ -454,7 +467,11 @@ export function createStaticContext<TEnv>(
454
467
  return throwUnavailable("originalUrl");
455
468
  },
456
469
  get env(): TEnv {
457
- return throwUnavailable("env");
470
+ if (buildEnv !== undefined) return buildEnv;
471
+ throw new Error(
472
+ "ctx.env is not available in Static() handlers. " +
473
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
474
+ );
458
475
  },
459
476
  _variables: variables,
460
477
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
@@ -91,10 +91,7 @@ export async function createMatchContextForFull<TEnv>(
91
91
  });
92
92
  }
93
93
 
94
- if (
95
- manifestEntry.type === "route" &&
96
- manifestEntry.prerenderDef?.options?.passthrough === true
97
- ) {
94
+ if (manifestEntry.type === "route" && manifestEntry.isPassthrough === true) {
98
95
  matched.pt = true;
99
96
  }
100
97
 
@@ -289,10 +286,7 @@ export async function createMatchContextForPartial<TEnv>(
289
286
  });
290
287
  }
291
288
 
292
- if (
293
- manifestEntry.type === "route" &&
294
- manifestEntry.prerenderDef?.options?.passthrough === true
295
- ) {
289
+ if (manifestEntry.type === "route" && manifestEntry.isPassthrough === true) {
296
290
  matched.pt = true;
297
291
  }
298
292
 
@@ -324,9 +324,7 @@ export function withCacheLookup<TEnv>(
324
324
  if (prerenderStoreInstance) {
325
325
  const paramHash = _hashParams!(ctx.matched.params);
326
326
  const isPassthroughPrerenderRoute = ctx.entries.some(
327
- (entry) =>
328
- entry.type === "route" &&
329
- entry.prerenderDef?.options?.passthrough === true,
327
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
330
328
  );
331
329
 
332
330
  if (ctx.isIntercept) {
@@ -396,9 +394,7 @@ export function withCacheLookup<TEnv>(
396
394
  if (prerenderStoreInstance) {
397
395
  const paramHash = _hashParams!(ctx.matched.params);
398
396
  const isPassthroughPrerenderRoute = ctx.entries.some(
399
- (entry) =>
400
- entry.type === "route" &&
401
- entry.prerenderDef?.options?.passthrough === true,
397
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
402
398
  );
403
399
 
404
400
  if (ctx.isIntercept) {
@@ -54,6 +54,9 @@ export async function matchForPrerender<TEnv = any>(
54
54
  deps: PrerenderMatchDeps<TEnv>,
55
55
  buildVars?: Record<string, any>,
56
56
  isPassthroughRoute?: boolean,
57
+ buildEnv?: TEnv,
58
+ /** Dev-only: check getParams() for passthrough routes to skip unknown params. */
59
+ devMode?: boolean,
57
60
  ): Promise<{
58
61
  segments: SerializedSegmentData[];
59
62
  handles: Record<string, SegmentHandleData>;
@@ -90,15 +93,100 @@ export async function matchForPrerender<TEnv = any>(
90
93
  entries.push(entry);
91
94
  }
92
95
 
96
+ // 3b. Dev-mode passthrough shortcut: if the route is a Passthrough route
97
+ // and has getParams(), check if the matched params are in the known list.
98
+ // In production, only known params are pre-rendered; unknown params fall
99
+ // through to the live handler. Mirror that behavior in dev mode to avoid
100
+ // rendering unknown params with build: true.
101
+ // Vars collected from getParams() probe — merged into render context below.
102
+ let devProbeBuildVars: Record<string, any> | undefined;
103
+
104
+ if (devMode && matchedPassthroughRoute) {
105
+ const routeEntry = entries.find(
106
+ (
107
+ e,
108
+ ): e is EntryData & {
109
+ type: "route";
110
+ prerenderDef: { getParams: (ctx: any) => Promise<any[]> | any[] };
111
+ } =>
112
+ e.type === "route" &&
113
+ !!(e as any).isPassthrough &&
114
+ !!(e as any).prerenderDef?.getParams,
115
+ );
116
+ if (routeEntry) {
117
+ try {
118
+ const probeBuildVars: Record<string, any> = {};
119
+ const knownParamsList = await routeEntry.prerenderDef.getParams({
120
+ build: true as const,
121
+ dev: true,
122
+ set: ((keyOrVar: any, value: any) => {
123
+ contextSet(probeBuildVars, keyOrVar, value);
124
+ }) as any,
125
+ reverse: createReverseFunction(deps.mergedRouteMap),
126
+ get env() {
127
+ if (buildEnv !== undefined) return buildEnv;
128
+ throw new Error(
129
+ "[rsc-router] ctx.env is not available during dev-mode getParams(). " +
130
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
131
+ );
132
+ },
133
+ });
134
+ // Compare only the keys returned by getParams — ignore mount params
135
+ // from include() prefixes that aren't part of the handler's params.
136
+ const isKnown = knownParamsList.some((known: Record<string, any>) => {
137
+ const knownKeys = Object.keys(known);
138
+ return knownKeys.every(
139
+ (k) => String(known[k]) === String(matchedParams[k]),
140
+ );
141
+ });
142
+ if (!isKnown) {
143
+ return {
144
+ segments: [],
145
+ handles: {},
146
+ routeName: matched.routeKey,
147
+ params: matchedParams,
148
+ passthrough: true as const,
149
+ };
150
+ }
151
+ // Preserve vars set by getParams() for the render context
152
+ if (
153
+ Object.keys(probeBuildVars).length > 0 ||
154
+ Object.getOwnPropertySymbols(probeBuildVars).length > 0
155
+ ) {
156
+ devProbeBuildVars = probeBuildVars;
157
+ }
158
+ } catch (err: any) {
159
+ // Mirror production semantics (prerender-collection.ts):
160
+ // Skip errors are intentional — treat as passthrough.
161
+ // All other errors propagate so dev surfaces them.
162
+ if (err?.name === "Skip") {
163
+ return {
164
+ segments: [],
165
+ handles: {},
166
+ routeName: matched.routeKey,
167
+ params: matchedParams,
168
+ passthrough: true as const,
169
+ };
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+ }
175
+
93
176
  // 4. Create handle store for collecting handle data
94
177
  const handleStore = createHandleStore();
95
178
 
96
179
  // 5. Create a minimal request context with the handle store
97
- // Shallow-copy getParams vars so each param set is independent
98
- const variables: Record<string, any> = buildVars ? { ...buildVars } : {};
180
+ // Shallow-copy getParams vars so each param set is independent.
181
+ // In dev mode, merge vars from the getParams() probe if the caller
182
+ // didn't provide buildVars (production passes them from expandPrerenderRoutes).
183
+ const effectiveBuildVars = buildVars ?? devProbeBuildVars;
184
+ const variables: Record<string, any> = effectiveBuildVars
185
+ ? { ...effectiveBuildVars }
186
+ : {};
99
187
  const stubRes = new Response(null, { status: 200 });
100
188
  const minimalRequestContext: RequestContext<TEnv> = {
101
- env: {} as TEnv,
189
+ env: buildEnv ?? ({} as TEnv),
102
190
  request: new Request("http://prerender" + pathname),
103
191
  url: new URL("http://prerender" + pathname),
104
192
  originalUrl: new URL("http://prerender" + pathname),
@@ -140,7 +228,7 @@ export async function matchForPrerender<TEnv = any>(
140
228
  return runWithRequestContext(minimalRequestContext, async () => {
141
229
  // 6. Create prerender context with synthetic URL.
142
230
  // Prerender handlers get params, pathname, url, searchParams, search,
143
- // reverse, and use(handle) but no request, env, headers, or cookies.
231
+ // reverse, use(handle), and optionally env (when buildEnv is configured).
144
232
  const buildCtx = createPrerenderContext<TEnv>(
145
233
  matchedParams,
146
234
  pathname,
@@ -148,6 +236,8 @@ export async function matchForPrerender<TEnv = any>(
148
236
  matched.routeKey,
149
237
  variables,
150
238
  matchedPassthroughRoute,
239
+ buildEnv,
240
+ devMode,
151
241
  );
152
242
 
153
243
  // 7. Wire use() for handles only (loaders throw)
@@ -320,6 +410,8 @@ export async function renderStaticSegment<TEnv = any>(
320
410
  handlerId: string,
321
411
  mergedRouteMap: Record<string, string>,
322
412
  routeName?: string,
413
+ buildEnv?: TEnv,
414
+ devMode?: boolean,
323
415
  ): Promise<{ encoded: string; handles: Record<string, unknown[]> } | null> {
324
416
  const syntheticUrl = new URL("http://prerender/");
325
417
  const syntheticRequest = new Request(syntheticUrl);
@@ -330,7 +422,7 @@ export async function renderStaticSegment<TEnv = any>(
330
422
  // Minimal request context so setupBuildUse can find the HandleStore
331
423
  const stubRes = new Response(null, { status: 200 });
332
424
  const minimalRequestContext: RequestContext<TEnv> = {
333
- env: {} as TEnv,
425
+ env: buildEnv ?? ({} as TEnv),
334
426
  request: syntheticRequest,
335
427
  url: syntheticUrl,
336
428
  originalUrl: syntheticUrl,
@@ -368,9 +460,13 @@ export async function renderStaticSegment<TEnv = any>(
368
460
  };
369
461
 
370
462
  return runWithRequestContext(minimalRequestContext, async () => {
371
- // Static handlers get only reverse and use(handle) no URL, params,
372
- // request, env, headers, or cookies.
373
- const buildCtx = createStaticContext<TEnv>(mergedRouteMap, routeName);
463
+ // Static handlers get only reverse, use(handle), and optionally env.
464
+ const buildCtx = createStaticContext<TEnv>(
465
+ mergedRouteMap,
466
+ routeName,
467
+ buildEnv,
468
+ devMode,
469
+ );
374
470
 
375
471
  // Set segment ID so handle pushes are keyed correctly
376
472
  (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
@@ -374,6 +374,8 @@ export interface RSCRouterInternal<
374
374
  params: Record<string, string>,
375
375
  buildVars?: Record<string, any>,
376
376
  isPassthroughRoute?: boolean,
377
+ buildEnv?: any,
378
+ devMode?: boolean,
377
379
  ): Promise<{
378
380
  segments: SerializedSegmentData[];
379
381
  handles: Record<string, SegmentHandleData>;
@@ -392,6 +394,8 @@ export interface RSCRouterInternal<
392
394
  handler: Function,
393
395
  handlerId: string,
394
396
  routeName?: string,
397
+ buildEnv?: any,
398
+ devMode?: boolean,
395
399
  ): Promise<{ encoded: string; handles: Record<string, unknown[]> } | null>;
396
400
 
397
401
  /**
@@ -284,9 +284,14 @@ export async function resolveSegment<TEnv>(
284
284
  entry.shortCode,
285
285
  );
286
286
  if (component === undefined) {
287
+ // For Passthrough routes at runtime, use the live handler instead of
288
+ // the build handler. At build time (context.build === true), always
289
+ // use the build handler from entry.handler.
290
+ const handler =
291
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
287
292
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
288
293
  if (entry.loading) {
289
- const result = handleHandlerResult(entry.handler(context));
294
+ const result = handleHandlerResult(handler(context));
290
295
  if (result instanceof Promise) {
291
296
  result.finally(doneRouteHandler).catch(() => {});
292
297
  const tracked = deps.trackHandler(result, {
@@ -307,7 +312,7 @@ export async function resolveSegment<TEnv>(
307
312
  component = result;
308
313
  }
309
314
  } else {
310
- component = handleHandlerResult(await entry.handler(context));
315
+ component = handleHandlerResult(await handler(context));
311
316
  doneRouteHandler();
312
317
  }
313
318
  }
@@ -688,13 +688,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
688
688
  return staticComponent;
689
689
  }
690
690
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
691
+ // For Passthrough routes at runtime, use the live handler instead of
692
+ // the build handler. At build time (context.build === true), always
693
+ // use the build handler from routeEntry.handler.
694
+ const handler =
695
+ !context.build && routeEntry.liveHandler
696
+ ? routeEntry.liveHandler
697
+ : routeEntry.handler;
691
698
  if (!routeEntry.loading) {
692
- const result = handleHandlerResult(await routeEntry.handler(context));
699
+ const result = handleHandlerResult(await handler(context));
693
700
  doneHandler();
694
701
  return result;
695
702
  }
696
703
  if (!actionContext) {
697
- const result = handleHandlerResult(routeEntry.handler(context));
704
+ const result = handleHandlerResult(handler(context));
698
705
  if (result instanceof Promise) {
699
706
  result.finally(doneHandler).catch(() => {});
700
707
  const tracked = deps.trackHandler(result, {
@@ -717,9 +724,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
717
724
  debugLog("segment.action", "resolving action route with awaited value", {
718
725
  entryId: entry.id,
719
726
  });
720
- const actionResult = handleHandlerResult(
721
- await routeEntry.handler(context),
722
- );
727
+ const actionResult = handleHandlerResult(await handler(context));
723
728
  doneHandler();
724
729
  return {
725
730
  content: Promise.resolve(actionResult),
package/src/router.ts CHANGED
@@ -625,6 +625,8 @@ export function createRouter<TEnv = any>(
625
625
  params: Record<string, string>,
626
626
  buildVars?: Record<string, any>,
627
627
  isPassthroughRoute?: boolean,
628
+ buildEnv?: TEnv,
629
+ devMode?: boolean,
628
630
  ) {
629
631
  return _matchForPrerender(
630
632
  pathname,
@@ -632,6 +634,8 @@ export function createRouter<TEnv = any>(
632
634
  prerenderDeps,
633
635
  buildVars,
634
636
  isPassthroughRoute,
637
+ buildEnv,
638
+ devMode,
635
639
  );
636
640
  }
637
641
 
@@ -639,12 +643,16 @@ export function createRouter<TEnv = any>(
639
643
  handler: Function,
640
644
  handlerId: string,
641
645
  routeName?: string,
646
+ buildEnv?: TEnv,
647
+ devMode?: boolean,
642
648
  ) {
643
649
  return _renderStaticSegment<TEnv>(
644
650
  handler,
645
651
  handlerId,
646
652
  mergedRouteMap,
647
653
  routeName,
654
+ buildEnv,
655
+ devMode,
648
656
  );
649
657
  }
650
658
 
@@ -748,7 +756,7 @@ export function createRouter<TEnv = any>(
748
756
  if (entry.type === "route" && entry.isPrerender) {
749
757
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
750
758
  prerenderRouteKeys.add(name);
751
- if (entry.prerenderDef?.options?.passthrough === true) {
759
+ if (entry.isPassthrough === true) {
752
760
  if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
753
761
  passthroughRouteKeys.add(name);
754
762
  }
@@ -191,8 +191,12 @@ export type EntryData =
191
191
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
192
192
  prerenderDef?: {
193
193
  getParams?: (ctx: any) => Promise<any[]> | any[];
194
- options?: { passthrough?: boolean };
194
+ options?: { concurrency?: number };
195
195
  };
196
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
197
+ isPassthrough?: true;
198
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
199
+ liveHandler?: Handler<any, any, any>;
196
200
  /** Set when handler is a Static definition (build-time only) */
197
201
  isStaticPrerender?: true;
198
202
  /** Static handler $$id for build-time store lookup */
@@ -32,11 +32,21 @@
32
32
  */
33
33
  import type { ReactNode } from "react";
34
34
  import type { Handler } from "./types.js";
35
- import type { PrerenderOptions, StaticBuildContext } from "./prerender.js";
35
+ import type { StaticBuildContext } from "./prerender.js";
36
+ import type { UseItems, HandlerUseItem } from "./route-types.js";
36
37
  import { isCachedFunction } from "./cache/taint.js";
37
38
 
38
39
  // -- Types ------------------------------------------------------------------
39
40
 
41
+ export interface StaticHandlerOptions {
42
+ /**
43
+ * Keep handler in server bundle for live fallback (default: false).
44
+ * false: handler replaced with stub, source-only APIs excluded from bundle.
45
+ * true: handler stays in bundle, renders live at request time.
46
+ */
47
+ passthrough?: boolean;
48
+ }
49
+
40
50
  export interface StaticHandlerDefinition<
41
51
  TParams extends Record<string, any> = any,
42
52
  > {
@@ -46,14 +56,16 @@ export interface StaticHandlerDefinition<
46
56
  /** In dev mode, the actual handler function that layout/path/parallel can call. */
47
57
  handler: Handler<TParams>;
48
58
  /** Static handler options (passthrough support). */
49
- options?: PrerenderOptions;
59
+ options?: StaticHandlerOptions;
60
+ /** Composable default DSL items merged when the handler is mounted. */
61
+ use?: () => UseItems<HandlerUseItem>;
50
62
  }
51
63
 
52
64
  // -- Function ---------------------------------------------------------------
53
65
 
54
66
  export function Static<TParams extends Record<string, any> = {}>(
55
67
  handler: (ctx: StaticBuildContext) => ReactNode | Promise<ReactNode>,
56
- options?: PrerenderOptions,
68
+ options?: StaticHandlerOptions,
57
69
  __injectedId?: string,
58
70
  ): StaticHandlerDefinition<TParams>;
59
71
 
@@ -61,7 +73,7 @@ export function Static<TParams extends Record<string, any> = {}>(
61
73
 
62
74
  export function Static<TParams extends Record<string, any>>(
63
75
  handler: Function,
64
- optionsOrId?: PrerenderOptions | string,
76
+ optionsOrId?: StaticHandlerOptions | string,
65
77
  maybeId?: string,
66
78
  ): StaticHandlerDefinition<TParams> {
67
79
  if (isCachedFunction(handler)) {
@@ -72,13 +84,13 @@ export function Static<TParams extends Record<string, any>>(
72
84
  );
73
85
  }
74
86
 
75
- let options: PrerenderOptions | undefined;
87
+ let options: StaticHandlerOptions | undefined;
76
88
  let id: string;
77
89
 
78
90
  if (typeof optionsOrId === "string") {
79
91
  id = optionsOrId;
80
92
  } else {
81
- options = optionsOrId as PrerenderOptions | undefined;
93
+ options = optionsOrId as StaticHandlerOptions | undefined;
82
94
  id = maybeId ?? "";
83
95
  }
84
96
 
@@ -19,6 +19,7 @@ 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";
22
23
 
23
24
  // Re-export MiddlewareFn for internal/advanced use
24
25
  export type { MiddlewareFn } from "../router/middleware.js";
@@ -135,7 +136,7 @@ export type Handler<
135
136
  | Record<string, any> = {},
136
137
  TRouteMap extends {} = DefaultHandlerRouteMap,
137
138
  TEnv = DefaultEnv,
138
- > = (
139
+ > = ((
139
140
  ctx: HandlerContext<
140
141
  T extends `.${infer Local}`
141
142
  ? Local extends keyof TRouteMap
@@ -160,7 +161,10 @@ export type Handler<
160
161
  : ExtractSearchFromEntry<DefaultHandlerRouteMap, T>,
161
162
  TRouteMap extends DefaultHandlerRouteMap ? never : TRouteMap
162
163
  >,
163
- ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>;
164
+ ) => ReactNode | Promise<ReactNode> | Response | Promise<Response>) & {
165
+ /** Composable default DSL items merged when the handler is mounted. */
166
+ use?: () => UseItems<HandlerUseItem>;
167
+ };
164
168
 
165
169
  /**
166
170
  * Context passed to handlers (Hono-inspired type-safe context)
@@ -205,6 +209,12 @@ export type HandlerContext<
205
209
  * Live request rendering, including passthrough fallback, uses `false`.
206
210
  */
207
211
  build: boolean;
212
+ /**
213
+ * True when running in Vite dev mode, false during production build or
214
+ * live request rendering. Use this to branch on runtime mode without
215
+ * changing build semantics (e.g., skip expensive operations in dev).
216
+ */
217
+ dev: boolean;
208
218
  /**
209
219
  * The original incoming Request object (transport URL intact).
210
220
  * Use `ctx.url` / `ctx.searchParams` for application logic — those have
@@ -69,7 +69,7 @@ export interface RouteEntry<TEnv = any> {
69
69
  prerenderRouteKeys?: Set<string>;
70
70
 
71
71
  /**
72
- * Route keys in this entry that use `{ passthrough: true }`.
72
+ * Route keys in this entry that are wrapped with `Passthrough()`.
73
73
  * Used by the non-trie match path to set the `pt` flag.
74
74
  */
75
75
  passthroughRouteKeys?: Set<string>;
@@ -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>,