@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.ea6d5eec

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 (43) hide show
  1. package/AGENTS.md +4 -0
  2. package/dist/bin/rango.js +8 -3
  3. package/dist/vite/index.js +136 -197
  4. package/package.json +15 -14
  5. package/skills/caching/SKILL.md +37 -4
  6. package/src/browser/navigation-bridge.ts +1 -3
  7. package/src/browser/navigation-client.ts +77 -24
  8. package/src/browser/navigation-transaction.ts +11 -9
  9. package/src/browser/partial-update.ts +39 -9
  10. package/src/browser/prefetch/cache.ts +54 -2
  11. package/src/browser/prefetch/fetch.ts +22 -12
  12. package/src/browser/prefetch/queue.ts +53 -13
  13. package/src/browser/react/Link.tsx +9 -1
  14. package/src/browser/react/NavigationProvider.tsx +27 -0
  15. package/src/browser/rsc-router.tsx +90 -57
  16. package/src/browser/scroll-restoration.ts +31 -34
  17. package/src/browser/types.ts +9 -0
  18. package/src/build/route-types/router-processing.ts +12 -2
  19. package/src/cache/cache-scope.ts +2 -2
  20. package/src/cache/cf/cf-cache-store.ts +453 -11
  21. package/src/cache/cf/index.ts +5 -1
  22. package/src/cache/index.ts +1 -0
  23. package/src/route-definition/redirect.ts +2 -2
  24. package/src/route-map-builder.ts +7 -1
  25. package/src/router/find-match.ts +4 -2
  26. package/src/router/intercept-resolution.ts +2 -0
  27. package/src/router/lazy-includes.ts +2 -0
  28. package/src/router/logging.ts +4 -1
  29. package/src/router/manifest.ts +3 -1
  30. package/src/router/match-middleware/segment-resolution.ts +1 -0
  31. package/src/router/middleware.ts +2 -1
  32. package/src/router/router-context.ts +5 -1
  33. package/src/router/segment-resolution/revalidation.ts +4 -1
  34. package/src/router/segment-wrappers.ts +2 -0
  35. package/src/router.ts +4 -0
  36. package/src/server/request-context.ts +10 -4
  37. package/src/types/route-entry.ts +7 -0
  38. package/src/vite/discovery/state.ts +0 -2
  39. package/src/vite/plugin-types.ts +0 -83
  40. package/src/vite/plugins/expose-action-id.ts +1 -3
  41. package/src/vite/plugins/version-plugin.ts +13 -1
  42. package/src/vite/rango.ts +144 -209
  43. package/src/vite/router-discovery.ts +0 -8
@@ -188,6 +188,7 @@ export async function resolveInterceptEntry<TEnv>(
188
188
  context,
189
189
  actionContext,
190
190
  stale,
191
+ traceSource: "intercept-loader",
191
192
  });
192
193
 
193
194
  if (!shouldRevalidate) {
@@ -355,6 +356,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
355
356
  context,
356
357
  actionContext,
357
358
  stale,
359
+ traceSource: "intercept-loader",
358
360
  });
359
361
 
