@rangojs/router 0.0.0-experimental.a6f6907d → 0.0.0-experimental.a99e6916
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 +2 -2
- package/package.json +2 -2
- package/skills/middleware/SKILL.md +32 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/rango/SKILL.md +24 -22
- 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/skills/rango/SKILL.md
CHANGED
|
@@ -10,28 +10,30 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
|
|
|
10
10
|
|
|
11
11
|
## Skills
|
|
12
12
|
|
|
13
|
-
| Skill
|
|
14
|
-
|
|
|
15
|
-
| `/router-setup`
|
|
16
|
-
| `/route`
|
|
17
|
-
| `/layout`
|
|
18
|
-
| `/loader`
|
|
19
|
-
| `/middleware`
|
|
20
|
-
| `/intercept`
|
|
21
|
-
| `/parallel`
|
|
22
|
-
| `/caching`
|
|
23
|
-
| `/use-cache`
|
|
24
|
-
| `/cache-guide`
|
|
25
|
-
| `/document-cache`
|
|
26
|
-
| `/theme`
|
|
27
|
-
| `/links`
|
|
28
|
-
| `/hooks`
|
|
29
|
-
| `/typesafety`
|
|
30
|
-
| `/host-router`
|
|
31
|
-
| `/tailwind`
|
|
32
|
-
| `/response-routes`
|
|
33
|
-
| `/mime-routes`
|
|
34
|
-
| `/fonts`
|
|
13
|
+
| Skill | Description |
|
|
14
|
+
| ----------------------- | -------------------------------------------------------------------------- |
|
|
15
|
+
| `/router-setup` | Create and configure the RSC router |
|
|
16
|
+
| `/route` | Define routes with `urls()` and `path()` |
|
|
17
|
+
| `/layout` | Layouts that wrap child routes |
|
|
18
|
+
| `/loader` | Data loaders with `createLoader()` |
|
|
19
|
+
| `/middleware` | Request processing and authentication |
|
|
20
|
+
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
21
|
+
| `/parallel` | Multi-column layouts and sidebars |
|
|
22
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
23
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
24
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
25
|
+
| `/document-cache` | Edge caching with Cache-Control headers |
|
|
26
|
+
| `/theme` | Light/dark mode with FOUC prevention |
|
|
27
|
+
| `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
|
|
28
|
+
| `/hooks` | Client-side React hooks |
|
|
29
|
+
| `/typesafety` | Type-safe routes, params, href, and environment |
|
|
30
|
+
| `/host-router` | Multi-app host routing with domain/subdomain patterns |
|
|
31
|
+
| `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
|
|
32
|
+
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|
|
33
|
+
| `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
|
|
34
|
+
| `/fonts` | Load web fonts with preload hints |
|
|
35
|
+
| `/migrate-nextjs` | Migrate a Next.js App Router project to Rango |
|
|
36
|
+
| `/migrate-react-router` | Migrate a React Router / Remix project to Rango |
|
|
35
37
|
|
|
36
38
|
## Quick Start
|
|
37
39
|
|
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
|