@rangojs/router 0.5.2 → 0.6.0

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.
Files changed (173) hide show
  1. package/dist/bin/rango.js +343 -125
  2. package/dist/types/browser/react/use-router.d.ts +10 -3
  3. package/dist/types/browser/react/use-search-params.d.ts +57 -10
  4. package/dist/types/browser/types.d.ts +22 -0
  5. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  6. package/dist/types/build/route-trie.d.ts +4 -73
  7. package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
  8. package/dist/types/build/route-types/router-processing.d.ts +2 -3
  9. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  10. package/dist/types/cache/taint.d.ts +12 -6
  11. package/dist/types/client-urls/client-root.d.ts +38 -0
  12. package/dist/types/client-urls/client-urls.d.ts +5 -0
  13. package/dist/types/client-urls/navigation.d.ts +38 -0
  14. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  15. package/dist/types/client-urls/server-projection.d.ts +62 -0
  16. package/dist/types/client-urls/types.d.ts +144 -0
  17. package/dist/types/client.d.ts +12 -4
  18. package/dist/types/client.rsc.d.ts +4 -1
  19. package/dist/types/decode-loader-results.d.ts +37 -0
  20. package/dist/types/errors.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/loader-redirect.d.ts +27 -0
  23. package/dist/types/outlet-context.d.ts +12 -0
  24. package/dist/types/outlet-provider.d.ts +3 -1
  25. package/dist/types/redirect-origin.d.ts +4 -0
  26. package/dist/types/route-content-wrapper.d.ts +42 -1
  27. package/dist/types/route-definition/helpers-types.d.ts +13 -2
  28. package/dist/types/router/error-handling.d.ts +35 -1
  29. package/dist/types/router/intercept-resolution.d.ts +12 -0
  30. package/dist/types/router/loader-resolution.d.ts +24 -2
  31. package/dist/types/router/revalidation.d.ts +7 -0
  32. package/dist/types/router/route-trie-builder.d.ts +77 -0
  33. package/dist/types/router/router-interfaces.d.ts +20 -0
  34. package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
  35. package/dist/types/router/trie-matching.d.ts +1 -1
  36. package/dist/types/rsc/manifest-init.d.ts +5 -5
  37. package/dist/types/rsc/shell-capture.d.ts +9 -0
  38. package/dist/types/rsc/shell-serve.d.ts +11 -0
  39. package/dist/types/rsc/types.d.ts +30 -0
  40. package/dist/types/segment-system.d.ts +2 -0
  41. package/dist/types/server/context.d.ts +10 -0
  42. package/dist/types/server/handle-store.d.ts +34 -3
  43. package/dist/types/server/request-context.d.ts +11 -1
  44. package/dist/types/server.d.ts +1 -0
  45. package/dist/types/ssr/index.d.ts +22 -0
  46. package/dist/types/ssr/ssr-root.d.ts +10 -0
  47. package/dist/types/testing/dom.entry.d.ts +1 -1
  48. package/dist/types/testing/render-route.d.ts +16 -6
  49. package/dist/types/testing/run-loader.d.ts +9 -0
  50. package/dist/types/types/boundaries.d.ts +22 -0
  51. package/dist/types/types/index.d.ts +1 -1
  52. package/dist/types/types/loader-types.d.ts +57 -5
  53. package/dist/types/types/segments.d.ts +7 -0
  54. package/dist/types/urls/path-helper-types.d.ts +10 -4
  55. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  56. package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
  57. package/dist/types/vite/discovery/state.d.ts +8 -1
  58. package/dist/vite/index.js +5313 -2365
  59. package/package.json +1 -1
  60. package/skills/breadcrumbs/SKILL.md +39 -9
  61. package/skills/catalog.json +7 -1
  62. package/skills/client-urls/SKILL.md +338 -0
  63. package/skills/comparison/references/framework-comparison.md +23 -9
  64. package/skills/hooks/SKILL.md +2 -2
  65. package/skills/hooks/data.md +11 -2
  66. package/skills/hooks/handle-and-actions.md +7 -0
  67. package/skills/hooks/outlets.md +26 -5
  68. package/skills/hooks/urls.md +40 -3
  69. package/skills/loader/SKILL.md +132 -20
  70. package/skills/migrate-nextjs/SKILL.md +70 -10
  71. package/skills/migrate-react-router/SKILL.md +49 -13
  72. package/skills/migrate-react-router/component-migration.md +18 -13
  73. package/skills/migrate-react-router/data-and-actions.md +14 -3
  74. package/skills/migrate-react-router/route-mapping.md +15 -2
  75. package/skills/parallel/SKILL.md +32 -1
  76. package/skills/ppr/SKILL.md +16 -6
  77. package/skills/prerender/SKILL.md +8 -4
  78. package/skills/rango/SKILL.md +21 -17
  79. package/skills/react-compiler/SKILL.md +3 -3
  80. package/skills/route/SKILL.md +5 -2
  81. package/skills/router-setup/SKILL.md +16 -2
  82. package/skills/scripts/SKILL.md +16 -6
  83. package/skills/shell-manifest/SKILL.md +16 -7
  84. package/skills/testing/SKILL.md +2 -2
  85. package/skills/testing/client-components.md +6 -0
  86. package/skills/testing/handles.md +30 -8
  87. package/skills/testing/loader.md +51 -49
  88. package/skills/testing/middleware.md +1 -1
  89. package/skills/theme/SKILL.md +8 -5
  90. package/src/bin/rango.ts +7 -3
  91. package/src/browser/navigation-bridge.ts +6 -0
  92. package/src/browser/navigation-client.ts +5 -0
  93. package/src/browser/partial-update.ts +65 -13
  94. package/src/browser/react/use-router.ts +40 -11
  95. package/src/browser/react/use-search-params.ts +140 -17
  96. package/src/browser/rsc-router.tsx +59 -0
  97. package/src/browser/server-action-bridge.ts +26 -0
  98. package/src/browser/types.ts +22 -0
  99. package/src/build/merge-full-manifests.ts +161 -0
  100. package/src/build/route-trie.ts +9 -332
  101. package/src/build/route-types/include-resolution.ts +66 -11
  102. package/src/build/route-types/per-module-writer.ts +11 -6
  103. package/src/build/route-types/router-processing.ts +184 -153
  104. package/src/build/runtime-discovery.ts +23 -12
  105. package/src/cache/cache-exec-scope.ts +47 -0
  106. package/src/cache/cache-runtime.ts +24 -25
  107. package/src/cache/taint.ts +28 -9
  108. package/src/client-urls/client-root.tsx +168 -0
  109. package/src/client-urls/client-urls.ts +698 -0
  110. package/src/client-urls/navigation.ts +237 -0
  111. package/src/client-urls/revalidation-protocol.ts +56 -0
  112. package/src/client-urls/server-projection.ts +579 -0
  113. package/src/client-urls/types.ts +195 -0
  114. package/src/client.rsc.tsx +12 -0
  115. package/src/client.tsx +49 -6
  116. package/src/decode-loader-results.ts +113 -0
  117. package/src/errors.ts +14 -0
  118. package/src/handles/deferred-resolution.ts +14 -7
  119. package/src/index.ts +1 -0
  120. package/src/loader-redirect.tsx +64 -0
  121. package/src/outlet-context.ts +12 -0
  122. package/src/outlet-provider.tsx +15 -1
  123. package/src/redirect-origin.ts +29 -0
  124. package/src/route-content-wrapper.tsx +96 -3
  125. package/src/route-definition/dsl-helpers.ts +28 -3
  126. package/src/route-definition/helpers-types.ts +13 -0
  127. package/src/route-definition/redirect.ts +17 -18
  128. package/src/router/error-handling.ts +65 -11
  129. package/src/router/intercept-resolution.ts +29 -0
  130. package/src/router/loader-resolution.ts +261 -28
  131. package/src/router/match-result.ts +7 -0
  132. package/src/router/revalidation.ts +24 -11
  133. package/src/router/route-trie-builder.ts +334 -0
  134. package/src/router/router-interfaces.ts +38 -0
  135. package/src/router/segment-resolution/fresh.ts +47 -0
  136. package/src/router/segment-resolution/helpers.ts +9 -11
  137. package/src/router/segment-resolution/loader-cache.ts +14 -24
  138. package/src/router/segment-resolution/revalidation.ts +20 -1
  139. package/src/router/trie-matching.ts +3 -3
  140. package/src/router.ts +46 -1
  141. package/src/rsc/full-payload.ts +6 -0
  142. package/src/rsc/handler.ts +10 -7
  143. package/src/rsc/loader-fetch.ts +2 -2
  144. package/src/rsc/manifest-init.ts +28 -9
  145. package/src/rsc/rsc-rendering.ts +15 -1
  146. package/src/rsc/shell-capture.ts +12 -0
  147. package/src/rsc/shell-serve.ts +15 -2
  148. package/src/rsc/ssr-setup.ts +10 -1
  149. package/src/rsc/types.ts +31 -2
  150. package/src/segment-system.tsx +83 -26
  151. package/src/server/context.ts +10 -0
  152. package/src/server/cookie-store.ts +19 -19
  153. package/src/server/handle-store.ts +185 -48
  154. package/src/server/request-context.ts +30 -6
  155. package/src/server.ts +7 -0
  156. package/src/ssr/index.tsx +37 -2
  157. package/src/ssr/ssr-root.tsx +29 -2
  158. package/src/testing/dom.entry.ts +1 -1
  159. package/src/testing/render-route.tsx +22 -8
  160. package/src/testing/run-loader.ts +51 -13
  161. package/src/types/boundaries.ts +19 -0
  162. package/src/types/index.ts +1 -0
  163. package/src/types/loader-types.ts +60 -5
  164. package/src/types/segments.ts +7 -0
  165. package/src/urls/include-helper.ts +22 -4
  166. package/src/urls/path-helper-types.ts +14 -1
  167. package/src/use-loader.tsx +67 -6
  168. package/src/vite/discovery/client-urls-projection.ts +322 -0
  169. package/src/vite/discovery/discover-routers.ts +43 -17
  170. package/src/vite/discovery/state.ts +11 -1
  171. package/src/vite/discovery/virtual-module-codegen.ts +20 -0
  172. package/src/vite/plugins/virtual-entries.ts +12 -3
  173. package/src/vite/router-discovery.ts +163 -12
