@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- 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 +170 -18
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- 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/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- 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/index.ts +0 -3
- 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 +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- 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 +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -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 +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- 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/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- 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 +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -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
|
}
|
|
@@ -13,11 +13,17 @@
|
|
|
13
13
|
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
|
+
import type { ScopedReverseFunction } from "../reverse.js";
|
|
17
|
+
import type {
|
|
18
|
+
DefaultEnv,
|
|
19
|
+
DefaultReverseRouteMap,
|
|
20
|
+
DefaultRouteName,
|
|
21
|
+
} from "../types/global-namespace.js";
|
|
16
22
|
import type { Handle } from "../handle.js";
|
|
17
23
|
import { type ContextVar, contextGet, contextSet } from "../context-var.js";
|
|
18
24
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
19
25
|
import { isHandle } from "../handle.js";
|
|
20
|
-
import { track } from "./context.js";
|
|
26
|
+
import { track, type MetricsStore } from "./context.js";
|
|
21
27
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
22
28
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
23
29
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
@@ -25,7 +31,9 @@ import { THEME_COOKIE } from "../theme/constants.js";
|
|
|
25
31
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
26
32
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
27
33
|
import { createReverseFunction } from "../router/handler-context.js";
|
|
28
|
-
import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
34
|
+
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
|
+
import { invariant } from "../errors.js";
|
|
36
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* Unified request context available via getRequestContext()
|
|
@@ -34,7 +42,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
|
34
42
|
* Use this when you need access to request data outside of route handlers.
|
|
35
43
|
*/
|
|
36
44
|
export interface RequestContext<
|
|
37
|
-
TEnv =
|
|
45
|
+
TEnv = DefaultEnv,
|
|
38
46
|
TParams = Record<string, string>,
|
|
39
47
|
> {
|
|
40
48
|
/** Platform bindings (Cloudflare env, etc.) */
|
|
@@ -65,24 +73,28 @@ export interface RequestContext<
|
|
|
65
73
|
*/
|
|
66
74
|
params: TParams;
|
|
67
75
|
/**
|
|
68
|
-
* Stub response for setting headers/cookies
|
|
69
|
-
* Headers set here are merged into the final response
|
|
76
|
+
* Stub response for setting headers/cookies (read-only).
|
|
77
|
+
* Headers set here are merged into the final response.
|
|
78
|
+
* Use header() or setStatus() to mutate response headers/status.
|
|
79
|
+
* Use cookies().set()/cookies().delete() for cookie mutations.
|
|
70
80
|
*/
|
|
71
|
-
res: Response;
|
|
81
|
+
readonly res: Response;
|
|
72
82
|
|
|
73
|
-
/** Get a cookie value
|
|
83
|
+
/** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
|
|
74
84
|
cookie(name: string): string | undefined;
|
|
75
|
-
/** Get all cookies
|
|
85
|
+
/** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
|
|
76
86
|
cookies(): Record<string, string>;
|
|
77
|
-
/** Set a cookie on the response */
|
|
87
|
+
/** @internal Set a cookie on the response. Use cookies().set() instead. */
|
|
78
88
|
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
79
|
-
/** Delete a cookie */
|
|
89
|
+
/** @internal Delete a cookie. Use cookies().delete() instead. */
|
|
80
90
|
deleteCookie(
|
|
81
91
|
name: string,
|
|
82
92
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
83
93
|
): void;
|
|
84
94
|
/** Set a response header */
|
|
85
95
|
header(name: string, value: string): void;
|
|
96
|
+
/** Set the response status code */
|
|
97
|
+
setStatus(status: number): void;
|
|
86
98
|
|
|
87
99
|
/**
|
|
88
100
|
* Access loader data or push handle data.
|
|
@@ -121,6 +133,12 @@ export interface RequestContext<
|
|
|
121
133
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
122
134
|
_cacheStore?: SegmentCacheStore;
|
|
123
135
|
|
|
136
|
+
/** @internal Cache profiles for "use cache" profile resolution (per-router) */
|
|
137
|
+
_cacheProfiles?: Record<
|
|
138
|
+
string,
|
|
139
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
140
|
+
>;
|
|
141
|
+
|
|
124
142
|
/**
|
|
125
143
|
* Schedule work to run after the response is sent.
|
|
126
144
|
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
@@ -207,29 +225,84 @@ export interface RequestContext<
|
|
|
207
225
|
*
|
|
208
226
|
* @example
|
|
209
227
|
* ```typescript
|
|
210
|
-
* ctx.setLocationState(
|
|
228
|
+
* ctx.setLocationState(Flash({ text: "Item saved!" }));
|
|
211
229
|
* ```
|
|
212
230
|
*/
|
|
213
|
-
setLocationState(entries: LocationStateEntry[]): void;
|
|
231
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
214
232
|
|
|
215
233
|
/** @internal Accumulated location state entries */
|
|
216
234
|
_locationState?: LocationStateEntry[];
|
|
217
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The matched route name, if the route has an explicit name.
|
|
238
|
+
* Undefined before route matching or for unnamed routes.
|
|
239
|
+
* Includes the namespace prefix from include() (e.g., "blog.post").
|
|
240
|
+
*/
|
|
241
|
+
routeName?: DefaultRouteName;
|
|
242
|
+
|
|
218
243
|
/**
|
|
219
244
|
* Generate URLs from route names.
|
|
220
245
|
* Uses the global route map. After route matching, scoped (`.name`) resolution
|
|
221
246
|
* works within the matched include() scope.
|
|
222
247
|
*/
|
|
223
|
-
reverse
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
): string;
|
|
248
|
+
reverse: ScopedReverseFunction<
|
|
249
|
+
Record<string, string>,
|
|
250
|
+
DefaultReverseRouteMap
|
|
251
|
+
>;
|
|
228
252
|
|
|
229
253
|
/** @internal Route name from route matching, used for scoped reverse resolution */
|
|
230
254
|
_routeName?: string;
|
|
255
|
+
|
|
256
|
+
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
257
|
+
_prevRouteKey?: string;
|
|
258
|
+
|
|
259
|
+
/** @internal Per-request error dedup set for onError reporting */
|
|
260
|
+
_reportedErrors: WeakSet<object>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @internal Report a non-fatal background error through the router's
|
|
264
|
+
* onError callback. Wired by the RSC handler / router during request
|
|
265
|
+
* creation. Cache-runtime and other subsystems call this to surface
|
|
266
|
+
* errors without failing the response.
|
|
267
|
+
*/
|
|
268
|
+
_reportBackgroundError?: (error: unknown, category: string) => void;
|
|
269
|
+
|
|
270
|
+
/** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
|
|
271
|
+
_debugPerformance?: boolean;
|
|
272
|
+
|
|
273
|
+
/** @internal Request-scoped performance metrics store */
|
|
274
|
+
_metricsStore?: MetricsStore;
|
|
231
275
|
}
|
|
232
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Public view of RequestContext, without internal methods and fields.
|
|
279
|
+
*
|
|
280
|
+
* This is the type exported to library consumers. Internal code should
|
|
281
|
+
* use the full RequestContext interface directly.
|
|
282
|
+
*/
|
|
283
|
+
export type PublicRequestContext<
|
|
284
|
+
TEnv = DefaultEnv,
|
|
285
|
+
TParams = Record<string, string>,
|
|
286
|
+
> = Omit<
|
|
287
|
+
RequestContext<TEnv, TParams>,
|
|
288
|
+
| "cookie"
|
|
289
|
+
| "cookies"
|
|
290
|
+
| "setCookie"
|
|
291
|
+
| "deleteCookie"
|
|
292
|
+
| "_handleStore"
|
|
293
|
+
| "_cacheStore"
|
|
294
|
+
| "_cacheProfiles"
|
|
295
|
+
| "_onResponseCallbacks"
|
|
296
|
+
| "_themeConfig"
|
|
297
|
+
| "_locationState"
|
|
298
|
+
| "_routeName"
|
|
299
|
+
| "_prevRouteKey"
|
|
300
|
+
| "_reportedErrors"
|
|
301
|
+
| "_reportBackgroundError"
|
|
302
|
+
| "_debugPerformance"
|
|
303
|
+
| "_metricsStore"
|
|
304
|
+
>;
|
|
305
|
+
|
|
233
306
|
// AsyncLocalStorage instance for request context
|
|
234
307
|
const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
|
|
235
308
|
|
|
@@ -246,9 +319,26 @@ export function runWithRequestContext<TEnv, T>(
|
|
|
246
319
|
|
|
247
320
|
/**
|
|
248
321
|
* Get the current request context
|
|
249
|
-
*
|
|
322
|
+
* Throws if called outside of a request context
|
|
323
|
+
*/
|
|
324
|
+
export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
|
|
325
|
+
const ctx = requestContextStorage.getStore() as
|
|
326
|
+
| RequestContext<TEnv>
|
|
327
|
+
| undefined;
|
|
328
|
+
invariant(
|
|
329
|
+
ctx,
|
|
330
|
+
"getRequestContext() called outside of a request context. " +
|
|
331
|
+
"This function must be called from within a route handler, loader, middleware, " +
|
|
332
|
+
"server action, or server component.",
|
|
333
|
+
);
|
|
334
|
+
return ctx;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @internal Get the request context without throwing — for internal code that
|
|
339
|
+
* may run outside a request context (cache stores, optional handle lookups, etc.)
|
|
250
340
|
*/
|
|
251
|
-
export function
|
|
341
|
+
export function _getRequestContext<TEnv = DefaultEnv>():
|
|
252
342
|
| RequestContext<TEnv>
|
|
253
343
|
| undefined {
|
|
254
344
|
return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
|
|
@@ -267,9 +357,34 @@ export function setRequestContextParams(
|
|
|
267
357
|
ctx.params = params;
|
|
268
358
|
if (routeName !== undefined) {
|
|
269
359
|
ctx._routeName = routeName;
|
|
360
|
+
ctx.routeName = (
|
|
361
|
+
routeName && !isAutoGeneratedRouteName(routeName)
|
|
362
|
+
? routeName
|
|
363
|
+
: undefined
|
|
364
|
+
) as DefaultRouteName | undefined;
|
|
270
365
|
}
|
|
271
366
|
// Update reverse with scoped resolution now that route is known
|
|
272
|
-
ctx.reverse = createReverseFunction(
|
|
367
|
+
ctx.reverse = createReverseFunction(
|
|
368
|
+
getGlobalRouteMap(),
|
|
369
|
+
routeName,
|
|
370
|
+
params,
|
|
371
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Store the previous route key on the request context.
|
|
378
|
+
* Called during partial-match context creation to make the navigation source
|
|
379
|
+
* route key available for revalidation and intercept evaluation.
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
382
|
+
export function setRequestContextPrevRouteKey(
|
|
383
|
+
prevRouteKey: string | undefined,
|
|
384
|
+
): void {
|
|
385
|
+
const ctx = requestContextStorage.getStore();
|
|
386
|
+
if (ctx && prevRouteKey !== undefined) {
|
|
387
|
+
ctx._prevRouteKey = prevRouteKey;
|
|
273
388
|
}
|
|
274
389
|
}
|
|
275
390
|
|
|
@@ -286,17 +401,12 @@ export function getLocationState(): LocationStateEntry[] | undefined {
|
|
|
286
401
|
|
|
287
402
|
/**
|
|
288
403
|
* Get the current request context, throwing if not available
|
|
289
|
-
* Use
|
|
404
|
+
* @deprecated Use getRequestContext() directly — it now throws if outside context
|
|
290
405
|
*/
|
|
291
|
-
export function requireRequestContext<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"Request context not available. This function must be called from within a server action " +
|
|
296
|
-
"executed through the RSC handler.",
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
return ctx;
|
|
406
|
+
export function requireRequestContext<
|
|
407
|
+
TEnv = DefaultEnv,
|
|
408
|
+
>(): RequestContext<TEnv> {
|
|
409
|
+
return getRequestContext<TEnv>();
|
|
300
410
|
}
|
|
301
411
|
|
|
302
412
|
/**
|
|
@@ -315,8 +425,15 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
315
425
|
request: Request;
|
|
316
426
|
url: URL;
|
|
317
427
|
variables: Record<string, any>;
|
|
428
|
+
/** Optional initial response stub headers/status to seed effective cookie reads */
|
|
429
|
+
initialResponse?: Response;
|
|
318
430
|
/** Optional cache store for segment caching (used by CacheScope) */
|
|
319
431
|
cacheStore?: SegmentCacheStore;
|
|
432
|
+
/** Optional cache profiles for "use cache" resolution (per-router) */
|
|
433
|
+
cacheProfiles?: Record<
|
|
434
|
+
string,
|
|
435
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
436
|
+
>;
|
|
320
437
|
/** Optional Cloudflare execution context for waitUntil support */
|
|
321
438
|
executionContext?: ExecutionContext;
|
|
322
439
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
@@ -339,21 +456,30 @@ export function createRequestContext<TEnv>(
|
|
|
339
456
|
request,
|
|
340
457
|
url,
|
|
341
458
|
variables,
|
|
459
|
+
initialResponse,
|
|
342
460
|
cacheStore,
|
|
461
|
+
cacheProfiles,
|
|
343
462
|
executionContext,
|
|
344
463
|
themeConfig,
|
|
345
464
|
} = options;
|
|
346
465
|
const cookieHeader = request.headers.get("Cookie");
|
|
347
466
|
let parsedCookies: Record<string, string> | null = null;
|
|
348
467
|
|
|
349
|
-
// Create stub response for collecting headers/cookies
|
|
350
|
-
|
|
468
|
+
// Create stub response for collecting headers/cookies.
|
|
469
|
+
// All cookie/header mutations go here; cookie reads derive from it.
|
|
470
|
+
let stubResponse = initialResponse
|
|
471
|
+
? new Response(null, {
|
|
472
|
+
status: initialResponse.status,
|
|
473
|
+
statusText: initialResponse.statusText,
|
|
474
|
+
headers: new Headers(initialResponse.headers),
|
|
475
|
+
})
|
|
476
|
+
: new Response(null, { status: 200 });
|
|
351
477
|
|
|
352
478
|
// Create handle store and loader memoization for this request
|
|
353
479
|
const handleStore = createHandleStore();
|
|
354
480
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
355
481
|
|
|
356
|
-
// Lazy parse cookies
|
|
482
|
+
// Lazy parse cookies from the original Cookie header
|
|
357
483
|
const getParsedCookies = (): Record<string, string> => {
|
|
358
484
|
if (!parsedCookies) {
|
|
359
485
|
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
@@ -361,11 +487,35 @@ export function createRequestContext<TEnv>(
|
|
|
361
487
|
return parsedCookies;
|
|
362
488
|
};
|
|
363
489
|
|
|
490
|
+
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
491
|
+
let responseCookieCache: Map<string, string | null> | null = null;
|
|
492
|
+
const getResponseCookies = (): Map<string, string | null> => {
|
|
493
|
+
if (!responseCookieCache) {
|
|
494
|
+
responseCookieCache = parseResponseCookies(stubResponse);
|
|
495
|
+
}
|
|
496
|
+
return responseCookieCache;
|
|
497
|
+
};
|
|
498
|
+
const invalidateResponseCookieCache = () => {
|
|
499
|
+
responseCookieCache = null;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
503
|
+
// The stub IS the source of truth for same-request mutations.
|
|
504
|
+
const effectiveCookie = (name: string): string | undefined => {
|
|
505
|
+
const mutations = getResponseCookies();
|
|
506
|
+
if (mutations.has(name)) {
|
|
507
|
+
const v = mutations.get(name);
|
|
508
|
+
return v === null ? undefined : v;
|
|
509
|
+
}
|
|
510
|
+
return getParsedCookies()[name];
|
|
511
|
+
};
|
|
512
|
+
|
|
364
513
|
// Theme helpers (only used when themeConfig is provided)
|
|
365
514
|
const getTheme = (): Theme | undefined => {
|
|
366
515
|
if (!themeConfig) return undefined;
|
|
367
516
|
|
|
368
|
-
|
|
517
|
+
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
518
|
+
const stored = effectiveCookie(themeConfig.storageKey);
|
|
369
519
|
if (stored) {
|
|
370
520
|
// Validate stored value
|
|
371
521
|
if (stored === "system" && themeConfig.enableSystem) {
|
|
@@ -389,7 +539,7 @@ export function createRequestContext<TEnv>(
|
|
|
389
539
|
return;
|
|
390
540
|
}
|
|
391
541
|
|
|
392
|
-
//
|
|
542
|
+
// Write to stub — effectiveCookie() will pick it up on next read
|
|
393
543
|
stubResponse.headers.append(
|
|
394
544
|
"Set-Cookie",
|
|
395
545
|
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
@@ -398,6 +548,7 @@ export function createRequestContext<TEnv>(
|
|
|
398
548
|
sameSite: THEME_COOKIE.sameSite,
|
|
399
549
|
}),
|
|
400
550
|
);
|
|
551
|
+
invalidateResponseCookieCache();
|
|
401
552
|
};
|
|
402
553
|
|
|
403
554
|
// Build the context object first (without use), then add use
|
|
@@ -415,14 +566,37 @@ export function createRequestContext<TEnv>(
|
|
|
415
566
|
contextSet(variables, keyOrVar, value);
|
|
416
567
|
}) as RequestContext<TEnv>["set"],
|
|
417
568
|
params: {} as Record<string, string>,
|
|
418
|
-
|
|
569
|
+
|
|
570
|
+
get res(): Response {
|
|
571
|
+
return stubResponse;
|
|
572
|
+
},
|
|
573
|
+
set res(_: Response) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
576
|
+
);
|
|
577
|
+
},
|
|
419
578
|
|
|
420
579
|
cookie(name: string): string | undefined {
|
|
421
|
-
return
|
|
580
|
+
return effectiveCookie(name);
|
|
422
581
|
},
|
|
423
582
|
|
|
424
583
|
cookies(): Record<string, string> {
|
|
425
|
-
|
|
584
|
+
const parsed = getParsedCookies();
|
|
585
|
+
const mutations = getResponseCookies();
|
|
586
|
+
if (mutations.size === 0) return { ...parsed };
|
|
587
|
+
// Build result without delete (avoids V8 dictionary-mode de-opt)
|
|
588
|
+
const deleted = new Set<string>();
|
|
589
|
+
for (const [k, v] of mutations) {
|
|
590
|
+
if (v === null) deleted.add(k);
|
|
591
|
+
}
|
|
592
|
+
const result: Record<string, string> = {};
|
|
593
|
+
for (const key of Object.keys(parsed)) {
|
|
594
|
+
if (!deleted.has(key)) result[key] = parsed[key];
|
|
595
|
+
}
|
|
596
|
+
for (const [k, v] of mutations) {
|
|
597
|
+
if (v !== null) result[k] = v;
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
426
600
|
},
|
|
427
601
|
|
|
428
602
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
@@ -431,6 +605,7 @@ export function createRequestContext<TEnv>(
|
|
|
431
605
|
"Set-Cookie",
|
|
432
606
|
serializeCookieValue(name, value, options),
|
|
433
607
|
);
|
|
608
|
+
invalidateResponseCookieCache();
|
|
434
609
|
},
|
|
435
610
|
|
|
436
611
|
deleteCookie(
|
|
@@ -442,6 +617,7 @@ export function createRequestContext<TEnv>(
|
|
|
442
617
|
"Set-Cookie",
|
|
443
618
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
444
619
|
);
|
|
620
|
+
invalidateResponseCookieCache();
|
|
445
621
|
},
|
|
446
622
|
|
|
447
623
|
header(name: string, value: string): void {
|
|
@@ -449,6 +625,16 @@ export function createRequestContext<TEnv>(
|
|
|
449
625
|
stubResponse.headers.set(name, value);
|
|
450
626
|
},
|
|
451
627
|
|
|
628
|
+
setStatus(status: number): void {
|
|
629
|
+
assertNotInsideCacheExec(ctx, "setStatus");
|
|
630
|
+
// Response.status is read-only, so we must create a new Response.
|
|
631
|
+
// Headers are passed by reference — no cookie cache invalidation needed.
|
|
632
|
+
stubResponse = new Response(null, {
|
|
633
|
+
status,
|
|
634
|
+
headers: stubResponse.headers,
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
|
|
452
638
|
// Placeholder - will be replaced below
|
|
453
639
|
use: null as any,
|
|
454
640
|
|
|
@@ -456,6 +642,7 @@ export function createRequestContext<TEnv>(
|
|
|
456
642
|
|
|
457
643
|
_handleStore: handleStore,
|
|
458
644
|
_cacheStore: cacheStore,
|
|
645
|
+
_cacheProfiles: cacheProfiles,
|
|
459
646
|
|
|
460
647
|
waitUntil(fn: () => Promise<void>): void {
|
|
461
648
|
if (executionContext?.waitUntil) {
|
|
@@ -477,7 +664,9 @@ export function createRequestContext<TEnv>(
|
|
|
477
664
|
},
|
|
478
665
|
|
|
479
666
|
// Theme properties (only set when themeConfig is provided)
|
|
480
|
-
theme
|
|
667
|
+
get theme() {
|
|
668
|
+
return themeConfig ? getTheme() : undefined;
|
|
669
|
+
},
|
|
481
670
|
setTheme: themeConfig
|
|
482
671
|
? (theme: Theme) => {
|
|
483
672
|
assertNotInsideCacheExec(ctx, "setTheme");
|
|
@@ -486,14 +675,18 @@ export function createRequestContext<TEnv>(
|
|
|
486
675
|
: undefined,
|
|
487
676
|
_themeConfig: themeConfig,
|
|
488
677
|
|
|
489
|
-
setLocationState(entries: LocationStateEntry[]): void {
|
|
678
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
|
|
490
679
|
assertNotInsideCacheExec(ctx, "setLocationState");
|
|
680
|
+
const arr = Array.isArray(entries) ? entries : [entries];
|
|
491
681
|
this._locationState = this._locationState
|
|
492
|
-
? [...this._locationState, ...
|
|
493
|
-
:
|
|
682
|
+
? [...this._locationState, ...arr]
|
|
683
|
+
: arr;
|
|
494
684
|
},
|
|
495
685
|
_locationState: undefined,
|
|
496
686
|
|
|
687
|
+
_reportedErrors: new WeakSet<object>(),
|
|
688
|
+
_metricsStore: undefined,
|
|
689
|
+
|
|
497
690
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
498
691
|
};
|
|
499
692
|
|
|
@@ -509,6 +702,43 @@ export function createRequestContext<TEnv>(
|
|
|
509
702
|
return ctx;
|
|
510
703
|
}
|
|
511
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Parse Set-Cookie headers from a response into effective cookie state.
|
|
707
|
+
* Returns a map of cookie name -> value (string) or name -> null (deleted).
|
|
708
|
+
* Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
|
|
709
|
+
* Max-Age=0 is treated as a delete.
|
|
710
|
+
*/
|
|
711
|
+
const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
|
|
712
|
+
|
|
713
|
+
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
714
|
+
const result = new Map<string, string | null>();
|
|
715
|
+
const setCookies = response.headers.getSetCookie();
|
|
716
|
+
|
|
717
|
+
for (const header of setCookies) {
|
|
718
|
+
// First segment before ';' is the name=value pair
|
|
719
|
+
const semiIdx = header.indexOf(";");
|
|
720
|
+
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
721
|
+
const eqIdx = pair.indexOf("=");
|
|
722
|
+
if (eqIdx === -1) continue;
|
|
723
|
+
|
|
724
|
+
let name: string;
|
|
725
|
+
let value: string;
|
|
726
|
+
try {
|
|
727
|
+
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
728
|
+
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
729
|
+
} catch {
|
|
730
|
+
// Malformed encoding — skip this entry
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Max-Age=0 means the cookie is being deleted
|
|
735
|
+
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
736
|
+
result.set(name, isDeleted ? null : value);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
|
|
512
742
|
/**
|
|
513
743
|
* Parse cookies from Cookie header
|
|
514
744
|
*/
|
|
@@ -635,6 +865,7 @@ export function createUseFunction<TEnv>(
|
|
|
635
865
|
// Create loader context with recursive use() support
|
|
636
866
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
637
867
|
params: ctx.params,
|
|
868
|
+
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
638
869
|
request: ctx.request,
|
|
639
870
|
searchParams: ctx.searchParams,
|
|
640
871
|
search: (ctx as any).search ?? {},
|
|
@@ -643,12 +874,6 @@ export function createUseFunction<TEnv>(
|
|
|
643
874
|
env: ctx.env as any,
|
|
644
875
|
var: ctx.var as any,
|
|
645
876
|
get: ctx.get as any,
|
|
646
|
-
cookie(name: string) {
|
|
647
|
-
return ctx.cookie(name);
|
|
648
|
-
},
|
|
649
|
-
cookies() {
|
|
650
|
-
return ctx.cookies();
|
|
651
|
-
},
|
|
652
877
|
use: <TDep, TDepParams = any>(
|
|
653
878
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
654
879
|
): Promise<TDep> => {
|
|
@@ -661,11 +886,12 @@ export function createUseFunction<TEnv>(
|
|
|
661
886
|
getGlobalRouteMap(),
|
|
662
887
|
ctx._routeName,
|
|
663
888
|
ctx.params as Record<string, string>,
|
|
889
|
+
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
664
890
|
),
|
|
665
891
|
};
|
|
666
892
|
|
|
667
893
|
// Start loader execution with tracking
|
|
668
|
-
const doneLoader = track(`loader:${loader.$$id}
|
|
894
|
+
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
669
895
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
670
896
|
doneLoader();
|
|
671
897
|
});
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// Router registry (used by Vite plugin for build-time discovery)
|
|
12
12
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router.js";
|
|
13
13
|
|
|
14
|
+
// Host router registry (used by Vite plugin for host-router lazy discovery)
|
|
15
|
+
export {
|
|
16
|
+
HostRouterRegistry,
|
|
17
|
+
type HostRouterRegistryEntry,
|
|
18
|
+
} from "./host/router.js";
|
|
19
|
+
|
|
14
20
|
// Route map builder (Vite plugin injects these via virtual modules)
|
|
15
21
|
export {
|
|
16
22
|
registerRouteMap,
|