@rangojs/router 0.0.0-experimental.0f44aca1
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 +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- 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 +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- 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 +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- 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 +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- 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 +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- 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 +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- 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 +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- 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 +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- 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 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- 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 +1239 -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 +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -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 +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- 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 -0
- 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 -0
- package/src/use-loader.tsx +354 -0
- 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 +16 -0
- 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/plugins/expose-action-id.ts +365 -0
- 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/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- 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/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Context - AsyncLocalStorage for passing request-scoped data throughout rendering
|
|
3
|
+
*
|
|
4
|
+
* This is the unified context used everywhere:
|
|
5
|
+
* - Middleware execution
|
|
6
|
+
* - Route handlers and loaders
|
|
7
|
+
* - Server components during rendering
|
|
8
|
+
* - Error boundaries and streaming
|
|
9
|
+
*
|
|
10
|
+
* Available via getRequestContext() anywhere in the request lifecycle.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import type { CookieOptions } from "../router/middleware.js";
|
|
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";
|
|
22
|
+
import type { Handle } from "../handle.js";
|
|
23
|
+
import { type ContextVar, contextGet, contextSet } from "../context-var.js";
|
|
24
|
+
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
25
|
+
import { isHandle } from "../handle.js";
|
|
26
|
+
import { track, type MetricsStore } from "./context.js";
|
|
27
|
+
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
28
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
29
|
+
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
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";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unified request context available via getRequestContext()
|
|
40
|
+
*
|
|
41
|
+
* This is the same context passed to middleware and handlers.
|
|
42
|
+
* Use this when you need access to request data outside of route handlers.
|
|
43
|
+
*/
|
|
44
|
+
export interface RequestContext<
|
|
45
|
+
TEnv = DefaultEnv,
|
|
46
|
+
TParams = Record<string, string>,
|
|
47
|
+
> {
|
|
48
|
+
/** Platform bindings (Cloudflare env, etc.) */
|
|
49
|
+
env: TEnv;
|
|
50
|
+
/** Original HTTP request */
|
|
51
|
+
request: Request;
|
|
52
|
+
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
53
|
+
url: URL;
|
|
54
|
+
/**
|
|
55
|
+
* The original request URL with all parameters intact, including
|
|
56
|
+
* internal `_rsc*` transport params.
|
|
57
|
+
*/
|
|
58
|
+
originalUrl: URL;
|
|
59
|
+
/** URL pathname */
|
|
60
|
+
pathname: string;
|
|
61
|
+
/** URL search params (system params like _rsc* are NOT filtered here) */
|
|
62
|
+
searchParams: URLSearchParams;
|
|
63
|
+
/** Variables set by middleware (same as ctx.var) */
|
|
64
|
+
var: Record<string, any>;
|
|
65
|
+
/** Get a variable set by middleware */
|
|
66
|
+
get: {
|
|
67
|
+
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
68
|
+
<K extends string>(key: K): any;
|
|
69
|
+
};
|
|
70
|
+
/** Set a variable (shared with middleware and handlers) */
|
|
71
|
+
set: {
|
|
72
|
+
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
73
|
+
<K extends string>(key: K, value: any): void;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Route params (populated after route matching)
|
|
77
|
+
* Initially empty, then set to matched params
|
|
78
|
+
*/
|
|
79
|
+
params: TParams;
|
|
80
|
+
/** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
|
|
81
|
+
readonly res: Response;
|
|
82
|
+
|
|
83
|
+
/** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
|
|
84
|
+
cookie(name: string): string | undefined;
|
|
85
|
+
/** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
|
|
86
|
+
cookies(): Record<string, string>;
|
|
87
|
+
/** @internal Set a cookie on the response. Use cookies().set() instead. */
|
|
88
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
89
|
+
/** @internal Delete a cookie. Use cookies().delete() instead. */
|
|
90
|
+
deleteCookie(
|
|
91
|
+
name: string,
|
|
92
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
93
|
+
): void;
|
|
94
|
+
/** Set a response header */
|
|
95
|
+
header(name: string, value: string): void;
|
|
96
|
+
/** Set the response status code */
|
|
97
|
+
setStatus(status: number): void;
|
|
98
|
+
/** @internal Set status bypassing cache-exec guard (for framework error handling) */
|
|
99
|
+
_setStatus(status: number): void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Access loader data or push handle data.
|
|
103
|
+
*
|
|
104
|
+
* For loaders: Returns a promise that resolves to the loader data.
|
|
105
|
+
* Loaders are executed in parallel and memoized per request.
|
|
106
|
+
*
|
|
107
|
+
* For handles: Returns a push function to add data for this segment.
|
|
108
|
+
* Handle data accumulates across all matched route segments.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* // Loader usage
|
|
113
|
+
* const cart = await ctx.use(CartLoader);
|
|
114
|
+
*
|
|
115
|
+
* // Handle usage
|
|
116
|
+
* const push = ctx.use(Breadcrumbs);
|
|
117
|
+
* push({ label: "Shop", href: "/shop" });
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
use: {
|
|
121
|
+
<T, TLoaderParams = any>(
|
|
122
|
+
loader: LoaderDefinition<T, TLoaderParams>,
|
|
123
|
+
): Promise<T>;
|
|
124
|
+
<TData, TAccumulated = TData[]>(
|
|
125
|
+
handle: Handle<TData, TAccumulated>,
|
|
126
|
+
): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
|
|
130
|
+
method: string;
|
|
131
|
+
|
|
132
|
+
/** @internal Handle store for tracking handle data across segments */
|
|
133
|
+
_handleStore: HandleStore;
|
|
134
|
+
|
|
135
|
+
/** @internal Cache store for segment caching (optional, used by CacheScope) */
|
|
136
|
+
_cacheStore?: SegmentCacheStore;
|
|
137
|
+
|
|
138
|
+
/** @internal Cache profiles for "use cache" profile resolution (per-router) */
|
|
139
|
+
_cacheProfiles?: Record<
|
|
140
|
+
string,
|
|
141
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
142
|
+
>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Schedule work to run after the response is sent.
|
|
146
|
+
* On Cloudflare Workers, uses ctx.waitUntil().
|
|
147
|
+
* On Node.js, runs as fire-and-forget.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* ctx.waitUntil(async () => {
|
|
152
|
+
* await cacheStore.set(key, data, ttl);
|
|
153
|
+
* });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
waitUntil(fn: () => Promise<void>): void;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a callback to run when the response is created.
|
|
160
|
+
* Callbacks are sync and receive the response. They can:
|
|
161
|
+
* - Inspect response status/headers
|
|
162
|
+
* - Return a modified response
|
|
163
|
+
* - Schedule async work via waitUntil
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* ctx.onResponse((res) => {
|
|
168
|
+
* if (res.status === 200) {
|
|
169
|
+
* ctx.waitUntil(async () => await cacheIt());
|
|
170
|
+
* }
|
|
171
|
+
* return res;
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
onResponse(callback: (response: Response) => Response): void;
|
|
176
|
+
|
|
177
|
+
/** @internal Registered onResponse callbacks */
|
|
178
|
+
_onResponseCallbacks: Array<(response: Response) => Response>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Current theme setting (only available when theme is enabled in router config)
|
|
182
|
+
*
|
|
183
|
+
* Returns the theme value from the cookie, or the default theme if not set.
|
|
184
|
+
* This is the user's preference ("light", "dark", or "system"), not the resolved value.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* route("settings", (ctx) => {
|
|
189
|
+
* const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
|
|
190
|
+
* return <SettingsPage theme={currentTheme} />;
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
theme?: Theme;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Set the theme (only available when theme is enabled in router config)
|
|
198
|
+
*
|
|
199
|
+
* Sets a cookie with the new theme value. The change takes effect on the next request.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* route("settings", (ctx) => {
|
|
204
|
+
* if (ctx.method === "POST") {
|
|
205
|
+
* const formData = await ctx.request.formData();
|
|
206
|
+
* const newTheme = formData.get("theme") as Theme;
|
|
207
|
+
* ctx.setTheme(newTheme);
|
|
208
|
+
* }
|
|
209
|
+
* return <SettingsPage />;
|
|
210
|
+
* });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
setTheme?: (theme: Theme) => void;
|
|
214
|
+
|
|
215
|
+
/** @internal Theme configuration (null if theme not enabled) */
|
|
216
|
+
_themeConfig?: ResolvedThemeConfig | null;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Attach location state entries to the current response.
|
|
220
|
+
*
|
|
221
|
+
* For partial (SPA) requests, the state is included in the RSC payload
|
|
222
|
+
* metadata and merged into history.pushState on the client. For redirect
|
|
223
|
+
* responses, the state travels through the redirect payload so the target
|
|
224
|
+
* page can read it via useLocationState.
|
|
225
|
+
*
|
|
226
|
+
* Multiple calls accumulate entries.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* ctx.setLocationState(Flash({ text: "Item saved!" }));
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
234
|
+
|
|
235
|
+
/** @internal Accumulated location state entries */
|
|
236
|
+
_locationState?: LocationStateEntry[];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The matched route name, if the route has an explicit name.
|
|
240
|
+
* Undefined before route matching or for unnamed routes.
|
|
241
|
+
* Includes the namespace prefix from include() (e.g., "blog.post").
|
|
242
|
+
*/
|
|
243
|
+
routeName?: DefaultRouteName;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate URLs from route names.
|
|
247
|
+
* Uses the global route map. After route matching, scoped (`.name`) resolution
|
|
248
|
+
* works within the matched include() scope.
|
|
249
|
+
*/
|
|
250
|
+
reverse: ScopedReverseFunction<
|
|
251
|
+
Record<string, string>,
|
|
252
|
+
DefaultReverseRouteMap
|
|
253
|
+
>;
|
|
254
|
+
|
|
255
|
+
/** @internal Route name from route matching, used for scoped reverse resolution */
|
|
256
|
+
_routeName?: string;
|
|
257
|
+
|
|
258
|
+
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
259
|
+
_prevRouteKey?: string;
|
|
260
|
+
|
|
261
|
+
/** @internal Per-request error dedup set for onError reporting */
|
|
262
|
+
_reportedErrors: WeakSet<object>;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @internal Report a non-fatal background error through the router's
|
|
266
|
+
* onError callback. Wired by the RSC handler / router during request
|
|
267
|
+
* creation. Cache-runtime and other subsystems call this to surface
|
|
268
|
+
* errors without failing the response.
|
|
269
|
+
*/
|
|
270
|
+
_reportBackgroundError?: (error: unknown, category: string) => void;
|
|
271
|
+
|
|
272
|
+
/** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
|
|
273
|
+
_debugPerformance?: boolean;
|
|
274
|
+
|
|
275
|
+
/** @internal Request-scoped performance metrics store */
|
|
276
|
+
_metricsStore?: MetricsStore;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Public view of RequestContext, without internal methods and fields.
|
|
281
|
+
*
|
|
282
|
+
* This is the type exported to library consumers. Internal code should
|
|
283
|
+
* use the full RequestContext interface directly.
|
|
284
|
+
*/
|
|
285
|
+
export type PublicRequestContext<
|
|
286
|
+
TEnv = DefaultEnv,
|
|
287
|
+
TParams = Record<string, string>,
|
|
288
|
+
> = Omit<
|
|
289
|
+
RequestContext<TEnv, TParams>,
|
|
290
|
+
| "cookie"
|
|
291
|
+
| "cookies"
|
|
292
|
+
| "setCookie"
|
|
293
|
+
| "deleteCookie"
|
|
294
|
+
| "_handleStore"
|
|
295
|
+
| "_cacheStore"
|
|
296
|
+
| "_cacheProfiles"
|
|
297
|
+
| "_onResponseCallbacks"
|
|
298
|
+
| "_themeConfig"
|
|
299
|
+
| "_locationState"
|
|
300
|
+
| "_routeName"
|
|
301
|
+
| "_prevRouteKey"
|
|
302
|
+
| "_reportedErrors"
|
|
303
|
+
| "_reportBackgroundError"
|
|
304
|
+
| "_debugPerformance"
|
|
305
|
+
| "_metricsStore"
|
|
306
|
+
| "_setStatus"
|
|
307
|
+
| "res"
|
|
308
|
+
>;
|
|
309
|
+
|
|
310
|
+
// AsyncLocalStorage instance for request context
|
|
311
|
+
const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Run a function within a request context
|
|
315
|
+
* Used by the RSC handler to provide context to server actions
|
|
316
|
+
*/
|
|
317
|
+
export function runWithRequestContext<TEnv, T>(
|
|
318
|
+
context: RequestContext<TEnv>,
|
|
319
|
+
fn: () => T,
|
|
320
|
+
): T {
|
|
321
|
+
return requestContextStorage.run(context, fn);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the current request context
|
|
326
|
+
* Throws if called outside of a request context
|
|
327
|
+
*/
|
|
328
|
+
export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
|
|
329
|
+
const ctx = requestContextStorage.getStore() as
|
|
330
|
+
| RequestContext<TEnv>
|
|
331
|
+
| undefined;
|
|
332
|
+
invariant(
|
|
333
|
+
ctx,
|
|
334
|
+
"getRequestContext() called outside of a request context. " +
|
|
335
|
+
"This function must be called from within a route handler, loader, middleware, " +
|
|
336
|
+
"server action, or server component.",
|
|
337
|
+
);
|
|
338
|
+
return ctx;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @internal Get the request context without throwing — for internal code that
|
|
343
|
+
* may run outside a request context (cache stores, optional handle lookups, etc.)
|
|
344
|
+
*/
|
|
345
|
+
export function _getRequestContext<TEnv = DefaultEnv>():
|
|
346
|
+
| RequestContext<TEnv>
|
|
347
|
+
| undefined {
|
|
348
|
+
return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Update params on the current request context
|
|
353
|
+
* Called after route matching to populate route params and route name
|
|
354
|
+
*/
|
|
355
|
+
export function setRequestContextParams(
|
|
356
|
+
params: Record<string, string>,
|
|
357
|
+
routeName?: string,
|
|
358
|
+
): void {
|
|
359
|
+
const ctx = requestContextStorage.getStore();
|
|
360
|
+
if (ctx) {
|
|
361
|
+
ctx.params = params;
|
|
362
|
+
if (routeName !== undefined) {
|
|
363
|
+
ctx._routeName = routeName;
|
|
364
|
+
ctx.routeName = (
|
|
365
|
+
routeName && !isAutoGeneratedRouteName(routeName)
|
|
366
|
+
? routeName
|
|
367
|
+
: undefined
|
|
368
|
+
) as DefaultRouteName | undefined;
|
|
369
|
+
}
|
|
370
|
+
// Update reverse with scoped resolution now that route is known
|
|
371
|
+
ctx.reverse = createReverseFunction(
|
|
372
|
+
getGlobalRouteMap(),
|
|
373
|
+
routeName,
|
|
374
|
+
params,
|
|
375
|
+
routeName ? isRouteRootScoped(routeName) : undefined,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Store the previous route key on the request context.
|
|
382
|
+
* Called during partial-match context creation to make the navigation source
|
|
383
|
+
* route key available for revalidation and intercept evaluation.
|
|
384
|
+
* @internal
|
|
385
|
+
*/
|
|
386
|
+
export function setRequestContextPrevRouteKey(
|
|
387
|
+
prevRouteKey: string | undefined,
|
|
388
|
+
): void {
|
|
389
|
+
const ctx = requestContextStorage.getStore();
|
|
390
|
+
if (ctx && prevRouteKey !== undefined) {
|
|
391
|
+
ctx._prevRouteKey = prevRouteKey;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get accumulated location state entries from the current request context.
|
|
397
|
+
* Returns undefined if no state has been set.
|
|
398
|
+
*
|
|
399
|
+
* @internal Used by the RSC handler to include state in payload metadata.
|
|
400
|
+
*/
|
|
401
|
+
export function getLocationState(): LocationStateEntry[] | undefined {
|
|
402
|
+
const ctx = getRequestContext();
|
|
403
|
+
return ctx?._locationState;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get the current request context, throwing if not available
|
|
408
|
+
* @deprecated Use getRequestContext() directly — it now throws if outside context
|
|
409
|
+
*/
|
|
410
|
+
export function requireRequestContext<
|
|
411
|
+
TEnv = DefaultEnv,
|
|
412
|
+
>(): RequestContext<TEnv> {
|
|
413
|
+
return getRequestContext<TEnv>();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Cloudflare Workers ExecutionContext (subset we need)
|
|
418
|
+
*/
|
|
419
|
+
export interface ExecutionContext {
|
|
420
|
+
waitUntil(promise: Promise<any>): void;
|
|
421
|
+
passThroughOnException(): void;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Options for creating a request context
|
|
426
|
+
*/
|
|
427
|
+
export interface CreateRequestContextOptions<TEnv> {
|
|
428
|
+
env: TEnv;
|
|
429
|
+
request: Request;
|
|
430
|
+
url: URL;
|
|
431
|
+
variables: Record<string, any>;
|
|
432
|
+
/** Optional initial response stub headers/status to seed effective cookie reads */
|
|
433
|
+
initialResponse?: Response;
|
|
434
|
+
/** Optional cache store for segment caching (used by CacheScope) */
|
|
435
|
+
cacheStore?: SegmentCacheStore;
|
|
436
|
+
/** Optional cache profiles for "use cache" resolution (per-router) */
|
|
437
|
+
cacheProfiles?: Record<
|
|
438
|
+
string,
|
|
439
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
440
|
+
>;
|
|
441
|
+
/** Optional Cloudflare execution context for waitUntil support */
|
|
442
|
+
executionContext?: ExecutionContext;
|
|
443
|
+
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
444
|
+
themeConfig?: ResolvedThemeConfig | null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Create a full request context with all methods implemented
|
|
449
|
+
*
|
|
450
|
+
* This is used by the RSC handler to create the unified context that's:
|
|
451
|
+
* - Available via getRequestContext() throughout the request
|
|
452
|
+
* - Passed to middleware as ctx
|
|
453
|
+
* - Passed to handlers as ctx
|
|
454
|
+
*/
|
|
455
|
+
export function createRequestContext<TEnv>(
|
|
456
|
+
options: CreateRequestContextOptions<TEnv>,
|
|
457
|
+
): RequestContext<TEnv> {
|
|
458
|
+
const {
|
|
459
|
+
env,
|
|
460
|
+
request,
|
|
461
|
+
url,
|
|
462
|
+
variables,
|
|
463
|
+
initialResponse,
|
|
464
|
+
cacheStore,
|
|
465
|
+
cacheProfiles,
|
|
466
|
+
executionContext,
|
|
467
|
+
themeConfig,
|
|
468
|
+
} = options;
|
|
469
|
+
const cookieHeader = request.headers.get("Cookie");
|
|
470
|
+
let parsedCookies: Record<string, string> | null = null;
|
|
471
|
+
|
|
472
|
+
// Create stub response for collecting headers/cookies.
|
|
473
|
+
// All cookie/header mutations go here; cookie reads derive from it.
|
|
474
|
+
let stubResponse = initialResponse
|
|
475
|
+
? new Response(null, {
|
|
476
|
+
status: initialResponse.status,
|
|
477
|
+
statusText: initialResponse.statusText,
|
|
478
|
+
headers: new Headers(initialResponse.headers),
|
|
479
|
+
})
|
|
480
|
+
: new Response(null, { status: 200 });
|
|
481
|
+
|
|
482
|
+
// Create handle store and loader memoization for this request
|
|
483
|
+
const handleStore = createHandleStore();
|
|
484
|
+
const loaderPromises = new Map<string, Promise<any>>();
|
|
485
|
+
|
|
486
|
+
// Lazy parse cookies from the original Cookie header
|
|
487
|
+
const getParsedCookies = (): Record<string, string> => {
|
|
488
|
+
if (!parsedCookies) {
|
|
489
|
+
parsedCookies = parseCookiesFromHeader(cookieHeader);
|
|
490
|
+
}
|
|
491
|
+
return parsedCookies;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
|
|
495
|
+
let responseCookieCache: Map<string, string | null> | null = null;
|
|
496
|
+
const getResponseCookies = (): Map<string, string | null> => {
|
|
497
|
+
if (!responseCookieCache) {
|
|
498
|
+
responseCookieCache = parseResponseCookies(stubResponse);
|
|
499
|
+
}
|
|
500
|
+
return responseCookieCache;
|
|
501
|
+
};
|
|
502
|
+
const invalidateResponseCookieCache = () => {
|
|
503
|
+
responseCookieCache = null;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
507
|
+
// The stub IS the source of truth for same-request mutations.
|
|
508
|
+
const effectiveCookie = (name: string): string | undefined => {
|
|
509
|
+
const mutations = getResponseCookies();
|
|
510
|
+
if (mutations.has(name)) {
|
|
511
|
+
const v = mutations.get(name);
|
|
512
|
+
return v === null ? undefined : v;
|
|
513
|
+
}
|
|
514
|
+
return getParsedCookies()[name];
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Theme helpers (only used when themeConfig is provided)
|
|
518
|
+
const getTheme = (): Theme | undefined => {
|
|
519
|
+
if (!themeConfig) return undefined;
|
|
520
|
+
|
|
521
|
+
// Use overlay-aware read so setTheme() in the same request is reflected
|
|
522
|
+
const stored = effectiveCookie(themeConfig.storageKey);
|
|
523
|
+
if (stored) {
|
|
524
|
+
// Validate stored value
|
|
525
|
+
if (stored === "system" && themeConfig.enableSystem) {
|
|
526
|
+
return "system";
|
|
527
|
+
}
|
|
528
|
+
if (themeConfig.themes.includes(stored)) {
|
|
529
|
+
return stored as Theme;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return themeConfig.defaultTheme;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const setTheme = (theme: Theme): void => {
|
|
536
|
+
if (!themeConfig) return;
|
|
537
|
+
|
|
538
|
+
// Validate theme value
|
|
539
|
+
if (theme !== "system" && !themeConfig.themes.includes(theme)) {
|
|
540
|
+
console.warn(
|
|
541
|
+
`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
|
|
542
|
+
);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Write to stub — effectiveCookie() will pick it up on next read
|
|
547
|
+
stubResponse.headers.append(
|
|
548
|
+
"Set-Cookie",
|
|
549
|
+
serializeCookieValue(themeConfig.storageKey, theme, {
|
|
550
|
+
path: THEME_COOKIE.path,
|
|
551
|
+
maxAge: THEME_COOKIE.maxAge,
|
|
552
|
+
sameSite: THEME_COOKIE.sameSite,
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
invalidateResponseCookieCache();
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// Build the context object first (without use), then add use
|
|
559
|
+
const ctx: RequestContext<TEnv> = {
|
|
560
|
+
env,
|
|
561
|
+
request,
|
|
562
|
+
url,
|
|
563
|
+
originalUrl: new URL(request.url),
|
|
564
|
+
pathname: url.pathname,
|
|
565
|
+
searchParams: url.searchParams,
|
|
566
|
+
var: variables,
|
|
567
|
+
get: ((keyOrVar: any) =>
|
|
568
|
+
contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
|
|
569
|
+
set: ((keyOrVar: any, value: any) => {
|
|
570
|
+
assertNotInsideCacheExec(ctx, "set");
|
|
571
|
+
contextSet(variables, keyOrVar, value);
|
|
572
|
+
}) as RequestContext<TEnv>["set"],
|
|
573
|
+
params: {} as Record<string, string>,
|
|
574
|
+
|
|
575
|
+
get res(): Response {
|
|
576
|
+
return stubResponse;
|
|
577
|
+
},
|
|
578
|
+
set res(_: Response) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
cookie(name: string): string | undefined {
|
|
585
|
+
return effectiveCookie(name);
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
cookies(): Record<string, string> {
|
|
589
|
+
const parsed = getParsedCookies();
|
|
590
|
+
const mutations = getResponseCookies();
|
|
591
|
+
if (mutations.size === 0) return { ...parsed };
|
|
592
|
+
// Build result without delete (avoids V8 dictionary-mode de-opt)
|
|
593
|
+
const deleted = new Set<string>();
|
|
594
|
+
for (const [k, v] of mutations) {
|
|
595
|
+
if (v === null) deleted.add(k);
|
|
596
|
+
}
|
|
597
|
+
const result: Record<string, string> = {};
|
|
598
|
+
for (const key of Object.keys(parsed)) {
|
|
599
|
+
if (!deleted.has(key)) result[key] = parsed[key];
|
|
600
|
+
}
|
|
601
|
+
for (const [k, v] of mutations) {
|
|
602
|
+
if (v !== null) result[k] = v;
|
|
603
|
+
}
|
|
604
|
+
return result;
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
608
|
+
assertNotInsideCacheExec(ctx, "setCookie");
|
|
609
|
+
stubResponse.headers.append(
|
|
610
|
+
"Set-Cookie",
|
|
611
|
+
serializeCookieValue(name, value, options),
|
|
612
|
+
);
|
|
613
|
+
invalidateResponseCookieCache();
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
deleteCookie(
|
|
617
|
+
name: string,
|
|
618
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
619
|
+
): void {
|
|
620
|
+
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
621
|
+
stubResponse.headers.append(
|
|
622
|
+
"Set-Cookie",
|
|
623
|
+
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
624
|
+
);
|
|
625
|
+
invalidateResponseCookieCache();
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
header(name: string, value: string): void {
|
|
629
|
+
assertNotInsideCacheExec(ctx, "header");
|
|
630
|
+
stubResponse.headers.set(name, value);
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
setStatus(status: number): void {
|
|
634
|
+
assertNotInsideCacheExec(ctx, "setStatus");
|
|
635
|
+
stubResponse = new Response(null, {
|
|
636
|
+
status,
|
|
637
|
+
headers: stubResponse.headers,
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
_setStatus(status: number): void {
|
|
642
|
+
stubResponse = new Response(null, {
|
|
643
|
+
status,
|
|
644
|
+
headers: stubResponse.headers,
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
// Placeholder - will be replaced below
|
|
649
|
+
use: null as any,
|
|
650
|
+
|
|
651
|
+
method: request.method,
|
|
652
|
+
|
|
653
|
+
_handleStore: handleStore,
|
|
654
|
+
_cacheStore: cacheStore,
|
|
655
|
+
_cacheProfiles: cacheProfiles,
|
|
656
|
+
|
|
657
|
+
waitUntil(fn: () => Promise<void>): void {
|
|
658
|
+
if (executionContext?.waitUntil) {
|
|
659
|
+
// Cloudflare Workers: use native waitUntil
|
|
660
|
+
executionContext.waitUntil(fn());
|
|
661
|
+
} else {
|
|
662
|
+
// Node.js / dev: fire-and-forget with error logging
|
|
663
|
+
fn().catch((err) =>
|
|
664
|
+
console.error("[waitUntil] Background task failed:", err),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
_onResponseCallbacks: [],
|
|
670
|
+
|
|
671
|
+
onResponse(callback: (response: Response) => Response): void {
|
|
672
|
+
assertNotInsideCacheExec(ctx, "onResponse");
|
|
673
|
+
this._onResponseCallbacks.push(callback);
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// Theme properties (only set when themeConfig is provided)
|
|
677
|
+
get theme() {
|
|
678
|
+
return themeConfig ? getTheme() : undefined;
|
|
679
|
+
},
|
|
680
|
+
setTheme: themeConfig
|
|
681
|
+
? (theme: Theme) => {
|
|
682
|
+
assertNotInsideCacheExec(ctx, "setTheme");
|
|
683
|
+
setTheme(theme);
|
|
684
|
+
}
|
|
685
|
+
: undefined,
|
|
686
|
+
_themeConfig: themeConfig,
|
|
687
|
+
|
|
688
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
|
|
689
|
+
assertNotInsideCacheExec(ctx, "setLocationState");
|
|
690
|
+
const arr = Array.isArray(entries) ? entries : [entries];
|
|
691
|
+
this._locationState = this._locationState
|
|
692
|
+
? [...this._locationState, ...arr]
|
|
693
|
+
: arr;
|
|
694
|
+
},
|
|
695
|
+
_locationState: undefined,
|
|
696
|
+
|
|
697
|
+
_reportedErrors: new WeakSet<object>(),
|
|
698
|
+
_metricsStore: undefined,
|
|
699
|
+
|
|
700
|
+
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Now create use() with access to ctx
|
|
704
|
+
ctx.use = createUseFunction({
|
|
705
|
+
handleStore,
|
|
706
|
+
loaderPromises,
|
|
707
|
+
getContext: () => ctx,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Brand with taint symbol so "use cache" excludes ctx from cache keys
|
|
711
|
+
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
712
|
+
return ctx;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Parse Set-Cookie headers from a response into effective cookie state.
|
|
717
|
+
* Returns a map of cookie name -> value (string) or name -> null (deleted).
|
|
718
|
+
* Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
|
|
719
|
+
* Max-Age=0 is treated as a delete.
|
|
720
|
+
*/
|
|
721
|
+
const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
|
|
722
|
+
|
|
723
|
+
function parseResponseCookies(response: Response): Map<string, string | null> {
|
|
724
|
+
const result = new Map<string, string | null>();
|
|
725
|
+
const setCookies = response.headers.getSetCookie();
|
|
726
|
+
|
|
727
|
+
for (const header of setCookies) {
|
|
728
|
+
// First segment before ';' is the name=value pair
|
|
729
|
+
const semiIdx = header.indexOf(";");
|
|
730
|
+
const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
|
|
731
|
+
const eqIdx = pair.indexOf("=");
|
|
732
|
+
if (eqIdx === -1) continue;
|
|
733
|
+
|
|
734
|
+
let name: string;
|
|
735
|
+
let value: string;
|
|
736
|
+
try {
|
|
737
|
+
name = decodeURIComponent(pair.substring(0, eqIdx).trim());
|
|
738
|
+
value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
|
|
739
|
+
} catch {
|
|
740
|
+
// Malformed encoding — skip this entry
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Max-Age=0 means the cookie is being deleted
|
|
745
|
+
const isDeleted = MAX_AGE_ZERO_RE.test(header);
|
|
746
|
+
result.set(name, isDeleted ? null : value);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Parse cookies from Cookie header
|
|
754
|
+
*/
|
|
755
|
+
function parseCookiesFromHeader(
|
|
756
|
+
cookieHeader: string | null,
|
|
757
|
+
): Record<string, string> {
|
|
758
|
+
if (!cookieHeader) return {};
|
|
759
|
+
|
|
760
|
+
const cookies: Record<string, string> = {};
|
|
761
|
+
const pairs = cookieHeader.split(";");
|
|
762
|
+
|
|
763
|
+
for (const pair of pairs) {
|
|
764
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
765
|
+
if (name) {
|
|
766
|
+
const raw = rest.join("=");
|
|
767
|
+
try {
|
|
768
|
+
cookies[name] = decodeURIComponent(raw);
|
|
769
|
+
} catch {
|
|
770
|
+
// Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
|
|
771
|
+
cookies[name] = raw;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return cookies;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Serialize a cookie for Set-Cookie header
|
|
781
|
+
*/
|
|
782
|
+
function serializeCookieValue(
|
|
783
|
+
name: string,
|
|
784
|
+
value: string,
|
|
785
|
+
options: CookieOptions = {},
|
|
786
|
+
): string {
|
|
787
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
788
|
+
|
|
789
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
790
|
+
if (options.path) cookie += `; Path=${options.path}`;
|
|
791
|
+
if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
|
|
792
|
+
if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
793
|
+
if (options.httpOnly) cookie += "; HttpOnly";
|
|
794
|
+
if (options.secure) cookie += "; Secure";
|
|
795
|
+
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
796
|
+
|
|
797
|
+
return cookie;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Options for creating the use() function
|
|
802
|
+
*/
|
|
803
|
+
export interface CreateUseFunctionOptions<TEnv> {
|
|
804
|
+
handleStore: HandleStore;
|
|
805
|
+
loaderPromises: Map<string, Promise<any>>;
|
|
806
|
+
getContext: () => RequestContext<TEnv>;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Create the use() function for loader and handle composition.
|
|
811
|
+
*
|
|
812
|
+
* This is the unified implementation used by both RequestContext and HandlerContext.
|
|
813
|
+
* - For loaders: executes and memoizes loader functions
|
|
814
|
+
* - For handles: returns a push function to add handle data
|
|
815
|
+
*/
|
|
816
|
+
export function createUseFunction<TEnv>(
|
|
817
|
+
options: CreateUseFunctionOptions<TEnv>,
|
|
818
|
+
): RequestContext["use"] {
|
|
819
|
+
const { handleStore, loaderPromises, getContext } = options;
|
|
820
|
+
|
|
821
|
+
return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
822
|
+
// Handle case: return a push function
|
|
823
|
+
if (isHandle(item)) {
|
|
824
|
+
const handle = item;
|
|
825
|
+
const ctx = getContext();
|
|
826
|
+
const segmentId = (ctx as any)._currentSegmentId;
|
|
827
|
+
|
|
828
|
+
if (!segmentId) {
|
|
829
|
+
throw new Error(
|
|
830
|
+
`Handle "${handle.$$id}" used outside of handler context. ` +
|
|
831
|
+
`Handles must be used within route/layout handlers.`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Return a push function bound to this handle and segment
|
|
836
|
+
return (
|
|
837
|
+
dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
|
|
838
|
+
) => {
|
|
839
|
+
// If it's a function, call it immediately to get the promise
|
|
840
|
+
const valueOrPromise =
|
|
841
|
+
typeof dataOrFn === "function"
|
|
842
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
843
|
+
: dataOrFn;
|
|
844
|
+
|
|
845
|
+
// Push directly - promises will be serialized by RSC and streamed
|
|
846
|
+
handleStore.push(handle.$$id, segmentId, valueOrPromise);
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Loader case
|
|
851
|
+
const loader = item as LoaderDefinition<any, any>;
|
|
852
|
+
|
|
853
|
+
// Return cached promise if already started
|
|
854
|
+
if (loaderPromises.has(loader.$$id)) {
|
|
855
|
+
return loaderPromises.get(loader.$$id);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Get loader function - either from loader object or fetchable registry
|
|
859
|
+
let loaderFn = loader.fn;
|
|
860
|
+
if (!loaderFn) {
|
|
861
|
+
const fetchable = getFetchableLoader(loader.$$id);
|
|
862
|
+
if (fetchable) {
|
|
863
|
+
loaderFn = fetchable.fn;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!loaderFn) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
`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.`,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const ctx = getContext();
|
|
874
|
+
|
|
875
|
+
// Create loader context with recursive use() support
|
|
876
|
+
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
877
|
+
params: ctx.params,
|
|
878
|
+
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
879
|
+
request: ctx.request,
|
|
880
|
+
searchParams: ctx.searchParams,
|
|
881
|
+
search: (ctx as any).search ?? {},
|
|
882
|
+
pathname: ctx.pathname,
|
|
883
|
+
url: ctx.url,
|
|
884
|
+
env: ctx.env as any,
|
|
885
|
+
var: ctx.var as any,
|
|
886
|
+
get: ctx.get as any,
|
|
887
|
+
use: <TDep, TDepParams = any>(
|
|
888
|
+
dep: LoaderDefinition<TDep, TDepParams>,
|
|
889
|
+
): Promise<TDep> => {
|
|
890
|
+
// Recursive call - will start dep loader if not already started
|
|
891
|
+
return ctx.use(dep);
|
|
892
|
+
},
|
|
893
|
+
method: "GET",
|
|
894
|
+
body: undefined,
|
|
895
|
+
reverse: createReverseFunction(
|
|
896
|
+
getGlobalRouteMap(),
|
|
897
|
+
ctx._routeName,
|
|
898
|
+
ctx.params as Record<string, string>,
|
|
899
|
+
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
900
|
+
),
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// Start loader execution with tracking
|
|
904
|
+
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
905
|
+
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
906
|
+
doneLoader();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Memoize for subsequent calls
|
|
910
|
+
loaderPromises.set(loader.$$id, promise);
|
|
911
|
+
|
|
912
|
+
return promise;
|
|
913
|
+
}) as RequestContext["use"];
|
|
914
|
+
}
|