360
362
  if (!shouldRevalidate) {
@@ -14,6 +14,7 @@ export interface LazyEvalDeps<TEnv = any> {
14
14
  mergedRouteMap: Record<string, string>;
15
15
  nextMountIndex: () => number;
16
16
  getPrecomputedByPrefix: () => Map<string, Record<string, string>> | null;
17
+ routerId?: string;
17
18
  }
18
19
 
19
20
  // Detect lazy includes in handler result and create placeholder entries
@@ -200,6 +201,7 @@ export function evaluateLazyEntry<TEnv = any>(
200
201
  trailingSlash: entry.trailingSlash,
201
202
  handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
202
203
  mountIndex: deps.nextMountIndex(),
204
+ routerId: deps.routerId,
203
205
  // Lazy evaluation fields
204
206
  lazy: true,
205
207
  lazyPatterns: lazyInclude.patterns,
@@ -12,7 +12,10 @@ export interface RevalidationTraceEntry {
12
12
  | "cache-hit"
13
13
  | "loader"
14
14
  | "parallel"
15
- | "orphan-layout";
15
+ | "orphan-layout"
16
+ | "route-handler"
17
+ | "layout-handler"
18
+ | "intercept-loader";
16
19
  defaultShouldRevalidate: boolean;
17
20
  finalShouldRevalidate: boolean;
18
21
  reason: string;
@@ -65,7 +65,9 @@ export async function loadManifest(
65
65
  const mountIndex = entry.mountIndex;
66
66
 
67
67
  // Check module-level cache (persists across requests within same isolate)
68
- const cacheKey = `${VERSION}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
68
+ // Include routerId so multi-router setups (host routing) don't share cached
69
+ // EntryData across routers with overlapping mountIndex + routeKey combinations.
70
+ const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
69
71
  const cached = manifestModuleCache.get(cacheKey);
70
72
  if (cached) {
71
73
  const cacheStart = performance.now();
@@ -168,6 +168,7 @@ export function withSegmentResolution<TEnv>(
168
168
  ctx.interceptResult,
169
169
  ctx.localRouteName,
170
170
  ctx.pathname,
171
+ ctx.stale,
171
172
  ),
172
173
  );
173
174
 
@@ -21,6 +21,7 @@ import type {
21
21
  import { _getRequestContext } from "../server/request-context.js";
22
22
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
23
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
+ import { stripInternalParams } from "./handler-context.js";
24
25
 
25
26
  // Re-export types and cookie utilities for backward compatibility
26
27
  export type {
@@ -147,7 +148,7 @@ export function createMiddlewareContext<TEnv>(
147
148
  search?: Record<string, unknown>,
148
149
  ) => string,
149
150
  ): MiddlewareContext<TEnv> {
150
- const url = new URL(request.url);
151
+ const url = stripInternalParams(new URL(request.url));
151
152
 
152
153
  // Track the initial response to detect pre/post-next() phase.
153
154
  // Before next(): responseHolder.response === initialResponse (the stub).
@@ -138,6 +138,7 @@ export interface RouterContext<TEnv = any> {
138
138
  interceptResult: InterceptResult | null,
139
139
  localRouteName: string,
140
140
  pathname: string,
141
+ stale?: boolean,
141
142
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
142
143
 
143
144
  // Generator-based segment resolution (for pipeline)
@@ -188,7 +189,10 @@ export interface RouterContext<TEnv = any> {
188
189
  | "cache-hit"
189
190
  | "loader"
190
191
  | "parallel"
191
- | "orphan-layout";
192
+ | "orphan-layout"
193
+ | "route-handler"
194
+ | "layout-handler"
195
+ | "intercept-loader";
192
196
  }) => Promise<boolean>;
193
197
 
194
198
  // Request context
@@ -608,6 +608,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
608
  context,
609
609
  actionContext,
610
610
  stale,
611
+ traceSource:
612
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
613
  });
612
614
  emitRevalidationDecision(
613
615
  entry.shortCode,
@@ -1165,6 +1167,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1165
1167
  localRouteName: string,
1166
1168
  pathname: string,
1167
1169
  deps: SegmentResolutionDeps<TEnv>,
1170
+ stale?: boolean,
1168
1171
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1172
  const allSegments: ResolvedSegment[] = [];
1170
1173
  const matchedIds: string[] = [];
@@ -1209,7 +1212,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1209
1212
  loaderPromises,
1210
1213
  deps,
1211
1214
  actionContext,
1212
- false,
1215
+ stale,
1213
1216
  ),
1214
1217
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1218
  deps,
@@ -204,6 +204,7 @@ export function createSegmentWrappers<TEnv = any>(
204
204
  interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
205
205
  localRouteName: string,
206
206
  pathname: string,
207
+ stale?: boolean,
207
208
  ): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
208
209
  return _resolveAllSegmentsWithRevalidation(
209
210
  entries,
@@ -221,6 +222,7 @@ export function createSegmentWrappers<TEnv = any>(
221
222
  localRouteName,
222
223
  pathname,
223
224
  segmentDeps,
225
+ stale,
224
226
  );
225
227
  }
226
228
 
package/src/router.ts CHANGED
@@ -560,6 +560,7 @@ export function createRouter<TEnv = any>(
560
560
  mergedRouteMap,
561
561
  nextMountIndex: () => mountIndex++,
562
562
  getPrecomputedByPrefix,
563
+ routerId,
563
564
  };
564
565
 
565
566
  function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
@@ -751,6 +752,7 @@ export function createRouter<TEnv = any>(
751
752
  trailingSlash: trailingSlashConfig,
752
753
  handler: urlPatterns.handler,
753
754
  mountIndex: currentMountIndex,
755
+ routerId,
754
756
  cacheProfiles: resolvedCacheProfiles,
755
757
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
756
758
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -770,6 +772,7 @@ export function createRouter<TEnv = any>(
770
772
  trailingSlash: trailingSlashConfig,
771
773
  handler: urlPatterns.handler,
772
774
  mountIndex: currentMountIndex,
775
+ routerId,
773
776
  cacheProfiles: resolvedCacheProfiles,
774
777
  ...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
775
778
  ...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
@@ -813,6 +816,7 @@ export function createRouter<TEnv = any>(
813
816
  trailingSlash: trailingSlashConfig,
814
817
  handler: urlPatterns.handler,
815
818
  mountIndex: mountIndex++,
819
+ routerId,
816
820
  // Lazy evaluation fields
817
821
  lazy: true,
818
822
  lazyPatterns: lazyInclude.patterns,
@@ -30,7 +30,10 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
30
30
  import { THEME_COOKIE } from "../theme/constants.js";
31
31
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
32
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
33
- import { createReverseFunction } from "../router/handler-context.js";
33
+ import {
34
+ createReverseFunction,
35
+ stripInternalParams,
36
+ } from "../router/handler-context.js";
34
37
  import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
35
38
  import { invariant } from "../errors.js";
36
39
  import { isAutoGeneratedRouteName } from "../route-name.js";
@@ -58,7 +61,7 @@ export interface RequestContext<
58
61
  originalUrl: URL;
59
62
  /** URL pathname */
60
63
  pathname: string;
61
- /** URL search params (system params like _rsc* are NOT filtered here) */
64
+ /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
62
65
  searchParams: URLSearchParams;
63
66
  /** Variables set by middleware (same as ctx.var) */
64
67
  var: Record<string, any>;
@@ -555,14 +558,17 @@ export function createRequestContext<TEnv>(
555
558
  invalidateResponseCookieCache();
556
559
  };
557
560
 
561
+ // Strip internal _rsc* params so userland sees a clean URL.
562
+ const cleanUrl = stripInternalParams(url);
563
+
558
564
  // Build the context object first (without use), then add use
559
565
  const ctx: RequestContext<TEnv> = {
560
566
  env,
561
567
  request,
562
- url,
568
+ url: cleanUrl,
563
569
  originalUrl: new URL(request.url),
564
570
  pathname: url.pathname,
565
- searchParams: url.searchParams,
571
+ searchParams: cleanUrl.searchParams,
566
572
  var: variables,
567
573
  get: ((keyOrVar: any) =>
568
574
  contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
@@ -55,6 +55,13 @@ export interface RouteEntry<TEnv = any> {
55
55
  | Promise<() => Array<AllUseItems>>;
56
56
  mountIndex: number;
57
57
 
58
+ /**
59
+ * Router ID that owns this entry. Used to namespace the manifest cache
60
+ * so multi-router setups (host routing) don't share cached EntryData
61
+ * across routers with overlapping mountIndex + routeKey combinations.
62
+ */
63
+ routerId?: string;
64
+
58
65
  /**
59
66
  * Route keys in this entry that have pre-render handlers.
60
67
  * Used by the non-trie match path to set the `pr` flag.
@@ -13,8 +13,6 @@ export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
13
13
  export interface PluginOptions {
14
14
  enableBuildPrerender?: boolean;
15
15
  staticRouteTypesGeneration?: boolean;
16
- include?: string[];
17
- exclude?: string[];
18
16
  // Mutable ref for deferred auto-discovery (node preset).
19
17
  // The auto-discover config() hook populates this before configResolved.
20
18
  routerPathRef?: { path?: string };
@@ -1,39 +1,3 @@
1
- /**
2
- * RSC plugin entry points configuration.
3
- * All entries use virtual modules by default. Specify a path to use a custom entry file.
4
- */
5
- export interface RscEntries {
6
- /**
7
- * Path to a custom browser/client entry file.
8
- * If not specified, a default virtual entry is used.
9
- */
10
- client?: string;
11
-
12
- /**
13
- * Path to a custom SSR entry file.
14
- * If not specified, a default virtual entry is used.
15
- */
16
- ssr?: string;
17
-
18
- /**
19
- * Path to a custom RSC entry file.
20
- * If not specified, a default virtual entry is used that imports the router from the `entry` option.
21
- */
22
- rsc?: string;
23
- }
24
-
25
- /**
26
- * Options for @vitejs/plugin-rsc integration
27
- */
28
- export interface RscPluginOptions {
29
- /**
30
- * Entry points for client, ssr, and rsc environments.
31
- * All entries use virtual modules by default.
32
- * Specify paths only when you need custom entry files.
33
- */
34
- entries?: RscEntries;
35
- }
36
-
37
1
  /**
38
2
  * Base options shared by all presets
39
3
  */
@@ -51,21 +15,6 @@ interface RangoBaseOptions {
51
15
  * @default true
52
16
  */
53
17
  staticRouteTypesGeneration?: boolean;
54
-
55
- /**
56
- * Glob patterns for files to include in route type scanning.
57
- * Only files matching at least one pattern will be scanned.
58
- * Patterns are relative to the project root.
59
- * When unset, all .ts/.tsx files are scanned.
60
- */
61
- include?: string[];
62
-
63
- /**
64
- * Glob patterns for files to exclude from route type scanning.
65
- * Takes precedence over `include`. Patterns are relative to the project root.
66
- * Defaults to common test/build directories.
67
- */
68
- exclude?: string[];
69
18
  }
70
19
 
71
20
  /**
@@ -76,38 +25,6 @@ export interface RangoNodeOptions extends RangoBaseOptions {
76
25
  * Deployment preset. Defaults to 'node' when not specified.
77
26
  */
78
27
  preset?: "node";
79
-
80
- /**
81
- * Path to your router configuration file that exports the route tree.
82
- * This file must export a `router` object created with `createRouter()`.
83
- *
84
- * When omitted, auto-discovers the router by scanning for files containing
85
- * `createRouter`. If exactly one is found, it is used automatically.
86
- * If multiple are found, an error is thrown with the list of candidates.
87
- *
88
- * @example
89
- * ```ts
90
- * rango({ router: './src/router.tsx' })
91
- * // or simply:
92
- * rango()
93
- * ```
94
- */
95
- router?: string;
96
-
97
- /**
98
- * RSC plugin configuration. By default, rsc-router includes @vitejs/plugin-rsc
99
- * with sensible defaults.
100
- *
101
- * Entry files (browser, ssr, rsc) are optional - if they don't exist,
102
- * virtual defaults are used.
103
- *
104
- * - Omit or pass `true`/`{}` to use defaults (recommended)
105
- * - Pass `{ entries: {...} }` to customize entry paths
106
- * - Pass `false` to disable (for manual @vitejs/plugin-rsc configuration)
107
- *
108
- * @default true
109
- */
110
- rsc?: boolean | RscPluginOptions;
111
28
  }
112
29
 
113
30
  /**
@@ -278,9 +278,7 @@ export function exposeActionId(): Plugin {
278
278
  if (!rscPluginApi) {
279
279
  throw new Error(
280
280
  "[rsc-router] Could not find @vitejs/plugin-rsc. " +
281
- "@rangojs/router requires the Vite RSC plugin.\n" +
282
- "The RSC plugin should be included automatically. If you disabled it with\n" +
283
- "rango({ rsc: false }), add rsc() before rango() in your config.",
281
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
284
282
  );
285
283
  }
286
284
 
@@ -135,8 +135,11 @@ export function createVersionPlugin(): Plugin {
135
135
  let server: any = null;
136
136
  const clientModuleSignatures = new Map<string, ClientModuleSignature>();
137
137
 
138
+ let versionCounter = 0;
138
139
  const bumpVersion = (reason: string) => {
139
- currentVersion = Date.now().toString(16);
140
+ // Use timestamp + counter to guarantee uniqueness even when multiple
141
+ // bumps happen within the same millisecond (e.g. cascading HMR events).
142
+ currentVersion = Date.now().toString(16) + String(++versionCounter);
140
143
  console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
141
144
 
142
145
  const rscEnv = server?.environments?.rsc;
@@ -211,6 +214,15 @@ export function createVersionPlugin(): Plugin {
211
214
 
212
215
  if (!isRscModule) return;
213
216
 
217
+ // Skip re-bumping when the version virtual module itself is invalidated
218
+ // (our own bumpVersion() invalidates it, which re-triggers hotUpdate).
219
+ if (
220
+ ctx.modules.length === 1 &&
221
+ ctx.modules[0].id === "\0" + VIRTUAL_IDS.version
222
+ ) {
223
+ return;
224
+ }
225
+
214
226
  if (isCodeModule(ctx.file)) {
215
227
  const filePath = normalizeModuleId(ctx.file);
216
228
  const previousSignature = clientModuleSignatures.get(filePath);