@rangojs/router 0.0.0-experimental.74 → 0.0.0-experimental.75
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 +1 -1
- package/src/build/route-trie.ts +50 -24
- package/src/index.ts +37 -9
- package/src/route-definition/dsl-helpers.ts +104 -11
- package/src/route-definition/helpers-types.ts +31 -11
- package/src/urls/path-helper-types.ts +13 -2
package/dist/vite/index.js
CHANGED
|
@@ -1864,7 +1864,7 @@ import { resolve } from "node:path";
|
|
|
1864
1864
|
// package.json
|
|
1865
1865
|
var package_default = {
|
|
1866
1866
|
name: "@rangojs/router",
|
|
1867
|
-
version: "0.0.0-experimental.
|
|
1867
|
+
version: "0.0.0-experimental.75",
|
|
1868
1868
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1869
1869
|
keywords: [
|
|
1870
1870
|
"react",
|
package/package.json
CHANGED
package/src/build/route-trie.ts
CHANGED
|
@@ -98,8 +98,14 @@ export function buildRouteTrie(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Insert a route into the trie
|
|
102
|
-
*
|
|
101
|
+
* Insert a route into the trie. Optional params expand into two branches at
|
|
102
|
+
* registration time (skip-first, then present), so each terminal lives at the
|
|
103
|
+
* correct depth for its number of bound params and carries a branch-local
|
|
104
|
+
* `pa` listing only those names. The trie's single-slot `node.p` is reused
|
|
105
|
+
* across branches because matching ignores `node.p.n` — the leaf's `pa` is
|
|
106
|
+
* the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
|
|
107
|
+
* last-wins rule produce greedy-leftmost semantics for free at any shared
|
|
108
|
+
* terminal depth.
|
|
103
109
|
*/
|
|
104
110
|
function insertRoute(
|
|
105
111
|
node: TrieNode,
|
|
@@ -107,14 +113,13 @@ function insertRoute(
|
|
|
107
113
|
index: number,
|
|
108
114
|
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
109
115
|
): void {
|
|
110
|
-
//
|
|
111
|
-
|
|
116
|
+
// op (full optional list) and cv (full constraint map) are route-level and
|
|
117
|
+
// identical on every terminal, so compute them once on the shared base.
|
|
112
118
|
const optionalParams: string[] = [];
|
|
113
119
|
const constraints: Record<string, string[]> = {};
|
|
114
120
|
|
|
115
121
|
for (const seg of segments) {
|
|
116
122
|
if (seg.type === "param") {
|
|
117
|
-
paramNames.push(seg.value);
|
|
118
123
|
if (seg.optional) {
|
|
119
124
|
optionalParams.push(seg.value);
|
|
120
125
|
}
|
|
@@ -124,21 +129,15 @@ function insertRoute(
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
const
|
|
132
|
+
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
128
133
|
...leaf,
|
|
129
|
-
...(paramNames.length > 0 ? { pa: paramNames } : {}),
|
|
130
134
|
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
131
135
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
132
136
|
};
|
|
133
137
|
|
|
134
|
-
insertSegments(node, segments, index,
|
|
138
|
+
insertSegments(node, segments, index, leafBase, []);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
/**
|
|
138
|
-
* Recursively insert segments into the trie.
|
|
139
|
-
* For optional params, we add a terminal at the current node (param absent)
|
|
140
|
-
* AND continue inserting into the param child (param present).
|
|
141
|
-
*/
|
|
142
141
|
/**
|
|
143
142
|
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
144
143
|
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
@@ -218,15 +217,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
|
218
217
|
node.r = mergeLeaves(node.r, leaf);
|
|
219
218
|
}
|
|
220
219
|
|
|
220
|
+
function buildLeaf(
|
|
221
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
222
|
+
paramNames: string[],
|
|
223
|
+
): TrieLeaf {
|
|
224
|
+
return paramNames.length > 0
|
|
225
|
+
? { ...leafBase, pa: [...paramNames] }
|
|
226
|
+
: { ...leafBase };
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
function insertSegments(
|
|
222
230
|
node: TrieNode,
|
|
223
231
|
segments: ParsedSegment[],
|
|
224
232
|
index: number,
|
|
225
|
-
|
|
233
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
234
|
+
paramNames: string[],
|
|
226
235
|
): void {
|
|
227
|
-
// Base case: all segments consumed, add terminal
|
|
236
|
+
// Base case: all segments consumed, add terminal with branch-local pa
|
|
228
237
|
if (index >= segments.length) {
|
|
229
|
-
mergeLeaf(node,
|
|
238
|
+
mergeLeaf(node, buildLeaf(leafBase, paramNames));
|
|
230
239
|
return;
|
|
231
240
|
}
|
|
232
241
|
|
|
@@ -235,12 +244,19 @@ function insertSegments(
|
|
|
235
244
|
if (segment.type === "static") {
|
|
236
245
|
if (!node.s) node.s = {};
|
|
237
246
|
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
238
|
-
insertSegments(
|
|
247
|
+
insertSegments(
|
|
248
|
+
node.s[segment.value],
|
|
249
|
+
segments,
|
|
250
|
+
index + 1,
|
|
251
|
+
leafBase,
|
|
252
|
+
paramNames,
|
|
253
|
+
);
|
|
239
254
|
} else if (segment.type === "param") {
|
|
240
255
|
if (segment.optional) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
//
|
|
256
|
+
// SKIP first: continue at the same node without binding this name.
|
|
257
|
+
// Skip-first ordering means the present-branch's TAKE overwrites any
|
|
258
|
+
// shared terminal later, giving greedy-leftmost semantics.
|
|
259
|
+
insertSegments(node, segments, index + 1, leafBase, paramNames);
|
|
244
260
|
}
|
|
245
261
|
if (segment.suffix) {
|
|
246
262
|
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
@@ -248,16 +264,26 @@ function insertSegments(
|
|
|
248
264
|
if (!node.xp[segment.suffix]) {
|
|
249
265
|
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
266
|
}
|
|
251
|
-
insertSegments(node.xp[segment.suffix].c, segments, index + 1,
|
|
267
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
|
|
268
|
+
...paramNames,
|
|
269
|
+
segment.value,
|
|
270
|
+
]);
|
|
252
271
|
} else {
|
|
253
272
|
if (!node.p) {
|
|
254
273
|
node.p = { n: segment.value, c: {} };
|
|
255
274
|
}
|
|
256
|
-
insertSegments(node.p.c, segments, index + 1,
|
|
275
|
+
insertSegments(node.p.c, segments, index + 1, leafBase, [
|
|
276
|
+
...paramNames,
|
|
277
|
+
segment.value,
|
|
278
|
+
]);
|
|
257
279
|
}
|
|
258
280
|
} else if (segment.type === "wildcard") {
|
|
259
|
-
// Wildcard consumes all remaining segments
|
|
260
|
-
|
|
281
|
+
// Wildcard consumes all remaining segments. Carry any params bound before
|
|
282
|
+
// the wildcard in pa so they zip correctly against paramValues at match.
|
|
283
|
+
const wildLeaf: TrieLeaf & { pn: string } = {
|
|
284
|
+
...buildLeaf(leafBase, paramNames),
|
|
285
|
+
pn: "*",
|
|
286
|
+
};
|
|
261
287
|
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
262
288
|
const merged = mergeLeaves(existing, wildLeaf);
|
|
263
289
|
node.w = merged as TrieLeaf & { pn: string };
|
package/src/index.ts
CHANGED
|
@@ -147,24 +147,52 @@ export { createVar, type ContextVar } from "./context-var.js";
|
|
|
147
147
|
export { nonce } from "./rsc/nonce.js";
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
150
|
+
* SSR/client stub for server-only `Prerender` function.
|
|
151
|
+
*
|
|
152
|
+
* Returns a lightweight stub object instead of throwing so that the
|
|
153
|
+
* production SSR build can safely bundle the RSC entry chunk — the SSR
|
|
154
|
+
* bundler resolves `@rangojs/router` to this (SSR) entry, so Prerender
|
|
155
|
+
* calls in RSC code must not crash at module-evaluation time.
|
|
151
156
|
*/
|
|
152
|
-
export function Prerender(
|
|
153
|
-
|
|
157
|
+
export function Prerender(
|
|
158
|
+
_handler?: any,
|
|
159
|
+
_optionsOrId?: any,
|
|
160
|
+
__injectedId?: string,
|
|
161
|
+
): any {
|
|
162
|
+
const id =
|
|
163
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
164
|
+
return { __brand: "prerenderHandler" as const, $$id: id };
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
/**
|
|
157
|
-
*
|
|
168
|
+
* SSR/client stub for server-only `Passthrough` function.
|
|
158
169
|
*/
|
|
159
|
-
export function Passthrough(
|
|
160
|
-
|
|
170
|
+
export function Passthrough(
|
|
171
|
+
_handler?: any,
|
|
172
|
+
_optionsOrId?: any,
|
|
173
|
+
__injectedId?: string,
|
|
174
|
+
): any {
|
|
175
|
+
const id =
|
|
176
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
177
|
+
return { __brand: "passthroughHandler" as const, $$id: id };
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
|
164
|
-
*
|
|
181
|
+
* SSR/client stub for server-only `Static` function.
|
|
182
|
+
*
|
|
183
|
+
* Returns a lightweight stub object instead of throwing so that the
|
|
184
|
+
* production SSR build can safely bundle the RSC entry chunk — the SSR
|
|
185
|
+
* bundler resolves `@rangojs/router` to this (SSR) entry, so Static
|
|
186
|
+
* calls in RSC code must not crash at module-evaluation time.
|
|
165
187
|
*/
|
|
166
|
-
export function Static(
|
|
167
|
-
|
|
188
|
+
export function Static(
|
|
189
|
+
_handler?: any,
|
|
190
|
+
_optionsOrId?: any,
|
|
191
|
+
__injectedId?: string,
|
|
192
|
+
): any {
|
|
193
|
+
const id =
|
|
194
|
+
typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
|
|
195
|
+
return { __brand: "staticHandler" as const, $$id: id };
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
/**
|
|
@@ -55,6 +55,9 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
|
55
55
|
if (item.type === "layout" && item.uses) {
|
|
56
56
|
return item.uses.some((child) => hasRoutesInItem(child));
|
|
57
57
|
}
|
|
58
|
+
if (item.type === "middleware" && item.uses) {
|
|
59
|
+
return item.uses.some((child) => hasRoutesInItem(child));
|
|
60
|
+
}
|
|
58
61
|
return false;
|
|
59
62
|
};
|
|
60
63
|
|
|
@@ -353,10 +356,37 @@ const cache: RouteHelpers<any, any>["cache"] = (
|
|
|
353
356
|
return { name: namespace, type: "cache", uses: result } as CacheItem;
|
|
354
357
|
};
|
|
355
358
|
|
|
356
|
-
const middleware: RouteHelpers<any, any>["middleware"] = (...
|
|
359
|
+
const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
|
|
360
|
+
// Four call forms:
|
|
361
|
+
// middleware(fn) — single fn, sibling
|
|
362
|
+
// middleware(fn, () => [...]) — single fn, wrapping
|
|
363
|
+
// middleware([fn1, fn2]) — array, sibling
|
|
364
|
+
// middleware([fn1, fn2], () => [...]) — array, wrapping
|
|
365
|
+
const isArray = Array.isArray(args[0]);
|
|
366
|
+
|
|
367
|
+
// Reject the removed variadic form before executing anything.
|
|
368
|
+
// middleware(fn1, fn2, fn3) — 3+ args, always wrong.
|
|
369
|
+
// middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
|
|
370
|
+
// children callback (length === 0) — legacy two-fn form, reject early.
|
|
371
|
+
if (
|
|
372
|
+
args.length > 2 ||
|
|
373
|
+
(!isArray &&
|
|
374
|
+
args.length === 2 &&
|
|
375
|
+
typeof args[1] === "function" &&
|
|
376
|
+
args[1].length > 0)
|
|
377
|
+
) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"middleware() no longer accepts variadic arguments. " +
|
|
380
|
+
"Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
|
|
385
|
+
const children: (() => any[]) | undefined =
|
|
386
|
+
typeof args[1] === "function" ? args[1] : undefined;
|
|
387
|
+
|
|
357
388
|
// Prevent "use cache" functions from being used as middleware.
|
|
358
|
-
|
|
359
|
-
for (const f of fn) {
|
|
389
|
+
for (const f of fns) {
|
|
360
390
|
if (isCachedFunction(f)) {
|
|
361
391
|
throw new Error(
|
|
362
392
|
`A "use cache" function cannot be used as middleware. ` +
|
|
@@ -367,17 +397,80 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
|
|
|
367
397
|
}
|
|
368
398
|
}
|
|
369
399
|
|
|
370
|
-
const
|
|
400
|
+
const store = getContext();
|
|
401
|
+
const ctx = store.getStore();
|
|
371
402
|
if (!ctx) throw new Error("middleware() must be called inside map()");
|
|
372
403
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
404
|
+
if (!children) {
|
|
405
|
+
// Sibling mode: attach to parent entry
|
|
406
|
+
const parent = ctx.parent;
|
|
407
|
+
if (!parent || !("middleware" in parent)) {
|
|
408
|
+
invariant(false, "No parent entry available for middleware()");
|
|
409
|
+
}
|
|
410
|
+
const name = `$${store.getNextIndex("middleware")}`;
|
|
411
|
+
parent.middleware.push(...fns);
|
|
412
|
+
return { name, type: "middleware" } as MiddlewareItem;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Wrapping mode: create a transparent layout that carries the middleware
|
|
416
|
+
const mwIndex = store.getNextIndex("middleware");
|
|
417
|
+
const namespace = `${ctx.namespace}.${mwIndex}`;
|
|
418
|
+
|
|
419
|
+
const urlPrefix = getUrlPrefix();
|
|
420
|
+
const entry = {
|
|
421
|
+
id: namespace,
|
|
422
|
+
shortCode: store.getShortCode("layout"),
|
|
423
|
+
type: "layout",
|
|
424
|
+
parent: ctx.parent,
|
|
425
|
+
handler: RootLayout,
|
|
426
|
+
loading: undefined,
|
|
427
|
+
middleware: [...fns],
|
|
428
|
+
revalidate: [],
|
|
429
|
+
errorBoundary: [],
|
|
430
|
+
notFoundBoundary: [],
|
|
431
|
+
layout: [],
|
|
432
|
+
parallel: {},
|
|
433
|
+
intercept: [],
|
|
434
|
+
loader: [],
|
|
435
|
+
...(urlPrefix ? { mountPath: urlPrefix } : {}),
|
|
436
|
+
} as EntryData;
|
|
437
|
+
|
|
438
|
+
// Run children callback. If the second arg was actually a middleware fn
|
|
439
|
+
// (old variadic form: middleware(mw1, mw2)), this will return a non-array
|
|
440
|
+
// and the invariant below gives a clear migration error.
|
|
441
|
+
const rawResult = store.run(namespace, entry, children);
|
|
442
|
+
|
|
443
|
+
invariant(
|
|
444
|
+
Array.isArray(rawResult),
|
|
445
|
+
"middleware(fn, children) expects the second argument to return an array of use items. " +
|
|
446
|
+
"To pass multiple middleware, use middleware([fn1, fn2]).",
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const result = rawResult.flat(3);
|
|
450
|
+
|
|
451
|
+
invariant(
|
|
452
|
+
result.every((item: any) => isValidUseItem(item)),
|
|
453
|
+
`middleware() children callback must return an array of use items [${namespace}]`,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const hasRoutes =
|
|
457
|
+
result &&
|
|
458
|
+
Array.isArray(result) &&
|
|
459
|
+
result.some((item) => item != null && hasRoutesInItem(item));
|
|
460
|
+
|
|
461
|
+
if (!hasRoutes) {
|
|
462
|
+
const parent = ctx.parent;
|
|
463
|
+
if (parent && "layout" in parent) {
|
|
464
|
+
entry.parent = null;
|
|
465
|
+
parent.layout.push(entry);
|
|
466
|
+
}
|
|
377
467
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
name: namespace,
|
|
471
|
+
type: "middleware",
|
|
472
|
+
uses: result,
|
|
473
|
+
} as MiddlewareItem;
|
|
381
474
|
};
|
|
382
475
|
|
|
383
476
|
const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
@@ -182,21 +182,41 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
182
182
|
): InterceptItem;
|
|
183
183
|
};
|
|
184
184
|
/**
|
|
185
|
-
* Attach middleware to the current route/layout
|
|
185
|
+
* Attach middleware to the current route/layout, or wrap child segments
|
|
186
|
+
*
|
|
187
|
+
* **Sibling mode** — attaches middleware to the parent entry:
|
|
186
188
|
* ```typescript
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
189
|
+
* layout(<DashboardShell />, () => [
|
|
190
|
+
* middleware(authMiddleware),
|
|
191
|
+
* middleware([authMiddleware, loggingMiddleware]),
|
|
192
|
+
* path("/", DashboardPage),
|
|
193
|
+
* ])
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* **Wrapping mode** — scopes middleware to the children only:
|
|
197
|
+
* ```typescript
|
|
198
|
+
* middleware(authMiddleware, () => [
|
|
199
|
+
* path("/dashboard", DashboardPage),
|
|
200
|
+
* path("/settings", SettingsPage),
|
|
201
|
+
* ])
|
|
193
202
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
203
|
+
* middleware([authMiddleware, loggingMiddleware], () => [
|
|
204
|
+
* path("/admin", AdminPage),
|
|
205
|
+
* ])
|
|
196
206
|
* ```
|
|
197
|
-
* @param fns - One or more middleware functions to execute in order
|
|
198
207
|
*/
|
|
199
|
-
middleware:
|
|
208
|
+
middleware: {
|
|
209
|
+
(fn: MiddlewareFn<TEnv>): MiddlewareItem;
|
|
210
|
+
(
|
|
211
|
+
fn: MiddlewareFn<TEnv>,
|
|
212
|
+
children: () => UseItems<LayoutUseItem>,
|
|
213
|
+
): MiddlewareItem;
|
|
214
|
+
(fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
|
|
215
|
+
(
|
|
216
|
+
fns: MiddlewareFn<TEnv>[],
|
|
217
|
+
children: () => UseItems<LayoutUseItem>,
|
|
218
|
+
): MiddlewareItem;
|
|
219
|
+
};
|
|
200
220
|
/**
|
|
201
221
|
* Control when a segment should revalidate during navigation
|
|
202
222
|
* ```typescript
|
|
@@ -264,9 +264,20 @@ export type PathHelpers<TEnv> = {
|
|
|
264
264
|
) => InterceptItem;
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
* Attach middleware to the current route/layout
|
|
267
|
+
* Attach middleware to the current route/layout, or wrap child segments
|
|
268
268
|
*/
|
|
269
|
-
middleware:
|
|
269
|
+
middleware: {
|
|
270
|
+
(fn: MiddlewareFn<TEnv>): MiddlewareItem;
|
|
271
|
+
(
|
|
272
|
+
fn: MiddlewareFn<TEnv>,
|
|
273
|
+
children: () => UseItems<LayoutUseItem>,
|
|
274
|
+
): MiddlewareItem;
|
|
275
|
+
(fns: MiddlewareFn<TEnv>[]): MiddlewareItem;
|
|
276
|
+
(
|
|
277
|
+
fns: MiddlewareFn<TEnv>[],
|
|
278
|
+
children: () => UseItems<LayoutUseItem>,
|
|
279
|
+
): MiddlewareItem;
|
|
280
|
+
};
|
|
270
281
|
|
|
271
282
|
/**
|
|
272
283
|
* Control when a segment should revalidate during navigation
|