@rangojs/router 0.0.0-experimental.0f44aca1
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/AGENTS.md +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time Route Trie Construction
|
|
3
|
+
*
|
|
4
|
+
* Builds a serializable trie from the route manifest for O(path_length)
|
|
5
|
+
* route matching at runtime. Each trie leaf embeds the route's ancestry
|
|
6
|
+
* shortCodes for layout pruning.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
parsePattern,
|
|
11
|
+
type ParsedSegment,
|
|
12
|
+
} from "../router/pattern-matching.js";
|
|
13
|
+
|
|
14
|
+
// -- Trie data structures (compact keys for JSON serialization) --
|
|
15
|
+
|
|
16
|
+
export interface TrieLeaf {
|
|
17
|
+
/** Route name (e.g., "site.l1_500") */
|
|
18
|
+
n: string;
|
|
19
|
+
/** Static prefix of the entry (e.g., "/site") */
|
|
20
|
+
sp: string;
|
|
21
|
+
/** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
|
|
22
|
+
a: string[];
|
|
23
|
+
/** Optional param names (absent params get empty string value) */
|
|
24
|
+
op?: string[];
|
|
25
|
+
/** Constraint validation: paramName -> allowed values */
|
|
26
|
+
cv?: Record<string, string[]>;
|
|
27
|
+
/** Ordered param names for this route (positional) */
|
|
28
|
+
pa?: string[];
|
|
29
|
+
/** Trailing slash mode */
|
|
30
|
+
ts?: string;
|
|
31
|
+
/** Route has pre-rendered data available */
|
|
32
|
+
pr?: true;
|
|
33
|
+
/** Passthrough: handler kept in bundle for live fallback on unknown params */
|
|
34
|
+
pt?: true;
|
|
35
|
+
/** Response type for non-RSC routes (json, text, image, any) */
|
|
36
|
+
rt?: string;
|
|
37
|
+
/** Negotiate variants: response-type routes sharing this path */
|
|
38
|
+
nv?: Array<{ routeKey: string; responseType: string }>;
|
|
39
|
+
/** RSC-first: RSC route was defined before response-type variants */
|
|
40
|
+
rf?: true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TrieNode {
|
|
44
|
+
/** Route terminal at this node */
|
|
45
|
+
r?: TrieLeaf;
|
|
46
|
+
/** Static segment children */
|
|
47
|
+
s?: Record<string, TrieNode>;
|
|
48
|
+
/** Param child: { n: paramName, c: child node } */
|
|
49
|
+
p?: { n: string; c: TrieNode };
|
|
50
|
+
/** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
|
|
51
|
+
xp?: Record<string, { n: string; c: TrieNode }>;
|
|
52
|
+
/** Wildcard terminal: leaf + paramName */
|
|
53
|
+
w?: TrieLeaf & { pn: string };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a route trie from build-time manifest data.
|
|
58
|
+
*
|
|
59
|
+
* @param routeManifest - Map of route name to full URL pattern
|
|
60
|
+
* @param routeAncestry - Map of route name to ancestry shortCodes
|
|
61
|
+
* @param routeToStaticPrefix - Map of route name to its entry's staticPrefix
|
|
62
|
+
* @param routeTrailingSlash - Optional map of route name to trailing slash mode
|
|
63
|
+
*/
|
|
64
|
+
export function buildRouteTrie(
|
|
65
|
+
routeManifest: Record<string, string>,
|
|
66
|
+
routeAncestry: Record<string, string[]>,
|
|
67
|
+
routeToStaticPrefix: Record<string, string>,
|
|
68
|
+
routeTrailingSlash?: Record<string, string>,
|
|
69
|
+
prerenderRouteNames?: Set<string>,
|
|
70
|
+
passthroughRouteNames?: Set<string>,
|
|
71
|
+
responseTypeRoutes?: Record<string, string>,
|
|
72
|
+
): TrieNode {
|
|
73
|
+
const root: TrieNode = {};
|
|
74
|
+
|
|
75
|
+
for (const [routeName, pattern] of Object.entries(routeManifest)) {
|
|
76
|
+
const ancestry = routeAncestry[routeName] || [];
|
|
77
|
+
const staticPrefix = routeToStaticPrefix[routeName] || "";
|
|
78
|
+
const trailingSlash = routeTrailingSlash?.[routeName];
|
|
79
|
+
const responseType = responseTypeRoutes?.[routeName];
|
|
80
|
+
|
|
81
|
+
// Detect and strip trailing slash from pattern for parsing
|
|
82
|
+
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
83
|
+
const normalizedPattern = hasTrailingSlash ? pattern.slice(0, -1) : pattern;
|
|
84
|
+
|
|
85
|
+
const segments = parsePattern(normalizedPattern);
|
|
86
|
+
insertRoute(root, segments, 0, {
|
|
87
|
+
n: routeName,
|
|
88
|
+
sp: staticPrefix,
|
|
89
|
+
a: ancestry,
|
|
90
|
+
...(trailingSlash ? { ts: trailingSlash } : {}),
|
|
91
|
+
...(prerenderRouteNames?.has(routeName) ? { pr: true } : {}),
|
|
92
|
+
...(passthroughRouteNames?.has(routeName) ? { pt: true } : {}),
|
|
93
|
+
...(responseType ? { rt: responseType } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return root;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Insert a route into the trie, handling optional params by forking
|
|
102
|
+
* the insertion path (one terminal without the param, one with).
|
|
103
|
+
*/
|
|
104
|
+
function insertRoute(
|
|
105
|
+
node: TrieNode,
|
|
106
|
+
segments: ParsedSegment[],
|
|
107
|
+
index: number,
|
|
108
|
+
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
109
|
+
): void {
|
|
110
|
+
// Collect param names, optional param names, and constraints across all segments
|
|
111
|
+
const paramNames: string[] = [];
|
|
112
|
+
const optionalParams: string[] = [];
|
|
113
|
+
const constraints: Record<string, string[]> = {};
|
|
114
|
+
|
|
115
|
+
for (const seg of segments) {
|
|
116
|
+
if (seg.type === "param") {
|
|
117
|
+
paramNames.push(seg.value);
|
|
118
|
+
if (seg.optional) {
|
|
119
|
+
optionalParams.push(seg.value);
|
|
120
|
+
}
|
|
121
|
+
if (seg.constraint) {
|
|
122
|
+
constraints[seg.value] = seg.constraint;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fullLeaf: TrieLeaf = {
|
|
128
|
+
...leaf,
|
|
129
|
+
...(paramNames.length > 0 ? { pa: paramNames } : {}),
|
|
130
|
+
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
131
|
+
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
insertSegments(node, segments, index, fullLeaf);
|
|
135
|
+
}
|
|
136
|
+
|
|
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
|
+
/**
|
|
143
|
+
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
144
|
+
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
145
|
+
*/
|
|
146
|
+
export function extractAncestryFromTrie(
|
|
147
|
+
root: TrieNode,
|
|
148
|
+
): Record<string, string[]> {
|
|
149
|
+
const result: Record<string, string[]> = {};
|
|
150
|
+
|
|
151
|
+
function visit(node: TrieNode): void {
|
|
152
|
+
if (node.r) {
|
|
153
|
+
result[node.r.n] = node.r.a;
|
|
154
|
+
}
|
|
155
|
+
if (node.w) {
|
|
156
|
+
result[node.w.n] = node.w.a;
|
|
157
|
+
}
|
|
158
|
+
if (node.s) {
|
|
159
|
+
for (const child of Object.values(node.s)) {
|
|
160
|
+
visit(child);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (node.xp) {
|
|
164
|
+
for (const child of Object.values(node.xp)) {
|
|
165
|
+
visit(child.c);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (node.p) {
|
|
169
|
+
visit(node.p.c);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
visit(root);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Merge a new leaf with an existing leaf, handling content negotiation.
|
|
179
|
+
* When an RSC route and response-type routes share the same URL pattern,
|
|
180
|
+
* the RSC route becomes the primary leaf and response-type routes are
|
|
181
|
+
* appended to the nv (negotiate variants) array.
|
|
182
|
+
* Multiple response types on the same path are supported (json + text + xml).
|
|
183
|
+
*/
|
|
184
|
+
function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
185
|
+
if (!existing) return leaf;
|
|
186
|
+
|
|
187
|
+
if (existing.rt && leaf.rt) {
|
|
188
|
+
// Both are response-type: preserve old as variant
|
|
189
|
+
const merged = leaf;
|
|
190
|
+
merged.nv = existing.nv || [];
|
|
191
|
+
merged.nv.push({ routeKey: existing.n, responseType: existing.rt });
|
|
192
|
+
return merged;
|
|
193
|
+
}
|
|
194
|
+
if (leaf.rt && !existing.rt) {
|
|
195
|
+
// RSC primary exists, new leaf is response-type: append variant
|
|
196
|
+
// RSC was defined first (it was already the existing leaf)
|
|
197
|
+
if (!existing.nv) {
|
|
198
|
+
existing.nv = [];
|
|
199
|
+
existing.rf = true;
|
|
200
|
+
}
|
|
201
|
+
existing.nv.push({ routeKey: leaf.n, responseType: leaf.rt });
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
if (!leaf.rt && existing.rt) {
|
|
205
|
+
// Response-type was primary, new leaf is RSC: swap and move old to variants
|
|
206
|
+
// RSC was defined second (response-type was already the existing leaf)
|
|
207
|
+
if (!leaf.nv) leaf.nv = [];
|
|
208
|
+
if (existing.nv) leaf.nv.push(...existing.nv);
|
|
209
|
+
leaf.nv.push({ routeKey: existing.n, responseType: existing.rt });
|
|
210
|
+
// rf intentionally not set — RSC came after response-type variants
|
|
211
|
+
return leaf;
|
|
212
|
+
}
|
|
213
|
+
// Both RSC (last wins): overwrite
|
|
214
|
+
return leaf;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
218
|
+
node.r = mergeLeaves(node.r, leaf);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function insertSegments(
|
|
222
|
+
node: TrieNode,
|
|
223
|
+
segments: ParsedSegment[],
|
|
224
|
+
index: number,
|
|
225
|
+
leaf: TrieLeaf,
|
|
226
|
+
): void {
|
|
227
|
+
// Base case: all segments consumed, add terminal
|
|
228
|
+
if (index >= segments.length) {
|
|
229
|
+
mergeLeaf(node, leaf);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const segment = segments[index];
|
|
234
|
+
|
|
235
|
+
if (segment.type === "static") {
|
|
236
|
+
if (!node.s) node.s = {};
|
|
237
|
+
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
238
|
+
insertSegments(node.s[segment.value], segments, index + 1, leaf);
|
|
239
|
+
} else if (segment.type === "param") {
|
|
240
|
+
if (segment.optional) {
|
|
241
|
+
// Optional param: add terminal at current node (param absent)
|
|
242
|
+
mergeLeaf(node, leaf);
|
|
243
|
+
// AND continue with param child (param present)
|
|
244
|
+
}
|
|
245
|
+
if (segment.suffix) {
|
|
246
|
+
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
247
|
+
if (!node.xp) node.xp = {};
|
|
248
|
+
if (!node.xp[segment.suffix]) {
|
|
249
|
+
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
250
|
+
}
|
|
251
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
|
|
252
|
+
} else {
|
|
253
|
+
if (!node.p) {
|
|
254
|
+
node.p = { n: segment.value, c: {} };
|
|
255
|
+
}
|
|
256
|
+
insertSegments(node.p.c, segments, index + 1, leaf);
|
|
257
|
+
}
|
|
258
|
+
} else if (segment.type === "wildcard") {
|
|
259
|
+
// Wildcard consumes all remaining segments
|
|
260
|
+
const wildLeaf = { ...leaf, pn: "*" };
|
|
261
|
+
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
262
|
+
const merged = mergeLeaves(existing, wildLeaf);
|
|
263
|
+
node.w = merged as TrieLeaf & { pn: string };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
export function getStringValue(node: ts.Node): string | null {
|
|
4
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
5
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractObjectStringProperties(
|
|
10
|
+
node: ts.ObjectLiteralExpression,
|
|
11
|
+
): Record<string, string> {
|
|
12
|
+
const result: Record<string, string> = {};
|
|
13
|
+
for (const prop of node.properties) {
|
|
14
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
15
|
+
const key = ts.isIdentifier(prop.name)
|
|
16
|
+
? prop.name.text
|
|
17
|
+
: ts.isStringLiteral(prop.name)
|
|
18
|
+
? prop.name.text
|
|
19
|
+
: null;
|
|
20
|
+
if (!key) continue;
|
|
21
|
+
const val = getStringValue(prop.initializer);
|
|
22
|
+
if (val !== null) result[key] = val;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import {
|
|
3
|
+
getStringValue,
|
|
4
|
+
extractObjectStringProperties,
|
|
5
|
+
} from "./ast-helpers.js";
|
|
6
|
+
import { extractParamsFromPattern } from "./param-extraction.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// AST-based route extraction
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract route definitions from source code by walking the TypeScript AST.
|
|
14
|
+
* Finds path() and path.json(), path.md(), etc. call expressions and extracts
|
|
15
|
+
* the pattern, name, params, and optional search schema from each.
|
|
16
|
+
* Skips unnamed paths (no { name: "..." }).
|
|
17
|
+
*/
|
|
18
|
+
export function extractRoutesFromSource(code: string): Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
pattern: string;
|
|
21
|
+
params?: Record<string, string>;
|
|
22
|
+
search?: Record<string, string>;
|
|
23
|
+
}> {
|
|
24
|
+
const sourceFile = ts.createSourceFile(
|
|
25
|
+
"input.tsx",
|
|
26
|
+
code,
|
|
27
|
+
ts.ScriptTarget.Latest,
|
|
28
|
+
true,
|
|
29
|
+
ts.ScriptKind.TSX,
|
|
30
|
+
);
|
|
31
|
+
const routes: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
pattern: string;
|
|
34
|
+
params?: Record<string, string>;
|
|
35
|
+
search?: Record<string, string>;
|
|
36
|
+
}> = [];
|
|
37
|
+
|
|
38
|
+
function visit(node: ts.Node) {
|
|
39
|
+
if (ts.isCallExpression(node)) {
|
|
40
|
+
const callee = node.expression;
|
|
41
|
+
const isPath =
|
|
42
|
+
(ts.isIdentifier(callee) && callee.text === "path") ||
|
|
43
|
+
(ts.isPropertyAccessExpression(callee) &&
|
|
44
|
+
ts.isIdentifier(callee.expression) &&
|
|
45
|
+
callee.expression.text === "path");
|
|
46
|
+
|
|
47
|
+
if (isPath && node.arguments.length >= 1) {
|
|
48
|
+
const route = extractRouteFromCallExpression(node);
|
|
49
|
+
if (route) routes.push(route);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
ts.forEachChild(node, visit);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
visit(sourceFile);
|
|
56
|
+
return routes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractRouteFromCallExpression(node: ts.CallExpression): {
|
|
60
|
+
name: string;
|
|
61
|
+
pattern: string;
|
|
62
|
+
params?: Record<string, string>;
|
|
63
|
+
search?: Record<string, string>;
|
|
64
|
+
} | null {
|
|
65
|
+
const patternNode = node.arguments[0];
|
|
66
|
+
const pattern = getStringValue(patternNode);
|
|
67
|
+
if (pattern === null) return null;
|
|
68
|
+
|
|
69
|
+
let name: string | null = null;
|
|
70
|
+
let search: Record<string, string> | undefined;
|
|
71
|
+
|
|
72
|
+
for (let i = 1; i < node.arguments.length; i++) {
|
|
73
|
+
const arg = node.arguments[i];
|
|
74
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
75
|
+
for (const prop of arg.properties) {
|
|
76
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
77
|
+
const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
|
|
78
|
+
if (propName === "name") {
|
|
79
|
+
name = getStringValue(prop.initializer);
|
|
80
|
+
} else if (
|
|
81
|
+
propName === "search" &&
|
|
82
|
+
ts.isObjectLiteralExpression(prop.initializer)
|
|
83
|
+
) {
|
|
84
|
+
search = extractObjectStringProperties(prop.initializer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!name) return null;
|
|
91
|
+
const params = extractParamsFromPattern(pattern);
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
pattern,
|
|
95
|
+
...(params ? { params } : {}),
|
|
96
|
+
...(search && Object.keys(search).length > 0 ? { search } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractParamsFromPattern,
|
|
3
|
+
formatRouteEntry,
|
|
4
|
+
} from "./param-extraction.js";
|
|
5
|
+
import { isAutoGeneratedRouteName } from "../../route-name.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Code generation
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a per-module types file from extracted routes.
|
|
13
|
+
* Output has zero imports, preventing circular references.
|
|
14
|
+
*/
|
|
15
|
+
export function generatePerModuleTypesSource(
|
|
16
|
+
routes: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
pattern: string;
|
|
19
|
+
params?: Record<string, string>;
|
|
20
|
+
search?: Record<string, string>;
|
|
21
|
+
}>,
|
|
22
|
+
): string {
|
|
23
|
+
const valid = routes.filter(({ name }) => {
|
|
24
|
+
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
25
|
+
console.warn(
|
|
26
|
+
`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`,
|
|
27
|
+
);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Deduplicate by name (first definition wins -- primary route before variants)
|
|
34
|
+
const deduped = new Map<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
pattern: string;
|
|
38
|
+
params?: Record<string, string>;
|
|
39
|
+
search?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
>();
|
|
42
|
+
for (const { name, pattern, params, search } of valid) {
|
|
43
|
+
if (deduped.has(name)) {
|
|
44
|
+
console.warn(
|
|
45
|
+
`[rsc-router] Duplicate route name "${name}" — keeping first definition`,
|
|
46
|
+
);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
deduped.set(name, { pattern, params, search });
|
|
50
|
+
}
|
|
51
|
+
const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
52
|
+
const body = sorted
|
|
53
|
+
.map(([name, { pattern, params, search }]) => {
|
|
54
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
55
|
+
return formatRouteEntry(key, pattern, params, search);
|
|
56
|
+
})
|
|
57
|
+
.join("\n");
|
|
58
|
+
return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generates a .ts file that augments RSCRouter.GeneratedRouteMap
|
|
63
|
+
* with route name -> pattern mappings. This enables Handler<"routeName">
|
|
64
|
+
* without circular references since the file has no imports from the app.
|
|
65
|
+
*/
|
|
66
|
+
export function generateRouteTypesSource(
|
|
67
|
+
routeManifest: Record<string, string>,
|
|
68
|
+
searchSchemas?: Record<string, Record<string, string>>,
|
|
69
|
+
): string {
|
|
70
|
+
const entries = Object.entries(routeManifest)
|
|
71
|
+
.filter(([name]) => !isAutoGeneratedRouteName(name))
|
|
72
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
73
|
+
|
|
74
|
+
const filteredSearchSchemas = searchSchemas
|
|
75
|
+
? Object.fromEntries(
|
|
76
|
+
Object.entries(searchSchemas).filter(
|
|
77
|
+
([name]) => !isAutoGeneratedRouteName(name),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
const objectBody = entries
|
|
83
|
+
.map(([name, pattern]) => {
|
|
84
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
85
|
+
const params = extractParamsFromPattern(pattern);
|
|
86
|
+
const search = filteredSearchSchemas?.[name];
|
|
87
|
+
return formatRouteEntry(key, pattern, params, search);
|
|
88
|
+
})
|
|
89
|
+
.join("\n");
|
|
90
|
+
|
|
91
|
+
return `// Auto-generated by @rangojs/router - do not edit
|
|
92
|
+
export const NamedRoutes = {
|
|
93
|
+
${objectBody}
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
declare global {
|
|
97
|
+
namespace RSCRouter {
|
|
98
|
+
interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
}
|