@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/rsc/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ResolvedSegment, SlotState } from "../types.js";
|
|
9
9
|
import type { HandleData } from "../server/handle-store.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { RSCRouterInternal } from "../router/router-interfaces.js";
|
|
11
11
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -114,6 +114,14 @@ export interface SSRRenderOptions {
|
|
|
114
114
|
* Nonce for Content Security Policy (CSP)
|
|
115
115
|
*/
|
|
116
116
|
nonce?: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* SSR stream mode.
|
|
120
|
+
*
|
|
121
|
+
* - `"stream"` (default) — start flushing HTML immediately.
|
|
122
|
+
* - `"allReady"` — await `stream.allReady` before returning.
|
|
123
|
+
*/
|
|
124
|
+
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
@@ -161,7 +169,7 @@ export interface CreateRSCHandlerOptions<
|
|
|
161
169
|
/**
|
|
162
170
|
* The RSC router instance
|
|
163
171
|
*/
|
|
164
|
-
router:
|
|
172
|
+
router: RSCRouterInternal<TEnv, TRoutes>;
|
|
165
173
|
|
|
166
174
|
/**
|
|
167
175
|
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
|
@@ -238,6 +246,16 @@ export interface CreateRSCHandlerOptions<
|
|
|
238
246
|
* nonce: (request, env) => env.nonce,
|
|
239
247
|
* });
|
|
240
248
|
* ```
|
|
249
|
+
*
|
|
250
|
+
* @example Access nonce in middleware
|
|
251
|
+
* ```tsx
|
|
252
|
+
* import { nonce } from "@rangojs/router";
|
|
253
|
+
*
|
|
254
|
+
* const cspMiddleware: Middleware = async (ctx, next) => {
|
|
255
|
+
* const value = ctx.get(nonce); // string | undefined
|
|
256
|
+
* await next();
|
|
257
|
+
* };
|
|
258
|
+
* ```
|
|
241
259
|
*/
|
|
242
260
|
nonce?: NonceProvider<TEnv>;
|
|
243
261
|
}
|
package/src/search-params.ts
CHANGED
|
@@ -55,14 +55,22 @@ type Simplify<T> = { [K in keyof T]: T[K] };
|
|
|
55
55
|
/**
|
|
56
56
|
* Resolve a SearchSchema to its typed object.
|
|
57
57
|
*
|
|
58
|
+
* Both required and optional params resolve to `T | undefined` at the handler
|
|
59
|
+
* level. The required/optional distinction is a consumer-facing contract
|
|
60
|
+
* (e.g., for href() and reverse() autocomplete) — it tells callers which
|
|
61
|
+
* params the route expects, but the handler must still check for undefined
|
|
62
|
+
* since the framework cannot trust the client to send all required params.
|
|
63
|
+
*
|
|
58
64
|
* @example
|
|
59
65
|
* type S = { q: "string"; page: "number?"; sort: "string?" };
|
|
60
66
|
* type R = ResolveSearchSchema<S>;
|
|
61
|
-
* // { q: string; page?: number; sort?: string }
|
|
67
|
+
* // { q: string | undefined; page?: number; sort?: string }
|
|
62
68
|
*/
|
|
63
69
|
export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
64
70
|
{
|
|
65
|
-
[K in RequiredKeys<T> & string]:
|
|
71
|
+
[K in RequiredKeys<T> & string]:
|
|
72
|
+
| ResolveBaseType<BaseType<T[K]>>
|
|
73
|
+
| undefined;
|
|
66
74
|
} & {
|
|
67
75
|
[K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
|
|
68
76
|
}
|
|
@@ -127,20 +135,32 @@ type ExtractRouteParamsFromMap<TRouteMap, TName> = TName extends keyof TRouteMap
|
|
|
127
135
|
: {}
|
|
128
136
|
: {};
|
|
129
137
|
|
|
138
|
+
/** Parse "a|b|c" into "a" | "b" | "c" */
|
|
139
|
+
type ParseConstraint<T extends string> =
|
|
140
|
+
T extends `${infer First}|${infer Rest}` ? First | ParseConstraint<Rest> : T;
|
|
141
|
+
|
|
130
142
|
/** Minimal inline param extraction (avoids importing from types.ts to prevent circular deps). */
|
|
131
143
|
type ExtractParamsFromPattern<T extends string> =
|
|
132
144
|
T extends `${string}:${infer Param}/${infer Rest}`
|
|
133
|
-
? Param extends `${infer Name}?`
|
|
134
|
-
? {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
? Param extends `${infer Name}(${infer C})?`
|
|
146
|
+
? {
|
|
147
|
+
[K in Name]?: ParseConstraint<C>;
|
|
148
|
+
} & ExtractParamsFromPattern<`/${Rest}`>
|
|
149
|
+
: Param extends `${infer Name}(${infer C})`
|
|
150
|
+
? {
|
|
151
|
+
[K in Name]: ParseConstraint<C>;
|
|
152
|
+
} & ExtractParamsFromPattern<`/${Rest}`>
|
|
153
|
+
: Param extends `${infer Name}?`
|
|
154
|
+
? { [K in Name]?: string } & ExtractParamsFromPattern<`/${Rest}`>
|
|
155
|
+
: { [K in Param]: string } & ExtractParamsFromPattern<`/${Rest}`>
|
|
138
156
|
: T extends `${string}:${infer Param}`
|
|
139
|
-
? Param extends `${infer Name}?`
|
|
140
|
-
? { [K in Name]?:
|
|
141
|
-
: Param extends `${infer Name}(${
|
|
142
|
-
? { [K in Name]:
|
|
143
|
-
:
|
|
157
|
+
? Param extends `${infer Name}(${infer C})?`
|
|
158
|
+
? { [K in Name]?: ParseConstraint<C> }
|
|
159
|
+
: Param extends `${infer Name}(${infer C})`
|
|
160
|
+
? { [K in Name]: ParseConstraint<C> }
|
|
161
|
+
: Param extends `${infer Name}?`
|
|
162
|
+
? { [K in Name]?: string }
|
|
163
|
+
: { [K in Param]: string }
|
|
144
164
|
: {};
|
|
145
165
|
|
|
146
166
|
// ============================================================================
|
|
@@ -154,7 +174,9 @@ type ExtractParamsFromPattern<T extends string> =
|
|
|
154
174
|
* - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
|
|
155
175
|
* - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
|
|
156
176
|
*
|
|
157
|
-
* Missing
|
|
177
|
+
* Missing params (both required and optional) are omitted from the result
|
|
178
|
+
* (undefined). The required/optional distinction is a consumer-facing contract
|
|
179
|
+
* only — the handler must check for undefined.
|
|
158
180
|
*/
|
|
159
181
|
export function parseSearchParams<T extends SearchSchema>(
|
|
160
182
|
searchParams: URLSearchParams,
|
|
@@ -168,13 +190,7 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
168
190
|
const raw = searchParams.get(key);
|
|
169
191
|
|
|
170
192
|
if (raw === null) {
|
|
171
|
-
|
|
172
|
-
// Required param missing: use zero value
|
|
173
|
-
if (baseType === "string") result[key] = "";
|
|
174
|
-
else if (baseType === "number") result[key] = 0;
|
|
175
|
-
else if (baseType === "boolean") result[key] = false;
|
|
176
|
-
}
|
|
177
|
-
// Optional params are omitted (undefined)
|
|
193
|
+
// Missing params are omitted (undefined) regardless of required/optional
|
|
178
194
|
continue;
|
|
179
195
|
}
|
|
180
196
|
|
|
@@ -182,11 +198,10 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
182
198
|
result[key] = raw;
|
|
183
199
|
} else if (baseType === "number") {
|
|
184
200
|
const num = Number(raw);
|
|
185
|
-
if (Number.isNaN(num)) {
|
|
186
|
-
if (!isOptional) result[key] = 0;
|
|
187
|
-
} else {
|
|
201
|
+
if (!Number.isNaN(num)) {
|
|
188
202
|
result[key] = num;
|
|
189
203
|
}
|
|
204
|
+
// NaN treated as missing (undefined)
|
|
190
205
|
} else if (baseType === "boolean") {
|
|
191
206
|
result[key] = raw === "true" || raw === "1";
|
|
192
207
|
}
|
package/src/server/context.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
TransitionConfig,
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { invariant } from "../errors";
|
|
14
|
+
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
14
15
|
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// Performance Metrics Types
|
|
@@ -25,6 +26,7 @@ export interface PerformanceMetric {
|
|
|
25
26
|
label: string; // e.g., "route-matching", "loader:UserLoader"
|
|
26
27
|
duration: number; // milliseconds
|
|
27
28
|
startTime: number; // relative to request start
|
|
29
|
+
depth?: number; // nesting level for hierarchical display (0 = top-level)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -120,6 +122,8 @@ export type InterceptSelectorContext<TEnv = any> = {
|
|
|
120
122
|
request: Request; // The HTTP request object
|
|
121
123
|
env: TEnv; // Platform bindings (Cloudflare env, etc.)
|
|
122
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)
|
|
123
127
|
};
|
|
124
128
|
|
|
125
129
|
/**
|
|
@@ -254,10 +258,18 @@ interface HelperContext {
|
|
|
254
258
|
urlPrefix?: string;
|
|
255
259
|
/** Name prefix from include() - applied to all named routes */
|
|
256
260
|
namePrefix?: string;
|
|
261
|
+
/** True when this scope is at root level (no named include boundary above).
|
|
262
|
+
* Routes at root scope allow dot-local reverse to fall back to bare names. */
|
|
263
|
+
rootScoped?: boolean;
|
|
257
264
|
/** Run helper for cleaner middleware code */
|
|
258
265
|
run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
|
|
259
266
|
/** Tracked includes for build-time manifest generation */
|
|
260
267
|
trackedIncludes?: TrackedInclude[];
|
|
268
|
+
/** Cache profiles for DSL-time cache("profileName") resolution */
|
|
269
|
+
cacheProfiles?: Record<
|
|
270
|
+
string,
|
|
271
|
+
import("../cache/profile-registry.js").CacheProfile
|
|
272
|
+
>;
|
|
261
273
|
}
|
|
262
274
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
263
275
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -399,7 +411,9 @@ export const getContext = (): {
|
|
|
399
411
|
searchSchemas: store.searchSchemas,
|
|
400
412
|
urlPrefix: store.urlPrefix,
|
|
401
413
|
namePrefix: store.namePrefix,
|
|
414
|
+
rootScoped: store.rootScoped,
|
|
402
415
|
trackedIncludes: store.trackedIncludes,
|
|
416
|
+
cacheProfiles: store.cacheProfiles,
|
|
403
417
|
},
|
|
404
418
|
callback,
|
|
405
419
|
);
|
|
@@ -436,7 +450,9 @@ export const getContext = (): {
|
|
|
436
450
|
searchSchemas,
|
|
437
451
|
urlPrefix: store?.urlPrefix,
|
|
438
452
|
namePrefix: store?.namePrefix,
|
|
453
|
+
rootScoped: store?.rootScoped,
|
|
439
454
|
trackedIncludes: store?.trackedIncludes,
|
|
455
|
+
cacheProfiles: store?.cacheProfiles,
|
|
440
456
|
},
|
|
441
457
|
callback,
|
|
442
458
|
);
|
|
@@ -469,17 +485,41 @@ export function runWithPrefixes<T>(
|
|
|
469
485
|
} else {
|
|
470
486
|
combinedUrlPrefix = urlPrefix;
|
|
471
487
|
}
|
|
472
|
-
const combinedNamePrefix =
|
|
473
|
-
|
|
474
|
-
?
|
|
475
|
-
|
|
476
|
-
|
|
488
|
+
const combinedNamePrefix =
|
|
489
|
+
namePrefix !== undefined
|
|
490
|
+
? namePrefix === ""
|
|
491
|
+
? store.namePrefix
|
|
492
|
+
: store.namePrefix
|
|
493
|
+
? `${store.namePrefix}.${namePrefix}`
|
|
494
|
+
: namePrefix
|
|
495
|
+
: store.namePrefix;
|
|
496
|
+
|
|
497
|
+
// Track root scope for dot-local reverse resolution.
|
|
498
|
+
//
|
|
499
|
+
// The flag answers: "can this route reach bare names at root scope?"
|
|
500
|
+
// It propagates through the include chain:
|
|
501
|
+
//
|
|
502
|
+
// { name: "" } — transparent: inherit parent, default true
|
|
503
|
+
// { name: "foo" } — inherit parent if already set, else create boundary (false)
|
|
504
|
+
// no name — inherit parent unchanged
|
|
505
|
+
//
|
|
506
|
+
// This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
|
|
507
|
+
// (the outer transparent include establishes root access, and the inner
|
|
508
|
+
// named include inherits it). But a direct { name: "sub" } at root gets
|
|
509
|
+
// rootScoped=false (no prior root-access grant, so it creates a boundary).
|
|
510
|
+
const combinedRootScoped =
|
|
511
|
+
namePrefix === ""
|
|
512
|
+
? (store.rootScoped ?? true)
|
|
513
|
+
: namePrefix !== undefined
|
|
514
|
+
? (store.rootScoped ?? false)
|
|
515
|
+
: store.rootScoped;
|
|
477
516
|
|
|
478
517
|
return RSCRouterContext.run(
|
|
479
518
|
{
|
|
480
519
|
...store,
|
|
481
520
|
urlPrefix: combinedUrlPrefix,
|
|
482
521
|
namePrefix: combinedNamePrefix,
|
|
522
|
+
rootScoped: combinedRootScoped,
|
|
483
523
|
},
|
|
484
524
|
callback,
|
|
485
525
|
);
|
|
@@ -501,6 +541,15 @@ export function getNamePrefix(): string | undefined {
|
|
|
501
541
|
return store?.namePrefix;
|
|
502
542
|
}
|
|
503
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Get whether the current scope is at root level (no named include boundary above).
|
|
546
|
+
* Returns true at root or inside { name: "" } includes, false inside named includes.
|
|
547
|
+
*/
|
|
548
|
+
export function getRootScoped(): boolean {
|
|
549
|
+
const store = RSCRouterContext.getStore();
|
|
550
|
+
return store?.rootScoped ?? true;
|
|
551
|
+
}
|
|
552
|
+
|
|
504
553
|
// Export HelperContext type for use in other modules
|
|
505
554
|
export type { HelperContext };
|
|
506
555
|
|
|
@@ -519,7 +568,7 @@ export type { HelperContext };
|
|
|
519
568
|
* done(); // Records duration
|
|
520
569
|
* ```
|
|
521
570
|
*/
|
|
522
|
-
export function track(label: string): () => void {
|
|
571
|
+
export function track(label: string, depth?: number): () => void {
|
|
523
572
|
const store = RSCRouterContext.getStore();
|
|
524
573
|
|
|
525
574
|
// No-op if context unavailable or metrics not enabled
|
|
@@ -532,6 +581,11 @@ export function track(label: string): () => void {
|
|
|
532
581
|
return () => {
|
|
533
582
|
const duration =
|
|
534
583
|
performance.now() - store.metrics!.requestStart - startTime;
|
|
535
|
-
store.metrics!.metrics.push({
|
|
584
|
+
store.metrics!.metrics.push({
|
|
585
|
+
label,
|
|
586
|
+
duration,
|
|
587
|
+
startTime,
|
|
588
|
+
...(depth != null ? { depth } : {}),
|
|
589
|
+
});
|
|
536
590
|
};
|
|
537
591
|
}
|
|
@@ -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
|
+
}
|
|
@@ -12,21 +12,26 @@
|
|
|
12
12
|
import type { LoaderFn } from "../types.js";
|
|
13
13
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
export interface LoaderRegistryEntry {
|
|
16
|
+
fn: LoaderFn<any, any, any>;
|
|
17
|
+
middleware: MiddlewareFn[];
|
|
18
|
+
/** Whether this loader is fetchable via the _rsc_loader endpoint. */
|
|
19
|
+
fetchable: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
|
|
19
23
|
|
|
20
24
|
export function registerFetchableLoader(
|
|
21
25
|
id: string,
|
|
22
26
|
fn: LoaderFn<any, any, any>,
|
|
23
27
|
middleware: MiddlewareFn[],
|
|
28
|
+
fetchable: boolean,
|
|
24
29
|
): void {
|
|
25
|
-
fetchableLoaderRegistry.set(id, { fn, middleware });
|
|
30
|
+
fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function getFetchableLoader(
|
|
29
34
|
id: string,
|
|
30
|
-
):
|
|
35
|
+
): LoaderRegistryEntry | undefined {
|
|
31
36
|
return fetchableLoaderRegistry.get(id);
|
|
32
37
|
}
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
|
+
function createLateHandlePushError(
|
|
17
|
+
handleName: string,
|
|
18
|
+
segmentId: string,
|
|
19
|
+
): Error {
|
|
20
|
+
const error = new Error(
|
|
21
|
+
`Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
|
|
22
|
+
`This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
|
|
23
|
+
`Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
|
|
24
|
+
);
|
|
25
|
+
error.name = "LateHandlePushError";
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/**
|
|
17
30
|
* Deep clone handle data to create a snapshot.
|
|
18
31
|
* @internal
|
|
@@ -44,11 +57,26 @@ export interface HandleStore {
|
|
|
44
57
|
track<T>(promise: Promise<T>): Promise<T>;
|
|
45
58
|
|
|
46
59
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
60
|
+
* Signal that no more track() calls will be made.
|
|
61
|
+
* settled will not resolve until seal() is called AND all tracked
|
|
62
|
+
* promises have settled. Calling stream() or getData() auto-seals.
|
|
63
|
+
*/
|
|
64
|
+
seal(): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Promise that resolves when the store is sealed AND all tracked
|
|
68
|
+
* handlers have settled.
|
|
49
69
|
*/
|
|
50
70
|
readonly settled: Promise<void>;
|
|
51
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Optional error callback for late streaming-handle failures.
|
|
74
|
+
* Called when push() throws LateHandlePushError (handle pushed after
|
|
75
|
+
* stream completion). Allows the router to surface these errors
|
|
76
|
+
* to onError and telemetry.
|
|
77
|
+
*/
|
|
78
|
+
onError?: (error: Error) => void;
|
|
79
|
+
|
|
52
80
|
/**
|
|
53
81
|
* Push handle data for a specific handle and segment.
|
|
54
82
|
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
@@ -58,9 +86,7 @@ export interface HandleStore {
|
|
|
58
86
|
|
|
59
87
|
/**
|
|
60
88
|
* Get all collected handle data after all handlers have settled.
|
|
61
|
-
*
|
|
62
|
-
* The data may contain unresolved promises which RSC will stream.
|
|
63
|
-
* @deprecated Use stream() for progressive updates
|
|
89
|
+
* Waits for `settled`, then returns the finalized data.
|
|
64
90
|
*/
|
|
65
91
|
getData(): Promise<HandleData>;
|
|
66
92
|
|
|
@@ -108,9 +134,31 @@ export interface HandleStore {
|
|
|
108
134
|
* ```
|
|
109
135
|
*/
|
|
110
136
|
export function createHandleStore(): HandleStore {
|
|
111
|
-
const pending: Promise<unknown>[] = [];
|
|
112
137
|
const data: HandleData = {};
|
|
113
138
|
|
|
139
|
+
// Settlement barrier: resolved only when sealed AND inflight === 0.
|
|
140
|
+
// seal() signals "no more track() calls". Each track() increments
|
|
141
|
+
// inflightCount, each promise.finally() decrements. settled resolves
|
|
142
|
+
// once both conditions are met — even if tracks are added while
|
|
143
|
+
// earlier ones are still in flight.
|
|
144
|
+
let sealed = false;
|
|
145
|
+
let inflightCount = 0;
|
|
146
|
+
let drainWaiters: (() => void)[] = [];
|
|
147
|
+
|
|
148
|
+
function notifyDrain() {
|
|
149
|
+
if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
|
|
150
|
+
const waiters = drainWaiters;
|
|
151
|
+
drainWaiters = [];
|
|
152
|
+
for (const resolve of waiters) resolve();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sealInternal() {
|
|
157
|
+
if (sealed) return;
|
|
158
|
+
sealed = true;
|
|
159
|
+
notifyDrain();
|
|
160
|
+
}
|
|
161
|
+
|
|
114
162
|
// Queue for pending emissions and resolver for waiting consumer
|
|
115
163
|
let pendingEmissions: HandleData[] = [];
|
|
116
164
|
let emissionResolver: (() => void) | null = null;
|
|
@@ -137,18 +185,38 @@ export function createHandleStore(): HandleStore {
|
|
|
137
185
|
|
|
138
186
|
return {
|
|
139
187
|
track<T>(promise: Promise<T>): Promise<T> {
|
|
140
|
-
|
|
188
|
+
inflightCount++;
|
|
189
|
+
// Use .then(onSettle, onSettle) instead of .finally() to avoid
|
|
190
|
+
// creating an unhandled rejection branch when the tracked promise
|
|
191
|
+
// rejects (e.g. error route handlers). .finally() re-throws the
|
|
192
|
+
// rejection on a new branch that nobody catches, which can crash
|
|
193
|
+
// the server process.
|
|
194
|
+
const onSettle = () => {
|
|
195
|
+
inflightCount--;
|
|
196
|
+
notifyDrain();
|
|
197
|
+
};
|
|
198
|
+
promise.then(onSettle, onSettle);
|
|
141
199
|
return promise;
|
|
142
200
|
},
|
|
143
201
|
|
|
202
|
+
seal() {
|
|
203
|
+
sealInternal();
|
|
204
|
+
},
|
|
205
|
+
|
|
144
206
|
get settled(): Promise<void> {
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
207
|
+
if (sealed && inflightCount === 0) return Promise.resolve();
|
|
208
|
+
return new Promise<void>((resolve) => {
|
|
209
|
+
drainWaiters.push(resolve);
|
|
210
|
+
});
|
|
149
211
|
},
|
|
150
212
|
|
|
151
213
|
push(handleName: string, segmentId: string, value: unknown): void {
|
|
214
|
+
if (completed) {
|
|
215
|
+
const error = createLateHandlePushError(handleName, segmentId);
|
|
216
|
+
if (this.onError) this.onError(error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
|
|
152
220
|
if (!data[handleName]) {
|
|
153
221
|
data[handleName] = {};
|
|
154
222
|
}
|
|
@@ -163,10 +231,14 @@ export function createHandleStore(): HandleStore {
|
|
|
163
231
|
},
|
|
164
232
|
|
|
165
233
|
getData(): Promise<HandleData> {
|
|
166
|
-
|
|
234
|
+
sealInternal();
|
|
235
|
+
return this.settled.then(() => cloneHandleData(data));
|
|
167
236
|
},
|
|
168
237
|
|
|
169
238
|
async *stream(): AsyncGenerator<HandleData, void, unknown> {
|
|
239
|
+
// Auto-seal: stream() is called after all track() registrations.
|
|
240
|
+
sealInternal();
|
|
241
|
+
|
|
170
242
|
// Set up completion handler
|
|
171
243
|
this.settled.then(() => {
|
|
172
244
|
completed = true;
|