@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847
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 +9 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1531 -212
- package/dist/vite/index.js +3995 -2489
- package/package.json +57 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +85 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/hooks/SKILL.md +328 -70
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +62 -15
- package/skills/loader/SKILL.md +368 -42
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +14 -10
- package/skills/parallel/SKILL.md +137 -1
- package/skills/prerender/SKILL.md +366 -28
- package/skills/rango/SKILL.md +85 -21
- package/skills/response-routes/SKILL.md +136 -83
- package/skills/route/SKILL.md +195 -21
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +240 -102
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +266 -558
- package/src/browser/navigation-client.ts +132 -75
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +303 -309
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +144 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +190 -70
- package/src/browser/react/NavigationProvider.tsx +78 -11
- 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 +6 -1
- 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 +29 -70
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- 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 +188 -57
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +488 -606
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +116 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +63 -21
- package/src/build/generate-route-types.ts +36 -1038
- package/src/build/index.ts +2 -5
- package/src/build/route-trie.ts +38 -12
- 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 +479 -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 +342 -0
- package/src/cache/cache-scope.ts +122 -303
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -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 +3 -1
- package/src/client.tsx +84 -126
- 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 +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +12 -7
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +104 -40
- package/src/index.ts +122 -67
- package/src/internal-debug.ts +9 -3
- package/src/loader.rsc.ts +18 -93
- package/src/loader.ts +26 -9
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +121 -17
- package/src/prerender.ts +325 -20
- package/src/reverse.ts +144 -124
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +959 -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 -1450
- package/src/route-map-builder.ts +87 -133
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +41 -6
- 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 +160 -0
- package/src/router/handler-context.ts +324 -116
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +179 -133
- package/src/router/logging.ts +112 -6
- package/src/router/manifest.ts +58 -19
- package/src/router/match-api.ts +89 -88
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +86 -89
- package/src/router/match-middleware/cache-lookup.ts +295 -49
- package/src/router/match-middleware/cache-store.ts +56 -13
- package/src/router/match-middleware/intercept-resolution.ts +45 -22
- package/src/router/match-middleware/segment-resolution.ts +20 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +44 -21
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +327 -369
- package/src/router/pattern-matching.ts +169 -31
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +105 -14
- package/src/router/router-context.ts +40 -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 +677 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1296 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1354
- package/src/router/segment-wrappers.ts +291 -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 +96 -29
- package/src/router/types.ts +15 -9
- package/src/router.ts +642 -2366
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +639 -1027
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- 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 +237 -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 +38 -11
- package/src/search-params.ts +66 -54
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +237 -54
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +438 -71
- package/src/server.ts +26 -164
- package/src/ssr/index.tsx +101 -31
- package/src/static-handler.ts +22 -4
- 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 +773 -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 +109 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1795
- 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 -1323
- 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 +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -2259
- package/src/vite/plugin-types.ts +48 -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 -47
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
- 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 +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
- 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 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- 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/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/server/context.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
PartialCacheOptions,
|
|
5
|
+
ErrorBoundaryHandler,
|
|
6
|
+
Handler,
|
|
7
|
+
LoaderDefinition,
|
|
8
|
+
MiddlewareFn,
|
|
9
|
+
NotFoundBoundaryHandler,
|
|
10
|
+
ShouldRevalidateFn,
|
|
11
|
+
TransitionConfig,
|
|
12
|
+
} from "../types";
|
|
4
13
|
import { invariant } from "../errors";
|
|
14
|
+
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
5
15
|
|
|
6
16
|
// ============================================================================
|
|
7
17
|
// Performance Metrics Types
|
|
@@ -13,9 +23,10 @@ import { invariant } from "../errors";
|
|
|
13
23
|
* @internal This type is an implementation detail and may change without notice.
|
|
14
24
|
*/
|
|
15
25
|
export interface PerformanceMetric {
|
|
16
|
-
label: string;
|
|
17
|
-
duration: number;
|
|
18
|
-
startTime: number;
|
|
26
|
+
label: string; // e.g., "route-matching", "loader:UserLoader"
|
|
27
|
+
duration: number; // milliseconds
|
|
28
|
+
startTime: number; // relative to request start
|
|
29
|
+
depth?: number; // nesting level for hierarchical display (0 = top-level)
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
/**
|
|
@@ -105,12 +116,14 @@ export type InterceptSegmentsState = {
|
|
|
105
116
|
* @internal This type is an implementation detail and may change without notice.
|
|
106
117
|
*/
|
|
107
118
|
export type InterceptSelectorContext<TEnv = any> = {
|
|
108
|
-
from: URL;
|
|
109
|
-
to: URL;
|
|
110
|
-
params: Record<string, string>;
|
|
111
|
-
request: Request;
|
|
112
|
-
env: TEnv;
|
|
113
|
-
segments: InterceptSegmentsState;
|
|
119
|
+
from: URL; // Source URL (where user is coming from)
|
|
120
|
+
to: URL; // Destination URL (where user is navigating to)
|
|
121
|
+
params: Record<string, string>; // Matched route params
|
|
122
|
+
request: Request; // The HTTP request object
|
|
123
|
+
env: TEnv; // Platform bindings (Cloudflare env, etc.)
|
|
124
|
+
segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
|
|
125
|
+
fromRouteName?: DefaultRouteName; // Named route being navigated away from (undefined for unnamed routes)
|
|
126
|
+
toRouteName?: DefaultRouteName; // Named route being navigated to (undefined for unnamed routes)
|
|
114
127
|
};
|
|
115
128
|
|
|
116
129
|
/**
|
|
@@ -119,7 +132,9 @@ export type InterceptSelectorContext<TEnv = any> = {
|
|
|
119
132
|
*
|
|
120
133
|
* @internal This type is an implementation detail and may change without notice.
|
|
121
134
|
*/
|
|
122
|
-
export type InterceptWhenFn<TEnv = any> = (
|
|
135
|
+
export type InterceptWhenFn<TEnv = any> = (
|
|
136
|
+
ctx: InterceptSelectorContext<TEnv>,
|
|
137
|
+
) => boolean;
|
|
123
138
|
|
|
124
139
|
/**
|
|
125
140
|
* Intercept entry stored in EntryData
|
|
@@ -128,8 +143,8 @@ export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>)
|
|
|
128
143
|
* @internal This type is an implementation detail and may change without notice.
|
|
129
144
|
*/
|
|
130
145
|
export type InterceptEntry = {
|
|
131
|
-
slotName: `@${string}`;
|
|
132
|
-
routeName: string;
|
|
146
|
+
slotName: `@${string}`; // e.g., "@modal"
|
|
147
|
+
routeName: string; // e.g., "card"
|
|
133
148
|
handler: ReactNode | Handler<any, any, any>;
|
|
134
149
|
middleware: MiddlewareFn<any, any>[];
|
|
135
150
|
revalidate: ShouldRevalidateFn<any, any>[];
|
|
@@ -137,14 +152,29 @@ export type InterceptEntry = {
|
|
|
137
152
|
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
138
153
|
loader: LoaderEntry[];
|
|
139
154
|
loading?: ReactNode | false;
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
transition?: TransitionConfig;
|
|
156
|
+
layout?: ReactNode | Handler<any, any, any>; // Wrapper layout with <Outlet /> for content
|
|
157
|
+
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
142
158
|
};
|
|
143
159
|
|
|
160
|
+
export interface ParallelEntryData
|
|
161
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
|
|
162
|
+
type: "parallel";
|
|
163
|
+
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
164
|
+
loading?: ReactNode | false;
|
|
165
|
+
transition?: TransitionConfig;
|
|
166
|
+
/** Set when any parallel slot is a Static definition */
|
|
167
|
+
isStaticPrerender?: true;
|
|
168
|
+
/** Per-slot static handler $$ids for build-time store lookup */
|
|
169
|
+
staticHandlerIds?: Record<string, string>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
173
|
+
|
|
144
174
|
export type EntryPropSegments = {
|
|
145
175
|
loader: LoaderEntry[];
|
|
146
176
|
layout: EntryData[];
|
|
147
|
-
parallel:
|
|
177
|
+
parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
|
|
148
178
|
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
149
179
|
};
|
|
150
180
|
|
|
@@ -153,14 +183,20 @@ export type EntryData =
|
|
|
153
183
|
type: "route";
|
|
154
184
|
handler: Handler<any, any, any>;
|
|
155
185
|
loading?: ReactNode | false;
|
|
186
|
+
transition?: TransitionConfig;
|
|
156
187
|
/** URL pattern for this route (used by path() in urls()) */
|
|
157
188
|
pattern?: string;
|
|
158
189
|
/** Set when handler is a Prerender definition */
|
|
159
190
|
isPrerender?: true;
|
|
160
191
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
161
|
-
prerenderDef?: {
|
|
192
|
+
prerenderDef?: {
|
|
193
|
+
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
194
|
+
options?: { passthrough?: boolean };
|
|
195
|
+
};
|
|
162
196
|
/** Set when handler is a Static definition (build-time only) */
|
|
163
197
|
isStaticPrerender?: true;
|
|
198
|
+
/** Static handler $$id for build-time store lookup */
|
|
199
|
+
staticHandlerId?: string;
|
|
164
200
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
165
201
|
responseType?: string;
|
|
166
202
|
} & EntryPropCommon &
|
|
@@ -170,25 +206,21 @@ export type EntryData =
|
|
|
170
206
|
type: "layout";
|
|
171
207
|
handler: ReactNode | Handler<any, any, any>;
|
|
172
208
|
loading?: ReactNode | false;
|
|
209
|
+
transition?: TransitionConfig;
|
|
173
210
|
/** Set when handler is a Static definition (build-time only) */
|
|
174
211
|
isStaticPrerender?: true;
|
|
212
|
+
/** Static handler $$id for build-time store lookup */
|
|
213
|
+
staticHandlerId?: string;
|
|
175
214
|
} & EntryPropCommon &
|
|
176
215
|
EntryPropDatas &
|
|
177
216
|
EntryPropSegments)
|
|
178
|
-
|
|
|
179
|
-
type: "parallel";
|
|
180
|
-
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
181
|
-
loading?: ReactNode | false;
|
|
182
|
-
/** Set when any parallel slot is a Static definition */
|
|
183
|
-
isStaticPrerender?: true;
|
|
184
|
-
} & EntryPropCommon &
|
|
185
|
-
EntryPropDatas &
|
|
186
|
-
EntryPropSegments)
|
|
217
|
+
| ParallelEntryData
|
|
187
218
|
| ({
|
|
188
219
|
type: "cache";
|
|
189
220
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
190
221
|
handler: ReactNode | Handler<any, any, any>;
|
|
191
222
|
loading?: ReactNode | false;
|
|
223
|
+
transition?: TransitionConfig;
|
|
192
224
|
} & EntryPropCommon &
|
|
193
225
|
EntryPropDatas &
|
|
194
226
|
EntryPropSegments);
|
|
@@ -229,13 +261,28 @@ interface HelperContext {
|
|
|
229
261
|
urlPrefix?: string;
|
|
230
262
|
/** Name prefix from include() - applied to all named routes */
|
|
231
263
|
namePrefix?: string;
|
|
264
|
+
/** True when this scope is at root level (no named include boundary above).
|
|
265
|
+
* Routes at root scope allow dot-local reverse to fall back to bare names. */
|
|
266
|
+
rootScoped?: boolean;
|
|
232
267
|
/** Run helper for cleaner middleware code */
|
|
233
268
|
run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
|
|
234
269
|
/** Tracked includes for build-time manifest generation */
|
|
235
270
|
trackedIncludes?: TrackedInclude[];
|
|
271
|
+
/** Cache profiles for DSL-time cache("profileName") resolution */
|
|
272
|
+
cacheProfiles?: Record<
|
|
273
|
+
string,
|
|
274
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
275
|
+
>;
|
|
236
276
|
}
|
|
237
|
-
|
|
238
|
-
|
|
277
|
+
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
278
|
+
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
279
|
+
// a new instance when context.ts is re-evaluated, while other modules still
|
|
280
|
+
// hold references to the old instance — causing getStore() to return
|
|
281
|
+
// undefined even inside a run() callback.
|
|
282
|
+
const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
|
|
283
|
+
export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
|
|
284
|
+
globalThis as any
|
|
285
|
+
)[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
|
|
239
286
|
|
|
240
287
|
export const getContext = (): {
|
|
241
288
|
context: AsyncLocalStorage<HelperContext>;
|
|
@@ -243,21 +290,21 @@ export const getContext = (): {
|
|
|
243
290
|
getParent: () => EntryData | null;
|
|
244
291
|
getOrCreateStore: (forRoute?: string) => HelperContext;
|
|
245
292
|
getNextIndex: (
|
|
246
|
-
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
|
|
293
|
+
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
|
|
247
294
|
) => string;
|
|
248
295
|
getShortCode: (
|
|
249
|
-
type: "layout" | "parallel" | "route" | "loader" | "cache"
|
|
296
|
+
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
250
297
|
) => string;
|
|
251
298
|
run: <T>(
|
|
252
299
|
namespace: string,
|
|
253
300
|
parent: EntryData | null,
|
|
254
|
-
callback: (...args: any[]) => T
|
|
301
|
+
callback: (...args: any[]) => T,
|
|
255
302
|
) => T;
|
|
256
303
|
runWithStore: <T>(
|
|
257
304
|
store: HelperContext,
|
|
258
305
|
namespace: string,
|
|
259
306
|
parent: EntryData | null,
|
|
260
|
-
callback: (...args: any[]) => T
|
|
307
|
+
callback: (...args: any[]) => T,
|
|
261
308
|
) => T;
|
|
262
309
|
} => {
|
|
263
310
|
const context = RSCRouterContext;
|
|
@@ -285,7 +332,7 @@ export const getContext = (): {
|
|
|
285
332
|
const store = context.getStore();
|
|
286
333
|
if (!store) {
|
|
287
334
|
throw new Error(
|
|
288
|
-
"RSC Router context store is not available. Make sure to run within RSC Router context."
|
|
335
|
+
"RSC Router context store is not available. Make sure to run within RSC Router context.",
|
|
289
336
|
);
|
|
290
337
|
}
|
|
291
338
|
return store;
|
|
@@ -299,7 +346,7 @@ export const getContext = (): {
|
|
|
299
346
|
return store.parent;
|
|
300
347
|
},
|
|
301
348
|
getNextIndex: (
|
|
302
|
-
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
|
|
349
|
+
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
|
|
303
350
|
) => {
|
|
304
351
|
const store = context.getStore();
|
|
305
352
|
invariant(store, "No context RSCRouterContext available");
|
|
@@ -308,17 +355,31 @@ export const getContext = (): {
|
|
|
308
355
|
store.counters[type] = index + 1;
|
|
309
356
|
return `$${type}.${index}`;
|
|
310
357
|
},
|
|
311
|
-
getShortCode: (
|
|
358
|
+
getShortCode: (
|
|
359
|
+
type: "layout" | "parallel" | "route" | "loader" | "cache",
|
|
360
|
+
) => {
|
|
312
361
|
const store = context.getStore();
|
|
313
362
|
invariant(store, "No context RSCRouterContext available");
|
|
314
363
|
|
|
315
364
|
const parent = store.parent;
|
|
316
|
-
const prefix =
|
|
317
|
-
|
|
365
|
+
const prefix =
|
|
366
|
+
type === "layout"
|
|
367
|
+
? "L"
|
|
368
|
+
: type === "parallel"
|
|
369
|
+
? "P"
|
|
370
|
+
: type === "loader"
|
|
371
|
+
? "D"
|
|
372
|
+
: type === "cache"
|
|
373
|
+
? "C"
|
|
374
|
+
: "R";
|
|
375
|
+
const mountPrefix =
|
|
376
|
+
store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
|
|
318
377
|
|
|
319
378
|
if (!parent) {
|
|
320
379
|
// Root entry: prefix with mount index and use mount-scoped counter
|
|
321
|
-
const counterKey = mountPrefix
|
|
380
|
+
const counterKey = mountPrefix
|
|
381
|
+
? `${mountPrefix}_root_${type}`
|
|
382
|
+
: `root_${type}`;
|
|
322
383
|
store.counters[counterKey] ??= 0;
|
|
323
384
|
const index = store.counters[counterKey];
|
|
324
385
|
store.counters[counterKey] = index + 1;
|
|
@@ -336,7 +397,7 @@ export const getContext = (): {
|
|
|
336
397
|
store: HelperContext,
|
|
337
398
|
namespace: string,
|
|
338
399
|
parent: EntryData | null,
|
|
339
|
-
callback: (...args: any[]) => T
|
|
400
|
+
callback: (...args: any[]) => T,
|
|
340
401
|
): T => {
|
|
341
402
|
return context.run(
|
|
342
403
|
{
|
|
@@ -353,23 +414,29 @@ export const getContext = (): {
|
|
|
353
414
|
searchSchemas: store.searchSchemas,
|
|
354
415
|
urlPrefix: store.urlPrefix,
|
|
355
416
|
namePrefix: store.namePrefix,
|
|
417
|
+
rootScoped: store.rootScoped,
|
|
356
418
|
trackedIncludes: store.trackedIncludes,
|
|
419
|
+
cacheProfiles: store.cacheProfiles,
|
|
357
420
|
},
|
|
358
|
-
callback
|
|
421
|
+
callback,
|
|
359
422
|
);
|
|
360
423
|
},
|
|
361
424
|
run: <T>(
|
|
362
425
|
namespace: string,
|
|
363
426
|
parent: EntryData | null,
|
|
364
|
-
callback: (...args: any[]) => T
|
|
427
|
+
callback: (...args: any[]) => T,
|
|
365
428
|
) => {
|
|
366
429
|
const store = context.getStore();
|
|
367
430
|
// Preserve parent counters to ensure globally unique shortCodes
|
|
368
431
|
const counters = store?.counters || {};
|
|
369
432
|
const manifest = store ? store.manifest : new Map<string, EntryData>();
|
|
370
433
|
const patterns = store?.patterns || new Map<string, string>();
|
|
371
|
-
const
|
|
372
|
-
const
|
|
434
|
+
const patternsByPrefix = store?.patternsByPrefix;
|
|
435
|
+
const trailingSlash =
|
|
436
|
+
store?.trailingSlash ||
|
|
437
|
+
new Map<string, "never" | "always" | "ignore">();
|
|
438
|
+
const searchSchemas =
|
|
439
|
+
store?.searchSchemas || new Map<string, Record<string, string>>();
|
|
373
440
|
return context.run(
|
|
374
441
|
{
|
|
375
442
|
manifest,
|
|
@@ -381,13 +448,16 @@ export const getContext = (): {
|
|
|
381
448
|
metrics: store?.metrics,
|
|
382
449
|
isSSR: store?.isSSR,
|
|
383
450
|
patterns,
|
|
451
|
+
patternsByPrefix,
|
|
384
452
|
trailingSlash,
|
|
385
453
|
searchSchemas,
|
|
386
454
|
urlPrefix: store?.urlPrefix,
|
|
387
455
|
namePrefix: store?.namePrefix,
|
|
456
|
+
rootScoped: store?.rootScoped,
|
|
388
457
|
trackedIncludes: store?.trackedIncludes,
|
|
458
|
+
cacheProfiles: store?.cacheProfiles,
|
|
389
459
|
},
|
|
390
|
-
callback
|
|
460
|
+
callback,
|
|
391
461
|
);
|
|
392
462
|
},
|
|
393
463
|
};
|
|
@@ -400,7 +470,7 @@ export const getContext = (): {
|
|
|
400
470
|
export function runWithPrefixes<T>(
|
|
401
471
|
urlPrefix: string,
|
|
402
472
|
namePrefix: string | undefined,
|
|
403
|
-
callback: () => T
|
|
473
|
+
callback: () => T,
|
|
404
474
|
): T {
|
|
405
475
|
const store = RSCRouterContext.getStore();
|
|
406
476
|
if (!store) {
|
|
@@ -418,19 +488,43 @@ export function runWithPrefixes<T>(
|
|
|
418
488
|
} else {
|
|
419
489
|
combinedUrlPrefix = urlPrefix;
|
|
420
490
|
}
|
|
421
|
-
const combinedNamePrefix =
|
|
422
|
-
|
|
423
|
-
?
|
|
424
|
-
|
|
425
|
-
|
|
491
|
+
const combinedNamePrefix =
|
|
492
|
+
namePrefix !== undefined
|
|
493
|
+
? namePrefix === ""
|
|
494
|
+
? store.namePrefix
|
|
495
|
+
: store.namePrefix
|
|
496
|
+
? `${store.namePrefix}.${namePrefix}`
|
|
497
|
+
: namePrefix
|
|
498
|
+
: store.namePrefix;
|
|
499
|
+
|
|
500
|
+
// Track root scope for dot-local reverse resolution.
|
|
501
|
+
//
|
|
502
|
+
// The flag answers: "can this route reach bare names at root scope?"
|
|
503
|
+
// It propagates through the include chain:
|
|
504
|
+
//
|
|
505
|
+
// { name: "" } — transparent: inherit parent, default true
|
|
506
|
+
// { name: "foo" } — inherit parent if already set, else create boundary (false)
|
|
507
|
+
// no name — inherit parent unchanged
|
|
508
|
+
//
|
|
509
|
+
// This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
|
|
510
|
+
// (the outer transparent include establishes root access, and the inner
|
|
511
|
+
// named include inherits it). But a direct { name: "sub" } at root gets
|
|
512
|
+
// rootScoped=false (no prior root-access grant, so it creates a boundary).
|
|
513
|
+
const combinedRootScoped =
|
|
514
|
+
namePrefix === ""
|
|
515
|
+
? (store.rootScoped ?? true)
|
|
516
|
+
: namePrefix !== undefined
|
|
517
|
+
? (store.rootScoped ?? false)
|
|
518
|
+
: store.rootScoped;
|
|
426
519
|
|
|
427
520
|
return RSCRouterContext.run(
|
|
428
521
|
{
|
|
429
522
|
...store,
|
|
430
523
|
urlPrefix: combinedUrlPrefix,
|
|
431
524
|
namePrefix: combinedNamePrefix,
|
|
525
|
+
rootScoped: combinedRootScoped,
|
|
432
526
|
},
|
|
433
|
-
callback
|
|
527
|
+
callback,
|
|
434
528
|
);
|
|
435
529
|
}
|
|
436
530
|
|
|
@@ -450,9 +544,92 @@ export function getNamePrefix(): string | undefined {
|
|
|
450
544
|
return store?.namePrefix;
|
|
451
545
|
}
|
|
452
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Get whether the current scope is at root level (no named include boundary above).
|
|
549
|
+
* Returns true at root or inside { name: "" } includes, false inside named includes.
|
|
550
|
+
*/
|
|
551
|
+
export function getRootScoped(): boolean {
|
|
552
|
+
const store = RSCRouterContext.getStore();
|
|
553
|
+
return store?.rootScoped ?? true;
|
|
554
|
+
}
|
|
555
|
+
|
|
453
556
|
// Export HelperContext type for use in other modules
|
|
454
557
|
export type { HelperContext };
|
|
455
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
561
|
+
*
|
|
562
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
563
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
564
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
565
|
+
* every other route that shares that root.
|
|
566
|
+
*
|
|
567
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
568
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
569
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
570
|
+
*/
|
|
571
|
+
export function getIsolatedLazyParent(
|
|
572
|
+
captured: EntryData | null | undefined,
|
|
573
|
+
): EntryData | null {
|
|
574
|
+
if (!captured) return null;
|
|
575
|
+
return {
|
|
576
|
+
...captured,
|
|
577
|
+
loader: [...captured.loader],
|
|
578
|
+
middleware: [...captured.middleware],
|
|
579
|
+
revalidate: [...captured.revalidate],
|
|
580
|
+
errorBoundary: [...captured.errorBoundary],
|
|
581
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
582
|
+
layout: [...captured.layout],
|
|
583
|
+
parallel: { ...captured.parallel },
|
|
584
|
+
intercept: [...captured.intercept],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getParallelEntries(
|
|
589
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
590
|
+
): ParallelEntryData[] {
|
|
591
|
+
if (!parallels) return [];
|
|
592
|
+
if (Array.isArray(parallels)) {
|
|
593
|
+
return parallels.filter(
|
|
594
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return Object.values(parallels).filter(
|
|
598
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function getParallelSlotEntries(
|
|
603
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
604
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
605
|
+
if (!parallels) return [];
|
|
606
|
+
|
|
607
|
+
if (Array.isArray(parallels)) {
|
|
608
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
609
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
610
|
+
slot,
|
|
611
|
+
entry,
|
|
612
|
+
})),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return Object.entries(parallels)
|
|
617
|
+
.filter(([, entry]) => !!entry)
|
|
618
|
+
.map(([slot, entry]) => ({
|
|
619
|
+
slot: slot as `@${string}`,
|
|
620
|
+
entry: entry!,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function getParallelSlotCount(
|
|
625
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
626
|
+
): number {
|
|
627
|
+
if (!parallels) return 0;
|
|
628
|
+
return Array.isArray(parallels)
|
|
629
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
630
|
+
: Object.keys(parallels).length;
|
|
631
|
+
}
|
|
632
|
+
|
|
456
633
|
// ============================================================================
|
|
457
634
|
// Performance Metrics Helpers
|
|
458
635
|
// ============================================================================
|
|
@@ -468,7 +645,7 @@ export type { HelperContext };
|
|
|
468
645
|
* done(); // Records duration
|
|
469
646
|
* ```
|
|
470
647
|
*/
|
|
471
|
-
export function track(label: string): () => void {
|
|
648
|
+
export function track(label: string, depth?: number): () => void {
|
|
472
649
|
const store = RSCRouterContext.getStore();
|
|
473
650
|
|
|
474
651
|
// No-op if context unavailable or metrics not enabled
|
|
@@ -479,7 +656,13 @@ export function track(label: string): () => void {
|
|
|
479
656
|
const startTime = performance.now() - store.metrics.requestStart;
|
|
480
657
|
|
|
481
658
|
return () => {
|
|
482
|
-
const duration =
|
|
483
|
-
|
|
659
|
+
const duration =
|
|
660
|
+
performance.now() - store.metrics!.requestStart - startTime;
|
|
661
|
+
store.metrics!.metrics.push({
|
|
662
|
+
label,
|
|
663
|
+
duration,
|
|
664
|
+
startTime,
|
|
665
|
+
...(depth != null ? { depth } : {}),
|
|
666
|
+
});
|
|
484
667
|
};
|
|
485
668
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Store — Next.js-style cookie facade backed by the response-derived model.
|
|
3
|
+
*
|
|
4
|
+
* `cookies()` returns a CookieStore scoped to the current request.
|
|
5
|
+
* Reads merge the original Cookie header with Set-Cookie mutations
|
|
6
|
+
* already queued on the response stub (last-write-wins).
|
|
7
|
+
* Writes append Set-Cookie to the response stub.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CookieOptions } from "../router/middleware-types.js";
|
|
11
|
+
import { getRequestContext } from "./request-context.js";
|
|
12
|
+
import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single cookie entry returned by get() and getAll().
|
|
16
|
+
*/
|
|
17
|
+
export interface Cookie {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Request-scoped cookie store.
|
|
24
|
+
*
|
|
25
|
+
* Reads see the effective merged view (original request + same-request mutations).
|
|
26
|
+
* Writes append Set-Cookie headers to the shared response stub.
|
|
27
|
+
*/
|
|
28
|
+
export interface CookieStore {
|
|
29
|
+
/** Get a single cookie by name. Returns undefined if not set or deleted. */
|
|
30
|
+
get(name: string): Cookie | undefined;
|
|
31
|
+
|
|
32
|
+
/** Get all effective cookies, or all cookies with a given name. */
|
|
33
|
+
getAll(name?: string): Cookie[];
|
|
34
|
+
|
|
35
|
+
/** Check whether a cookie exists in the effective view. */
|
|
36
|
+
has(name: string): boolean;
|
|
37
|
+
|
|
38
|
+
/** Set a cookie (appends Set-Cookie to the response stub). */
|
|
39
|
+
set(name: string, value: string, options?: CookieOptions): void;
|
|
40
|
+
|
|
41
|
+
/** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
|
|
42
|
+
delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the request-scoped cookie store.
|
|
47
|
+
*
|
|
48
|
+
* Must be called inside a request context (middleware, handler, loader, action).
|
|
49
|
+
* Throws if called outside request scope.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { cookies } from "@rangojs/router";
|
|
54
|
+
*
|
|
55
|
+
* // In a handler, loader, or action:
|
|
56
|
+
* const session = cookies().get("session")?.value;
|
|
57
|
+
* cookies().set("session", "new-token", { httpOnly: true });
|
|
58
|
+
* cookies().delete("session");
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function cookies(): CookieStore {
|
|
62
|
+
const ctx = getRequestContext();
|
|
63
|
+
assertNotInsideCacheContext(ctx, "cookies");
|
|
64
|
+
return createCookieStore(ctx);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read-only view of HTTP headers.
|
|
69
|
+
* Exposes only the read methods of the Headers API.
|
|
70
|
+
*/
|
|
71
|
+
export interface ReadonlyHeaders {
|
|
72
|
+
get(name: string): string | null;
|
|
73
|
+
has(name: string): boolean;
|
|
74
|
+
entries(): HeadersIterator<[string, string]>;
|
|
75
|
+
keys(): HeadersIterator<string>;
|
|
76
|
+
values(): HeadersIterator<string>;
|
|
77
|
+
forEach(
|
|
78
|
+
callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
|
|
79
|
+
): void;
|
|
80
|
+
[Symbol.iterator](): HeadersIterator<[string, string]>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
|
|
84
|
+
type HeadersIterator<T> = IterableIterator<T>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Throw if called inside a "use cache" function.
|
|
88
|
+
* Reading request-scoped data (cookies, headers) inside a cached function
|
|
89
|
+
* produces results that vary per request but the cache key does not include
|
|
90
|
+
* those values, leading to one user's data being served to another.
|
|
91
|
+
*/
|
|
92
|
+
function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
|
|
93
|
+
if (
|
|
94
|
+
ctx !== null &&
|
|
95
|
+
ctx !== undefined &&
|
|
96
|
+
typeof ctx === "object" &&
|
|
97
|
+
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${fnName}() cannot be called inside a "use cache" function. ` +
|
|
101
|
+
`Request-scoped data (cookies, headers) varies per request but is not ` +
|
|
102
|
+
`reflected in the cache key, so cached results would be served to the ` +
|
|
103
|
+
`wrong users. Extract the value before the cached function and pass it ` +
|
|
104
|
+
`as an argument:\n\n` +
|
|
105
|
+
` const locale = cookies().get("locale")?.value ?? "en";\n` +
|
|
106
|
+
` const data = await getCachedData(locale); // locale is now in the cache key`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the original request headers (read-only).
|
|
115
|
+
*
|
|
116
|
+
* Must be called inside a request context.
|
|
117
|
+
* Returns a read-only view of the incoming request's headers.
|
|
118
|
+
* Mutation methods (set, append, delete) throw at runtime.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* import { headers } from "@rangojs/router";
|
|
123
|
+
*
|
|
124
|
+
* const auth = headers().get("authorization");
|
|
125
|
+
* const contentType = headers().get("content-type");
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function headers(): ReadonlyHeaders {
|
|
129
|
+
const ctx = getRequestContext();
|
|
130
|
+
assertNotInsideCacheContext(ctx, "headers");
|
|
131
|
+
return new Proxy(ctx.request.headers, {
|
|
132
|
+
get(target, prop, receiver) {
|
|
133
|
+
if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
|
|
134
|
+
return () => {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
|
|
137
|
+
`Use ctx.header() to set response headers.`,
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const value = Reflect.get(target, prop, receiver);
|
|
142
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
143
|
+
},
|
|
144
|
+
}) as unknown as ReadonlyHeaders;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a CookieStore backed by a RequestContext.
|
|
149
|
+
* @internal Shared between cookies() shorthand and context methods.
|
|
150
|
+
*/
|
|
151
|
+
function createCookieStore(ctx: {
|
|
152
|
+
cookie(name: string): string | undefined;
|
|
153
|
+
cookies(): Record<string, string>;
|
|
154
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
155
|
+
deleteCookie(
|
|
156
|
+
name: string,
|
|
157
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
158
|
+
): void;
|
|
159
|
+
}): CookieStore {
|
|
160
|
+
return {
|
|
161
|
+
get(name: string): Cookie | undefined {
|
|
162
|
+
const value = ctx.cookie(name);
|
|
163
|
+
return value !== undefined ? { name, value } : undefined;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
getAll(name?: string): Cookie[] {
|
|
167
|
+
const all = ctx.cookies();
|
|
168
|
+
if (name !== undefined) {
|
|
169
|
+
const value = all[name];
|
|
170
|
+
return value !== undefined ? [{ name, value }] : [];
|
|
171
|
+
}
|
|
172
|
+
return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
has(name: string): boolean {
|
|
176
|
+
return ctx.cookie(name) !== undefined;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
set(name: string, value: string, options?: CookieOptions): void {
|
|
180
|
+
ctx.setCookie(name, value, options);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
delete(
|
|
184
|
+
name: string,
|
|
185
|
+
options?: Pick<CookieOptions, "domain" | "path">,
|
|
186
|
+
): void {
|
|
187
|
+
ctx.deleteCookie(name, options);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|