@@ -0,0 +1,237 @@
1
+ "use client";
2
+
3
+ import { startTransition } from "react";
4
+ import { encodeClientRevalidationDecisions } from "./revalidation-protocol.js";
5
+ import type { ClientRevalidateArgs, ClientUrlPatterns } from "./types.js";
6
+
7
+ export interface ClientUrlNavigationIntent {
8
+ readonly routeId: string;
9
+ }
10
+
11
+ interface ActiveClientUrlGroup {
12
+ readonly definition: ClientUrlPatterns;
13
+ /** include() mount prefix the group is rendered under ("/" at root). */
14
+ readonly mount: string;
15
+ /** include() route-name prefix ("" at root) for canonical name composition. */
16
+ readonly namePrefix: string;
17
+ readonly setIntent: (intent: ClientUrlNavigationIntent | null) => void;
18
+ intent: ClientUrlNavigationIntent | null;
19
+ }
20
+
21
+ /**
22
+ * Intercept TARGET route names reachable from the CURRENT location as a
23
+ * navigation origin, shipped in payload metadata (MatchResult.interceptTargets)
24
+ * and refreshed on every commit. When a local match's canonical route name is
25
+ * in this set, the optimistic presentation DECLINES: the canonical response
26
+ * will commit the intercept over the ORIGIN page, so destination loading would
27
+ * flash and revert. Conservative for `when`-conditional intercepts (their
28
+ * selectors need live navigation context): a non-intercepted navigation may
29
+ * lose its optimistic loading, never the reverse.
30
+ */
31
+ let activeInterceptTargets: ReadonlySet<string> = new Set();
32
+
33
+ export function setActiveInterceptTargets(
34
+ targets: readonly string[] | undefined,
35
+ ): void {
36
+ activeInterceptTargets = new Set(targets ?? []);
37
+ }
38
+
39
+ export interface ClientUrlNavigationPresentation {
40
+ readonly routeId: string;
41
+ clear(): void;
42
+ }
43
+
44
+ let activeGroup: ActiveClientUrlGroup | null = null;
45
+
46
+ /**
47
+ * Strip the include mount prefix from an absolute pathname, yielding the
48
+ * definition-local pathname the module trie was built from. Returns null when
49
+ * the pathname lies outside the mount — the navigation then targets a server
50
+ * route and gets no optimistic presentation. Mirrors joinMount() in
51
+ * use-reverse.ts: the bare mount maps to the module index "/".
52
+ */
53
+ function stripMountPrefix(pathname: string, mount: string): string | null {
54
+ if (mount === "" || mount === "/") return pathname;
55
+ const normalized = mount.endsWith("/") ? mount.slice(0, -1) : mount;
56
+ if (pathname === normalized) return "/";
57
+ if (pathname.startsWith(`${normalized}/`)) {
58
+ return pathname.slice(normalized.length);
59
+ }
60
+ return null;
61
+ }
62
+
63
+ export function registerClientUrlGroup(
64
+ definition: ClientUrlPatterns,
65
+ mount: string,
66
+ namePrefix: string,
67
+ setIntent: (intent: ClientUrlNavigationIntent | null) => void,
68
+ ): () => void {
69
+ const group: ActiveClientUrlGroup = {
70
+ definition,
71
+ mount,
72
+ namePrefix,
73
+ setIntent,
74
+ intent: null,
75
+ };
76
+ activeGroup = group;
77
+
78
+ return () => {
79
+ if (activeGroup === group) activeGroup = null;
80
+ };
81
+ }
82
+
83
+ export function beginClientUrlNavigation(
84
+ targetUrl: URL,
85
+ signal: AbortSignal,
86
+ ): ClientUrlNavigationPresentation | null {
87
+ const group = activeGroup;
88
+ if (!group) return null;
89
+
90
+ const localPathname = stripMountPrefix(targetUrl.pathname, group.mount);
91
+ if (localPathname === null) return null;
92
+
93
+ const match = group.definition.match(localPathname);
94
+ if (!match || match.redirectTo) return null;
95
+
96
+ // Decline when an intercept would claim this target: compose the matched
97
+ // record's canonical route name (include namePrefix + local name) and check
98
+ // it against the current location's intercept target set. Unnamed records
99
+ // cannot be intercept targets (intercepts target route NAMES).
100
+ const record = group.definition.routes.find(
101
+ (candidate) => candidate.id === match.routeKey,
102
+ );
103
+ if (record?.name) {
104
+ const canonicalName = group.namePrefix
105
+ ? `${group.namePrefix}.${record.name}`
106
+ : record.name;
107
+ if (activeInterceptTargets.has(canonicalName)) return null;
108
+ }
109
+
110
+ const intent: ClientUrlNavigationIntent = { routeId: match.routeKey };
111
+ group.intent = intent;
112
+ group.setIntent(intent);
113
+
114
+ const clear = (): void => {
115
+ if (group.intent !== intent) return;
116
+ group.intent = null;
117
+ // The canonical commit may be held in a startTransition (explicit
118
+ // transition() routes, view transitions, action revalidations in
119
+ // partial-update.ts). An urgent clear would flush against the
120
+ // pre-transition tree first — loading UI → ORIGIN content → destination.
121
+ // Clearing inside a transition keeps the optimistic presentation until
122
+ // the destination commit lands; on the plain urgent commit path both
123
+ // updates land in order, so this changes nothing there.
124
+ startTransition(() => group.setIntent(null));
125
+ };
126
+ signal.addEventListener("abort", clear, { once: true });
127
+
128
+ return {
129
+ routeId: match.routeKey,
130
+ clear() {
131
+ signal.removeEventListener("abort", clear);
132
+ clear();
133
+ },
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Run the held clientUrls route's per-loader revalidate() predicates and
139
+ * encode their decisions for the request header. Predicates are CLIENT code
140
+ * (declared in the "use client" definition module); only decisions cross the
141
+ * wire — the synthesized per-loader revalidate() on every materialized stub
142
+ * (server-projection.ts) reads them back by loader $$id.
143
+ *
144
+ * Decisions exist only for the loaders the client currently HOLDS: the local
145
+ * match of currentUrl. They matter when the server would re-evaluate those
146
+ * held loader segments — same-route param/search navs, actions, and stale
147
+ * restores. Cross-route targets render new segments unconditionally, so a
148
+ * decision would be ignored; we still compute against the current route since
149
+ * only its held segments are addressable.
150
+ *
151
+ * Returns null when no group is active, the current location is not a client
152
+ * route, or no predicate produced a decision that differs from the locked
153
+ * default — the server then applies defaults, which is also the behavior for
154
+ * requests that cannot carry decisions (no-JS, PE, prefetch, document loads).
155
+ */
156
+ export function collectClientRevalidationDecisions(options: {
157
+ currentUrl: URL;
158
+ nextUrl: URL;
159
+ isAction: boolean;
160
+ actionId?: string;
161
+ stale: boolean;
162
+ }): string | null {
163
+ const group = activeGroup;
164
+ if (!group) return null;
165
+
166
+ const { currentUrl, nextUrl, isAction, actionId, stale } = options;
167
+ const currentLocal = stripMountPrefix(currentUrl.pathname, group.mount);
168
+ if (currentLocal === null) return null;
169
+ const currentMatch = group.definition.match(currentLocal);
170
+ if (!currentMatch || currentMatch.redirectTo) return null;
171
+ const record = group.definition.routes.find(
172
+ (candidate) => candidate.id === currentMatch.routeKey,
173
+ );
174
+ if (!record) return null;
175
+
176
+ const nextLocal = stripMountPrefix(nextUrl.pathname, group.mount);
177
+ const nextMatch =
178
+ nextLocal === null ? null : group.definition.match(nextLocal);
179
+ const nextParams = nextMatch?.params ?? {};
180
+
181
+ // Locked default, mirrored client-side: actions re-run route-owned loaders;
182
+ // navigations re-run them when params or search changed.
183
+ const paramsEqual = (
184
+ a: Record<string, string>,
185
+ b: Record<string, string>,
186
+ ): boolean => {
187
+ const aKeys = Object.keys(a);
188
+ return (
189
+ aKeys.length === Object.keys(b).length &&
190
+ aKeys.every((key) => a[key] === b[key])
191
+ );
192
+ };
193
+ const defaultShouldRevalidate = isAction
194
+ ? true
195
+ : !paramsEqual(currentMatch.params, nextParams) ||
196
+ currentUrl.search !== nextUrl.search;
197
+
198
+ const skip: string[] = [];
199
+ const force: string[] = [];
200
+ for (const { loader, revalidate } of record.loaders) {
201
+ if (revalidate.length === 0) continue;
202
+ const args: ClientRevalidateArgs = {
203
+ currentUrl,
204
+ nextUrl,
205
+ currentParams: currentMatch.params,
206
+ nextParams,
207
+ defaultShouldRevalidate,
208
+ stale,
209
+ isAction,
210
+ ...(actionId !== undefined ? { actionId } : {}),
211
+ };
212
+ // Same iteration contract as the server: every predicate runs, the last
213
+ // boolean verdict wins. A throwing predicate fails open to the default
214
+ // (mirrors evaluateRevalidation's fail-open).
215
+ let decision = defaultShouldRevalidate;
216
+ for (const fn of revalidate) {
217
+ try {
218
+ const verdict = fn(args);
219
+ if (typeof verdict === "boolean") decision = verdict;
220
+ } catch (error) {
221
+ console.error(
222
+ `[@rangojs/router] clientUrls revalidate() threw for loader "${loader.$$id}"; using default decision:`,
223
+ error,
224
+ );
225
+ }
226
+ }
227
+ if (decision === defaultShouldRevalidate) continue;
228
+ (decision ? force : skip).push(loader.$$id);
229
+ }
230
+
231
+ return encodeClientRevalidationDecisions({ skip, force });
232
+ }
233
+
234
+ export function clearClientUrlNavigationRegistry(): void {
235
+ activeGroup = null;
236
+ activeInterceptTargets = new Set();
237
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Wire protocol for CLIENT-RUN per-loader revalidation decisions.
3
+ *
4
+ * clientUrls() revalidate() predicates are declared in a "use client" module,
5
+ * so they cannot cross the projection boundary as functions — instead they
6
+ * EXECUTE in the browser and only their DECISIONS cross: the browser attaches
7
+ * this header to partial-navigation and action requests, and the synthesized
8
+ * per-loader revalidate() on every materialized loader stub reads it back
9
+ * (see materializeRouteItems in server-projection.ts).
10
+ *
11
+ * Trust model: same class as `_rsc_segments` — a decision can only make the
12
+ * CLIENT's own view staler (skip) or fresher (force), and the synthesized
13
+ * predicates exist only on client-urls loader stubs, so the header can never
14
+ * influence server-tree loaders. Requests without the header (no-JS, PE,
15
+ * prefetch, document loads) fall back to the locked server defaults.
16
+ */
17
+
18
+ export const CLIENT_REVALIDATION_HEADER = "X-Rango-Client-Reval";
19
+
20
+ export interface ClientRevalidationDecisions {
21
+ /** Loader $$ids whose client predicate said false (keep held data). */
22
+ readonly skip: readonly string[];
23
+ /** Loader $$ids whose client predicate said true against a false default. */
24
+ readonly force: readonly string[];
25
+ }
26
+
27
+ export function encodeClientRevalidationDecisions(
28
+ decisions: ClientRevalidationDecisions,
29
+ ): string | null {
30
+ if (decisions.skip.length === 0 && decisions.force.length === 0) return null;
31
+ // encodeURIComponent keeps the header value ISO-8859-1-safe regardless of
32
+ // what characters loader module ids contain.
33
+ return encodeURIComponent(
34
+ JSON.stringify({ skip: decisions.skip, force: decisions.force }),
35
+ );
36
+ }
37
+
38
+ export function decodeClientRevalidationDecisions(
39
+ headerValue: string | null,
40
+ ): ClientRevalidationDecisions | null {
41
+ if (!headerValue) return null;
42
+ try {
43
+ const parsed = JSON.parse(decodeURIComponent(headerValue)) as {
44
+ skip?: unknown;
45
+ force?: unknown;
46
+ };
47
+ const toIds = (value: unknown): readonly string[] =>
48
+ Array.isArray(value)
49
+ ? value.filter((entry): entry is string => typeof entry === "string")
50
+ : [];
51
+ return { skip: toIds(parsed.skip), force: toIds(parsed.force) };
52
+ } catch {
53
+ // A malformed header is treated as absent: locked defaults apply.
54
+ return null;
55
+ }
56
+ }