@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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/README.md +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -12,21 +12,26 @@
|
|
|
12
12
|
import type { LoaderFn } from "../types.js";
|
|
13
13
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
export interface LoaderRegistryEntry {
|
|
16
|
+
fn: LoaderFn<any, any, any>;
|
|
17
|
+
middleware: MiddlewareFn[];
|
|
18
|
+
/** Whether this loader is fetchable via the _rsc_loader endpoint. */
|
|
19
|
+
fetchable: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
|
|
19
23
|
|
|
20
24
|
export function registerFetchableLoader(
|
|
21
25
|
id: string,
|
|
22
26
|
fn: LoaderFn<any, any, any>,
|
|
23
27
|
middleware: MiddlewareFn[],
|
|
28
|
+
fetchable: boolean,
|
|
24
29
|
): void {
|
|
25
|
-
fetchableLoaderRegistry.set(id, { fn, middleware });
|
|
30
|
+
fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function getFetchableLoader(
|
|
29
34
|
id: string,
|
|
30
|
-
):
|
|
35
|
+
): LoaderRegistryEntry | undefined {
|
|
31
36
|
return fetchableLoaderRegistry.get(id);
|
|
32
37
|
}
|
|
@@ -57,11 +57,26 @@ export interface HandleStore {
|
|
|
57
57
|
track<T>(promise: Promise<T>): Promise<T>;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Signal that no more track() calls will be made.
|
|
61
|
+
* settled will not resolve until seal() is called AND all tracked
|
|
62
|
+
* promises have settled. Calling stream() or getData() auto-seals.
|
|
63
|
+
*/
|
|
64
|
+
seal(): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Promise that resolves when the store is sealed AND all tracked
|
|
68
|
+
* handlers have settled.
|
|
62
69
|
*/
|
|
63
70
|
readonly settled: Promise<void>;
|
|
64
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Optional error callback for late streaming-handle failures.
|
|
74
|
+
* Called when push() throws LateHandlePushError (handle pushed after
|
|
75
|
+
* stream completion). Allows the router to surface these errors
|
|
76
|
+
* to onError and telemetry.
|
|
77
|
+
*/
|
|
78
|
+
onError?: (error: Error) => void;
|
|
79
|
+
|
|
65
80
|
/**
|
|
66
81
|
* Push handle data for a specific handle and segment.
|
|
67
82
|
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
@@ -119,9 +134,31 @@ export interface HandleStore {
|
|
|
119
134
|
* ```
|
|
120
135
|
*/
|
|
121
136
|
export function createHandleStore(): HandleStore {
|
|
122
|
-
const pending: Promise<unknown>[] = [];
|
|
123
137
|
const data: HandleData = {};
|
|
124
138
|
|
|
139
|
+
// Settlement barrier: resolved only when sealed AND inflight === 0.
|
|
140
|
+
// seal() signals "no more track() calls". Each track() increments
|
|
141
|
+
// inflightCount, each promise.finally() decrements. settled resolves
|
|
142
|
+
// once both conditions are met — even if tracks are added while
|
|
143
|
+
// earlier ones are still in flight.
|
|
144
|
+
let sealed = false;
|
|
145
|
+
let inflightCount = 0;
|
|
146
|
+
let drainWaiters: (() => void)[] = [];
|
|
147
|
+
|
|
148
|
+
function notifyDrain() {
|
|
149
|
+
if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
|
|
150
|
+
const waiters = drainWaiters;
|
|
151
|
+
drainWaiters = [];
|
|
152
|
+
for (const resolve of waiters) resolve();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sealInternal() {
|
|
157
|
+
if (sealed) return;
|
|
158
|
+
sealed = true;
|
|
159
|
+
notifyDrain();
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
// Queue for pending emissions and resolver for waiting consumer
|
|
126
163
|
let pendingEmissions: HandleData[] = [];
|
|
127
164
|
let emissionResolver: (() => void) | null = null;
|
|
@@ -148,20 +185,36 @@ export function createHandleStore(): HandleStore {
|
|
|
148
185
|
|
|
149
186
|
return {
|
|
150
187
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
151
|
-
|
|
188
|
+
inflightCount++;
|
|
189
|
+
// Use .then(onSettle, onSettle) instead of .finally() to avoid
|
|
190
|
+
// creating an unhandled rejection branch when the tracked promise
|
|
191
|
+
// rejects (e.g. error route handlers). .finally() re-throws the
|
|
192
|
+
// rejection on a new branch that nobody catches, which can crash
|
|
193
|
+
// the server process.
|
|
194
|
+
const onSettle = () => {
|
|
195
|
+
inflightCount--;
|
|
196
|
+
notifyDrain();
|
|
197
|
+
};
|
|
198
|
+
promise.then(onSettle, onSettle);
|
|
152
199
|
return promise;
|
|
153
200
|
},
|
|
154
201
|
|
|
202
|
+
seal() {
|
|
203
|
+
sealInternal();
|
|
204
|
+
},
|
|
205
|
+
|
|
155
206
|
get settled(): Promise<void> {
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
207
|
+
if (sealed && inflightCount === 0) return Promise.resolve();
|
|
208
|
+
return new Promise<void>((resolve) => {
|
|
209
|
+
drainWaiters.push(resolve);
|
|
210
|
+
});
|
|
160
211
|
},
|
|
161
212
|
|
|
162
213
|
push(handleName: string, segmentId: string, value: unknown): void {
|
|
163
214
|
if (completed) {
|
|
164
|
-
|
|
215
|
+
const error = createLateHandlePushError(handleName, segmentId);
|
|
216
|
+
if (this.onError) this.onError(error);
|
|
217
|
+
throw error;
|
|
165
218
|
}
|
|
166
219
|
|
|
167
220
|
if (!data[handleName]) {
|
|
@@ -178,10 +231,14 @@ export function createHandleStore(): HandleStore {
|
|
|
178
231
|
},
|
|
179
232
|
|
|
180
233
|
getData(): Promise<HandleData> {
|
|
234
|
+
sealInternal();
|
|
181
235
|
return this.settled.then(() => cloneHandleData(data));
|
|
182
236
|
},
|
|
183
237
|
|
|
184
238
|
async *stream(): AsyncGenerator<HandleData, void, unknown> {
|
|
239
|
+
// Auto-seal: stream() is called after all track() registrations.
|
|
240
|
+
sealInternal();
|
|
241
|
+
|
|
185
242
|
// Set up completion handler
|
|
186
243
|
this.settled.then(() => {
|
|
187
244
|
completed = true;
|
|
@@ -6,13 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { LoaderFn } from "../types.js";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
fn: LoaderFn<any, any, any>;
|
|
14
|
-
middleware: MiddlewareFn[];
|
|
15
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
getFetchableLoader,
|
|
11
|
+
type LoaderRegistryEntry,
|
|
12
|
+
} from "./fetchable-loader-store.js";
|
|
16
13
|
|
|
17
14
|
// Server-side cache - maps loader $$id to function and middleware
|
|
18
15
|
// This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
|
|
@@ -21,7 +18,7 @@ interface RegisteredLoader {
|
|
|
21
18
|
// 1. Avoid repeated lookups/imports for the same loader
|
|
22
19
|
// 2. Support lazy loading in production (loaders imported on-demand)
|
|
23
20
|
// 3. Provide a stable reference for the RSC handler
|
|
24
|
-
const loaderRegistry = new Map<string,
|
|
21
|
+
const loaderRegistry = new Map<string, LoaderRegistryEntry>();
|
|
25
22
|
|
|
26
23
|
// Lazy import map - set by the loader manifest
|
|
27
24
|
// Maps loader $$id to a function that imports the loader module
|
|
@@ -37,28 +34,6 @@ export function setLoaderImports(
|
|
|
37
34
|
lazyLoaderImports = new Map(Object.entries(imports));
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
/**
|
|
41
|
-
* Register a fetchable loader by $$id
|
|
42
|
-
* Called by createLoader when fetchable option is provided
|
|
43
|
-
*/
|
|
44
|
-
export function registerLoader(
|
|
45
|
-
id: string,
|
|
46
|
-
fn: LoaderFn<any, any, any>,
|
|
47
|
-
middleware: MiddlewareFn[] = [],
|
|
48
|
-
): void {
|
|
49
|
-
// Always update the registry entry. During HMR, the module is re-executed
|
|
50
|
-
// with the new loader function, so we must replace the stale reference.
|
|
51
|
-
loaderRegistry.set(id, { fn, middleware });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get a registered loader by $$id (synchronous)
|
|
56
|
-
* Returns undefined if loader is not registered
|
|
57
|
-
*/
|
|
58
|
-
export function getLoader(id: string): RegisteredLoader | undefined {
|
|
59
|
-
return loaderRegistry.get(id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
37
|
/**
|
|
63
38
|
* Get a loader by $$id, loading it lazily if needed
|
|
64
39
|
* This is the primary method for the RSC handler to get loaders
|
|
@@ -68,7 +43,7 @@ export function getLoader(id: string): RegisteredLoader | undefined {
|
|
|
68
43
|
*/
|
|
69
44
|
export async function getLoaderLazy(
|
|
70
45
|
id: string,
|
|
71
|
-
): Promise<
|
|
46
|
+
): Promise<LoaderRegistryEntry | undefined> {
|
|
72
47
|
// Check if already cached in main registry
|
|
73
48
|
const existing = loaderRegistry.get(id);
|
|
74
49
|
if (existing) {
|
|
@@ -128,20 +103,6 @@ export async function getLoaderLazy(
|
|
|
128
103
|
return undefined;
|
|
129
104
|
}
|
|
130
105
|
|
|
131
|
-
/**
|
|
132
|
-
* Check if a loader is registered by $$id
|
|
133
|
-
*/
|
|
134
|
-
export function hasLoader(id: string): boolean {
|
|
135
|
-
return loaderRegistry.has(id) || getFetchableLoader(id) !== undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get all registered loader IDs (for debugging)
|
|
140
|
-
*/
|
|
141
|
-
export function getRegisteredLoaderIds(): string[] {
|
|
142
|
-
return Array.from(loaderRegistry.keys());
|
|
143
|
-
}
|
|
144
|
-
|
|
145
106
|
/**
|
|
146
107
|
* Register a loader by its $$id (injected by Vite plugin)
|
|
147
108
|
* This is called during module loading to cache loaders
|
|
@@ -163,6 +124,10 @@ export function registerLoaderById(loader: {
|
|
|
163
124
|
|
|
164
125
|
// Fall back to using fn from the loader object (non-fetchable loaders)
|
|
165
126
|
if (loader.fn) {
|
|
166
|
-
loaderRegistry.set(loader.$$id, {
|
|
127
|
+
loaderRegistry.set(loader.$$id, {
|
|
128
|
+
fn: loader.fn,
|
|
129
|
+
middleware: [],
|
|
130
|
+
fetchable: false,
|
|
131
|
+
});
|
|
167
132
|
}
|
|
168
133
|
}
|
|
@@ -14,7 +14,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
14
14
|
import type { CookieOptions } from "../router/middleware.js";
|
|
15
15
|
import type { LoaderDefinition, LoaderContext } from "../types.js";
|
|
16
16
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
DefaultReverseRouteMap,
|
|
19
|
+
DefaultRouteName,
|
|
20
|
+
} from "../types/global-namespace.js";
|
|
18
21
|
import type { Handle } from "../handle.js";
|
|
19
22
|
import { type ContextVar, contextGet, contextSet } from "../context-var.js";
|
|
20
23
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
@@ -27,8 +30,9 @@ import { THEME_COOKIE } from "../theme/constants.js";
|
|
|
27
30
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
28
31
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
29
32
|
import { createReverseFunction } from "../router/handler-context.js";
|
|
30
|
-
import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
33
|
+
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
31
34
|
import { invariant } from "../errors.js";
|
|
35
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* Unified request context available via getRequestContext()
|
|
@@ -128,6 +132,12 @@ export interface RequestContext<
|
|
|
128
132
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
129
133
|
_cacheStore?: SegmentCacheStore;
|
|
130
134
|
|
|
135
|
+
/** @internal Cache profiles for "use cache" profile resolution (per-router) */
|
|
136
|
+
_cacheProfiles?: Record<
|
|
137
|
+
string,
|
|
138
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
139
|
+
>;
|
|
140
|
+
|
|
131
141
|
/**
|
|
132
142
|
* Schedule work to run after the response is sent.
|
|
133
143
|
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
@@ -214,14 +224,21 @@ export interface RequestContext<
|
|
|
214
224
|
*
|
|
215
225
|
* @example
|
|
216
226
|
* ```typescript
|
|
217
|
-
* ctx.setLocationState(
|
|
227
|
+
* ctx.setLocationState(Flash({ text: "Item saved!" }));
|
|
218
228
|
* ```
|
|
219
229
|
*/
|
|
220
|
-
setLocationState(entries: LocationStateEntry[]): void;
|
|
230
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
221
231
|
|
|
222
232
|
/** @internal Accumulated location state entries */
|
|
223
233
|
_locationState?: LocationStateEntry[];
|
|
224
234
|
|
|
235
|
+
/**
|
|
236
|
+
* The matched route name, if the route has an explicit name.
|
|
237
|
+
* Undefined before route matching or for unnamed routes.
|
|
238
|
+
* Includes the namespace prefix from include() (e.g., "blog.post").
|
|
239
|
+
*/
|
|
240
|
+
routeName?: DefaultRouteName;
|
|
241
|
+
|
|
225
242
|
/**
|
|
226
243
|
* Generate URLs from route names.
|
|
227
244
|
* Uses the global route map. After route matching, scoped (`.name`) resolution
|
|
@@ -234,6 +251,20 @@ export interface RequestContext<
|
|
|
234
251
|
|
|
235
252
|
/** @internal Route name from route matching, used for scoped reverse resolution */
|
|
236
253
|
_routeName?: string;
|
|
254
|
+
|
|
255
|
+
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
256
|
+
_prevRouteKey?: string;
|
|
257
|
+
|
|
258
|
+
/** @internal Per-request error dedup set for onError reporting */
|
|
259
|
+
_reportedErrors: WeakSet<object>;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @internal Report a non-fatal background error through the router's
|
|
263
|
+
* onError callback. Wired by the RSC handler / router during request
|
|
264
|
+
* creation. Cache-runtime and other subsystems call this to surface
|
|
265
|
+
* errors without failing the response.
|
|
266
|
+
*/
|
|
267
|
+
_reportBackgroundError?: (error: unknown, category: string) => void;
|
|
237
268
|
}
|
|
238
269
|
|
|
239
270
|
/**
|
|
@@ -253,10 +284,14 @@ export type PublicRequestContext<
|
|
|
253
284
|
| "deleteCookie"
|
|
254
285
|
| "_handleStore"
|
|
255
286
|
| "_cacheStore"
|
|
287
|
+
| "_cacheProfiles"
|
|
256
288
|
| "_onResponseCallbacks"
|
|
257
289
|
| "_themeConfig"
|
|
258
290
|
| "_locationState"
|
|
259
291
|
| "_routeName"
|
|
292
|
+
| "_prevRouteKey"
|
|
293
|
+
| "_reportedErrors"
|
|
294
|
+
| "_reportBackgroundError"
|
|
260
295
|
>;
|
|
261
296
|
|
|
262
297
|
// AsyncLocalStorage instance for request context
|
|
@@ -313,9 +348,34 @@ export function setRequestContextParams(
|
|
|
313
348
|
ctx.params = params;
|
|
314
349
|
if (routeName !== undefined) {
|
|
315
350
|
ctx._routeName = routeName;
|
|
351
|
+
ctx.routeName = (
|
|
352
|
+
routeName && !isAutoGeneratedRouteName(routeName)
|
|
353
|
+
? routeName
|
|
354
|
+
: undefined
|
|
355
|
+
) as DefaultRouteName | undefined;
|
|
316
356
|
}
|
|
317
357
|
// Update reverse with scoped resolution now that route is known
|
|
318
|
-
ctx.reverse = createReverseFunction(
|
|
358
|
+
ctx.reverse = createReverseFunction(
|
|
359
|
+
getGlobalRouteMap(),
|
|
360
|
+
routeName,
|
|
361
|
+
params,
|
|
362
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Store the previous route key on the request context.
|
|
369
|
+
* Called during partial-match context creation to make the navigation source
|
|
370
|
+
* route key available for revalidation and intercept evaluation.
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
export function setRequestContextPrevRouteKey(
|
|
374
|
+
prevRouteKey: string | undefined,
|
|
375
|
+
): void {
|
|
376
|
+
const ctx = requestContextStorage.getStore();
|
|
377
|
+
if (ctx && prevRouteKey !== undefined) {
|
|
378
|
+
ctx._prevRouteKey = prevRouteKey;
|
|
319
379
|
}
|
|
320
380
|
}
|
|
321
381
|
|
|
@@ -354,8 +414,15 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
354
414
|
request: Request;
|
|
355
415
|
url: URL;
|
|
356
416
|
variables: Record<string, any>;
|
|
417
|
+
/** Optional initial response stub headers/status to seed effective cookie reads */
|
|
418
|
+
initialResponse?: Response;
|
|
357
419
|
/** Optional cache store for segment caching (used by CacheScope) */
|
|
358
420
|
cacheStore?: SegmentCacheStore;
|
|
421
|
+
/** Optional cache profiles for "use cache" resolution (per-router) */
|
|
422
|
+
cacheProfiles?: Record<
|
|
423
|
+
string,
|
|
424
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
425
|
+
>;
|
|
359
426
|
/** Optional Cloudflare execution context for waitUntil support */
|
|
360
427
|
executionContext?: ExecutionContext;
|
|
361
428
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
@@ -378,7 +445,9 @@ export function createRequestContext<TEnv>(
|
|
|
378
445
|
request,
|
|
379
446
|
url,
|
|
380
447
|
variables,
|
|
448
|
+
initialResponse,
|
|
381
449
|
cacheStore,
|
|
450
|
+
cacheProfiles,
|
|
382
451
|
executionContext,
|
|
383
452
|
themeConfig,
|
|
384
453
|
} = options;
|
|
@@ -387,7 +456,13 @@ export function createRequestContext<TEnv>(
|
|
|
387
456
|
|
|
388
457
|
// Create stub response for collecting headers/cookies.
|
|
389
458
|
// All cookie/header mutations go here; cookie reads derive from it.
|
|
390
|
-
let stubResponse =
|
|
459
|
+
let stubResponse = initialResponse
|
|
460
|
+
? new Response(null, {
|
|
461
|
+
status: initialResponse.status,
|
|
462
|
+
statusText: initialResponse.statusText,
|
|
463
|
+
headers: new Headers(initialResponse.headers),
|
|
464
|
+
})
|
|
465
|
+
: new Response(null, { status: 200 });
|
|
391
466
|
|
|
392
467
|
// Create handle store and loader memoization for this request
|
|
393
468
|
const handleStore = createHandleStore();
|
|
@@ -556,6 +631,7 @@ export function createRequestContext<TEnv>(
|
|
|
556
631
|
|
|
557
632
|
_handleStore: handleStore,
|
|
558
633
|
_cacheStore: cacheStore,
|
|
634
|
+
_cacheProfiles: cacheProfiles,
|
|
559
635
|
|
|
560
636
|
waitUntil(fn: () => Promise<void>): void {
|
|
561
637
|
if (executionContext?.waitUntil) {
|
|
@@ -588,14 +664,17 @@ export function createRequestContext<TEnv>(
|
|
|
588
664
|
: undefined,
|
|
589
665
|
_themeConfig: themeConfig,
|
|
590
666
|
|
|
591
|
-
setLocationState(entries: LocationStateEntry[]): void {
|
|
667
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
|
|
592
668
|
assertNotInsideCacheExec(ctx, "setLocationState");
|
|
669
|
+
const arr = Array.isArray(entries) ? entries : [entries];
|
|
593
670
|
this._locationState = this._locationState
|
|
594
|
-
? [...this._locationState, ...
|
|
595
|
-
:
|
|
671
|
+
? [...this._locationState, ...arr]
|
|
672
|
+
: arr;
|
|
596
673
|
},
|
|
597
674
|
_locationState: undefined,
|
|
598
675
|
|
|
676
|
+
_reportedErrors: new WeakSet<object>(),
|
|
677
|
+
|
|
599
678
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
600
679
|
};
|
|
601
680
|
|
|
@@ -774,6 +853,7 @@ export function createUseFunction<TEnv>(
|
|
|
774
853
|
// Create loader context with recursive use() support
|
|
775
854
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
776
855
|
params: ctx.params,
|
|
856
|
+
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
777
857
|
request: ctx.request,
|
|
778
858
|
searchParams: ctx.searchParams,
|
|
779
859
|
search: (ctx as any).search ?? {},
|
|
@@ -794,6 +874,7 @@ export function createUseFunction<TEnv>(
|
|
|
794
874
|
getGlobalRouteMap(),
|
|
795
875
|
ctx._routeName,
|
|
796
876
|
ctx.params as Record<string, string>,
|
|
877
|
+
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
797
878
|
),
|
|
798
879
|
};
|
|
799
880
|
|
package/src/ssr/index.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { renderSegments } from "../segment-system.js";
|
|
3
|
-
import {
|
|
4
|
-
import { initSegmentsSync } from "../browser/react/use-segments.js";
|
|
5
|
-
import { initThemeConfigSync } from "../theme/theme-context.js";
|
|
3
|
+
import { filterSegmentOrder } from "../browser/react/filter-segment-order.js";
|
|
6
4
|
import { ThemeProvider } from "../theme/ThemeProvider.js";
|
|
5
|
+
import { NonceContext } from "../browser/react/nonce-context.js";
|
|
7
6
|
import { NavigationStoreContext } from "../browser/react/context.js";
|
|
8
7
|
import type { NavigationStoreContextValue } from "../browser/react/context.js";
|
|
9
8
|
import type { HandleData } from "../browser/types.js";
|
|
@@ -34,6 +33,13 @@ interface RenderToReadableStreamOptions {
|
|
|
34
33
|
formState?: unknown;
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
/**
|
|
37
|
+
* ReadableStream with the allReady promise added by react-dom/server.edge.
|
|
38
|
+
*/
|
|
39
|
+
interface ReactDOMReadableStream extends ReadableStream<Uint8Array> {
|
|
40
|
+
allReady: Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
/**
|
|
38
44
|
* Options for the renderHTML function
|
|
39
45
|
*/
|
|
@@ -50,6 +56,14 @@ export interface SSRRenderOptions {
|
|
|
50
56
|
* Nonce for Content Security Policy (CSP)
|
|
51
57
|
*/
|
|
52
58
|
nonce?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* SSR stream mode.
|
|
62
|
+
*
|
|
63
|
+
* - `"stream"` (default) — start flushing HTML immediately.
|
|
64
|
+
* - `"allReady"` — await `stream.allReady` before returning.
|
|
65
|
+
*/
|
|
66
|
+
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -69,7 +83,7 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
69
83
|
renderToReadableStream: (
|
|
70
84
|
element: React.ReactNode,
|
|
71
85
|
options?: RenderToReadableStreamOptions,
|
|
72
|
-
) => Promise<
|
|
86
|
+
) => Promise<ReactDOMReadableStream>;
|
|
73
87
|
|
|
74
88
|
/**
|
|
75
89
|
* injectRSCPayload from rsc-html-stream/server
|
|
@@ -139,8 +153,18 @@ async function consumeAsyncGenerator(
|
|
|
139
153
|
* Create a minimal event controller for SSR.
|
|
140
154
|
* This provides the correct pathname so useNavigation returns the right value during SSR.
|
|
141
155
|
*/
|
|
142
|
-
function createSsrEventController(
|
|
143
|
-
|
|
156
|
+
function createSsrEventController(opts: {
|
|
157
|
+
pathname: string;
|
|
158
|
+
params?: Record<string, string>;
|
|
159
|
+
handleData?: HandleData;
|
|
160
|
+
matched?: string[];
|
|
161
|
+
}): EventController {
|
|
162
|
+
const location = new URL(opts.pathname, "http://localhost");
|
|
163
|
+
let params = opts.params ?? {};
|
|
164
|
+
const handleState = {
|
|
165
|
+
data: opts.handleData ?? {},
|
|
166
|
+
segmentOrder: filterSegmentOrder(opts.matched ?? []),
|
|
167
|
+
};
|
|
144
168
|
const state: DerivedNavigationState = {
|
|
145
169
|
state: "idle",
|
|
146
170
|
isStreaming: false,
|
|
@@ -163,9 +187,11 @@ function createSsrEventController(pathname: string): EventController {
|
|
|
163
187
|
subscribeToAction: () => () => {},
|
|
164
188
|
subscribeToHandles: () => () => {},
|
|
165
189
|
setHandleData: () => {},
|
|
166
|
-
getHandleState: () =>
|
|
167
|
-
setParams: () => {
|
|
168
|
-
|
|
190
|
+
getHandleState: () => handleState,
|
|
191
|
+
setParams: (nextParams) => {
|
|
192
|
+
params = nextParams;
|
|
193
|
+
},
|
|
194
|
+
getParams: () => params,
|
|
169
195
|
setLocation: () => {},
|
|
170
196
|
startNavigation: () => {
|
|
171
197
|
throw new Error("Navigation not supported during SSR");
|
|
@@ -177,6 +203,7 @@ function createSsrEventController(pathname: string): EventController {
|
|
|
177
203
|
abortAllActions: () => {},
|
|
178
204
|
getCurrentNavigation: () => null,
|
|
179
205
|
getInflightActions: () => new Map(),
|
|
206
|
+
hadAnyConcurrentActions: () => false,
|
|
180
207
|
};
|
|
181
208
|
}
|
|
182
209
|
|
|
@@ -218,7 +245,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
218
245
|
rscStream: ReadableStream<Uint8Array>,
|
|
219
246
|
options?: SSRRenderOptions,
|
|
220
247
|
): Promise<ReadableStream<Uint8Array>> {
|
|
221
|
-
const { nonce, formState } = options ?? {};
|
|
248
|
+
const { nonce, formState, streamMode } = options ?? {};
|
|
222
249
|
|
|
223
250
|
try {
|
|
224
251
|
// Tee the stream:
|
|
@@ -233,33 +260,28 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
233
260
|
function SsrRoot() {
|
|
234
261
|
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
235
262
|
const resolved = React.use(payload);
|
|
236
|
-
|
|
237
|
-
// Initialize segments state before children render (for useSegments hook)
|
|
238
|
-
initSegmentsSync(
|
|
239
|
-
resolved.metadata?.matched,
|
|
240
|
-
resolved.metadata?.pathname,
|
|
241
|
-
resolved.metadata?.params,
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
// Initialize theme config for MetaTags to render theme script
|
|
245
263
|
const themeConfig = resolved.metadata?.themeConfig ?? null;
|
|
246
|
-
|
|
264
|
+
const pathname = resolved.metadata?.pathname ?? "/";
|
|
247
265
|
|
|
248
|
-
// Await handles
|
|
266
|
+
// Await handles before creating SSR event controller so hooks can
|
|
267
|
+
// read request-local handle data via NavigationStoreContext.
|
|
249
268
|
// The handles property is an async generator that yields on each push
|
|
250
269
|
// Memoize the promise since async generators can only be iterated once
|
|
270
|
+
let handleData: HandleData = {};
|
|
251
271
|
if (resolved.metadata?.handles) {
|
|
252
272
|
handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
|
|
253
|
-
|
|
254
|
-
initHandleDataSync(handleData, resolved.metadata.matched);
|
|
273
|
+
handleData = React.use(handlesPromise);
|
|
255
274
|
}
|
|
256
275
|
|
|
257
|
-
// Create SSR context with
|
|
276
|
+
// Create SSR context with request-local pathname/params/handles.
|
|
258
277
|
ssrContextValue ??= {
|
|
259
278
|
store: null as any,
|
|
260
|
-
eventController: createSsrEventController(
|
|
261
|
-
|
|
262
|
-
|
|
279
|
+
eventController: createSsrEventController({
|
|
280
|
+
pathname,
|
|
281
|
+
params: resolved.metadata?.params,
|
|
282
|
+
handleData,
|
|
283
|
+
matched: resolved.metadata?.matched,
|
|
284
|
+
}),
|
|
263
285
|
navigate: async () => {},
|
|
264
286
|
refresh: async () => {},
|
|
265
287
|
version: resolved.metadata?.version,
|
|
@@ -290,6 +312,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
290
312
|
);
|
|
291
313
|
}
|
|
292
314
|
|
|
315
|
+
// Wrap with NonceContext so client components (e.g. MetaTags) can
|
|
316
|
+
// apply CSP nonces to inline scripts during SSR. Always present to
|
|
317
|
+
// match the browser-side NavigationProvider tree shape for hydration.
|
|
318
|
+
content = (
|
|
319
|
+
<NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
|
|
320
|
+
);
|
|
321
|
+
|
|
293
322
|
// Wrap with NavigationStoreContext for useNavigation hook
|
|
294
323
|
return (
|
|
295
324
|
<NavigationStoreContext.Provider value={ssrContextValue!}>
|
|
@@ -310,6 +339,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
310
339
|
nonce,
|
|
311
340
|
});
|
|
312
341
|
|
|
342
|
+
// Wait for all Suspense boundaries to resolve when streamMode is "allReady".
|
|
343
|
+
// This buffers the entire HTML before flushing — used for bots that
|
|
344
|
+
// cannot process streamed HTML.
|
|
345
|
+
if (streamMode === "allReady") {
|
|
346
|
+
await htmlStream.allReady;
|
|
347
|
+
}
|
|
348
|
+
|
|
313
349
|
// Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
|
|
314
350
|
return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
|
|
315
351
|
} catch (error) {
|
package/src/static-handler.ts
CHANGED
|
@@ -82,6 +82,13 @@ export function Static<TParams extends Record<string, any>>(
|
|
|
82
82
|
id = maybeId ?? "";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (!id) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"[rsc-router] Static: missing $$id. " +
|
|
88
|
+
"Ensure the exposeInternalIds Vite plugin is configured.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
return {
|
|
86
93
|
__brand: "staticHandler" as const,
|
|
87
94
|
$$id: id,
|
|
@@ -50,7 +50,12 @@ function readThemeFromCookie(storageKey: string): string | null {
|
|
|
50
50
|
for (const cookie of cookies) {
|
|
51
51
|
const [name, ...rest] = cookie.trim().split("=");
|
|
52
52
|
if (name === storageKey) {
|
|
53
|
-
|
|
53
|
+
const raw = rest.join("=");
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(raw);
|
|
56
|
+
} catch {
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
return null;
|
package/src/theme/index.ts
CHANGED
|
@@ -54,9 +54,4 @@ export {
|
|
|
54
54
|
export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
|
|
55
55
|
|
|
56
56
|
// Context (for advanced use cases)
|
|
57
|
-
export {
|
|
58
|
-
ThemeContext,
|
|
59
|
-
useThemeContext,
|
|
60
|
-
initThemeConfigSync,
|
|
61
|
-
getSSRThemeConfig,
|
|
62
|
-
} from "./theme-context.js";
|
|
57
|
+
export { ThemeContext, useThemeContext } from "./theme-context.js";
|