@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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/AGENTS.md +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -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 +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +37 -0
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- 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 +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -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 +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- package/src/browser/lru-cache.ts +0 -69
- 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/href.ts +0 -255
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -13,13 +13,27 @@
|
|
|
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";
|
|
23
|
+
import { type ContextVar, contextGet, contextSet } from "../context-var.js";
|
|
17
24
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
18
25
|
import { isHandle } from "../handle.js";
|
|
19
|
-
import { track } from "./context.js";
|
|
26
|
+
import { track, type MetricsStore } from "./context.js";
|
|
27
|
+
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
20
28
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
21
29
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
22
30
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
|
+
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
|
+
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
33
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
34
|
+
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
|
+
import { invariant } from "../errors.js";
|
|
36
|
+
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
37
|
|
|
24
38
|
/**
|
|
25
39
|
* Unified request context available via getRequestContext()
|
|
@@ -28,15 +42,20 @@ import { THEME_COOKIE } from "../theme/constants.js";
|
|
|
28
42
|
* Use this when you need access to request data outside of route handlers.
|
|
29
43
|
*/
|
|
30
44
|
export interface RequestContext<
|
|
31
|
-
TEnv =
|
|
45
|
+
TEnv = DefaultEnv,
|
|
32
46
|
TParams = Record<string, string>,
|
|
33
47
|
> {
|
|
34
48
|
/** Platform bindings (Cloudflare env, etc.) */
|
|
35
49
|
env: TEnv;
|
|
36
50
|
/** Original HTTP request */
|
|
37
51
|
request: Request;
|
|
38
|
-
/** Parsed URL (
|
|
52
|
+
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
39
53
|
url: URL;
|
|
54
|
+
/**
|
|
55
|
+
* The original request URL with all parameters intact, including
|
|
56
|
+
* internal `_rsc*` transport params.
|
|
57
|
+
*/
|
|
58
|
+
originalUrl: URL;
|
|
40
59
|
/** URL pathname */
|
|
41
60
|
pathname: string;
|
|
42
61
|
/** URL search params (system params like _rsc* are NOT filtered here) */
|
|
@@ -44,30 +63,38 @@ export interface RequestContext<
|
|
|
44
63
|
/** Variables set by middleware (same as ctx.var) */
|
|
45
64
|
var: Record<string, any>;
|
|
46
65
|
/** Get a variable set by middleware */
|
|
47
|
-
get:
|
|
66
|
+
get: {
|
|
67
|
+
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
68
|
+
<K extends string>(key: K): any;
|
|
69
|
+
};
|
|
48
70
|
/** Set a variable (shared with middleware and handlers) */
|
|
49
|
-
set:
|
|
71
|
+
set: {
|
|
72
|
+
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
73
|
+
<K extends string>(key: K, value: any): void;
|
|
74
|
+
};
|
|
50
75
|
/**
|
|
51
76
|
* Route params (populated after route matching)
|
|
52
77
|
* Initially empty, then set to matched params
|
|
53
78
|
*/
|
|
54
79
|
params: TParams;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
* Headers set here are merged into the final response
|
|
58
|
-
*/
|
|
59
|
-
res: Response;
|
|
80
|
+
/** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
|
|
81
|
+
readonly res: Response;
|
|
60
82
|
|
|
61
|
-
/** Get a cookie value
|
|
83
|
+
/** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
|
|
62
84
|
cookie(name: string): string | undefined;
|
|
63
|
-
/** Get all cookies
|
|
85
|
+
/** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
|
|
64
86
|
cookies(): Record<string, string>;
|
|
65
|
-
/** Set a cookie on the response */
|
|
87
|
+
/** @internal Set a cookie on the response. Use cookies().set() instead. */
|
|
66
88
|
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
67
|
-
/** Delete a cookie */
|
|
68
|
-
deleteCookie(
|
|
89
|
+
/** @internal Delete a cookie. Use cookies().delete() instead. */
|
|
90
|
+
deleteCookie(
|
|
91
|
+
name: string,
|
|
92
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
93
|
+
): void;
|
|
69
94
|
/** Set a response header */
|
|
70
95
|
header(name: string, value: string): void;
|
|
96
|
+
/** Set the response status code */
|
|
97
|
+
setStatus(status: number): void;
|
|
71
98
|
|
|
72
99
|
/**
|
|
73
100
|
* Access loader data or push handle data.
|
|
@@ -89,10 +116,12 @@ export interface RequestContext<
|
|
|
89
116
|
* ```
|
|
90
117
|
*/
|
|
91
118
|
use: {
|
|
92
|
-
<T, TLoaderParams = any>(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
<T, TLoaderParams = any>(
|
|
120
|
+
loader: LoaderDefinition<T, TLoaderParams>,
|
|
121
|
+
): Promise<T>;
|
|
122
|
+
<TData, TAccumulated = TData[]>(
|
|
123
|
+
handle: Handle<TData, TAccumulated>,
|
|
124
|
+
): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
|
|
96
125
|
};
|
|
97
126
|
|
|
98
127
|
/** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
|
|
@@ -104,6 +133,12 @@ export interface RequestContext<
|
|
|
104
133
|
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
105
134
|
_cacheStore?: SegmentCacheStore;
|
|
106
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
|
+
|
|
107
142
|
/**
|
|
108
143
|
* Schedule work to run after the response is sent.
|
|
109
144
|
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
@@ -177,8 +212,98 @@ export interface RequestContext<
|
|
|
177
212
|
|
|
178
213
|
/** @internal Theme configuration (null if theme not enabled) */
|
|
179
214
|
_themeConfig?: ResolvedThemeConfig | null;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Attach location state entries to the current response.
|
|
218
|
+
*
|
|
219
|
+
* For partial (SPA) requests, the state is included in the RSC payload
|
|
220
|
+
* metadata and merged into history.pushState on the client. For redirect
|
|
221
|
+
* responses, the state travels through the redirect payload so the target
|
|
222
|
+
* page can read it via useLocationState.
|
|
223
|
+
*
|
|
224
|
+
* Multiple calls accumulate entries.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* ctx.setLocationState(Flash({ text: "Item saved!" }));
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
232
|
+
|
|
233
|
+
/** @internal Accumulated location state entries */
|
|
234
|
+
_locationState?: LocationStateEntry[];
|
|
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
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate URLs from route names.
|
|
245
|
+
* Uses the global route map. After route matching, scoped (`.name`) resolution
|
|
246
|
+
* works within the matched include() scope.
|
|
247
|
+
*/
|
|
248
|
+
reverse: ScopedReverseFunction<
|
|
249
|
+
Record<string, string>,
|
|
250
|
+
DefaultReverseRouteMap
|
|
251
|
+
>;
|
|
252
|
+
|
|
253
|
+
/** @internal Route name from route matching, used for scoped reverse resolution */
|
|
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;
|
|
180
275
|
}
|
|
181
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
|
+
| "res"
|
|
305
|
+
>;
|
|
306
|
+
|
|
182
307
|
// AsyncLocalStorage instance for request context
|
|
183
308
|
const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
|
|
184
309
|
|
|
@@ -188,16 +313,33 @@ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
|
|
|
188
313
|
*/
|
|
189
314
|
export function runWithRequestContext<TEnv, T>(
|
|
190
315
|
context: RequestContext<TEnv>,
|
|
191
|
-
fn: () => T
|
|
316
|
+
fn: () => T,
|
|
192
317
|
): T {
|
|
193
318
|
return requestContextStorage.run(context, fn);
|
|
194
319
|
}
|
|
195
320
|
|
|
196
321
|
/**
|
|
197
322
|
* Get the current request context
|
|
198
|
-
*
|
|
323
|
+
* Throws if called outside of a request context
|
|
199
324
|
*/
|
|
200
|
-
export function getRequestContext<TEnv =
|
|
325
|
+
export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
|
|
326
|
+
const ctx = requestContextStorage.getStore() as
|
|
327
|
+
| RequestContext<TEnv>
|
|
328
|
+
| undefined;
|
|
329
|
+
invariant(
|
|
330
|
+
ctx,
|
|
331
|
+
"getRequestContext() called outside of a request context. " +
|
|
332
|
+
"This function must be called from within a route handler, loader, middleware, " +
|
|
333
|
+
"server action, or server component.",
|
|
334
|
+
);
|
|
335
|
+
return ctx;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @internal Get the request context without throwing — for internal code that
|
|
340
|
+
* may run outside a request context (cache stores, optional handle lookups, etc.)
|
|
341
|
+
*/
|
|
342
|
+
export function _getRequestContext<TEnv = DefaultEnv>():
|
|
201
343
|
| RequestContext<TEnv>
|
|
202
344
|
| undefined {
|
|
203
345
|
return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
|
|
@@ -205,28 +347,67 @@ export function getRequestContext<TEnv = unknown>():
|
|
|
205
347
|
|
|
206
348
|
/**
|
|
207
349
|
* Update params on the current request context
|
|
208
|
-
* Called after route matching to populate route params
|
|
350
|
+
* Called after route matching to populate route params and route name
|
|
209
351
|
*/
|
|
210
|
-
export function setRequestContextParams(
|
|
352
|
+
export function setRequestContextParams(
|
|
353
|
+
params: Record<string, string>,
|
|
354
|
+
routeName?: string,
|
|
355
|
+
): void {
|
|
211
356
|
const ctx = requestContextStorage.getStore();
|
|
212
357
|
if (ctx) {
|
|
213
358
|
ctx.params = params;
|
|
359
|
+
if (routeName !== undefined) {
|
|
360
|
+
ctx._routeName = routeName;
|
|
361
|
+
ctx.routeName = (
|
|
362
|
+
routeName && !isAutoGeneratedRouteName(routeName)
|
|
363
|
+
? routeName
|
|
364
|
+
: undefined
|
|
365
|
+
) as DefaultRouteName | undefined;
|
|
366
|
+
}
|
|
367
|
+
// Update reverse with scoped resolution now that route is known
|
|
368
|
+
ctx.reverse = createReverseFunction(
|
|
369
|
+
getGlobalRouteMap(),
|
|
370
|
+
routeName,
|
|
371
|
+
params,
|
|
372
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
373
|
+
);
|
|
214
374
|
}
|
|
215
375
|
}
|
|
216
376
|
|
|
217
377
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
378
|
+
* Store the previous route key on the request context.
|
|
379
|
+
* Called during partial-match context creation to make the navigation source
|
|
380
|
+
* route key available for revalidation and intercept evaluation.
|
|
381
|
+
* @internal
|
|
220
382
|
*/
|
|
221
|
-
export function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
383
|
+
export function setRequestContextPrevRouteKey(
|
|
384
|
+
prevRouteKey: string | undefined,
|
|
385
|
+
): void {
|
|
386
|
+
const ctx = requestContextStorage.getStore();
|
|
387
|
+
if (ctx && prevRouteKey !== undefined) {
|
|
388
|
+
ctx._prevRouteKey = prevRouteKey;
|
|
228
389
|
}
|
|
229
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get accumulated location state entries from the current request context.
|
|
394
|
+
* Returns undefined if no state has been set.
|
|
395
|
+
*
|
|
396
|
+
* @internal Used by the RSC handler to include state in payload metadata.
|
|
397
|
+
*/
|
|
398
|
+
export function getLocationState(): LocationStateEntry[] | undefined {
|
|
399
|
+
const ctx = getRequestContext();
|
|
400
|
+
return ctx?._locationState;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get the current request context, throwing if not available
|
|
405
|
+
* @deprecated Use getRequestContext() directly — it now throws if outside context
|
|
406
|
+
*/
|
|
407
|
+
export function requireRequestContext<
|
|
408
|
+
TEnv = DefaultEnv,
|
|
409
|
+
>(): RequestContext<TEnv> {
|
|
410
|
+
return getRequestContext<TEnv>();
|
|
230
411
|
}
|
|
231
412
|
|
|
232
413
|
/**
|
|
@@ -245,8 +426,15 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
245
426
|
request: Request;
|
|
246
427
|
url: URL;
|
|
247
428
|
variables: Record<string, any>;
|
|
429
|
+
/** Optional initial response stub headers/status to seed effective cookie reads */
|
|
430
|
+
initialResponse?: Response;
|
|
248
431
|
/** Optional cache store for segment caching (used by CacheScope) */
|
|
249
432
|
cacheStore?: SegmentCacheStore;
|
|
433
|
+
/** Optional cache profiles for "use cache" resolution (per-router) */
|
|
434
|
+
cacheProfiles?: Record<
|
|
435
|
+
string,
|
|
436
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
437
|
+
>;
|
|
250
438
|
/** Optional Cloudflare execution context for waitUntil support */
|
|
251
439
|
executionContext?: ExecutionContext;
|
|
252
440
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
@@ -262,20 +450,37 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
262
450
|
* - Passed to handlers as ctx
|
|
263
451
|
*/
|
|
264
452
|
export function createRequestContext<TEnv>(
|
|
265
|
-
options: CreateRequestContextOptions<TEnv
|
|
453
|
+
options: CreateRequestContextOptions<TEnv>,
|
|
266
454
|
): RequestContext<TEnv> {
|
|
267
|
-
const {
|
|
455
|
+
const {
|
|
456
|
+
env,
|
|
457
|
+
request,
|
|
458
|
+
url,
|
|
459
|
+
variables,
|
|
460
|
+
initialResponse,
|
|
461
|
+
cacheStore,
|
|
462
|
+
cacheProfiles,
|
|
463
|
+
executionContext,
|
|
464
|
+
themeConfig,
|
|
465
|
+
} = options;
|
|
268
466
|
const cookieHeader = request.headers.get("Cookie");
|
|
269
467
|
let parsedCookies: Record<string, string> | null = null;
|
|
270
468
|
|
|
271
|
-
// Create stub response for collecting headers/cookies
|
|
272
|
-
|
|
469
|
+
// Create stub response for collecting headers/cookies.
|
|
470
|
+
// All cookie/header mutations go here; cookie reads derive from it.
|
|
471
|
+
let stubResponse = initialResponse
|
|
472
|
+
? new Response(null, {
|
|
473
|
+
status: initialResponse.status,
|
|
474
|
+
statusText: initialResponse.statusText,
|
|
475
|
+
headers: new Headers(initialResponse.headers),
|
|
476
|
+
})
|
|
477
|
+
: new Response(null, { status: 200 });
|
|
273
478
|
|
|
274
479
|
// Create handle store and loader memoization for this request
|
|
275
480
|
const handleStore = createHandleStore();
|
|
276
481
|
const loaderPromises = new Map<string, Promise<any>>();
|
|
277
482
|
|
|
278
|
-
// Lazy parse cookies
|
|
483
|
+
// Lazy parse cookies from the original Cookie header
|
|
279
484
|
const getParsedCookies = (): Record<string, string> => {
|
|
280
485
|
if (!parsedCookies) {
|
|
281
486
|
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
@@ -283,11 +488,35 @@ export function createRequestContext<TEnv>(
|
|
|
283
488
|
return parsedCookies;
|
|
284
489
|
};
|
|
285
490
|
|
|
491
|
+
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
492
|
+
let responseCookieCache: Map<string, string | null> | null = null;
|
|
493
|
+
const getResponseCookies = (): Map<string, string | null> => {
|
|
494
|
+
if (!responseCookieCache) {
|
|
495
|
+
responseCookieCache = parseResponseCookies(stubResponse);
|
|
496
|
+
}
|
|
497
|
+
return responseCookieCache;
|
|
498
|
+
};
|
|
499
|
+
const invalidateResponseCookieCache = () => {
|
|
500
|
+
responseCookieCache = null;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
504
|
+
// The stub IS the source of truth for same-request mutations.
|
|
505
|
+
const effectiveCookie = (name: string): string | undefined => {
|
|
506
|
+
const mutations = getResponseCookies();
|
|
507
|
+
if (mutations.has(name)) {
|
|
508
|
+
const v = mutations.get(name);
|
|
509
|
+
return v === null ? undefined : v;
|
|
510
|
+
}
|
|
511
|
+
return getParsedCookies()[name];
|
|
512
|
+
};
|
|
513
|
+
|
|
286
514
|
// Theme helpers (only used when themeConfig is provided)
|
|
287
515
|
const getTheme = (): Theme | undefined => {
|
|
288
516
|
if (!themeConfig) return undefined;
|
|
289
517
|
|
|
290
|
-
|
|
518
|
+
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
519
|
+
const stored = effectiveCookie(themeConfig.storageKey);
|
|
291
520
|
if (stored) {
|
|
292
521
|
// Validate stored value
|
|
293
522
|
if (stored === "system" && themeConfig.enableSystem) {
|
|
@@ -305,19 +534,22 @@ export function createRequestContext<TEnv>(
|
|
|
305
534
|
|
|
306
535
|
// Validate theme value
|
|
307
536
|
if (theme !== "system" && !themeConfig.themes.includes(theme)) {
|
|
308
|
-
console.warn(
|
|
537
|
+
console.warn(
|
|
538
|
+
`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
|
|
539
|
+
);
|
|
309
540
|
return;
|
|
310
541
|
}
|
|
311
542
|
|
|
312
|
-
//
|
|
543
|
+
// Write to stub — effectiveCookie() will pick it up on next read
|
|
313
544
|
stubResponse.headers.append(
|
|
314
545
|
"Set-Cookie",
|
|
315
546
|
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
316
547
|
path: THEME_COOKIE.path,
|
|
317
548
|
maxAge: THEME_COOKIE.maxAge,
|
|
318
549
|
sameSite: THEME_COOKIE.sameSite,
|
|
319
|
-
})
|
|
550
|
+
}),
|
|
320
551
|
);
|
|
552
|
+
invalidateResponseCookieCache();
|
|
321
553
|
};
|
|
322
554
|
|
|
323
555
|
// Build the context object first (without use), then add use
|
|
@@ -325,45 +557,86 @@ export function createRequestContext<TEnv>(
|
|
|
325
557
|
env,
|
|
326
558
|
request,
|
|
327
559
|
url,
|
|
560
|
+
originalUrl: new URL(request.url),
|
|
328
561
|
pathname: url.pathname,
|
|
329
562
|
searchParams: url.searchParams,
|
|
330
563
|
var: variables,
|
|
331
|
-
get:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
564
|
+
get: ((keyOrVar: any) =>
|
|
565
|
+
contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
|
|
566
|
+
set: ((keyOrVar: any, value: any) => {
|
|
567
|
+
assertNotInsideCacheExec(ctx, "set");
|
|
568
|
+
contextSet(variables, keyOrVar, value);
|
|
569
|
+
}) as RequestContext<TEnv>["set"],
|
|
335
570
|
params: {} as Record<string, string>,
|
|
336
|
-
|
|
571
|
+
|
|
572
|
+
get res(): Response {
|
|
573
|
+
return stubResponse;
|
|
574
|
+
},
|
|
575
|
+
set res(_: Response) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
578
|
+
);
|
|
579
|
+
},
|
|
337
580
|
|
|
338
581
|
cookie(name: string): string | undefined {
|
|
339
|
-
return
|
|
582
|
+
return effectiveCookie(name);
|
|
340
583
|
},
|
|
341
584
|
|
|
342
585
|
cookies(): Record<string, string> {
|
|
343
|
-
|
|
586
|
+
const parsed = getParsedCookies();
|
|
587
|
+
const mutations = getResponseCookies();
|
|
588
|
+
if (mutations.size === 0) return { ...parsed };
|
|
589
|
+
// Build result without delete (avoids V8 dictionary-mode de-opt)
|
|
590
|
+
const deleted = new Set<string>();
|
|
591
|
+
for (const [k, v] of mutations) {
|
|
592
|
+
if (v === null) deleted.add(k);
|
|
593
|
+
}
|
|
594
|
+
const result: Record<string, string> = {};
|
|
595
|
+
for (const key of Object.keys(parsed)) {
|
|
596
|
+
if (!deleted.has(key)) result[key] = parsed[key];
|
|
597
|
+
}
|
|
598
|
+
for (const [k, v] of mutations) {
|
|
599
|
+
if (v !== null) result[k] = v;
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
344
602
|
},
|
|
345
603
|
|
|
346
604
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
605
|
+
assertNotInsideCacheExec(ctx, "setCookie");
|
|
347
606
|
stubResponse.headers.append(
|
|
348
607
|
"Set-Cookie",
|
|
349
|
-
serializeCookieValue(name, value, options)
|
|
608
|
+
serializeCookieValue(name, value, options),
|
|
350
609
|
);
|
|
610
|
+
invalidateResponseCookieCache();
|
|
351
611
|
},
|
|
352
612
|
|
|
353
613
|
deleteCookie(
|
|
354
614
|
name: string,
|
|
355
|
-
options?: Pick<CookieOptions, "domain" | "path"
|
|
615
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
356
616
|
): void {
|
|
617
|
+
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
357
618
|
stubResponse.headers.append(
|
|
358
619
|
"Set-Cookie",
|
|
359
|
-
serializeCookieValue(name, "", { ...options, maxAge: 0 })
|
|
620
|
+
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
360
621
|
);
|
|
622
|
+
invalidateResponseCookieCache();
|
|
361
623
|
},
|
|
362
624
|
|
|
363
625
|
header(name: string, value: string): void {
|
|
626
|
+
assertNotInsideCacheExec(ctx, "header");
|
|
364
627
|
stubResponse.headers.set(name, value);
|
|
365
628
|
},
|
|
366
629
|
|
|
630
|
+
setStatus(status: number): void {
|
|
631
|
+
assertNotInsideCacheExec(ctx, "setStatus");
|
|
632
|
+
// Response.status is read-only, so we must create a new Response.
|
|
633
|
+
// Headers are passed by reference — no cookie cache invalidation needed.
|
|
634
|
+
stubResponse = new Response(null, {
|
|
635
|
+
status,
|
|
636
|
+
headers: stubResponse.headers,
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
|
|
367
640
|
// Placeholder - will be replaced below
|
|
368
641
|
use: null as any,
|
|
369
642
|
|
|
@@ -371,6 +644,7 @@ export function createRequestContext<TEnv>(
|
|
|
371
644
|
|
|
372
645
|
_handleStore: handleStore,
|
|
373
646
|
_cacheStore: cacheStore,
|
|
647
|
+
_cacheProfiles: cacheProfiles,
|
|
374
648
|
|
|
375
649
|
waitUntil(fn: () => Promise<void>): void {
|
|
376
650
|
if (executionContext?.waitUntil) {
|
|
@@ -378,20 +652,44 @@ export function createRequestContext<TEnv>(
|
|
|
378
652
|
executionContext.waitUntil(fn());
|
|
379
653
|
} else {
|
|
380
654
|
// Node.js / dev: fire-and-forget with error logging
|
|
381
|
-
fn().catch((err) =>
|
|
655
|
+
fn().catch((err) =>
|
|
656
|
+
console.error("[waitUntil] Background task failed:", err),
|
|
657
|
+
);
|
|
382
658
|
}
|
|
383
659
|
},
|
|
384
660
|
|
|
385
661
|
_onResponseCallbacks: [],
|
|
386
662
|
|
|
387
663
|
onResponse(callback: (response: Response) => Response): void {
|
|
664
|
+
assertNotInsideCacheExec(ctx, "onResponse");
|
|
388
665
|
this._onResponseCallbacks.push(callback);
|
|
389
666
|
},
|
|
390
667
|
|
|
391
668
|
// Theme properties (only set when themeConfig is provided)
|
|
392
|
-
theme
|
|
393
|
-
|
|
669
|
+
get theme() {
|
|
670
|
+
return themeConfig ? getTheme() : undefined;
|
|
671
|
+
},
|
|
672
|
+
setTheme: themeConfig
|
|
673
|
+
? (theme: Theme) => {
|
|
674
|
+
assertNotInsideCacheExec(ctx, "setTheme");
|
|
675
|
+
setTheme(theme);
|
|
676
|
+
}
|
|
677
|
+
: undefined,
|
|
394
678
|
_themeConfig: themeConfig,
|
|
679
|
+
|
|
680
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
|
|
681
|
+
assertNotInsideCacheExec(ctx, "setLocationState");
|
|
682
|
+
const arr = Array.isArray(entries) ? entries : [entries];
|
|
683
|
+
this._locationState = this._locationState
|
|
684
|
+
? [...this._locationState, ...arr]
|
|
685
|
+
: arr;
|
|
686
|
+
},
|
|
687
|
+
_locationState: undefined,
|
|
688
|
+
|
|
689
|
+
_reportedErrors: new WeakSet<object>(),
|
|
690
|
+
_metricsStore: undefined,
|
|
691
|
+
|
|
692
|
+
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
395
693
|
};
|
|
396
694
|
|
|
397
695
|
// Now create use() with access to ctx
|
|
@@ -401,14 +699,53 @@ export function createRequestContext<TEnv>(
|
|
|
401
699
|
getContext: () => ctx,
|
|
402
700
|
});
|
|
403
701
|
|
|
702
|
+
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
703
|
+
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
404
704
|
return ctx;
|
|
405
705
|
}
|
|
406
706
|
|
|
707
|
+
/**
|
|
708
|
+
* Parse Set-Cookie headers from a response into effective cookie state.
|
|
709
|
+
* Returns a map of cookie name -> value (string) or name -> null (deleted).
|
|
710
|
+
* Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
|
|
711
|
+
* Max-Age=0 is treated as a delete.
|
|
712
|
+
*/
|
|
713
|
+
const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
|
|
714
|
+
|
|
715
|
+
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
716
|
+
const result = new Map<string, string | null>();
|
|
717
|
+
const setCookies = response.headers.getSetCookie();
|
|
718
|
+
|
|
719
|
+
for (const header of setCookies) {
|
|
720
|
+
// First segment before ';' is the name=value pair
|
|
721
|
+
const semiIdx = header.indexOf(";");
|
|
722
|
+
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
723
|
+
const eqIdx = pair.indexOf("=");
|
|
724
|
+
if (eqIdx === -1) continue;
|
|
725
|
+
|
|
726
|
+
let name: string;
|
|
727
|
+
let value: string;
|
|
728
|
+
try {
|
|
729
|
+
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
730
|
+
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
731
|
+
} catch {
|
|
732
|
+
// Malformed encoding — skip this entry
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Max-Age=0 means the cookie is being deleted
|
|
737
|
+
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
738
|
+
result.set(name, isDeleted ? null : value);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
|
|
407
744
|
/**
|
|
408
745
|
* Parse cookies from Cookie header
|
|
409
746
|
*/
|
|
410
747
|
function parseCookiesFromHeader(
|
|
411
|
-
cookieHeader: string | null
|
|
748
|
+
cookieHeader: string | null,
|
|
412
749
|
): Record<string, string> {
|
|
413
750
|
if (!cookieHeader) return {};
|
|
414
751
|
|
|
@@ -418,7 +755,13 @@ function parseCookiesFromHeader(
|
|
|
418
755
|
for (const pair of pairs) {
|
|
419
756
|
const [name, ...rest] = pair.trim().split("=");
|
|
420
757
|
if (name) {
|
|
421
|
-
|
|
758
|
+
const raw = rest.join("=");
|
|
759
|
+
try {
|
|
760
|
+
cookies[name] = decodeURIComponent(raw);
|
|
761
|
+
} catch {
|
|
762
|
+
// Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
|
|
763
|
+
cookies[name] = raw;
|
|
764
|
+
}
|
|
422
765
|
}
|
|
423
766
|
}
|
|
424
767
|
|
|
@@ -431,7 +774,7 @@ function parseCookiesFromHeader(
|
|
|
431
774
|
function serializeCookieValue(
|
|
432
775
|
name: string,
|
|
433
776
|
value: string,
|
|
434
|
-
options: CookieOptions = {}
|
|
777
|
+
options: CookieOptions = {},
|
|
435
778
|
): string {
|
|
436
779
|
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
437
780
|
|
|
@@ -463,7 +806,7 @@ export interface CreateUseFunctionOptions<TEnv> {
|
|
|
463
806
|
* - For handles: returns a push function to add handle data
|
|
464
807
|
*/
|
|
465
808
|
export function createUseFunction<TEnv>(
|
|
466
|
-
options: CreateUseFunctionOptions<TEnv
|
|
809
|
+
options: CreateUseFunctionOptions<TEnv>,
|
|
467
810
|
): RequestContext["use"] {
|
|
468
811
|
const { handleStore, loaderPromises, getContext } = options;
|
|
469
812
|
|
|
@@ -477,16 +820,19 @@ export function createUseFunction<TEnv>(
|
|
|
477
820
|
if (!segmentId) {
|
|
478
821
|
throw new Error(
|
|
479
822
|
`Handle "${handle.$$id}" used outside of handler context. ` +
|
|
480
|
-
`Handles must be used within route/layout handlers
|
|
823
|
+
`Handles must be used within route/layout handlers.`,
|
|
481
824
|
);
|
|
482
825
|
}
|
|
483
826
|
|
|
484
827
|
// Return a push function bound to this handle and segment
|
|
485
|
-
return (
|
|
828
|
+
return (
|
|
829
|
+
dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
|
|
830
|
+
) => {
|
|
486
831
|
// If it's a function, call it immediately to get the promise
|
|
487
|
-
const valueOrPromise =
|
|
488
|
-
|
|
489
|
-
|
|
832
|
+
const valueOrPromise =
|
|
833
|
+
typeof dataOrFn === "function"
|
|
834
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
835
|
+
: dataOrFn;
|
|
490
836
|
|
|
491
837
|
// Push directly - promises will be serialized by RSC and streamed
|
|
492
838
|
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
@@ -504,8 +850,6 @@ export function createUseFunction<TEnv>(
|
|
|
504
850
|
// Get loader function - either from loader object or fetchable registry
|
|
505
851
|
let loaderFn = loader.fn;
|
|
506
852
|
if (!loaderFn) {
|
|
507
|
-
// Lazy import to avoid circular dependency
|
|
508
|
-
const { getFetchableLoader } = require("../loader.rsc.js");
|
|
509
853
|
const fetchable = getFetchableLoader(loader.$$id);
|
|
510
854
|
if (fetchable) {
|
|
511
855
|
loaderFn = fetchable.fn;
|
|
@@ -514,7 +858,7 @@ export function createUseFunction<TEnv>(
|
|
|
514
858
|
|
|
515
859
|
if (!loaderFn) {
|
|
516
860
|
throw new Error(
|
|
517
|
-
`Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build
|
|
861
|
+
`Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`,
|
|
518
862
|
);
|
|
519
863
|
}
|
|
520
864
|
|
|
@@ -523,25 +867,33 @@ export function createUseFunction<TEnv>(
|
|
|
523
867
|
// Create loader context with recursive use() support
|
|
524
868
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
525
869
|
params: ctx.params,
|
|
870
|
+
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
526
871
|
request: ctx.request,
|
|
527
872
|
searchParams: ctx.searchParams,
|
|
873
|
+
search: (ctx as any).search ?? {},
|
|
528
874
|
pathname: ctx.pathname,
|
|
529
875
|
url: ctx.url,
|
|
530
876
|
env: ctx.env as any,
|
|
531
877
|
var: ctx.var as any,
|
|
532
878
|
get: ctx.get as any,
|
|
533
879
|
use: <TDep, TDepParams = any>(
|
|
534
|
-
dep: LoaderDefinition<TDep, TDepParams
|
|
880
|
+
dep: LoaderDefinition<TDep, TDepParams>,
|
|
535
881
|
): Promise<TDep> => {
|
|
536
882
|
// Recursive call - will start dep loader if not already started
|
|
537
883
|
return ctx.use(dep);
|
|
538
884
|
},
|
|
539
885
|
method: "GET",
|
|
540
886
|
body: undefined,
|
|
887
|
+
reverse: createReverseFunction(
|
|
888
|
+
getGlobalRouteMap(),
|
|
889
|
+
ctx._routeName,
|
|
890
|
+
ctx.params as Record<string, string>,
|
|
891
|
+
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
892
|
+
),
|
|
541
893
|
};
|
|
542
894
|
|
|
543
895
|
// Start loader execution with tracking
|
|
544
|
-
const doneLoader = track(`loader:${loader.$$id}
|
|
896
|
+
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
545
897
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
546
898
|
doneLoader();
|
|
547
899
|
});
|