@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109
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/README.md +4 -4
- package/dist/bin/rango.js +16 -16
- package/dist/vite/index.js +146 -150
- package/package.json +6 -6
- package/skills/hooks/SKILL.md +2 -0
- package/skills/links/SKILL.md +13 -1
- package/skills/loader/SKILL.md +1 -1
- package/skills/middleware/SKILL.md +3 -3
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/prerender/SKILL.md +13 -13
- package/skills/rango/SKILL.md +9 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/typesafety/SKILL.md +273 -31
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/react/location-state-shared.ts +3 -3
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/rsc-router.tsx +14 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/build/generate-manifest.ts +3 -3
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +3 -3
- package/src/build/route-types/router-processing.ts +4 -4
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/client.tsx +4 -7
- package/src/errors.ts +1 -1
- package/src/handle.ts +2 -2
- package/src/href-client.ts +136 -19
- package/src/index.rsc.ts +4 -4
- package/src/index.ts +2 -2
- package/src/loader.rsc.ts +1 -1
- package/src/loader.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/route-definition/dsl-helpers.ts +2 -2
- package/src/route-definition/helpers-types.ts +2 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/prerender-match.ts +1 -1
- package/src/router/router-interfaces.ts +34 -28
- package/src/router/router-options.ts +1 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/revalidation.ts +2 -2
- package/src/router.ts +13 -16
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +16 -16
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +3 -3
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/pattern-types.ts +34 -0
- package/src/urls/type-extraction.ts +6 -1
- package/src/use-loader.tsx +6 -4
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +3 -3
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +3 -3
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +33 -9
- package/src/vite/plugins/expose-internal-ids.ts +1 -1
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +1 -1
- package/src/vite/plugins/version-plugin.ts +2 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +11 -11
- package/src/vite/router-discovery.ts +26 -29
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/forward-user-plugins.ts +46 -17
- package/src/vite/utils/shared-utils.ts +26 -22
|
@@ -32,27 +32,35 @@ import { shallowEqual } from "./shallow-equal.js";
|
|
|
32
32
|
* const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
-
export function useHandle<T, A>(handle: Handle<T, A>): A
|
|
35
|
+
export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
|
|
36
36
|
export function useHandle<T, A, S>(
|
|
37
37
|
handle: Handle<T, A>,
|
|
38
|
-
selector: (data: A) => S,
|
|
38
|
+
selector: (data: Rango.FlightSerialize<A>) => S,
|
|
39
39
|
): S;
|
|
40
40
|
export function useHandle<T, A, S>(
|
|
41
41
|
handle: Handle<T, A>,
|
|
42
|
-
selector?: (data: A) => S,
|
|
43
|
-
): A | S {
|
|
42
|
+
selector?: (data: Rango.FlightSerialize<A>) => S,
|
|
43
|
+
): Rango.FlightSerialize<A> | S {
|
|
44
44
|
const ctx = useContext(NavigationStoreContext);
|
|
45
45
|
|
|
46
46
|
// Initial state from context event controller, or empty fallback without provider.
|
|
47
|
-
const [value, setValue] = useState<A | S>(() => {
|
|
47
|
+
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
48
48
|
if (!ctx) {
|
|
49
|
-
const collected = collectHandleData(
|
|
49
|
+
const collected = collectHandleData(
|
|
50
|
+
handle,
|
|
51
|
+
{},
|
|
52
|
+
[],
|
|
53
|
+
) as Rango.FlightSerialize<A>;
|
|
50
54
|
return selector ? selector(collected) : collected;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
// On client, use event controller state
|
|
54
58
|
const state = ctx.eventController.getHandleState();
|
|
55
|
-
const collected = collectHandleData(
|
|
59
|
+
const collected = collectHandleData(
|
|
60
|
+
handle,
|
|
61
|
+
state.data,
|
|
62
|
+
state.segmentOrder,
|
|
63
|
+
) as Rango.FlightSerialize<A>;
|
|
56
64
|
return selector ? selector(collected) : collected;
|
|
57
65
|
});
|
|
58
66
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
@@ -76,7 +84,7 @@ export function useHandle<T, A, S>(
|
|
|
76
84
|
handle,
|
|
77
85
|
currentHandleState.data,
|
|
78
86
|
currentHandleState.segmentOrder,
|
|
79
|
-
)
|
|
87
|
+
) as Rango.FlightSerialize<A>;
|
|
80
88
|
const currentValue = selectorRef.current
|
|
81
89
|
? selectorRef.current(currentCollected)
|
|
82
90
|
: currentCollected;
|
|
@@ -93,7 +101,7 @@ export function useHandle<T, A, S>(
|
|
|
93
101
|
handle,
|
|
94
102
|
state.data,
|
|
95
103
|
state.segmentOrder,
|
|
96
|
-
)
|
|
104
|
+
) as Rango.FlightSerialize<A>;
|
|
97
105
|
const nextValue = selectorRef.current
|
|
98
106
|
? selectorRef.current(collected)
|
|
99
107
|
: collected;
|
|
@@ -128,7 +128,7 @@ export interface BrowserAppContext {
|
|
|
128
128
|
let browserAppContext: BrowserAppContext | null = null;
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Initialize the browser app. Must be called before rendering
|
|
131
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
132
132
|
*
|
|
133
133
|
* This function:
|
|
134
134
|
* - Loads the initial RSC payload from the stream
|
|
@@ -325,11 +325,11 @@ export async function initBrowserApp(
|
|
|
325
325
|
// full lifecycle (fetching + streaming, before commit) without
|
|
326
326
|
// blocking on server actions.
|
|
327
327
|
if (eventController.getState().isNavigating) {
|
|
328
|
-
console.log("[
|
|
328
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
console.log("[
|
|
332
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
333
333
|
|
|
334
334
|
const abort = new AbortController();
|
|
335
335
|
hmrAbort = abort;
|
|
@@ -367,7 +367,7 @@ export async function initBrowserApp(
|
|
|
367
367
|
const newVersion = payload.metadata.version;
|
|
368
368
|
if (newVersion && newVersion !== version) {
|
|
369
369
|
console.log(
|
|
370
|
-
"[
|
|
370
|
+
"[Rango] HMR: version changed",
|
|
371
371
|
version,
|
|
372
372
|
"→",
|
|
373
373
|
newVersion,
|
|
@@ -415,10 +415,10 @@ export async function initBrowserApp(
|
|
|
415
415
|
|
|
416
416
|
await streamComplete;
|
|
417
417
|
handle.complete(new URL(window.location.href));
|
|
418
|
-
console.log("[
|
|
418
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
419
419
|
} catch (err) {
|
|
420
420
|
if (abort.signal.aborted) return;
|
|
421
|
-
console.warn("[
|
|
421
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
422
422
|
window.location.reload();
|
|
423
423
|
return;
|
|
424
424
|
} finally {
|
|
@@ -430,7 +430,7 @@ export async function initBrowserApp(
|
|
|
430
430
|
});
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
// Store context for
|
|
433
|
+
// Store context for Rango component
|
|
434
434
|
const context: BrowserAppContext = {
|
|
435
435
|
store,
|
|
436
436
|
eventController,
|
|
@@ -454,7 +454,7 @@ export async function initBrowserApp(
|
|
|
454
454
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
455
455
|
if (!browserAppContext) {
|
|
456
456
|
throw new Error(
|
|
457
|
-
"
|
|
457
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
return browserAppContext;
|
|
@@ -468,18 +468,18 @@ export function resetBrowserAppContext(): void {
|
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
/**
|
|
471
|
-
* Props for the
|
|
471
|
+
* Props for the Rango component
|
|
472
472
|
*/
|
|
473
|
-
export interface
|
|
473
|
+
export interface RangoProps {}
|
|
474
474
|
|
|
475
475
|
/**
|
|
476
|
-
*
|
|
476
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
477
477
|
*
|
|
478
478
|
* Must be called after initBrowserApp() has completed.
|
|
479
479
|
*
|
|
480
480
|
* @example
|
|
481
481
|
* ```tsx
|
|
482
|
-
* import { initBrowserApp,
|
|
482
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
483
483
|
* import { rscStream } from "rsc-html-stream/client";
|
|
484
484
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
485
485
|
*
|
|
@@ -489,14 +489,14 @@ export interface RSCRouterProps {}
|
|
|
489
489
|
* hydrateRoot(
|
|
490
490
|
* document,
|
|
491
491
|
* <React.StrictMode>
|
|
492
|
-
* <
|
|
492
|
+
* <Rango />
|
|
493
493
|
* </React.StrictMode>
|
|
494
494
|
* );
|
|
495
495
|
* }
|
|
496
496
|
* main();
|
|
497
497
|
* ```
|
|
498
498
|
*/
|
|
499
|
-
export function
|
|
499
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
500
500
|
const {
|
|
501
501
|
store,
|
|
502
502
|
eventController,
|
|
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
|
|
|
48
48
|
|
|
49
49
|
if (cachedCategory !== incomingCategory) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[
|
|
51
|
+
`[Rango] Tree structure mismatch detected in ${context} ` +
|
|
52
52
|
`for segment "${cached.id}": loading category changed from ` +
|
|
53
53
|
`"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
|
|
54
54
|
`"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
|
|
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
|
|
|
64
64
|
const incomingHasMount = !!incoming.mountPath;
|
|
65
65
|
if (cachedHasMount !== incomingHasMount) {
|
|
66
66
|
console.warn(
|
|
67
|
-
`[
|
|
67
|
+
`[Rango] MountContextProvider mismatch detected in ${context} ` +
|
|
68
68
|
`for segment "${cached.id}": mountPath changed from ` +
|
|
69
69
|
`${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
|
|
70
70
|
`${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { UrlPatterns } from "../urls.js";
|
|
12
12
|
import type { AllUseItems } from "../route-types.js";
|
|
13
13
|
import { extractStaticPrefix } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import { RangoContext, runWithPrefixes } from "../server/context.js";
|
|
15
15
|
import type { EntryData, TrackedInclude } from "../server/context.js";
|
|
16
16
|
import type { TrailingSlashMode } from "../types.js";
|
|
17
17
|
import { createRouteHelpers } from "../route-definition.js";
|
|
@@ -93,7 +93,7 @@ function buildPrefixTreeNode(
|
|
|
93
93
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
94
94
|
const trackedIncludes: TrackedInclude[] = [];
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
RangoContext.run(
|
|
97
97
|
{
|
|
98
98
|
manifest,
|
|
99
99
|
patterns: patternsMap,
|
|
@@ -296,7 +296,7 @@ export function generateManifestFull<TEnv>(
|
|
|
296
296
|
const searchSchemasMap = new Map<string, Record<string, string>>();
|
|
297
297
|
const trackedIncludes: TrackedInclude[] = [];
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
RangoContext.run(
|
|
300
300
|
{
|
|
301
301
|
manifest,
|
|
302
302
|
patterns: patternsMap,
|
|
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
|
|
|
23
23
|
const valid = routes.filter(({ name }) => {
|
|
24
24
|
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
25
25
|
console.warn(
|
|
26
|
-
`[
|
|
26
|
+
`[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
|
|
27
27
|
);
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
|
|
|
42
42
|
for (const { name, pattern, params, search } of valid) {
|
|
43
43
|
if (deduped.has(name)) {
|
|
44
44
|
console.warn(
|
|
45
|
-
`[
|
|
45
|
+
`[rango] Duplicate route name "${name}" — keeping first definition`,
|
|
46
46
|
);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Generates a .ts file that augments
|
|
62
|
+
* Generates a .ts file that augments Rango.GeneratedRouteMap
|
|
63
63
|
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
64
64
|
* without circular references since the file has no imports from the app.
|
|
65
65
|
*/
|
|
@@ -94,7 +94,7 @@ ${objectBody}
|
|
|
94
94
|
} as const;
|
|
95
95
|
|
|
96
96
|
declare global {
|
|
97
|
-
namespace
|
|
97
|
+
namespace Rango {
|
|
98
98
|
interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -376,7 +376,7 @@ export function buildCombinedRouteMapWithSearch(
|
|
|
376
376
|
const realPath = resolve(filePath);
|
|
377
377
|
const key = variableName ? `${realPath}:${variableName}` : realPath;
|
|
378
378
|
if (visited.has(key)) {
|
|
379
|
-
console.warn(`[
|
|
379
|
+
console.warn(`[rango] Circular include detected, skipping: ${key}`);
|
|
380
380
|
return { routes: {}, searchSchemas: {} };
|
|
381
381
|
}
|
|
382
382
|
visited.add(key);
|
|
@@ -106,7 +106,7 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
106
106
|
if (varNames.length > 0 && !existsSync(genPath)) {
|
|
107
107
|
writeFileSync(genPath, generatePerModuleTypesSource([]));
|
|
108
108
|
console.log(
|
|
109
|
-
`[
|
|
109
|
+
`[rango] Generated route types (placeholder) -> ${genPath}`,
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
return;
|
|
@@ -118,11 +118,11 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
|
|
|
118
118
|
: null;
|
|
119
119
|
if (existing !== genSource) {
|
|
120
120
|
writeFileSync(genPath, genSource);
|
|
121
|
-
console.log(`[
|
|
121
|
+
console.log(`[rango] Generated route types -> ${genPath}`);
|
|
122
122
|
}
|
|
123
123
|
} catch (err) {
|
|
124
124
|
console.warn(
|
|
125
|
-
`[
|
|
125
|
+
`[rango] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -61,7 +61,7 @@ function findRouterFilesRecursive(
|
|
|
61
61
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
62
|
} catch (err) {
|
|
63
63
|
console.warn(
|
|
64
|
-
`[
|
|
64
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
65
65
|
);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
@@ -142,7 +142,7 @@ export function findNestedRouterConflict(
|
|
|
142
142
|
|
|
143
143
|
export function formatNestedRouterConflictError(
|
|
144
144
|
conflict: { ancestor: string; nested: string },
|
|
145
|
-
prefix = "[
|
|
145
|
+
prefix = "[rango]",
|
|
146
146
|
): string {
|
|
147
147
|
return (
|
|
148
148
|
`${prefix} Nested router roots are not supported.\n` +
|
|
@@ -536,7 +536,7 @@ export function writeCombinedRouteTypes(
|
|
|
536
536
|
if (existsSync(oldCombinedPath)) {
|
|
537
537
|
unlinkSync(oldCombinedPath);
|
|
538
538
|
console.log(
|
|
539
|
-
`[
|
|
539
|
+
`[rango] Removed stale combined route types: ${oldCombinedPath}`,
|
|
540
540
|
);
|
|
541
541
|
}
|
|
542
542
|
} catch {}
|
|
@@ -611,7 +611,7 @@ export function writeCombinedRouteTypes(
|
|
|
611
611
|
}
|
|
612
612
|
writeFileSync(outPath, source);
|
|
613
613
|
console.log(
|
|
614
|
-
`[
|
|
614
|
+
`[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
|
|
615
615
|
);
|
|
616
616
|
}
|
|
617
617
|
}
|
|
@@ -54,7 +54,7 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
54
54
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
55
|
} catch (err) {
|
|
56
56
|
console.warn(
|
|
57
|
-
`[
|
|
57
|
+
`[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
|
|
58
58
|
);
|
|
59
59
|
return results;
|
|
60
60
|
}
|
package/src/client.tsx
CHANGED
|
@@ -409,13 +409,10 @@ export {
|
|
|
409
409
|
type LocationStateOptions,
|
|
410
410
|
} from "./browser/react/location-state.js";
|
|
411
411
|
|
|
412
|
-
// Type-safe href for client-side path validation
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
type PatternToPath,
|
|
417
|
-
type PathResponse,
|
|
418
|
-
} from "./href-client.js";
|
|
412
|
+
// Type-safe href for client-side path validation. The path and response types
|
|
413
|
+
// are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
|
|
414
|
+
// href-client.ts) — no import needed.
|
|
415
|
+
export { href, type PatternToPath } from "./href-client.js";
|
|
419
416
|
|
|
420
417
|
// Response envelope types for consuming JSON response routes
|
|
421
418
|
export type { ResponseEnvelope, ResponseError } from "./urls.js";
|
package/src/errors.ts
CHANGED
package/src/handle.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
97
97
|
|
|
98
98
|
if (!handleId && process.env.NODE_ENV === "development") {
|
|
99
99
|
throw new Error(
|
|
100
|
-
"[
|
|
100
|
+
"[rango] Handle is missing $$id. " +
|
|
101
101
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
102
102
|
"the handle is exported with: export const MyHandle = createHandle(...)",
|
|
103
103
|
);
|
|
@@ -151,7 +151,7 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
151
151
|
const collectFn = getCollectFn(handle.$$id);
|
|
152
152
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
153
153
|
console.warn(
|
|
154
|
-
`[
|
|
154
|
+
`[rango] Handle "${handle.$$id}" has no registered collect function. ` +
|
|
155
155
|
`Falling back to flat array. Ensure the handle module is imported so ` +
|
|
156
156
|
`createHandle() runs and registers the collect function.`,
|
|
157
157
|
);
|
package/src/href-client.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { GetRegisteredRoutes } from "./types.js";
|
|
18
|
+
import type { JsonSerialize } from "./serialize.js";
|
|
18
19
|
import type { ResponseEnvelope } from "./urls.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -103,29 +104,75 @@ type NameForPattern<TPattern extends string, TRoutes = GetRegisteredRoutes> = {
|
|
|
103
104
|
}[keyof TRoutes];
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
107
|
+
* Strip a query (`?…`) and/or hash (`#…`) suffix before matching, so a concrete
|
|
108
|
+
* URL like `/api/health?ts=1` still resolves to its route's response. Removes
|
|
109
|
+
* from the earliest of `?`/`#`: a `#` before the first `?` (the query is part of
|
|
110
|
+
* a fragment, e.g. `/health#top?x=1`) is handled, as is a `/:` that only appears
|
|
111
|
+
* inside the query (e.g. `/health?next=/:id`).
|
|
112
|
+
*/
|
|
113
|
+
type StripPathSuffix<T extends string> = T extends `${infer Base}?${string}`
|
|
114
|
+
? Base extends `${infer Frag}#${string}`
|
|
115
|
+
? Frag
|
|
116
|
+
: Base
|
|
117
|
+
: T extends `${infer Base}#${string}`
|
|
118
|
+
? Base
|
|
119
|
+
: T;
|
|
120
|
+
|
|
121
|
+
/** Extract a route entry's response payload (or `never` for RSC routes). */
|
|
122
|
+
type ResponsePayloadOf<TRoutes, K extends keyof TRoutes> = TRoutes[K] extends {
|
|
123
|
+
readonly response: infer R;
|
|
124
|
+
}
|
|
125
|
+
? Exclude<R, Response>
|
|
126
|
+
: never;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Look up the response payload for a route, keyed by either a route pattern
|
|
130
|
+
* (`/api/products/:id`) or a concrete path (`/api/products/123`). The same type
|
|
131
|
+
* serves a pattern lookup and a typed `fetch` wrapper that forwards a concrete
|
|
132
|
+
* `Rango.Path`:
|
|
110
133
|
*
|
|
111
|
-
*
|
|
112
|
-
* PathResponse<"/api/
|
|
134
|
+
* PathResponse<"/api/products/:id"> → Product // by pattern
|
|
135
|
+
* PathResponse<"/api/products/123"> → Product // by concrete path
|
|
113
136
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
137
|
+
* The query/hash suffix is stripped first; the stripped key is then treated as a
|
|
138
|
+
* pattern when it contains a `/:param` segment and matched exactly (precise even
|
|
139
|
+
* for nested dynamic routes), otherwise as a concrete path matched against each
|
|
140
|
+
* route's `PatternToPath` template. Because those holes are `${string}`
|
|
141
|
+
* (slash-greedy), a concrete path under a *nested* dynamic route can match several
|
|
142
|
+
* patterns and union their responses — pattern lookups do not have this
|
|
143
|
+
* looseness. RSC routes (no response) and unmatched keys resolve to `never`.
|
|
144
|
+
*/
|
|
145
|
+
type ResponsePayloadFor<
|
|
146
|
+
TPath extends string,
|
|
147
|
+
TRoutes = GetRegisteredRoutes,
|
|
148
|
+
> = ResponsePayloadForKey<StripPathSuffix<TPath>, TRoutes>;
|
|
149
|
+
|
|
150
|
+
type ResponsePayloadForKey<
|
|
151
|
+
TKey extends string,
|
|
152
|
+
TRoutes,
|
|
153
|
+
> = TKey extends `${string}/:${string}`
|
|
154
|
+
? {
|
|
155
|
+
[K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TKey
|
|
156
|
+
? ResponsePayloadOf<TRoutes, K>
|
|
157
|
+
: never;
|
|
158
|
+
}[keyof TRoutes]
|
|
159
|
+
: {
|
|
160
|
+
[K in keyof TRoutes]: TKey extends PatternToPath<RoutePattern<TRoutes, K>>
|
|
161
|
+
? ResponsePayloadOf<TRoutes, K>
|
|
162
|
+
: never;
|
|
163
|
+
}[keyof TRoutes];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Public response type for a route, keyed by pattern or concrete path. The
|
|
167
|
+
* payload is wrapped in `JsonSerialize` so it describes the JSON **wire** value a
|
|
168
|
+
* consumer receives from `fetch().then(r => r.json())`, not the handler's raw
|
|
169
|
+
* return type — e.g. a handler returning `{ createdAt: Date }` resolves here to
|
|
170
|
+
* `ResponseEnvelope<{ createdAt: string }>`.
|
|
116
171
|
*/
|
|
117
172
|
export type PathResponse<
|
|
118
|
-
|
|
173
|
+
TPath extends string,
|
|
119
174
|
TRoutes = GetRegisteredRoutes,
|
|
120
|
-
> = ResponseEnvelope<
|
|
121
|
-
{
|
|
122
|
-
[K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TPattern
|
|
123
|
-
? TRoutes[K] extends { readonly response: infer R }
|
|
124
|
-
? Exclude<R, Response>
|
|
125
|
-
: never
|
|
126
|
-
: never;
|
|
127
|
-
}[keyof TRoutes]
|
|
128
|
-
>;
|
|
175
|
+
> = ResponseEnvelope<JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>>;
|
|
129
176
|
|
|
130
177
|
/**
|
|
131
178
|
* Strip trailing slash from a path (e.g., "/blog/" -> "/blog" | "/blog/")
|
|
@@ -140,7 +187,7 @@ type OptionalTrailingSlash<T extends string> = T extends `${infer Base}/`
|
|
|
140
187
|
/**
|
|
141
188
|
* Union of all valid paths from registered routes
|
|
142
189
|
*
|
|
143
|
-
* Generated from
|
|
190
|
+
* Generated from Rango.RegisteredRoutes via module augmentation.
|
|
144
191
|
* Allows optional query strings and hash fragments.
|
|
145
192
|
*/
|
|
146
193
|
export type ValidPaths<TRoutes = GetRegisteredRoutes> =
|
|
@@ -154,6 +201,76 @@ export type ValidPaths<TRoutes = GetRegisteredRoutes> =
|
|
|
154
201
|
}[keyof TRoutes]
|
|
155
202
|
>;
|
|
156
203
|
|
|
204
|
+
// Module-scoped alias so the ambient `Rango.PathResponse` below can reference
|
|
205
|
+
// the module-level `PathResponse` without the global namespace shadowing the
|
|
206
|
+
// name when both are called `PathResponse`.
|
|
207
|
+
type GlobalPathResponse<
|
|
208
|
+
TPattern extends string,
|
|
209
|
+
TRoutes = GetRegisteredRoutes,
|
|
210
|
+
> = PathResponse<TPattern, TRoutes>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Ambient path types on the `Rango` namespace.
|
|
214
|
+
*
|
|
215
|
+
* These live on the same global namespace consumers already augment for
|
|
216
|
+
* `Rango.Env` / `Rango.Vars`, so they are reachable with no import wherever the
|
|
217
|
+
* router's types are in scope. They are the public, recommended surface for
|
|
218
|
+
* typing anything that wraps `href()`. `ValidPaths` / `PathResponse` stay as the
|
|
219
|
+
* internal building blocks behind them.
|
|
220
|
+
*/
|
|
221
|
+
declare global {
|
|
222
|
+
namespace Rango {
|
|
223
|
+
/**
|
|
224
|
+
* Union of every valid route path accepted by `href()`.
|
|
225
|
+
*
|
|
226
|
+
* Type a wrapper's path parameter as `Rango.Path` so it shares `href()`'s
|
|
227
|
+
* compile-time validation against the registered routes:
|
|
228
|
+
*
|
|
229
|
+
* ```ts
|
|
230
|
+
* import { href } from "@rangojs/router/client";
|
|
231
|
+
*
|
|
232
|
+
* export const appHref = (path: Rango.Path) => href(path);
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* Resolves from `Rango.RegisteredRoutes` when augmented, otherwise the
|
|
236
|
+
* auto-generated `Rango.GeneratedRouteMap`, otherwise a permissive
|
|
237
|
+
* `/${string}` fallback.
|
|
238
|
+
*/
|
|
239
|
+
type Path<TRoutes = GetRegisteredRoutes> = ValidPaths<TRoutes>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Response payload for a route, looked up from the global route map by
|
|
243
|
+
* either a route pattern (`/api/products/:id`) or a concrete path
|
|
244
|
+
* (`/api/products/123`). Because it accepts a concrete `Rango.Path`, it
|
|
245
|
+
* doubles as the return type of a typed `fetch` wrapper:
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
|
|
249
|
+
* type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
|
|
250
|
+
*
|
|
251
|
+
* const get = async <T extends Rango.Path>(
|
|
252
|
+
* path: T,
|
|
253
|
+
* ): Promise<Rango.PathResponse<T>> =>
|
|
254
|
+
* fetch(href(path)).then((r) => r.json());
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* The payload is the JSON **wire** shape (via `Rango.JsonSerialize`), not the
|
|
258
|
+
* handler's raw return — a handler returning `{ createdAt: Date }` resolves
|
|
259
|
+
* here to `ResponseEnvelope<{ createdAt: string }>`, matching what
|
|
260
|
+
* `fetch().then(r => r.json())` actually yields.
|
|
261
|
+
*
|
|
262
|
+
* Only resolves once `Rango.RegisteredRoutes` carries response metadata (the
|
|
263
|
+
* generated map has paths and search but no payloads). Pass an explicit route
|
|
264
|
+
* map as the second argument to look up against a non-global map (rarely
|
|
265
|
+
* needed in app code).
|
|
266
|
+
*/
|
|
267
|
+
type PathResponse<
|
|
268
|
+
TPath extends string,
|
|
269
|
+
TRoutes = GetRegisteredRoutes,
|
|
270
|
+
> = GlobalPathResponse<TPath, TRoutes>;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
157
274
|
/**
|
|
158
275
|
* Type-safe href function for client-side use
|
|
159
276
|
*
|
package/src/index.rsc.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type {
|
|
|
68
68
|
|
|
69
69
|
// Router options type (server-only, so import directly)
|
|
70
70
|
export type {
|
|
71
|
-
|
|
71
|
+
RangoOptions,
|
|
72
72
|
SSRStreamMode,
|
|
73
73
|
SSROptions,
|
|
74
74
|
ResolveStreamingContext,
|
|
@@ -152,7 +152,7 @@ export {
|
|
|
152
152
|
// Core router (server-side)
|
|
153
153
|
export {
|
|
154
154
|
createRouter,
|
|
155
|
-
type
|
|
155
|
+
type Rango,
|
|
156
156
|
type RootLayoutProps,
|
|
157
157
|
type RouterRequestInput,
|
|
158
158
|
} from "./router.js";
|
|
@@ -221,8 +221,8 @@ export {
|
|
|
221
221
|
type LocationStateOptions,
|
|
222
222
|
} from "./browser/react/location-state-shared.js";
|
|
223
223
|
|
|
224
|
-
// Path
|
|
225
|
-
|
|
224
|
+
// Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
|
|
225
|
+
// `Rango.PathResponse`, declared in href-client.ts) — no import needed.
|
|
226
226
|
|
|
227
227
|
// Telemetry sink
|
|
228
228
|
export { createConsoleSink } from "./router/telemetry.js";
|
package/src/index.ts
CHANGED
|
@@ -303,8 +303,8 @@ export {
|
|
|
303
303
|
type LocationStateOptions,
|
|
304
304
|
} from "./browser/react/location-state-shared.js";
|
|
305
305
|
|
|
306
|
-
// Path
|
|
307
|
-
|
|
306
|
+
// Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
|
|
307
|
+
// `Rango.PathResponse`, declared in href-client.ts) — no import needed.
|
|
308
308
|
|
|
309
309
|
// Telemetry types only — the createConsoleSink/createOTelSink values are
|
|
310
310
|
// server-only and live in index.rsc.ts (the `react-server` condition of the
|
package/src/loader.rsc.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function createLoader<T>(
|
|
|
54
54
|
|
|
55
55
|
if (!loaderId && process.env.NODE_ENV === "development") {
|
|
56
56
|
throw new Error(
|
|
57
|
-
"[
|
|
57
|
+
"[rango] Loader is missing $$id. " +
|
|
58
58
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
59
59
|
"the loader is exported with: export const MyLoader = createLoader(...)",
|
|
60
60
|
);
|
package/src/loader.ts
CHANGED
|
@@ -51,7 +51,7 @@ export function createLoader<T>(
|
|
|
51
51
|
|
|
52
52
|
if (!loaderId && process.env.NODE_ENV === "development") {
|
|
53
53
|
throw new Error(
|
|
54
|
-
"[
|
|
54
|
+
"[rango] Loader is missing $$id. " +
|
|
55
55
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
56
56
|
"the loader is exported with: export const MyLoader = createLoader(...)",
|
|
57
57
|
);
|
package/src/prerender.ts
CHANGED
|
@@ -69,9 +69,9 @@ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
|
|
|
69
69
|
* Default route map for Prerender named route resolution.
|
|
70
70
|
* Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
|
|
71
71
|
*/
|
|
72
|
-
type DefaultPrerenderRouteMap = keyof
|
|
72
|
+
type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never
|
|
73
73
|
? {}
|
|
74
|
-
:
|
|
74
|
+
: Rango.GeneratedRouteMap;
|
|
75
75
|
|
|
76
76
|
/** Extract params from a route map entry (string pattern or { path } object). */
|
|
77
77
|
type ExtractParamsFromEntry<TEntry> = TEntry extends string
|
|
@@ -378,7 +378,7 @@ export function Prerender<TParams extends Record<string, any>>(
|
|
|
378
378
|
|
|
379
379
|
if (!id) {
|
|
380
380
|
throw new Error(
|
|
381
|
-
"[
|
|
381
|
+
"[rango] Prerender: missing $$id. " +
|
|
382
382
|
"Ensure the exposeInternalIds Vite plugin is configured.",
|
|
383
383
|
);
|
|
384
384
|
}
|
|
@@ -499,7 +499,7 @@ export function Passthrough<
|
|
|
499
499
|
): PassthroughHandlerDefinition<TParams, TEnv> {
|
|
500
500
|
if (!isPrerenderHandler(prerenderDef)) {
|
|
501
501
|
throw new Error(
|
|
502
|
-
"[
|
|
502
|
+
"[rango] Passthrough: first argument must be a Prerender() definition.",
|
|
503
503
|
);
|
|
504
504
|
}
|
|
505
505
|
return {
|