@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.111
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.
- package/dist/vite/index.js +1 -1
- package/package.json +15 -14
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/rango/SKILL.md +20 -0
- package/src/errors.ts +18 -0
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +1 -0
- package/src/route-definition/dsl-helpers.ts +231 -259
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/server/context.ts +76 -35
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +2 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +20 -115
- package/src/urls/urls-function.ts +1 -5
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
getContext,
|
|
12
12
|
getNamePrefix,
|
|
13
13
|
getUrlPrefix,
|
|
14
|
+
requireDslContext,
|
|
14
15
|
type EntryData,
|
|
16
|
+
type EntryPropDatas,
|
|
17
|
+
type EntryPropSegments,
|
|
18
|
+
type HelperContext,
|
|
15
19
|
type InterceptEntry,
|
|
16
20
|
} from "../server/context";
|
|
17
21
|
import { invariant } from "../errors";
|
|
@@ -38,6 +42,7 @@ import type {
|
|
|
38
42
|
} from "../route-types.js";
|
|
39
43
|
import type { RouteHelpers } from "./helpers-types.js";
|
|
40
44
|
import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
|
|
45
|
+
import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* Check if an item contains routes (directly or inside nested structures like cache).
|
|
@@ -61,16 +66,105 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
|
61
66
|
return false;
|
|
62
67
|
};
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Fresh empty collections shared by every from-scratch segment entry. Returns
|
|
71
|
+
* new arrays/objects per call so no two entries share mutable references.
|
|
72
|
+
* mountPath is intentionally NOT included here — each call site adds it from
|
|
73
|
+
* getUrlPrefix() where applicable: the route() and transition() helpers add
|
|
74
|
+
* none, while path() (which also builds a `type: "route"` entry) and the
|
|
75
|
+
* structural helpers (layout/cache/middleware/parallel) do.
|
|
76
|
+
*/
|
|
77
|
+
const emptySegmentBase = (): EntryPropDatas &
|
|
78
|
+
EntryPropSegments & { loading: undefined } => ({
|
|
79
|
+
loading: undefined,
|
|
80
|
+
middleware: [],
|
|
81
|
+
revalidate: [],
|
|
82
|
+
errorBoundary: [],
|
|
83
|
+
notFoundBoundary: [],
|
|
84
|
+
layout: [],
|
|
85
|
+
parallel: {},
|
|
86
|
+
intercept: [],
|
|
87
|
+
loader: [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run a children/use callback as a nested scope, flatten the result, and assert
|
|
92
|
+
* every item is a valid use item. `kind` preserves the existing error wording
|
|
93
|
+
* ("use()" vs "children" callback).
|
|
94
|
+
*/
|
|
95
|
+
function runAndValidateUseItems(
|
|
96
|
+
store: ReturnType<typeof getContext>,
|
|
97
|
+
namespace: string,
|
|
98
|
+
entry: EntryData,
|
|
99
|
+
cb: () => any,
|
|
100
|
+
label: string,
|
|
101
|
+
kind: "use" | "children",
|
|
102
|
+
): AllUseItems[] {
|
|
103
|
+
const result = store.run(namespace, entry, cb)?.flat(3);
|
|
104
|
+
return validateUseItems(result, namespace, label, kind);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Assert an already-invoked, flattened callback result is a use-item array. */
|
|
108
|
+
function validateUseItems(
|
|
109
|
+
result: any,
|
|
110
|
+
namespace: string,
|
|
111
|
+
label: string,
|
|
112
|
+
kind: "use" | "children",
|
|
113
|
+
): AllUseItems[] {
|
|
114
|
+
invariant(
|
|
115
|
+
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
116
|
+
`${label}() ${kind === "use" ? "use()" : "children"} callback must return an array of use items [${namespace}]`,
|
|
117
|
+
);
|
|
118
|
+
return result as AllUseItems[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** True when a children/use result contains no routes (directly or nested). */
|
|
122
|
+
const isOrphan = (result: AllUseItems[]): boolean =>
|
|
123
|
+
!result.some((item) => item != null && hasRoutesInItem(item));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Register a routeless structural entry as an orphan sibling: clear its parent
|
|
127
|
+
* pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
|
|
128
|
+
* docs/tree-structure.md) and push it onto the parent's layout[] so it renders
|
|
129
|
+
* as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
|
|
130
|
+
* validation and registers inline.
|
|
131
|
+
*/
|
|
132
|
+
const attachOrphanSibling = (
|
|
133
|
+
parent: EntryData | null,
|
|
134
|
+
entry: EntryData,
|
|
135
|
+
): void => {
|
|
136
|
+
entry.parent = null;
|
|
137
|
+
if (parent && "layout" in parent) parent.layout.push(entry);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
|
|
142
|
+
* entry that captures the attachments declared by a use() callback — restoring
|
|
143
|
+
* the original parent afterward, including on throw. loader()/intercept() each
|
|
144
|
+
* build their own tempParent shape (intercept keeps a loading get/set accessor
|
|
145
|
+
* and a captured-layouts array); this only centralizes the save/restore.
|
|
146
|
+
*/
|
|
147
|
+
function withParent<T>(ctx: HelperContext, temp: EntryData, fn: () => T): T {
|
|
148
|
+
const original = ctx.parent;
|
|
149
|
+
ctx.parent = temp;
|
|
150
|
+
try {
|
|
151
|
+
return fn();
|
|
152
|
+
} finally {
|
|
153
|
+
ctx.parent = original;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
64
157
|
const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
65
|
-
const ctx =
|
|
66
|
-
|
|
158
|
+
const { store, ctx } = requireDslContext(
|
|
159
|
+
"revalidate() must be called inside urls()",
|
|
160
|
+
);
|
|
67
161
|
|
|
68
162
|
// Attach to last entry in stack
|
|
69
163
|
const parent = ctx.parent;
|
|
70
164
|
if (!parent || !("revalidate" in parent)) {
|
|
71
165
|
invariant(false, "No parent entry available for revalidate()");
|
|
72
166
|
}
|
|
73
|
-
const name = `$${
|
|
167
|
+
const name = `$${store.getNextIndex("revalidate")}`;
|
|
74
168
|
parent.revalidate.push(fn);
|
|
75
169
|
return { name, type: "revalidate" } as RevalidateItem;
|
|
76
170
|
};
|
|
@@ -108,15 +202,16 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
|
|
|
108
202
|
* ```
|
|
109
203
|
*/
|
|
110
204
|
const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
|
|
111
|
-
const ctx =
|
|
112
|
-
|
|
205
|
+
const { store, ctx } = requireDslContext(
|
|
206
|
+
"errorBoundary() must be called inside urls()",
|
|
207
|
+
);
|
|
113
208
|
|
|
114
209
|
// Attach to parent entry in stack
|
|
115
210
|
const parent = ctx.parent;
|
|
116
211
|
if (!parent || !("errorBoundary" in parent)) {
|
|
117
212
|
invariant(false, "No parent entry available for errorBoundary()");
|
|
118
213
|
}
|
|
119
|
-
const name = `$${
|
|
214
|
+
const name = `$${store.getNextIndex("errorBoundary")}`;
|
|
120
215
|
parent.errorBoundary.push(fallback);
|
|
121
216
|
return { name, type: "errorBoundary" } as ErrorBoundaryItem;
|
|
122
217
|
};
|
|
@@ -155,15 +250,16 @@ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
|
|
|
155
250
|
const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
156
251
|
fallback,
|
|
157
252
|
) => {
|
|
158
|
-
const ctx =
|
|
159
|
-
|
|
253
|
+
const { store, ctx } = requireDslContext(
|
|
254
|
+
"notFoundBoundary() must be called inside urls()",
|
|
255
|
+
);
|
|
160
256
|
|
|
161
257
|
// Attach to parent entry in stack
|
|
162
258
|
const parent = ctx.parent;
|
|
163
259
|
if (!parent || !("notFoundBoundary" in parent)) {
|
|
164
260
|
invariant(false, "No parent entry available for notFoundBoundary()");
|
|
165
261
|
}
|
|
166
|
-
const name = `$${
|
|
262
|
+
const name = `$${store.getNextIndex("notFoundBoundary")}`;
|
|
167
263
|
parent.notFoundBoundary.push(fallback);
|
|
168
264
|
return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
|
|
169
265
|
};
|
|
@@ -177,8 +273,9 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
|
|
|
177
273
|
* for the intercept to activate.
|
|
178
274
|
*/
|
|
179
275
|
const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
180
|
-
const ctx =
|
|
181
|
-
|
|
276
|
+
const { store, ctx } = requireDslContext(
|
|
277
|
+
"when() must be called inside intercept()",
|
|
278
|
+
);
|
|
182
279
|
|
|
183
280
|
// The when() function needs to be captured by the intercept's tempParent
|
|
184
281
|
// which should have a `when` array. If not present, we're not inside intercept()
|
|
@@ -190,7 +287,7 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
|
|
|
190
287
|
);
|
|
191
288
|
}
|
|
192
289
|
|
|
193
|
-
const name = `$${
|
|
290
|
+
const name = `$${store.getNextIndex("when")}`;
|
|
194
291
|
parent.when.push(fn);
|
|
195
292
|
return { name, type: "when" } as WhenItem;
|
|
196
293
|
};
|
|
@@ -217,9 +314,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
217
314
|
| (() => UseItems<AllUseItems>),
|
|
218
315
|
maybeChildren?: () => UseItems<AllUseItems>,
|
|
219
316
|
) => {
|
|
220
|
-
const store =
|
|
221
|
-
|
|
222
|
-
|
|
317
|
+
const { store, ctx } = requireDslContext(
|
|
318
|
+
"cache() must be called inside urls()",
|
|
319
|
+
);
|
|
223
320
|
|
|
224
321
|
// Handle overloaded signature
|
|
225
322
|
let options: PartialCacheOptions | false;
|
|
@@ -271,26 +368,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
271
368
|
// Create orphan cache entry (like orphan layout)
|
|
272
369
|
// Subsequent siblings in the same array will attach to this entry
|
|
273
370
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
274
|
-
const
|
|
371
|
+
const urlPrefix = getUrlPrefix();
|
|
275
372
|
|
|
276
373
|
const entry = {
|
|
374
|
+
...emptySegmentBase(),
|
|
277
375
|
id: namespace,
|
|
278
376
|
shortCode: store.getShortCode("cache"),
|
|
279
377
|
type: "cache",
|
|
280
378
|
parent: parent, // link to current parent for hierarchy
|
|
281
379
|
cache: cacheConfig,
|
|
282
380
|
handler: RootLayout,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
revalidate: [],
|
|
286
|
-
errorBoundary: [],
|
|
287
|
-
notFoundBoundary: [],
|
|
288
|
-
layout: [],
|
|
289
|
-
parallel: {},
|
|
290
|
-
intercept: [],
|
|
291
|
-
loader: [],
|
|
292
|
-
...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
|
|
293
|
-
} as EntryData;
|
|
381
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
382
|
+
} satisfies EntryData;
|
|
294
383
|
|
|
295
384
|
// Attach to parent's layout array (cache entries are structural like layouts)
|
|
296
385
|
if (parent && "layout" in parent) {
|
|
@@ -317,9 +406,10 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
317
406
|
const namespace = `${ctx.namespace}.${cacheIndex}`;
|
|
318
407
|
const cacheShortCode = store.getShortCode("cache");
|
|
319
408
|
|
|
320
|
-
const
|
|
409
|
+
const urlPrefix = getUrlPrefix();
|
|
321
410
|
|
|
322
411
|
const entry = {
|
|
412
|
+
...emptySegmentBase(),
|
|
323
413
|
id: namespace,
|
|
324
414
|
shortCode: cacheShortCode,
|
|
325
415
|
type: "cache",
|
|
@@ -327,40 +417,22 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
327
417
|
cache: cacheConfig,
|
|
328
418
|
// Cache entries render like layouts (with Outlet as default handler)
|
|
329
419
|
handler: RootLayout, // RootLayout just renders <Outlet />
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
revalidate: [],
|
|
333
|
-
errorBoundary: [],
|
|
334
|
-
notFoundBoundary: [],
|
|
335
|
-
layout: [],
|
|
336
|
-
parallel: {},
|
|
337
|
-
intercept: [],
|
|
338
|
-
loader: [],
|
|
339
|
-
...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
|
|
340
|
-
} as EntryData;
|
|
420
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
421
|
+
} satisfies EntryData;
|
|
341
422
|
|
|
342
423
|
// Run children with cache entry as parent
|
|
343
|
-
const result =
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
424
|
+
const result = runAndValidateUseItems(
|
|
425
|
+
store,
|
|
426
|
+
namespace,
|
|
427
|
+
entry,
|
|
428
|
+
children,
|
|
429
|
+
"cache",
|
|
430
|
+
"children",
|
|
348
431
|
);
|
|
349
432
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
Array.isArray(result) &&
|
|
354
|
-
result.some((item) => hasRoutesInItem(item));
|
|
355
|
-
|
|
356
|
-
if (!hasRoutes) {
|
|
357
|
-
const parent = ctx.parent;
|
|
358
|
-
if (parent && "layout" in parent) {
|
|
359
|
-
// Attach to parent's layout array (cache entries are structural like layouts)
|
|
360
|
-
entry.parent = null;
|
|
361
|
-
parent.layout.push(entry);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
433
|
+
// Cache entries are structural like layouts: with no routes inside, register
|
|
434
|
+
// as an orphan sibling.
|
|
435
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
364
436
|
|
|
365
437
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
366
438
|
};
|
|
@@ -406,9 +478,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
406
478
|
}
|
|
407
479
|
}
|
|
408
480
|
|
|
409
|
-
const store =
|
|
410
|
-
|
|
411
|
-
|
|
481
|
+
const { store, ctx } = requireDslContext(
|
|
482
|
+
"middleware() must be called inside urls()",
|
|
483
|
+
);
|
|
412
484
|
|
|
413
485
|
if (!children) {
|
|
414
486
|
// Sibling mode: attach to parent entry
|
|
@@ -427,22 +499,15 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
427
499
|
|
|
428
500
|
const urlPrefix = getUrlPrefix();
|
|
429
501
|
const entry = {
|
|
502
|
+
...emptySegmentBase(),
|
|
430
503
|
id: namespace,
|
|
431
504
|
shortCode: store.getShortCode("layout"),
|
|
432
505
|
type: "layout",
|
|
433
506
|
parent: ctx.parent,
|
|
434
507
|
handler: RootLayout,
|
|
435
|
-
loading: undefined,
|
|
436
508
|
middleware: [...fns],
|
|
437
|
-
revalidate: [],
|
|
438
|
-
errorBoundary: [],
|
|
439
|
-
notFoundBoundary: [],
|
|
440
|
-
layout: [],
|
|
441
|
-
parallel: {},
|
|
442
|
-
intercept: [],
|
|
443
|
-
loader: [],
|
|
444
509
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
445
|
-
}
|
|
510
|
+
} satisfies EntryData;
|
|
446
511
|
|
|
447
512
|
// Run children callback. If the second arg was actually a middleware fn
|
|
448
513
|
// (old variadic form: middleware(mw1, mw2)), this will return a non-array
|
|
@@ -455,25 +520,14 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
455
520
|
"To pass multiple middleware, use middleware([fn1, fn2]).",
|
|
456
521
|
);
|
|
457
522
|
|
|
458
|
-
const result =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
523
|
+
const result = validateUseItems(
|
|
524
|
+
rawResult.flat(3),
|
|
525
|
+
namespace,
|
|
526
|
+
"middleware",
|
|
527
|
+
"children",
|
|
463
528
|
);
|
|
464
529
|
|
|
465
|
-
|
|
466
|
-
result &&
|
|
467
|
-
Array.isArray(result) &&
|
|
468
|
-
result.some((item) => item != null && hasRoutesInItem(item));
|
|
469
|
-
|
|
470
|
-
if (!hasRoutes) {
|
|
471
|
-
const parent = ctx.parent;
|
|
472
|
-
if (parent && "layout" in parent) {
|
|
473
|
-
entry.parent = null;
|
|
474
|
-
parent.layout.push(entry);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
530
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
477
531
|
|
|
478
532
|
return {
|
|
479
533
|
name: namespace,
|
|
@@ -483,9 +537,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
|
483
537
|
};
|
|
484
538
|
|
|
485
539
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
486
|
-
const store =
|
|
487
|
-
|
|
488
|
-
|
|
540
|
+
const { store, ctx } = requireDslContext(
|
|
541
|
+
"parallel() must be called inside urls()",
|
|
542
|
+
);
|
|
489
543
|
|
|
490
544
|
if (!ctx.parent || !ctx.parent?.parallel) {
|
|
491
545
|
invariant(false, "No parent entry available for parallel()");
|
|
@@ -537,20 +591,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
537
591
|
// Create full EntryData for parallel with its own loaders/revalidate/loading
|
|
538
592
|
const parallelUrlPrefix = getUrlPrefix();
|
|
539
593
|
const entry = {
|
|
594
|
+
...emptySegmentBase(),
|
|
540
595
|
id: namespace,
|
|
541
596
|
shortCode: store.getShortCode("parallel"),
|
|
542
597
|
type: "parallel",
|
|
543
598
|
parent: null, // Parallels don't participate in parent chain traversal
|
|
544
599
|
handler: unwrappedSlots,
|
|
545
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
546
|
-
middleware: [],
|
|
547
|
-
revalidate: [],
|
|
548
|
-
errorBoundary: [],
|
|
549
|
-
notFoundBoundary: [],
|
|
550
|
-
layout: [],
|
|
551
|
-
parallel: {},
|
|
552
|
-
intercept: [],
|
|
553
|
-
loader: [],
|
|
554
600
|
...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
|
|
555
601
|
...(hasStaticSlot
|
|
556
602
|
? {
|
|
@@ -605,10 +651,13 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
605
651
|
"parallel",
|
|
606
652
|
);
|
|
607
653
|
if (slotMergedUse) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
654
|
+
runAndValidateUseItems(
|
|
655
|
+
store,
|
|
656
|
+
namespace,
|
|
657
|
+
slotEntry,
|
|
658
|
+
slotMergedUse,
|
|
659
|
+
"parallel",
|
|
660
|
+
"use",
|
|
612
661
|
);
|
|
613
662
|
}
|
|
614
663
|
|
|
@@ -648,9 +697,9 @@ const intercept = (
|
|
|
648
697
|
handler: any,
|
|
649
698
|
use?: () => any[],
|
|
650
699
|
) => {
|
|
651
|
-
const store =
|
|
652
|
-
|
|
653
|
-
|
|
700
|
+
const { store, ctx } = requireDslContext(
|
|
701
|
+
"intercept() must be called inside urls()",
|
|
702
|
+
);
|
|
654
703
|
|
|
655
704
|
if (!ctx.parent || !ctx.parent?.intercept) {
|
|
656
705
|
invariant(false, "No parent entry available for intercept()");
|
|
@@ -689,15 +738,13 @@ const intercept = (
|
|
|
689
738
|
|
|
690
739
|
// Run merged use callback to collect loaders, revalidate, middleware, etc.
|
|
691
740
|
if (mergedUse) {
|
|
692
|
-
//
|
|
693
|
-
// so that middleware, loader, revalidate attach to the intercept entry
|
|
694
|
-
const originalParent = ctx.parent;
|
|
695
|
-
|
|
696
|
-
// Capture layouts in a temporary array
|
|
741
|
+
// Capture layout() calls into a temporary array
|
|
697
742
|
const capturedLayouts: EntryData[] = [];
|
|
698
743
|
|
|
744
|
+
// Temporary parent so middleware/loader/revalidate/when attach to the
|
|
745
|
+
// intercept entry; the loading get/set accessor mirrors writes onto `entry`.
|
|
699
746
|
const tempParent = {
|
|
700
|
-
...
|
|
747
|
+
...ctx.parent,
|
|
701
748
|
middleware: entry.middleware,
|
|
702
749
|
revalidate: entry.revalidate,
|
|
703
750
|
errorBoundary: entry.errorBoundary,
|
|
@@ -705,7 +752,6 @@ const intercept = (
|
|
|
705
752
|
loader: entry.loader,
|
|
706
753
|
layout: capturedLayouts, // Capture layout() calls
|
|
707
754
|
when: entry.when, // Capture when() conditions
|
|
708
|
-
// Use getter/setter to capture loading on the entry
|
|
709
755
|
get loading() {
|
|
710
756
|
return entry.loading;
|
|
711
757
|
},
|
|
@@ -713,12 +759,10 @@ const intercept = (
|
|
|
713
759
|
entry.loading = value;
|
|
714
760
|
},
|
|
715
761
|
};
|
|
716
|
-
ctx.parent = tempParent as EntryData;
|
|
717
|
-
|
|
718
|
-
const result = mergedUse()?.flat(3);
|
|
719
762
|
|
|
720
|
-
|
|
721
|
-
|
|
763
|
+
const result = withParent(ctx, tempParent as EntryData, () =>
|
|
764
|
+
mergedUse()?.flat(3),
|
|
765
|
+
);
|
|
722
766
|
|
|
723
767
|
// Extract layout from captured layouts (use first one if multiple)
|
|
724
768
|
// Layout inside intercept should always be ReactNode or Handler, not Record slots
|
|
@@ -728,10 +772,7 @@ const intercept = (
|
|
|
728
772
|
| Handler<any, any, any>;
|
|
729
773
|
}
|
|
730
774
|
|
|
731
|
-
|
|
732
|
-
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
733
|
-
`intercept() use() callback must return an array of use items [${namespace}]`,
|
|
734
|
-
);
|
|
775
|
+
validateUseItems(result, namespace, "intercept", "use");
|
|
735
776
|
}
|
|
736
777
|
|
|
737
778
|
ctx.parent.intercept.push(entry);
|
|
@@ -741,10 +782,10 @@ const intercept = (
|
|
|
741
782
|
/**
|
|
742
783
|
* Loader helper - attaches a loader to the current entry
|
|
743
784
|
*/
|
|
744
|
-
const
|
|
745
|
-
const store =
|
|
746
|
-
|
|
747
|
-
|
|
785
|
+
const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
786
|
+
const { store, ctx } = requireDslContext(
|
|
787
|
+
"loader() must be called inside urls()",
|
|
788
|
+
);
|
|
748
789
|
|
|
749
790
|
// Attach to last entry in stack
|
|
750
791
|
if (!ctx.parent || !ctx.parent?.loader) {
|
|
@@ -765,23 +806,22 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
765
806
|
|
|
766
807
|
// If any use callback is in effect, run it to collect revalidation rules and cache config
|
|
767
808
|
if (mergedUse) {
|
|
768
|
-
// Temporarily set context for revalidate()/cache() calls to target this loader
|
|
769
|
-
const originalParent = ctx.parent;
|
|
770
809
|
// Create a temporary "parent" with type "loader" so cache() can detect it.
|
|
771
810
|
// Save existing .cache to distinguish inherited config from newly set config.
|
|
772
|
-
const parentCache = (
|
|
811
|
+
const parentCache = (ctx.parent as any).cache;
|
|
773
812
|
const tempParent = {
|
|
774
|
-
...
|
|
813
|
+
...ctx.parent,
|
|
775
814
|
type: "loader",
|
|
776
815
|
revalidate: loaderEntry.revalidate,
|
|
777
816
|
};
|
|
778
|
-
ctx.parent = tempParent as EntryData;
|
|
779
817
|
|
|
780
|
-
const result =
|
|
818
|
+
const result = withParent(ctx, tempParent as EntryData, () =>
|
|
819
|
+
mergedUse()?.flat(3),
|
|
820
|
+
);
|
|
781
821
|
|
|
782
822
|
// Copy cache config only if cache() was called during the use() callback.
|
|
783
|
-
// The spread
|
|
784
|
-
//
|
|
823
|
+
// The spread may carry an inherited .cache from a parent cache() boundary —
|
|
824
|
+
// only copy if it was newly set.
|
|
785
825
|
if (
|
|
786
826
|
(tempParent as any).cache &&
|
|
787
827
|
(tempParent as any).cache !== parentCache
|
|
@@ -789,13 +829,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
789
829
|
(loaderEntry as any).cache = (tempParent as any).cache;
|
|
790
830
|
}
|
|
791
831
|
|
|
792
|
-
|
|
793
|
-
ctx.parent = originalParent;
|
|
794
|
-
|
|
795
|
-
invariant(
|
|
796
|
-
Array.isArray(result) && result.every((item) => isValidUseItem(item)),
|
|
797
|
-
`loader() use() callback must return an array of use items [${name}]`,
|
|
798
|
-
);
|
|
832
|
+
validateUseItems(result, name, "loader", "use");
|
|
799
833
|
}
|
|
800
834
|
|
|
801
835
|
ctx.parent.loader.push(loaderEntry);
|
|
@@ -806,10 +840,10 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
|
|
|
806
840
|
* Loading helper - attaches a loading component to the current entry
|
|
807
841
|
* Loading components are static (no context) and shown during navigation
|
|
808
842
|
*/
|
|
809
|
-
const
|
|
810
|
-
const store =
|
|
811
|
-
|
|
812
|
-
|
|
843
|
+
const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
844
|
+
const { store, ctx } = requireDslContext(
|
|
845
|
+
"loading() must be called inside urls()",
|
|
846
|
+
);
|
|
813
847
|
|
|
814
848
|
const parent = ctx.parent;
|
|
815
849
|
if (!parent || !("loading" in parent)) {
|
|
@@ -835,7 +869,7 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
835
869
|
* Transition helper - attaches a ViewTransition config to the current entry
|
|
836
870
|
* or wraps a group of routes in a transparent layout with ViewTransition
|
|
837
871
|
*/
|
|
838
|
-
const
|
|
872
|
+
const transition = (
|
|
839
873
|
configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
|
|
840
874
|
maybeChildren?: () => UseItems<AllUseItems>,
|
|
841
875
|
): TransitionItem => {
|
|
@@ -849,9 +883,9 @@ const transitionFn = (
|
|
|
849
883
|
const children: (() => UseItems<AllUseItems>) | undefined =
|
|
850
884
|
typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
|
|
851
885
|
|
|
852
|
-
const store =
|
|
853
|
-
|
|
854
|
-
|
|
886
|
+
const { store, ctx } = requireDslContext(
|
|
887
|
+
"transition() must be called inside urls()",
|
|
888
|
+
);
|
|
855
889
|
|
|
856
890
|
const name = `$${store.getNextIndex("transition")}`;
|
|
857
891
|
|
|
@@ -868,68 +902,43 @@ const transitionFn = (
|
|
|
868
902
|
// Position 2: wrapper — create a transparent layout with transition config
|
|
869
903
|
const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
|
|
870
904
|
const entry = {
|
|
905
|
+
...emptySegmentBase(),
|
|
871
906
|
id: namespace,
|
|
872
907
|
shortCode: store.getShortCode("layout"),
|
|
873
908
|
type: "layout",
|
|
874
909
|
parent: ctx.parent,
|
|
875
910
|
handler: RootLayout,
|
|
876
|
-
loading: undefined,
|
|
877
911
|
transition: config,
|
|
878
|
-
|
|
879
|
-
revalidate: [],
|
|
880
|
-
errorBoundary: [],
|
|
881
|
-
notFoundBoundary: [],
|
|
882
|
-
layout: [],
|
|
883
|
-
parallel: {},
|
|
884
|
-
intercept: [],
|
|
885
|
-
loader: [],
|
|
886
|
-
} as EntryData;
|
|
887
|
-
|
|
888
|
-
const result = store.run(namespace, entry, children)?.flat(3);
|
|
912
|
+
} satisfies EntryData;
|
|
889
913
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
914
|
+
const result = runAndValidateUseItems(
|
|
915
|
+
store,
|
|
916
|
+
namespace,
|
|
917
|
+
entry,
|
|
918
|
+
children,
|
|
919
|
+
"transition",
|
|
920
|
+
"children",
|
|
893
921
|
);
|
|
894
922
|
|
|
895
|
-
|
|
896
|
-
result &&
|
|
897
|
-
Array.isArray(result) &&
|
|
898
|
-
result.some((item) => hasRoutesInItem(item));
|
|
899
|
-
|
|
900
|
-
if (!hasRoutes) {
|
|
901
|
-
const parent = ctx.parent;
|
|
902
|
-
if (parent && "layout" in parent) {
|
|
903
|
-
entry.parent = null;
|
|
904
|
-
parent.layout.push(entry);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
923
|
+
if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
|
|
907
924
|
|
|
908
925
|
return { name: namespace, type: "transition" } as TransitionItem;
|
|
909
926
|
};
|
|
910
927
|
|
|
911
|
-
const
|
|
912
|
-
const store =
|
|
913
|
-
|
|
914
|
-
|
|
928
|
+
const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
929
|
+
const { store, ctx } = requireDslContext(
|
|
930
|
+
"route() must be called inside urls()",
|
|
931
|
+
);
|
|
915
932
|
|
|
916
933
|
const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
|
|
917
934
|
|
|
918
935
|
const entry = {
|
|
936
|
+
...emptySegmentBase(),
|
|
919
937
|
id: namespace,
|
|
920
938
|
shortCode: store.getShortCode("route"),
|
|
921
939
|
type: "route",
|
|
922
940
|
parent: ctx.parent,
|
|
923
941
|
handler: handler as unknown as Handler<any, any, any>,
|
|
924
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
925
|
-
middleware: [],
|
|
926
|
-
revalidate: [],
|
|
927
|
-
errorBoundary: [],
|
|
928
|
-
notFoundBoundary: [],
|
|
929
|
-
layout: [],
|
|
930
|
-
parallel: {},
|
|
931
|
-
intercept: [],
|
|
932
|
-
loader: [],
|
|
933
942
|
} satisfies EntryData;
|
|
934
943
|
|
|
935
944
|
/* We will throw if user is registring same route name twice */
|
|
@@ -944,10 +953,13 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
944
953
|
const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
|
|
945
954
|
/* Run use and attach handlers */
|
|
946
955
|
if (mergedUse) {
|
|
947
|
-
const result =
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
956
|
+
const result = runAndValidateUseItems(
|
|
957
|
+
store,
|
|
958
|
+
namespace,
|
|
959
|
+
entry,
|
|
960
|
+
mergedUse,
|
|
961
|
+
"route",
|
|
962
|
+
"use",
|
|
951
963
|
);
|
|
952
964
|
return { name: namespace, type: "route", uses: result } as RouteItem;
|
|
953
965
|
}
|
|
@@ -957,9 +969,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
|
|
|
957
969
|
};
|
|
958
970
|
|
|
959
971
|
const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
960
|
-
const store =
|
|
961
|
-
|
|
962
|
-
|
|
972
|
+
const { store, ctx } = requireDslContext(
|
|
973
|
+
"layout() must be called inside urls()",
|
|
974
|
+
);
|
|
963
975
|
|
|
964
976
|
invariant(
|
|
965
977
|
!ctx.parent || ctx.parent.type !== "parallel",
|
|
@@ -977,20 +989,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
977
989
|
|
|
978
990
|
const urlPrefix = getUrlPrefix();
|
|
979
991
|
const entry = {
|
|
992
|
+
...emptySegmentBase(),
|
|
980
993
|
id: namespace,
|
|
981
994
|
shortCode,
|
|
982
995
|
type: "layout",
|
|
983
996
|
parent: ctx.parent,
|
|
984
997
|
handler: unwrappedHandler,
|
|
985
|
-
loading: undefined, // Allow loading() to attach loading state
|
|
986
|
-
middleware: [],
|
|
987
|
-
revalidate: [],
|
|
988
|
-
errorBoundary: [],
|
|
989
|
-
notFoundBoundary: [],
|
|
990
|
-
parallel: {},
|
|
991
|
-
intercept: [],
|
|
992
|
-
layout: [],
|
|
993
|
-
loader: [],
|
|
994
998
|
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
995
999
|
...(isStatic
|
|
996
1000
|
? {
|
|
@@ -1012,11 +1016,13 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1012
1016
|
// Run merged use callback if present
|
|
1013
1017
|
let result: AllUseItems[] | undefined;
|
|
1014
1018
|
if (mergedUse) {
|
|
1015
|
-
result =
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1019
|
+
result = runAndValidateUseItems(
|
|
1020
|
+
store,
|
|
1021
|
+
namespace,
|
|
1022
|
+
entry,
|
|
1023
|
+
mergedUse,
|
|
1024
|
+
"layout",
|
|
1025
|
+
"use",
|
|
1020
1026
|
);
|
|
1021
1027
|
}
|
|
1022
1028
|
|
|
@@ -1058,9 +1064,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1058
1064
|
`Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
|
|
1059
1065
|
);
|
|
1060
1066
|
|
|
1061
|
-
|
|
1062
|
-
entry.parent = null;
|
|
1063
|
-
parent.layout.push(entry);
|
|
1067
|
+
attachOrphanSibling(parent, entry);
|
|
1064
1068
|
}
|
|
1065
1069
|
}
|
|
1066
1070
|
|
|
@@ -1073,33 +1077,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1073
1077
|
} as LayoutItem;
|
|
1074
1078
|
};
|
|
1075
1079
|
|
|
1076
|
-
const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
(item
|
|
1081
|
-
typeof item === "object" &&
|
|
1082
|
-
"type" in item &&
|
|
1083
|
-
[
|
|
1084
|
-
"layout",
|
|
1085
|
-
"route",
|
|
1086
|
-
"middleware",
|
|
1087
|
-
"revalidate",
|
|
1088
|
-
"parallel",
|
|
1089
|
-
"intercept",
|
|
1090
|
-
"loader",
|
|
1091
|
-
"loading",
|
|
1092
|
-
"errorBoundary",
|
|
1093
|
-
"notFoundBoundary",
|
|
1094
|
-
"when",
|
|
1095
|
-
"cache",
|
|
1096
|
-
"transition",
|
|
1097
|
-
"include", // For urls() include() helper
|
|
1098
|
-
].includes(item.type))
|
|
1099
|
-
);
|
|
1100
|
-
};
|
|
1080
|
+
const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
|
|
1081
|
+
item == null ||
|
|
1082
|
+
(typeof item === "object" &&
|
|
1083
|
+
"type" in item &&
|
|
1084
|
+
ALL_USE_ITEM_TYPES.has(item.type));
|
|
1101
1085
|
|
|
1102
|
-
//
|
|
1086
|
+
// DSL helpers exported for direct import from @rangojs/router and for
|
|
1087
|
+
// assembly into the RouteHelpers object in helper-factories.ts. The route-item
|
|
1088
|
+
// types are discriminated by their `type` literal, so the helpers carry no brand.
|
|
1103
1089
|
export {
|
|
1104
1090
|
layout,
|
|
1105
1091
|
cache,
|
|
@@ -1110,25 +1096,11 @@ export {
|
|
|
1110
1096
|
when,
|
|
1111
1097
|
errorBoundary,
|
|
1112
1098
|
notFoundBoundary,
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const isOrphanLayout = (item: AllUseItems): boolean => {
|
|
1119
|
-
return (
|
|
1120
|
-
item.type === "layout" &&
|
|
1121
|
-
!item.uses?.some((child) => hasRoutesInItem(child))
|
|
1122
|
-
);
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// Internal exports used by helper-factories.ts
|
|
1126
|
-
export {
|
|
1127
|
-
routeFn,
|
|
1128
|
-
loaderFn,
|
|
1129
|
-
loadingFn,
|
|
1130
|
-
transitionFn,
|
|
1131
|
-
hasRoutesInItem,
|
|
1099
|
+
route,
|
|
1100
|
+
loader,
|
|
1101
|
+
loading,
|
|
1102
|
+
transition,
|
|
1132
1103
|
isValidUseItem,
|
|
1133
|
-
|
|
1104
|
+
emptySegmentBase,
|
|
1105
|
+
runAndValidateUseItems,
|
|
1134
1106
|
};
|