@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +5091 -941
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +61 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +340 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- 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/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +91 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -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 +291 -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 +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +393 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +358 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -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/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -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 +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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 +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +977 -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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
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. 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.
|
|
109
|
+
*/
|
|
110
|
+
function insertRoute(
|
|
111
|
+
node: TrieNode,
|
|
112
|
+
segments: ParsedSegment[],
|
|
113
|
+
index: number,
|
|
114
|
+
leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
|
|
115
|
+
): void {
|
|
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.
|
|
118
|
+
const optionalParams: string[] = [];
|
|
119
|
+
const constraints: Record<string, string[]> = {};
|
|
120
|
+
|
|
121
|
+
for (const seg of segments) {
|
|
122
|
+
if (seg.type === "param") {
|
|
123
|
+
if (seg.optional) {
|
|
124
|
+
optionalParams.push(seg.value);
|
|
125
|
+
}
|
|
126
|
+
if (seg.constraint) {
|
|
127
|
+
constraints[seg.value] = seg.constraint;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
133
|
+
...leaf,
|
|
134
|
+
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
135
|
+
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
insertSegments(node, segments, index, leafBase, []);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract ancestry map from a built trie by visiting all leaf nodes.
|
|
143
|
+
* Returns { routeName: ancestryShortCodes[] } for every route in the trie.
|
|
144
|
+
*/
|
|
145
|
+
export function extractAncestryFromTrie(
|
|
146
|
+
root: TrieNode,
|
|
147
|
+
): Record<string, string[]> {
|
|
148
|
+
const result: Record<string, string[]> = {};
|
|
149
|
+
|
|
150
|
+
function visit(node: TrieNode): void {
|
|
151
|
+
if (node.r) {
|
|
152
|
+
result[node.r.n] = node.r.a;
|
|
153
|
+
}
|
|
154
|
+
if (node.w) {
|
|
155
|
+
result[node.w.n] = node.w.a;
|
|
156
|
+
}
|
|
157
|
+
if (node.s) {
|
|
158
|
+
for (const child of Object.values(node.s)) {
|
|
159
|
+
visit(child);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (node.xp) {
|
|
163
|
+
for (const child of Object.values(node.xp)) {
|
|
164
|
+
visit(child.c);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (node.p) {
|
|
168
|
+
visit(node.p.c);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
visit(root);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Merge a new leaf with an existing leaf, handling content negotiation.
|
|
178
|
+
* When an RSC route and response-type routes share the same URL pattern,
|
|
179
|
+
* the RSC route becomes the primary leaf and response-type routes are
|
|
180
|
+
* appended to the nv (negotiate variants) array.
|
|
181
|
+
* Multiple response types on the same path are supported (json + text + xml).
|
|
182
|
+
*/
|
|
183
|
+
function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
|
|
184
|
+
if (!existing) return leaf;
|
|
185
|
+
|
|
186
|
+
if (existing.rt && leaf.rt) {
|
|
187
|
+
// Both are response-type: preserve old as variant
|
|
188
|
+
const merged = leaf;
|
|
189
|
+
merged.nv = existing.nv || [];
|
|
190
|
+
merged.nv.push({ routeKey: existing.n, responseType: existing.rt });
|
|
191
|
+
return merged;
|
|
192
|
+
}
|
|
193
|
+
if (leaf.rt && !existing.rt) {
|
|
194
|
+
// RSC primary exists, new leaf is response-type: append variant
|
|
195
|
+
// RSC was defined first (it was already the existing leaf)
|
|
196
|
+
if (!existing.nv) {
|
|
197
|
+
existing.nv = [];
|
|
198
|
+
existing.rf = true;
|
|
199
|
+
}
|
|
200
|
+
existing.nv.push({ routeKey: leaf.n, responseType: leaf.rt });
|
|
201
|
+
return existing;
|
|
202
|
+
}
|
|
203
|
+
if (!leaf.rt && existing.rt) {
|
|
204
|
+
// Response-type was primary, new leaf is RSC: swap and move old to variants
|
|
205
|
+
// RSC was defined second (response-type was already the existing leaf)
|
|
206
|
+
if (!leaf.nv) leaf.nv = [];
|
|
207
|
+
if (existing.nv) leaf.nv.push(...existing.nv);
|
|
208
|
+
leaf.nv.push({ routeKey: existing.n, responseType: existing.rt });
|
|
209
|
+
// rf intentionally not set — RSC came after response-type variants
|
|
210
|
+
return leaf;
|
|
211
|
+
}
|
|
212
|
+
// Both RSC (last wins): overwrite
|
|
213
|
+
return leaf;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
|
|
217
|
+
node.r = mergeLeaves(node.r, leaf);
|
|
218
|
+
}
|
|
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
|
+
|
|
229
|
+
function insertSegments(
|
|
230
|
+
node: TrieNode,
|
|
231
|
+
segments: ParsedSegment[],
|
|
232
|
+
index: number,
|
|
233
|
+
leafBase: Omit<TrieLeaf, "pa">,
|
|
234
|
+
paramNames: string[],
|
|
235
|
+
): void {
|
|
236
|
+
// Base case: all segments consumed, add terminal with branch-local pa
|
|
237
|
+
if (index >= segments.length) {
|
|
238
|
+
mergeLeaf(node, buildLeaf(leafBase, paramNames));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const segment = segments[index];
|
|
243
|
+
|
|
244
|
+
if (segment.type === "static") {
|
|
245
|
+
if (!node.s) node.s = {};
|
|
246
|
+
if (!node.s[segment.value]) node.s[segment.value] = {};
|
|
247
|
+
insertSegments(
|
|
248
|
+
node.s[segment.value],
|
|
249
|
+
segments,
|
|
250
|
+
index + 1,
|
|
251
|
+
leafBase,
|
|
252
|
+
paramNames,
|
|
253
|
+
);
|
|
254
|
+
} else if (segment.type === "param") {
|
|
255
|
+
if (segment.optional) {
|
|
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);
|
|
260
|
+
}
|
|
261
|
+
if (segment.suffix) {
|
|
262
|
+
// Suffix param: keyed by suffix string (e.g., ".html")
|
|
263
|
+
if (!node.xp) node.xp = {};
|
|
264
|
+
if (!node.xp[segment.suffix]) {
|
|
265
|
+
node.xp[segment.suffix] = { n: segment.value, c: {} };
|
|
266
|
+
}
|
|
267
|
+
insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
|
|
268
|
+
...paramNames,
|
|
269
|
+
segment.value,
|
|
270
|
+
]);
|
|
271
|
+
} else {
|
|
272
|
+
if (!node.p) {
|
|
273
|
+
node.p = { n: segment.value, c: {} };
|
|
274
|
+
}
|
|
275
|
+
insertSegments(node.p.c, segments, index + 1, leafBase, [
|
|
276
|
+
...paramNames,
|
|
277
|
+
segment.value,
|
|
278
|
+
]);
|
|
279
|
+
}
|
|
280
|
+
} else if (segment.type === "wildcard") {
|
|
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
|
+
};
|
|
287
|
+
const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
|
|
288
|
+
const merged = mergeLeaves(existing, wildLeaf);
|
|
289
|
+
node.w = merged as TrieLeaf & { pn: string };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -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
|
+
}
|