@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8

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 (89) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +292 -204
  3. package/package.json +1 -1
  4. package/skills/cache-guide/SKILL.md +32 -0
  5. package/skills/caching/SKILL.md +45 -4
  6. package/skills/loader/SKILL.md +53 -43
  7. package/skills/parallel/SKILL.md +126 -0
  8. package/skills/route/SKILL.md +31 -0
  9. package/skills/router-setup/SKILL.md +52 -2
  10. package/skills/typesafety/SKILL.md +10 -0
  11. package/src/browser/debug-channel.ts +93 -0
  12. package/src/browser/event-controller.ts +5 -0
  13. package/src/browser/navigation-bridge.ts +1 -5
  14. package/src/browser/navigation-client.ts +84 -27
  15. package/src/browser/navigation-transaction.ts +11 -9
  16. package/src/browser/partial-update.ts +50 -9
  17. package/src/browser/prefetch/cache.ts +57 -5
  18. package/src/browser/prefetch/fetch.ts +30 -21
  19. package/src/browser/prefetch/queue.ts +92 -20
  20. package/src/browser/prefetch/resource-ready.ts +77 -0
  21. package/src/browser/react/Link.tsx +9 -1
  22. package/src/browser/react/NavigationProvider.tsx +32 -3
  23. package/src/browser/rsc-router.tsx +109 -57
  24. package/src/browser/scroll-restoration.ts +31 -34
  25. package/src/browser/segment-reconciler.ts +6 -1
  26. package/src/browser/server-action-bridge.ts +12 -0
  27. package/src/browser/types.ts +17 -1
  28. package/src/build/route-types/router-processing.ts +12 -2
  29. package/src/cache/cache-runtime.ts +15 -11
  30. package/src/cache/cache-scope.ts +48 -7
  31. package/src/cache/cf/cf-cache-store.ts +453 -11
  32. package/src/cache/cf/index.ts +5 -1
  33. package/src/cache/document-cache.ts +17 -7
  34. package/src/cache/index.ts +1 -0
  35. package/src/cache/taint.ts +55 -0
  36. package/src/context-var.ts +72 -2
  37. package/src/debug.ts +2 -2
  38. package/src/deps/browser.ts +1 -0
  39. package/src/route-definition/dsl-helpers.ts +32 -7
  40. package/src/route-definition/helpers-types.ts +6 -5
  41. package/src/route-definition/redirect.ts +2 -2
  42. package/src/route-map-builder.ts +7 -1
  43. package/src/router/find-match.ts +4 -2
  44. package/src/router/handler-context.ts +31 -8
  45. package/src/router/intercept-resolution.ts +2 -0
  46. package/src/router/lazy-includes.ts +4 -1
  47. package/src/router/loader-resolution.ts +7 -1
  48. package/src/router/logging.ts +5 -2
  49. package/src/router/manifest.ts +9 -3
  50. package/src/router/match-middleware/background-revalidation.ts +30 -2
  51. package/src/router/match-middleware/cache-lookup.ts +66 -9
  52. package/src/router/match-middleware/cache-store.ts +53 -10
  53. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  54. package/src/router/match-middleware/segment-resolution.ts +8 -5
  55. package/src/router/match-result.ts +22 -6
  56. package/src/router/metrics.ts +6 -1
  57. package/src/router/middleware-types.ts +6 -2
  58. package/src/router/middleware.ts +4 -3
  59. package/src/router/router-context.ts +6 -1
  60. package/src/router/segment-resolution/fresh.ts +130 -17
  61. package/src/router/segment-resolution/helpers.ts +29 -24
  62. package/src/router/segment-resolution/loader-cache.ts +1 -0
  63. package/src/router/segment-resolution/revalidation.ts +352 -290
  64. package/src/router/segment-wrappers.ts +2 -0
  65. package/src/router/types.ts +1 -0
  66. package/src/router.ts +6 -1
  67. package/src/rsc/handler.ts +28 -2
  68. package/src/rsc/loader-fetch.ts +7 -2
  69. package/src/rsc/progressive-enhancement.ts +4 -1
  70. package/src/rsc/rsc-rendering.ts +4 -1
  71. package/src/rsc/server-action.ts +2 -0
  72. package/src/rsc/types.ts +7 -1
  73. package/src/segment-system.tsx +140 -4
  74. package/src/server/context.ts +102 -13
  75. package/src/server/request-context.ts +59 -12
  76. package/src/ssr/index.tsx +1 -0
  77. package/src/types/handler-context.ts +120 -22
  78. package/src/types/loader-types.ts +4 -4
  79. package/src/types/route-entry.ts +7 -0
  80. package/src/types/segments.ts +2 -0
  81. package/src/urls/path-helper.ts +1 -1
  82. package/src/vite/discovery/state.ts +0 -2
  83. package/src/vite/plugin-types.ts +0 -83
  84. package/src/vite/plugins/expose-action-id.ts +1 -3
  85. package/src/vite/plugins/performance-tracks.ts +235 -0
  86. package/src/vite/plugins/version-plugin.ts +13 -1
  87. package/src/vite/rango.ts +148 -209
  88. package/src/vite/router-discovery.ts +0 -8
  89. package/src/vite/utils/banner.ts +3 -3
