@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.112

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 (65) hide show
  1. package/dist/bin/rango.js +41 -37
  2. package/dist/vite/index.js +144 -191
  3. package/package.json +17 -14
  4. package/skills/handler-use/SKILL.md +1 -1
  5. package/skills/rango/SKILL.md +20 -0
  6. package/src/browser/action-coordinator.ts +53 -36
  7. package/src/browser/event-controller.ts +42 -66
  8. package/src/browser/navigation-bridge.ts +4 -0
  9. package/src/browser/navigation-client.ts +12 -15
  10. package/src/browser/navigation-store.ts +7 -8
  11. package/src/browser/navigation-transaction.ts +7 -21
  12. package/src/browser/partial-update.ts +8 -16
  13. package/src/browser/react/NavigationProvider.tsx +29 -40
  14. package/src/browser/react/use-params.ts +3 -4
  15. package/src/browser/response-adapter.ts +25 -0
  16. package/src/browser/rsc-router.tsx +16 -2
  17. package/src/browser/server-action-bridge.ts +23 -30
  18. package/src/browser/types.ts +2 -0
  19. package/src/build/generate-manifest.ts +29 -31
  20. package/src/build/generate-route-types.ts +2 -0
  21. package/src/build/route-types/router-processing.ts +37 -9
  22. package/src/build/runtime-discovery.ts +9 -20
  23. package/src/decode-loader-results.ts +36 -0
  24. package/src/errors.ts +29 -0
  25. package/src/index.rsc.ts +1 -0
  26. package/src/index.ts +1 -0
  27. package/src/response-utils.ts +9 -0
  28. package/src/route-content-wrapper.tsx +6 -28
  29. package/src/route-definition/dsl-helpers.ts +231 -259
  30. package/src/route-definition/helper-factories.ts +29 -139
  31. package/src/route-definition/use-item-types.ts +32 -0
  32. package/src/route-types.ts +19 -41
  33. package/src/router/content-negotiation.ts +15 -2
  34. package/src/router/intercept-resolution.ts +4 -18
  35. package/src/router/match-result.ts +32 -30
  36. package/src/router/middleware.ts +46 -78
  37. package/src/router/preview-match.ts +3 -1
  38. package/src/router/request-classification.ts +4 -28
  39. package/src/rsc/handler.ts +20 -65
  40. package/src/rsc/helpers.ts +3 -2
  41. package/src/rsc/origin-guard.ts +28 -10
  42. package/src/rsc/response-route-handler.ts +32 -52
  43. package/src/rsc/rsc-rendering.ts +27 -53
  44. package/src/rsc/runtime-warnings.ts +9 -10
  45. package/src/rsc/server-action.ts +13 -37
  46. package/src/rsc/ssr-setup.ts +16 -0
  47. package/src/segment-system.tsx +5 -39
  48. package/src/server/context.ts +76 -35
  49. package/src/urls/include-helper.ts +10 -53
  50. package/src/urls/index.ts +0 -3
  51. package/src/urls/path-helper.ts +17 -52
  52. package/src/urls/pattern-types.ts +2 -19
  53. package/src/urls/response-types.ts +20 -19
  54. package/src/urls/type-extraction.ts +20 -115
  55. package/src/urls/urls-function.ts +1 -5
  56. package/src/vite/discovery/discover-routers.ts +10 -22
  57. package/src/vite/discovery/route-types-writer.ts +38 -82
  58. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  59. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  60. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  61. package/src/vite/plugins/expose-internal-ids.ts +34 -62
  62. package/src/vite/plugins/version-injector.ts +2 -12
  63. package/src/vite/router-discovery.ts +71 -26
  64. package/src/vite/utils/shared-utils.ts +13 -1
  65. package/src/browser/action-response-classifier.ts +0 -99
@@ -8,6 +8,7 @@ import {
8
8
  createResponseWithMergedHeaders,
9
9
  carryOverRedirectHeaders,
10
10
  } from "./helpers.js";
11
+ import { isRedirectResponse } from "../response-utils.js";
11
12
 
12
13
  // W3 -----------------------------------------------------------------------
13
14
 
