@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.
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +292 -204
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/loader/SKILL.md +53 -43
- package/skills/parallel/SKILL.md +126 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +52 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/debug-channel.ts +93 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -5
- package/src/browser/navigation-client.ts +84 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +32 -3
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +12 -0
- package/src/browser/types.ts +17 -1
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/deps/browser.ts +1 -0
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +31 -8
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -2
- package/src/router/middleware.ts +4 -3
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +130 -17
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +352 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +6 -1
- package/src/rsc/handler.ts +28 -2
- package/src/rsc/loader-fetch.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -1
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +7 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +102 -13
- package/src/server/request-context.ts +59 -12
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +120 -22
- package/src/types/loader-types.ts +4 -4
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +235 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +148 -209
- package/src/vite/router-discovery.ts +0 -8
- 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)
|
package/src/browser/types.ts
CHANGED
|
@@ -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?: {
|
|
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 (
|
|
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
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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;
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -73,7 +73,7 @@ function getDefaultRouteCacheKey(
|
|
|
73
73
|
isIntercept?: boolean,
|
|
74
74
|
): string {
|
|
75
75
|
const ctx = getRequestContext();
|
|
76
|
-
const isPartial = ctx?.
|
|
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.
|
|
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
|
-
|
|
335
|
-
|
|
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
|
|
338
|
-
(s) => s.component
|
|
356
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
357
|
+
(s) => s.component === null && s.type === "layout",
|
|
339
358
|
);
|
|
340
|
-
if (
|
|
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) {
|