@@ -4,6 +4,8 @@ import type {
4
4
  RscPayload,
5
5
  } from "./types.js";
6
6
  import { createPartialUpdater } from "./partial-update.js";
7
+ import { createClientDebugChannel, DEBUG_ID_HEADER } from "./debug-channel.js";
8
+ import { findSourceMapURL } from "../deps/browser.js";
7
9
  import { createNavigationTransaction } from "./navigation-transaction.js";
8
10
  import {
9
11
  reconcileSegments,
@@ -199,6 +201,14 @@ export function createServerActionBridge(
199
201
  const onHandleAbort = () => fetchAbort.abort();
200
202
  handle.signal.addEventListener("abort", onHandleAbort, { once: true });
201
203
 
204
+ // Dev-only: create debug channel for React Performance Tracks
205
+ const debugId = (import.meta as any).hot
206
+ ? crypto.randomUUID()
207
+ : undefined;
208
+ const debugChannel = debugId
209
+ ? createClientDebugChannel(debugId)
210
+ : undefined;
211
+
202
212
  // Send action request with stream tracking
203
213
  const responsePromise = fetch(url, {
204
214
  method: "POST",
@@ -210,6 +220,7 @@ export function createServerActionBridge(
210
220
  ...(interceptSourceUrl && {
211
221
  "X-RSC-Router-Intercept-Source": interceptSourceUrl,
212
222
  }),
223
+ ...(debugId && { [DEBUG_ID_HEADER]: debugId }),
213
224
  },
214
225
  body: encodedBody,
215
226
  signal: fetchAbort.signal,
@@ -272,6 +283,7 @@ export function createServerActionBridge(
272
283
  try {
273
284
  payload = await deps.createFromFetch<RscPayload>(responsePromise, {
274
285
  temporaryReferences,
286
+ ...(debugChannel && { debugChannel, findSourceMapURL }),
275
287
  });
276
288
  } catch (error) {
277
289
  // Clean up streaming token on error (may be null if fetch failed before .then() ran)
@@ -215,6 +215,15 @@ export interface SegmentState {
215
215
  export interface NavigationUpdate {
216
216
  root: ReactNode | Promise<ReactNode>;
217
217
  metadata: RscMetadata;
218
+ /** Scroll behavior to apply after React commits this update */
219
+ scroll?: {
220
+ /** For back/forward: restore saved position */
221
+ restore?: boolean;
222
+ /** Set to false to disable scrolling entirely */
223
+ enabled?: boolean;
224
+ /** Function to check if streaming is in progress */
225
+ isStreaming?: () => boolean;
226
+ };
218
227
  }
219
228
 
220
229
  /**
@@ -332,7 +341,14 @@ export type ReadonlyURLSearchParams = Omit<
332
341
  export interface RscBrowserDependencies {
333
342
  createFromFetch: <T>(
334
343
  response: Promise<Response>,
335
- options?: { temporaryReferences?: any },
344
+ options?: {
345
+ temporaryReferences?: any;
346
+ debugChannel?: { readable?: ReadableStream; writable?: WritableStream };
347
+ findSourceMapURL?: (
348
+ filename: string,
349
+ environmentName: string,
350
+ ) => string | null;
351
+ },
336
352
  ) => Promise<T>;
337
353
  createFromReadableStream: <T>(stream: ReadableStream) => Promise<T>;
338
354
  encodeReply: (
@@ -45,7 +45,9 @@ function isRoutableSourceFile(name: string): boolean {
45
45
  name.endsWith(".tsx") ||
46
46
  name.endsWith(".js") ||
47
47
  name.endsWith(".jsx")) &&
48
- !name.includes(".gen.")
48
+ !name.includes(".gen.") &&
49
+ !name.includes(".test.") &&
50
+ !name.includes(".spec.")
49
51
  );
50
52
  }
51
53
 
@@ -70,7 +72,15 @@ function findRouterFilesRecursive(
70
72
  for (const entry of entries) {
71
73
  const fullPath = join(dir, entry.name);
72
74
  if (entry.isDirectory()) {
73
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
75
+ if (
76
+ entry.name === "node_modules" ||
77
+ entry.name === "dist" ||
78
+ entry.name === "coverage" ||
79
+ entry.name === "__tests__" ||
80
+ entry.name === "__mocks__" ||
81
+ entry.name.startsWith(".")
82
+ )
83
+ continue;
74
84
  childDirs.push(fullPath);
75
85
  continue;
76
86
  }
@@ -214,11 +214,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
214
214
  bgStopCapture = c.stop;
215
215
  }
216
216
 
217
- // Stamp tainted args and RequestContext so request-scoped
218
- // reads (cookies, headers) and side effects (ctx.set, etc.)
219
- // throw inside background revalidation, same as the miss path.
220
- // Uses ref-counted stamp/unstamp so overlapping executions
221
- // sharing the same ctx don't clear each other's guards.
217
+ // Stamp tainted ARGS only not requestCtx. The args stamp guards
218
+ // direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
219
+ // which is sufficient for correctness.
220
+ //
221
+ // We intentionally skip stamping requestCtx here because:
222
+ // 1. runBackground starts the async task synchronously (before the
223
+ // first await), so stampCacheExec would pollute the shared
224
+ // requestCtx while the foreground pipeline is still running.
225
+ // This causes assertNotInsideCacheExec to fire when cache-store
226
+ // later calls requestCtx.onResponse().
227
+ // 2. requestCtx methods are closure-bound to the original ctx, so
228
+ // neither Object.create() nor a proxy can isolate the stamp.
229
+ // 3. The foreground miss path already stamps requestCtx and catches
230
+ // cookies()/headers() misuse on first execution. The background
231
+ // re-runs the same function with the same request.
222
232
  const bgTaintedArgs: unknown[] = [];
223
233
  for (const arg of args) {
224
234
  if (isTainted(arg)) {
@@ -226,9 +236,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
226
236
  bgTaintedArgs.push(arg);
227
237
  }
228
238
  }
229
- if (requestCtx) {
230
- stampCacheExec(requestCtx as object);
231
- }
232
239
 
233
240
  try {
234
241
  const freshResult = await fn.apply(this, args);
@@ -249,9 +256,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
249
256
  for (const arg of bgTaintedArgs) {
250
257
  unstampCacheExec(arg as object);
251
258
  }
252
- if (requestCtx) {
253
- unstampCacheExec(requestCtx as object);
254
- }
255
259
  // Restore original handle store
256
260
  if (originalHandleStore && requestCtx) {
257
261
  requestCtx._handleStore = originalHandleStore;
@@ -73,7 +73,7 @@ function getDefaultRouteCacheKey(
73
73
  isIntercept?: boolean,
74
74
  ): string {
75
75
  const ctx = getRequestContext();
76
- const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
76
+ const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
77
77
  const searchParams = ctx?.url.searchParams;
78
78
  const host = ctx?.url.host ?? "localhost";
79
79
 
@@ -326,24 +326,61 @@ export class CacheScope {
326
326
  const key = await this.resolveKey(pathname, params, isIntercept);
327
327
 
328
328
  // Check if this is a partial request (navigation) vs document request
329
- const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
329
+ const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
330
+
331
+ if (INTERNAL_RANGO_DEBUG) {
332
+ debugCacheLog(
333
+ `[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
334
+ );
335
+ }
330
336
 
331
337
  requestCtx.waitUntil(async () => {
338
+ if (INTERNAL_RANGO_DEBUG) {
339
+ debugCacheLog(
340
+ `[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
341
+ );
342
+ }
343
+
332
344
  await handleStore.settled;
333
345
 
334
- // For document requests: only cache if ALL segments have components (complete render)
335
- // For partial requests: null components are expected (client already has them)
346
+ if (INTERNAL_RANGO_DEBUG) {
347
+ debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
348
+ }
349
+
350
+ // For document requests: only cache if layout segments have components
351
+ // (complete render). Parallel and route segments may legitimately have
352
+ // null components — UI-less @meta parallels return null, and void route
353
+ // handlers produce null when the UI lives in parallel slots/layouts.
354
+ // Partial requests always allow null components (client already has them).
336
355
  if (!isPartial) {
337
- const hasAllComponents = nonLoaderSegments.every(
338
- (s) => s.component !== null,
356
+ const hasIncompleteLayouts = nonLoaderSegments.some(
357
+ (s) => s.component === null && s.type === "layout",
339
358
  );
340
- if (!hasAllComponents) return;
359
+ if (hasIncompleteLayouts) {
360
+ const nullSegments = nonLoaderSegments
361
+ .filter((s) => s.component === null && s.type === "layout")
362
+ .map((s) => s.id);
363
+ const error = new Error(
364
+ `[CacheScope] Cache write skipped: layout segments have null components ` +
365
+ `(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
366
+ `layout handlers must return JSX for document requests to be cacheable.`,
367
+ );
368
+ error.name = "CacheScopeInvariantError";
369
+ console.error(error.message);
370
+ return;
371
+ }
341
372
  }
342
373
 
343
374
  // Collect handle data for non-loader segments only
344
375
  const handles = captureHandles(nonLoaderSegments, handleStore);
345
376
 
346
377
  try {
378
+ if (INTERNAL_RANGO_DEBUG) {
379
+ debugCacheLog(
380
+ `[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
381
+ );
382
+ }
383
+
347
384
  // Serialize non-loader segments only
348
385
  const serializedSegments = await serializeSegments(nonLoaderSegments);
349
386
 
@@ -353,6 +390,10 @@ export class CacheScope {
353
390
  expiresAt: Date.now() + ttl * 1000,
354
391
  };
355
392
 
393
+ if (INTERNAL_RANGO_DEBUG) {
394
+ debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
395
+ }
396
+
356
397
  await store.set(key, data, ttl, swr);
357
398
 
358
399
  if (INTERNAL_RANGO_DEBUG) {