@@ -18,16 +19,14 @@ import {
18
19
  */
19
20
  export function extractRedirectResponse(value: unknown): Response | null {
20
21
  if (!(value instanceof Response)) return null;
21
- const location = value.headers.get("Location");
22
- if (value.status >= 300 && value.status < 400 && location) {
23
- const redirect = createResponseWithMergedHeaders(null, {
24
- status: value.status,
25
- headers: { Location: location },
26
- });
27
- carryOverRedirectHeaders(value, redirect);
28
- return redirect;
29
- }
30
- return null;
22
+ if (!isRedirectResponse(value)) return null;
23
+ const location = value.headers.get("Location")!;
24
+ const redirect = createResponseWithMergedHeaders(null, {
25
+ status: value.status,
26
+ headers: { Location: location },
27
+ });
28
+ carryOverRedirectHeaders(value, redirect);
29
+ return redirect;
31
30
  }
32
31
 
33
32
  /**
@@ -27,7 +27,7 @@ import {
27
27
  hasBodyContent,
28
28
  createResponseWithMergedHeaders,
29
29
  createSimpleRedirectResponse,
30
- carryOverRedirectHeaders,
30
+ interceptRedirectForPartial,
31
31
  } from "./helpers.js";
32
32
  import type { HandlerContext } from "./handler-context.js";
33
33
 
@@ -111,49 +111,25 @@ export async function executeServerAction<TEnv>(
111
111
  loadedAction = await ctx.loadServerAction(actionId);
112
112
  const data = await loadedAction!.apply(null, args);
113
113
 
114
- // Intercept redirect responses from actions. Without this, the redirect
115
- // Response would be serialized as the action returnValue (which fails)
116
- // and the revalidation step would run unnecessarily.
114
+ // Intercept redirect Responses: serializing one as the action returnValue
115
+ // would fail, and revalidation would run needlessly.
117
116
  if (data instanceof Response) {
118
- const redirectUrl = data.headers.get("Location");
119
- const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
120
- if (isRedirect) {
121
- const locationState = getLocationState();
122
- let redirect: Response;
123
- if (locationState) {
124
- redirect = ctx.createRedirectFlightResponse(
125
- redirectUrl,
126
- resolveLocationStateEntries(locationState),
127
- );
128
- } else {
129
- redirect = createSimpleRedirectResponse(redirectUrl);
130
- }
131
- carryOverRedirectHeaders(data, redirect);
132
- return redirect;
133
- }
117
+ const intercepted = interceptRedirectForPartial(
118
+ data,
119
+ ctx.createRedirectFlightResponse,
120
+ );
121
+ if (intercepted) return intercepted;
134
122
  }
135
123
 
136
124
  returnValue = { ok: true, data };
137
125
  } catch (error) {
138
126
  // Handle thrown redirect (e.g., throw redirect('/path'))
139
127
  if (error instanceof Response) {
140
- const redirectUrl = error.headers.get("Location");
141
- const isRedirect =
142
- error.status >= 300 && error.status < 400 && redirectUrl;
143
- if (isRedirect) {
144
- const locationState = getLocationState();
145
- let redirect: Response;
146
- if (locationState) {
147
- redirect = ctx.createRedirectFlightResponse(
148
- redirectUrl,
149
- resolveLocationStateEntries(locationState),
150
- );
151
- } else {
152
- redirect = createSimpleRedirectResponse(redirectUrl);
153
- }
154
- carryOverRedirectHeaders(error, redirect);
155
- return redirect;
156
- }
128
+ const intercepted = interceptRedirectForPartial(
129
+ error,
130
+ ctx.createRedirectFlightResponse,
131
+ );
132
+ if (intercepted) return intercepted;
157
133
 
158
134
  // Non-redirect Response thrown from action — this will be treated
159
135
  // as a regular error and routed to the error boundary. Warn in dev
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
126
126
 
127
127
  return true;
128
128
  }
129
+
130
+ // Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
131
+ // from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
132
+ // Accept header as needing SSR; this treats it as RSC).
133
+ export function isRscRequest(
134
+ request: Request,
135
+ url: URL,
136
+ isPartial: boolean,
137
+ ): boolean {
138
+ return (
139
+ isPartial ||
140
+ (!request.headers.get("accept")?.includes("text/html") &&
141
+ !url.searchParams.has("__html")) ||
142
+ url.searchParams.has("__rsc")
143
+ );
144
+ }
@@ -3,7 +3,7 @@ import { createElement, type ReactNode, type ComponentType } from "react";
3
3
  import { OutletProvider } from "./client.js";
4
4
  import { MountContextProvider } from "./browser/react/mount-context.js";
5
5
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
6
- import { isLoaderDataResult } from "./types.js";
6
+ import { decodeLoaderResults } from "./decode-loader-results.js";
7
7
  import { invariant } from "./errors.js";
8
8
  import {
9
9
  RouteContentWrapper,
@@ -59,42 +59,6 @@ function restoreParallelLoaderMarkers(
59
59
  return nextSegments ?? segments;
60
60
  }
61
61
 
62
- /**
63
- * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
64
- */
65
- function resolveLoaderData(
66
- resolvedData: any[],
67
- loaderIds: string[],
68
- ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
69
- const loaderData: Record<string, any> = {};
70
- let errorFallback: ReactNode = null;
71
-
72
- for (let i = 0; i < loaderIds.length; i++) {
73
- const id = loaderIds[i];
74
- const result = resolvedData[i];
75
-
76
- if (!isLoaderDataResult(result)) {
77
- // Legacy format - direct data
78
- loaderData[id] = result;
79
- continue;
80
- }
81
-
82
- if (result.ok) {
83
- loaderData[id] = result.data;
84
- continue;
85
- }
86
-
87
- // Error case
88
- if (result.fallback) {
89
- errorFallback = result.fallback;
90
- } else {
91
- throw new Error(result.error.message);
92
- }
93
- }
94
-
95
- return { loaderData, errorFallback };
96
- }
97
-
98
62
  /**
99
63
  * Options for renderSegments
100
64
  */
@@ -337,13 +301,15 @@ export async function renderSegments(
337
301
 
338
302
  // Prepare loader data if there are loaders
339
303
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
340
- const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
341
304
 
342
305
  // Use LoaderBoundary when loading is defined to maintain consistent tree structure
343
306
  // This ensures cached segments (which may not have loader segments) have the same
344
307
  // tree structure as fresh segments, preventing React remounts
345
308
  // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
346
309
  if (loading !== undefined && loading !== null) {
310
+ // Aggregate built here only — the loaderless and no-loading branches don't
311
+ // read it (the latter builds its own per-parallel promises).
312
+ const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
347
313
  content = createElement(LoaderBoundary, {
348
314
  key: `loader-boundary-${key}`,
349
315
  loaderDataPromise:
@@ -387,7 +353,7 @@ export async function renderSegments(
387
353
  )
388
354
  : Promise.resolve([]);
389
355
  const resolvedData = await layoutLoaderDataPromise;
390
- const { loaderData, errorFallback } = resolveLoaderData(
356
+ const { loaderData, errorFallback } = decodeLoaderResults(
391
357
  resolvedData,
392
358
  layoutLoaderIds,
393
359
  );
@@ -10,7 +10,7 @@ import type {
10
10
  ShouldRevalidateFn,
11
11
  TransitionConfig,
12
12
  } from "../types";
13
- import { invariant } from "../errors";
13
+ import { invariant, DslContextError } from "../errors";
14
14
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
15
 
16
16
  // ============================================================================
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
71
71
  };
72
72
 
73
73
  /**
74
+ * Attachments resolved by walking the parent chain, not owned by the entry:
75
+ * middleware composes downward; revalidate and the error/notFound boundaries are
76
+ * resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
77
+ *
74
78
  * @internal This type is an implementation detail and may change without notice.
75
79
  */
76
80
  export type EntryPropDatas = {
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
80
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
81
85
  };
82
86
 
87
+ /**
88
+ * Render-time presentation fields shared by every entry variant.
89
+ *
90
+ * @internal This type is an implementation detail and may change without notice.
91
+ */
92
+ export type EntryPropRender = {
93
+ loading?: ReactNode | false;
94
+ transition?: TransitionConfig;
95
+ };
96
+
83
97
  /**
84
98
  * Loader entry stored in EntryData
85
99
  * Contains the loader definition and its revalidation rules
@@ -158,11 +172,9 @@ export type InterceptEntry = {
158
172
  };
159
173
 
160
174
  export interface ParallelEntryData
161
- extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
175
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
162
176
  type: "parallel";
163
177
  handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
- loading?: ReactNode | false;
165
- transition?: TransitionConfig;
166
178
  /** Set when any parallel slot is a Static definition */
167
179
  isStaticPrerender?: true;
168
180
  /** Per-slot static handler $$ids for build-time store lookup */
@@ -171,6 +183,13 @@ export interface ParallelEntryData
171
183
 
172
184
  export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
185
 
186
+ /**
187
+ * This entry's own structural children plus its owned loaders. `loader` lives
188
+ * here (not in EntryPropDatas) because loaders are owned by the entry, not
189
+ * inherited from ancestors.
190
+ *
191
+ * @internal This type is an implementation detail and may change without notice.
192
+ */
174
193
  export type EntryPropSegments = {
175
194
  loader: LoaderEntry[];
176
195
  layout: EntryData[];
@@ -182,8 +201,6 @@ export type EntryData =
182
201
  | ({
183
202
  type: "route";
184
203
  handler: Handler<any, any, any>;
185
- loading?: ReactNode | false;
186
- transition?: TransitionConfig;
187
204
  /** URL pattern for this route (used by path() in urls()) */
188
205
  pattern?: string;
189
206
  /** Set when handler is a Prerender definition */
@@ -205,29 +222,28 @@ export type EntryData =
205
222
  responseType?: string;
206
223
  } & EntryPropCommon &
207
224
  EntryPropDatas &
208
- EntryPropSegments)
225
+ EntryPropSegments &
226
+ EntryPropRender)
209
227
  | ({
210
228
  type: "layout";
211
229
  handler: ReactNode | Handler<any, any, any>;
212
- loading?: ReactNode | false;
213
- transition?: TransitionConfig;
214
230
  /** Set when handler is a Static definition (build-time only) */
215
231
  isStaticPrerender?: true;
216
232
  /** Static handler $$id for build-time store lookup */
217
233
  staticHandlerId?: string;
218
234
  } & EntryPropCommon &
219
235
  EntryPropDatas &
220
- EntryPropSegments)
236
+ EntryPropSegments &
237
+ EntryPropRender)
221
238
  | ParallelEntryData
222
239
  | ({
223
240
  type: "cache";
224
241
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
225
242
  handler: ReactNode | Handler<any, any, any>;
226
- loading?: ReactNode | false;
227
- transition?: TransitionConfig;
228
243
  } & EntryPropCommon &
229
244
  EntryPropDatas &
230
- EntryPropSegments);
245
+ EntryPropSegments &
246
+ EntryPropRender);
231
247
 
232
248
  /**
233
249
  * Tracked include info for build-time manifest generation
@@ -307,6 +323,24 @@ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
307
323
  globalThis as any
308
324
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
309
325
 
326
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
327
+ const SHORT_CODE_PREFIX: Record<
328
+ "layout" | "parallel" | "route" | "loader" | "cache",
329
+ string
330
+ > = {
331
+ layout: "L",
332
+ parallel: "P",
333
+ route: "R",
334
+ loader: "D",
335
+ cache: "C",
336
+ };
337
+
338
+ /** Post-increment a named per-store counter, returning the prior value. */
339
+ function bumpCounter(store: HelperContext, key: string): number {
340
+ store.counters[key] ??= 0;
341
+ return store.counters[key]++;
342
+ }
343
+
310
344
  export const getContext = (): {
311
345
  context: AsyncLocalStorage<HelperContext>;
312
346
  getStore: () => HelperContext;
@@ -373,10 +407,7 @@ export const getContext = (): {
373
407
  ) => {
374
408
  const store = context.getStore();
375
409
  invariant(store, "No context RangoContext available");
376
- store.counters[type] ??= 0;
377
- const index = store.counters[type];
378
- store.counters[type] = index + 1;
379
- return `$${type}.${index}`;
410
+ return `$${type}.${bumpCounter(store, type)}`;
380
411
  },
381
412
  getShortCode: (
382
413
  type: "layout" | "parallel" | "route" | "loader" | "cache",
@@ -385,16 +416,7 @@ export const getContext = (): {
385
416
  invariant(store, "No context RangoContext available");
386
417
 
387
418
  const parent = store.parent;
388
- const prefix =
389
- type === "layout"
390
- ? "L"
391
- : type === "parallel"
392
- ? "P"
393
- : type === "loader"
394
- ? "D"
395
- : type === "cache"
396
- ? "C"
397
- : "R";
419
+ const prefix = SHORT_CODE_PREFIX[type];
398
420
  const mountPrefix =
399
421
  store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
400
422
 
@@ -405,10 +427,7 @@ export const getContext = (): {
405
427
  const counterKey = mountPrefix
406
428
  ? `${mountPrefix}_root_${type}`
407
429
  : `root_${type}`;
408
- store.counters[counterKey] ??= 0;
409
- const index = store.counters[counterKey];
410
- store.counters[counterKey] = index + 1;
411
- return `${mountPrefix}${prefix}${index}`;
430
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
412
431
  } else {
413
432
  // Child entry: use parent-scoped counter with includeScope appended.
414
433
  // When we're evaluating a lazy include's direct children, includeScope
@@ -416,10 +435,7 @@ export const getContext = (): {
416
435
  // parent's counter namespace so routes inside one include cannot
417
436
  // collide with siblings declared outside it.
418
437
  const counterKey = `${parent.shortCode}${includeScope}_${type}`;
419
- store.counters[counterKey] ??= 0;
420
- const index = store.counters[counterKey];
421
- store.counters[counterKey] = index + 1;
422
- return `${parent.shortCode}${includeScope}${prefix}${index}`;
438
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
423
439
  }
424
440
  },
425
441
  runWithStore: <T>(
@@ -493,6 +509,31 @@ export const getContext = (): {
493
509
  };
494
510
  };
495
511
 
512
+ /**
513
+ * Acquire the active DSL build context, throwing `message` if a helper was
514
+ * called outside a urls()/map() builder. Returns the store API and the live
515
+ * HelperContext so callers avoid a second getContext() lookup.
516
+ */
517
+ export function requireDslContext(message: string): {
518
+ store: ReturnType<typeof getContext>;
519
+ ctx: HelperContext;
520
+ } {
521
+ const store = getContext();
522
+ const ctx = store.context.getStore();
523
+ if (!ctx) {
524
+ // The only reason the store is absent here is that a route-definition helper
525
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
526
+ // Record that as the cause so the throw is self-explanatory, not a bare
527
+ // "must be called inside urls()" with no indication of the mechanism.
528
+ throw new DslContextError(message, {
529
+ cause:
530
+ "RangoContext store is undefined: a route-definition helper was called " +
531
+ "outside an active urls()/map() builder.",
532
+ });
533
+ }
534
+ return { store, ctx };
535
+ }
536
+
496
537
  /**
497
538
  * Run a callback with specific URL and name prefixes
498
539
  * Used by include() to apply prefixes to nested patterns
@@ -1,9 +1,8 @@
1
1
  import type { AllUseItems, IncludeItem } from "../route-types.js";
2
2
  import {
3
- getContext,
4
- runWithPrefixes,
5
3
  getUrlPrefix,
6
4
  getNamePrefix,
5
+ requireDslContext,
7
6
  } from "../server/context";
8
7
  import {
9
8
  INTERNAL_INCLUDE_SCOPE_PREFIX,
@@ -26,28 +25,10 @@ function allocateInternalIncludeScopeId(
26
25
  }
27
26
 
28
27
  /**
29
- * Process an IncludeItem by executing its nested patterns with prefixes
30
- * This expands the include into actual route registrations
31
- */
32
- function processIncludeItem(item: IncludeItem): AllUseItems[] {
33
- const { prefix, patterns } = item;
34
- const namePrefix =
35
- (item as IncludeItem & { _lazyContext?: { namePrefix?: string } })
36
- ._lazyContext?.namePrefix ?? item.options?.name;
37
-
38
- // Execute the nested patterns' handler with URL and name prefixes
39
- // The urlPrefix being set tells nested urls() to skip RootLayout wrapping
40
- return runWithPrefixes(prefix, namePrefix, () => {
41
- // Call the nested patterns' handler - this registers routes with prefixed patterns/names
42
- return (patterns as UrlPatterns).handler();
43
- });
44
- }
45
-
46
- /**
47
- * Recursively process items, expanding any IncludeItems
48
- * Returns items with IncludeItems expanded into actual route items
28
+ * Recursively walk items, recursing into layout children.
49
29
  *
50
- * Lazy includes are kept as-is (not expanded) for the router to handle later.
30
+ * All includes are lazy and kept as-is; the router expands them on the first
31
+ * matching request.
51
32
  */
52
33
  export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
53
34
  const result: AllUseItems[] = [];
@@ -56,26 +37,8 @@ export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
56
37
  if (!item) continue;
57
38
 
58
39
  if (item.type === "include") {
59
- const includeItem = item as IncludeItem & {
60
- _expanded?: AllUseItems[];
61
- lazy?: boolean;
62
- };
63
-
64
- // Lazy includes are NOT expanded here - kept for router to handle
65
- if (includeItem.lazy) {
66
- result.push(item);
67
- continue;
68
- }
69
-
70
- // Eager includes are already expanded during include() call
71
- if (includeItem._expanded) {
72
- // Items were expanded immediately - just process them recursively
73
- result.push(...processItems(includeItem._expanded));
74
- } else {
75
- // Fallback for legacy include items without _expanded
76
- const expanded = processIncludeItem(item as IncludeItem);
77
- result.push(...processItems(expanded));
78
- }
40
+ // All includes are lazy; the router expands them on first matching request.
41
+ result.push(item);
79
42
  } else if (item.type === "layout" && (item as any).uses) {
80
43
  // Process nested items in layout
81
44
  const layoutItem = item as any;
@@ -92,13 +55,9 @@ export function processItems(items: readonly AllUseItems[]): AllUseItems[] {
92
55
  /**
93
56
  * Create include() helper for composing URL patterns
94
57
  *
95
- * By default, include() IMMEDIATELY expands the nested patterns. This ensures
96
- * that routes from included patterns inherit the correct parent context
97
- * (the layout they're included in).
98
- *
99
- * With `lazy: true`, patterns are NOT expanded at definition time. Instead,
100
- * they're evaluated on first request that matches the prefix. This improves
101
- * cold start time for apps with many routes.
58
+ * All includes are lazy: the nested patterns are NOT expanded at definition
59
+ * time. Instead they are evaluated on the first request that matches the
60
+ * prefix, which improves cold start time for apps with many routes.
102
61
  */
103
62
  export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
104
63
  return (
@@ -106,9 +65,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
106
65
  patterns: UrlPatterns<TEnv>,
107
66
  options?: IncludeOptions,
108
67
  ): IncludeItem => {
109
- const store = getContext();
110
- const ctx = store.getStore();
111
- if (!ctx) throw new Error("include() must be called inside urls()");
68
+ const { ctx } = requireDslContext("include() must be called inside urls()");
112
69
 
113
70
  const explicitName = options?.name;
114
71
  const hasExplicitName = hasExplicitNameOption(options);
package/src/urls/index.ts CHANGED
@@ -13,7 +13,6 @@ export type {
13
13
  UnnamedRoute,
14
14
  LocalOnlyInclude,
15
15
  PathOptions,
16
- PathDefinition,
17
16
  UrlPatterns,
18
17
  IncludeOptions,
19
18
  } from "./pattern-types.js";
@@ -22,8 +21,6 @@ export type {
22
21
  export type {
23
22
  ExtractRoutes,
24
23
  ExtractResponses,
25
- ExtractRouteNames,
26
- ExtractPathParams,
27
24
  ResponseError,
28
25
  ResponseEnvelope,
29
26
  RouteResponse,
@@ -1,16 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { Handler } from "../types.js";
3
- import type {
4
- AllUseItems,
5
- RouteItem,
6
- RouteUseItem,
7
- UseItems,
8
- } from "../route-types.js";
3
+ import type { RouteItem, RouteUseItem, UseItems } from "../route-types.js";
9
4
  import {
10
- getContext,
11
5
  getUrlPrefix,
12
6
  getNamePrefix,
13
7
  getRootScoped,
8
+ requireDslContext,
14
9
  } from "../server/context";
15
10
  import { invariant, DataNotFoundError } from "../errors";
16
11
  import { validateUserRouteName } from "../route-name.js";
@@ -39,35 +34,10 @@ import {
39
34
  resolveHandlerUse,
40
35
  mergeHandlerUse,
41
36
  } from "../route-definition/resolve-handler-use.js";
42
-
43
- /**
44
- * Check if a value is a valid use item
45
- */
46
- const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
47
- return (
48
- typeof item === "undefined" ||
49
- item === null ||
50
- (item &&
51
- typeof item === "object" &&
52
- "type" in item &&
53
- [
54
- "layout",
55
- "route",
56
- "middleware",
57
- "revalidate",
58
- "parallel",
59
- "intercept",
60
- "loader",
61
- "loading",
62
- "errorBoundary",
63
- "notFoundBoundary",
64
- "when",
65
- "cache",
66
- "transition",
67
- "include",
68
- ].includes(item.type))
69
- );
70
- };
37
+ import {
38
+ emptySegmentBase,
39
+ runAndValidateUseItems,
40
+ } from "../route-definition/dsl-helpers.js";
71
41
 
72
42
  /**
73
43
  * Apply URL prefix to a pattern
@@ -112,9 +82,9 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
112
82
  optionsOrUse?: PathOptions | (() => UseItems<RouteUseItem>),
113
83
  maybeUse?: () => UseItems<RouteUseItem>,
114
84
  ): RouteItem => {
115
- const store = getContext();
116
- const ctx = store.getStore();
117
- if (!ctx) throw new Error("path() must be called inside urls()");
85
+ const { store, ctx } = requireDslContext(
86
+ "path() must be called inside urls()",
87
+ );
118
88
 
119
89
  invariant(
120
90
  !ctx.parent || ctx.parent.type !== "parallel",
@@ -214,6 +184,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
214
184
  : () => handler;
215
185
 
216
186
  const entry = {
187
+ ...emptySegmentBase(),
217
188
  id: namespace,
218
189
  shortCode: store.getShortCode("route"),
219
190
  type: "route" as const,
@@ -221,15 +192,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
221
192
  handler: wrappedHandler,
222
193
  // Store the PREFIXED pattern for route matching
223
194
  pattern: prefixedPattern,
224
- loading: undefined,
225
- middleware: [],
226
- revalidate: [],
227
- errorBoundary: [],
228
- notFoundBoundary: [],
229
- layout: [],
230
- parallel: {},
231
- intercept: [],
232
- loader: [],
233
195
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
234
196
  ...(isPassthroughHandler(handler)
235
197
  ? {
@@ -301,10 +263,13 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
301
263
 
302
264
  // Run merged use callback (handler.use defaults + explicit use) if present
303
265
  if (mergedUse) {
304
- const result = store.run(namespace, entry, mergedUse)?.flat(3);
305
- invariant(
306
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
307
- `path() use() callback must return an array of use items [${namespace}]`,
266
+ const result = runAndValidateUseItems(
267
+ store,
268
+ namespace,
269
+ entry,
270
+ mergedUse,
271
+ "path",
272
+ "use",
308
273
  );
309
274
  return { name: namespace, type: "route", uses: result } as RouteItem;
310
275
  }
@@ -1,10 +1,5 @@
1
- import type { ReactNode } from "react";
2
- import type { Handler, TrailingSlashMode } from "../types.js";
3
- import type {
4
- AllUseItems,
5
- RouteUseItem,
6
- UrlPatternsBrand,
7
- } from "../route-types.js";
1
+ import type { TrailingSlashMode } from "../types.js";
2
+ import type { AllUseItems, UrlPatternsBrand } from "../route-types.js";
8
3
  import type { SearchSchema } from "../search-params.js";
9
4
  import { RESPONSE_TYPE } from "./response-types.js";
10
5
  import type { DefaultEnv } from "../types.js";
@@ -54,16 +49,6 @@ export interface PathOptions<
54
49
  [RESPONSE_TYPE]?: string;
55
50
  }
56
51
 
57
- /**
58
- * Internal representation of a URL pattern definition
59
- */
60
- export interface PathDefinition {
61
- pattern: string;
62
- name?: string;
63
- handler: ReactNode | Handler<any, any, any>;
64
- use?: RouteUseItem[];
65
- }
66
-
67
52
  /**
68
53
  * Result of urls() - contains the route definitions
69
54
  */
@@ -72,8 +57,6 @@ export interface UrlPatterns<
72
57
  TRoutes extends Record<string, any> = Record<string, string>,
73
58
  TResponses extends Record<string, unknown> = Record<string, unknown>,
74
59
  > {
75
- /** Internal: route definitions */
76
- readonly definitions: PathDefinition[];
77
60
  /** Internal: compiled handler function */
78
61
  readonly handler: () => AllUseItems[];
79
62
  /** Internal: trailing slash config per route name */