@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +469 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1241 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +289 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -726
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/errors.ts
CHANGED
|
@@ -22,6 +22,7 @@ export class RouteNotFoundError extends Error {
|
|
|
22
22
|
|
|
23
23
|
constructor(message: string, options?: ErrorOptions) {
|
|
24
24
|
super(message);
|
|
25
|
+
Object.setPrototypeOf(this, RouteNotFoundError.prototype);
|
|
25
26
|
this.cause = options?.cause;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -45,6 +46,7 @@ export class DataNotFoundError extends Error {
|
|
|
45
46
|
|
|
46
47
|
constructor(message: string = "Not found", options?: ErrorOptions) {
|
|
47
48
|
super(message);
|
|
49
|
+
Object.setPrototypeOf(this, DataNotFoundError.prototype);
|
|
48
50
|
this.cause = options?.cause;
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -74,6 +76,7 @@ export class MiddlewareError extends Error {
|
|
|
74
76
|
|
|
75
77
|
constructor(message: string, options?: ErrorOptions) {
|
|
76
78
|
super(message);
|
|
79
|
+
Object.setPrototypeOf(this, MiddlewareError.prototype);
|
|
77
80
|
this.cause = options?.cause;
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -87,6 +90,7 @@ export class HandlerError extends Error {
|
|
|
87
90
|
|
|
88
91
|
constructor(message: string, options?: ErrorOptions) {
|
|
89
92
|
super(message);
|
|
93
|
+
Object.setPrototypeOf(this, HandlerError.prototype);
|
|
90
94
|
this.cause = options?.cause;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -100,6 +104,7 @@ export class BuildError extends Error {
|
|
|
100
104
|
|
|
101
105
|
constructor(message: string, options?: ErrorOptions) {
|
|
102
106
|
super(message);
|
|
107
|
+
Object.setPrototypeOf(this, BuildError.prototype);
|
|
103
108
|
this.cause = options?.cause;
|
|
104
109
|
}
|
|
105
110
|
}
|
|
@@ -133,9 +138,10 @@ export class NetworkError extends Error {
|
|
|
133
138
|
options?: ErrorOptions & {
|
|
134
139
|
url?: string;
|
|
135
140
|
operation?: "action" | "navigation" | "revalidation";
|
|
136
|
-
}
|
|
141
|
+
},
|
|
137
142
|
) {
|
|
138
143
|
super(message);
|
|
144
|
+
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
139
145
|
this.cause = options?.cause;
|
|
140
146
|
this.url = options?.url;
|
|
141
147
|
this.operation = options?.operation;
|
|
@@ -171,6 +177,100 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
171
177
|
return false;
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Structured error for JSON response routes.
|
|
182
|
+
* Thrown by handlers to return a typed error envelope with a specific HTTP status.
|
|
183
|
+
*
|
|
184
|
+
* Unlike standard Error, RouterError messages are always exposed to the client
|
|
185
|
+
* (the developer intentionally crafted them for consumers).
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* path("/products/:id", (ctx) => {
|
|
190
|
+
* const product = products.find(p => p.id === ctx.params.id);
|
|
191
|
+
* if (!product) throw new RouterError("NOT_FOUND", "Product not found", { status: 404 });
|
|
192
|
+
* return product;
|
|
193
|
+
* }, { name: "productDetail" })
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export class RouterError extends Error {
|
|
197
|
+
name = "RouterError" as const;
|
|
198
|
+
code: string;
|
|
199
|
+
type?: string;
|
|
200
|
+
status: number;
|
|
201
|
+
cause?: unknown;
|
|
202
|
+
|
|
203
|
+
constructor(
|
|
204
|
+
code: string,
|
|
205
|
+
message: string,
|
|
206
|
+
options?: {
|
|
207
|
+
status?: number;
|
|
208
|
+
type?: string;
|
|
209
|
+
cause?: unknown;
|
|
210
|
+
},
|
|
211
|
+
) {
|
|
212
|
+
super(message);
|
|
213
|
+
Object.setPrototypeOf(this, RouterError.prototype);
|
|
214
|
+
this.code = code;
|
|
215
|
+
this.status = options?.status ?? 500;
|
|
216
|
+
this.type = options?.type;
|
|
217
|
+
this.cause = options?.cause;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Thrown inside a Prerender or Static handler at build time to signal
|
|
223
|
+
* "skip this entry, log it, and continue with the rest."
|
|
224
|
+
*
|
|
225
|
+
* When thrown, the entry is excluded from the pre-render manifest
|
|
226
|
+
* but the build continues normally. Regular throws (non-Skip)
|
|
227
|
+
* stop all further pre-rendering and fail the build.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* export const BlogPost = Prerender(
|
|
232
|
+
* async () => allPosts.map(p => ({ slug: p.slug })),
|
|
233
|
+
* async (ctx) => {
|
|
234
|
+
* const post = await getPost(ctx.params.slug);
|
|
235
|
+
* if (post.draft) throw new Skip(`"${ctx.params.slug}" is a draft`);
|
|
236
|
+
* return <PostPage post={post} />;
|
|
237
|
+
* },
|
|
238
|
+
* );
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
export class Skip extends Error {
|
|
242
|
+
name = "Skip" as const;
|
|
243
|
+
cause?: unknown;
|
|
244
|
+
|
|
245
|
+
constructor(message: string = "Entry skipped", options?: ErrorOptions) {
|
|
246
|
+
super(message);
|
|
247
|
+
Object.setPrototypeOf(this, Skip.prototype);
|
|
248
|
+
this.cause = options?.cause;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Type guard to check if a thrown value is a Skip signal.
|
|
254
|
+
*/
|
|
255
|
+
export function isSkip(value: unknown): value is Skip {
|
|
256
|
+
return value instanceof Skip;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Thrown by the partial updater when the server responds with a redirect payload
|
|
261
|
+
* that carries location state. Caught by navigate() to re-navigate to the
|
|
262
|
+
* redirect target with the server-set state merged into history.pushState.
|
|
263
|
+
*
|
|
264
|
+
* Not an Error subclass -- this is a control flow signal, not a failure.
|
|
265
|
+
*/
|
|
266
|
+
export class ServerRedirect {
|
|
267
|
+
readonly name = "ServerRedirect";
|
|
268
|
+
constructor(
|
|
269
|
+
public readonly url: string,
|
|
270
|
+
public readonly state: Record<string, unknown> | undefined,
|
|
271
|
+
) {}
|
|
272
|
+
}
|
|
273
|
+
|
|
174
274
|
/**
|
|
175
275
|
* Thrown when route handler returns invalid type
|
|
176
276
|
*/
|
|
@@ -180,6 +280,7 @@ export class InvalidHandlerError extends Error {
|
|
|
180
280
|
|
|
181
281
|
constructor(message: string, options?: ErrorOptions) {
|
|
182
282
|
super(message);
|
|
283
|
+
Object.setPrototypeOf(this, InvalidHandlerError.prototype);
|
|
183
284
|
this.cause = options?.cause;
|
|
184
285
|
}
|
|
185
286
|
}
|
|
@@ -226,7 +327,12 @@ export function sanitizeError(error: unknown): Response {
|
|
|
226
327
|
return error;
|
|
227
328
|
}
|
|
228
329
|
|
|
229
|
-
|
|
330
|
+
// Vite replaces import.meta.env.DEV at compile time. The fallback covers
|
|
331
|
+
// non-Vite environments (plain Node, test runners without Vite transforms).
|
|
332
|
+
// SECURITY: fail closed — default to production when the environment is ambiguous.
|
|
333
|
+
const isDev =
|
|
334
|
+
(import.meta as any).env?.DEV ??
|
|
335
|
+
globalThis.process?.env?.NODE_ENV === "development";
|
|
230
336
|
|
|
231
337
|
if (isDev) {
|
|
232
338
|
// Development: Send full error details for debugging
|
package/src/handle.ts
CHANGED
|
@@ -29,16 +29,6 @@ export interface Handle<TData, TAccumulated = TData[]> {
|
|
|
29
29
|
* Format: "filePath#ExportName" in dev, "hash#ExportName" in production
|
|
30
30
|
*/
|
|
31
31
|
readonly $$id: string;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Collect function to transform segment data into final value.
|
|
35
|
-
* Receives array of arrays - each inner array contains values pushed
|
|
36
|
-
* by one segment, ordered parent-to-child.
|
|
37
|
-
*
|
|
38
|
-
* @param segments - Array of segment data arrays, e.g. [[a, b], [c], [d, e]]
|
|
39
|
-
* @returns The accumulated value
|
|
40
|
-
*/
|
|
41
|
-
readonly collect: (segments: TData[][]) => TAccumulated;
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
/**
|
|
@@ -48,10 +38,25 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
48
38
|
return segments.flat();
|
|
49
39
|
}
|
|
50
40
|
|
|
41
|
+
// Module-level registry mapping $$id to collect functions.
|
|
42
|
+
// Populated when createHandle() runs (both server and client).
|
|
43
|
+
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
44
|
+
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Look up a collect function from the registry by handle $$id.
|
|
48
|
+
* Returns undefined if not registered (falls back to defaultCollect in useHandle).
|
|
49
|
+
*/
|
|
50
|
+
export function getCollectFn(
|
|
51
|
+
id: string,
|
|
52
|
+
): ((segments: unknown[][]) => unknown) | undefined {
|
|
53
|
+
return collectRegistry.get(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
/**
|
|
52
57
|
* Create a handle definition for accumulating data across route segments.
|
|
53
58
|
*
|
|
54
|
-
* The $$id is auto-generated by the Vite
|
|
59
|
+
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
|
|
55
60
|
* file path and export name. No manual naming required.
|
|
56
61
|
*
|
|
57
62
|
* @param collect - Optional collect function (default: flatten into array)
|
|
@@ -86,24 +91,34 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
86
91
|
*/
|
|
87
92
|
export function createHandle<TData, TAccumulated = TData[]>(
|
|
88
93
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
89
|
-
__injectedId?: string
|
|
94
|
+
__injectedId?: string,
|
|
90
95
|
): Handle<TData, TAccumulated> {
|
|
91
96
|
const handleId = __injectedId ?? "";
|
|
92
97
|
|
|
93
|
-
if (!handleId && process.env.NODE_ENV
|
|
94
|
-
|
|
98
|
+
if (!handleId && process.env.NODE_ENV === "development") {
|
|
99
|
+
throw new Error(
|
|
95
100
|
"[rsc-router] Handle is missing $$id. " +
|
|
96
|
-
"Make sure the
|
|
97
|
-
"the handle is exported with: export const MyHandle = createHandle(...)"
|
|
101
|
+
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
102
|
+
"the handle is exported with: export const MyHandle = createHandle(...)",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const collectFn =
|
|
107
|
+
collect ??
|
|
108
|
+
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
109
|
+
|
|
110
|
+
// Register collect in module-level registry so useHandle() can recover it
|
|
111
|
+
// when the handle is deserialized from RSC props (toJSON strips collect).
|
|
112
|
+
if (handleId) {
|
|
113
|
+
collectRegistry.set(
|
|
114
|
+
handleId,
|
|
115
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
98
116
|
);
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
return {
|
|
102
120
|
__brand: "handle" as const,
|
|
103
121
|
$$id: handleId,
|
|
104
|
-
collect:
|
|
105
|
-
collect ??
|
|
106
|
-
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated),
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -28,8 +28,9 @@ import { use } from "react";
|
|
|
28
28
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
29
29
|
import { Meta } from "./meta.js";
|
|
30
30
|
import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
|
|
31
|
-
import {
|
|
31
|
+
import { useThemeContext } from "../theme/theme-context.js";
|
|
32
32
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
33
|
+
import { useNonce } from "../browser/react/nonce-context.js";
|
|
33
34
|
|
|
34
35
|
// Type guards for MetaDescriptorBase variants
|
|
35
36
|
function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
|
|
@@ -40,30 +41,53 @@ function hasTitle(d: MetaDescriptorBase): d is { title: string } {
|
|
|
40
41
|
return "title" in d && typeof (d as { title?: unknown }).title === "string";
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function hasNameContent(
|
|
44
|
-
|
|
44
|
+
function hasNameContent(
|
|
45
|
+
d: MetaDescriptorBase,
|
|
46
|
+
): d is { name: string; content: string } {
|
|
47
|
+
return (
|
|
48
|
+
"name" in d &&
|
|
49
|
+
"content" in d &&
|
|
45
50
|
typeof (d as { name?: unknown }).name === "string" &&
|
|
46
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
51
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
52
|
+
);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
function hasPropertyContent(
|
|
50
|
-
|
|
55
|
+
function hasPropertyContent(
|
|
56
|
+
d: MetaDescriptorBase,
|
|
57
|
+
): d is { property: string; content: string } {
|
|
58
|
+
return (
|
|
59
|
+
"property" in d &&
|
|
60
|
+
"content" in d &&
|
|
51
61
|
typeof (d as { property?: unknown }).property === "string" &&
|
|
52
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
62
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
63
|
+
);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
function hasHttpEquivContent(
|
|
56
|
-
|
|
66
|
+
function hasHttpEquivContent(
|
|
67
|
+
d: MetaDescriptorBase,
|
|
68
|
+
): d is { httpEquiv: string; content: string } {
|
|
69
|
+
return (
|
|
70
|
+
"httpEquiv" in d &&
|
|
71
|
+
"content" in d &&
|
|
57
72
|
typeof (d as { httpEquiv?: unknown }).httpEquiv === "string" &&
|
|
58
|
-
typeof (d as { content?: unknown }).content === "string"
|
|
73
|
+
typeof (d as { content?: unknown }).content === "string"
|
|
74
|
+
);
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
function hasScriptLdJson(
|
|
77
|
+
function hasScriptLdJson(
|
|
78
|
+
d: MetaDescriptorBase,
|
|
79
|
+
): d is { "script:ld+json": object } {
|
|
62
80
|
return "script:ld+json" in d;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
|
-
function hasTagName(
|
|
66
|
-
|
|
83
|
+
function hasTagName(
|
|
84
|
+
d: MetaDescriptorBase,
|
|
85
|
+
): d is { tagName: "meta" | "link"; [name: string]: string } {
|
|
86
|
+
return (
|
|
87
|
+
"tagName" in d &&
|
|
88
|
+
((d as { tagName?: unknown }).tagName === "meta" ||
|
|
89
|
+
(d as { tagName?: unknown }).tagName === "link")
|
|
90
|
+
);
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
/**
|
|
@@ -78,7 +102,7 @@ function isPromise(value: unknown): value is Promise<unknown> {
|
|
|
78
102
|
*/
|
|
79
103
|
function renderMetaDescriptor(
|
|
80
104
|
descriptor: MetaDescriptorBase,
|
|
81
|
-
index: number
|
|
105
|
+
index: number,
|
|
82
106
|
): React.ReactNode {
|
|
83
107
|
// charset
|
|
84
108
|
if (hasCharSet(descriptor)) {
|
|
@@ -139,21 +163,42 @@ function renderMetaDescriptor(
|
|
|
139
163
|
if (hasTagName(descriptor)) {
|
|
140
164
|
const { tagName, ...rest } = descriptor;
|
|
141
165
|
if (tagName === "link") {
|
|
142
|
-
return
|
|
166
|
+
return (
|
|
167
|
+
<link
|
|
168
|
+
key={`link-${index}`}
|
|
169
|
+
{...(rest as React.LinkHTMLAttributes<HTMLLinkElement>)}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
143
172
|
}
|
|
144
173
|
if (tagName === "meta") {
|
|
145
|
-
return
|
|
174
|
+
return (
|
|
175
|
+
<meta
|
|
176
|
+
key={`meta-${index}`}
|
|
177
|
+
{...(rest as React.MetaHTMLAttributes<HTMLMetaElement>)}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
146
180
|
}
|
|
147
181
|
}
|
|
148
182
|
|
|
149
183
|
// Fallback: treat as meta attributes
|
|
150
|
-
return
|
|
184
|
+
return (
|
|
185
|
+
<meta
|
|
186
|
+
key={`meta-fallback-${index}`}
|
|
187
|
+
{...(descriptor as React.MetaHTMLAttributes<HTMLMetaElement>)}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
151
190
|
}
|
|
152
191
|
|
|
153
192
|
/**
|
|
154
193
|
* Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
|
|
155
194
|
*/
|
|
156
|
-
function AsyncMetaTag({
|
|
195
|
+
function AsyncMetaTag({
|
|
196
|
+
promise,
|
|
197
|
+
index,
|
|
198
|
+
}: {
|
|
199
|
+
promise: Promise<MetaDescriptorBase>;
|
|
200
|
+
index: number;
|
|
201
|
+
}): React.ReactNode {
|
|
157
202
|
const resolved = use(promise);
|
|
158
203
|
return renderMetaDescriptor(resolved, index);
|
|
159
204
|
}
|
|
@@ -172,19 +217,27 @@ function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>
|
|
|
172
217
|
*/
|
|
173
218
|
export function MetaTags(): React.ReactNode {
|
|
174
219
|
const descriptors = useHandle(Meta) as MetaDescriptor[];
|
|
175
|
-
const themeConfig =
|
|
220
|
+
const themeConfig = useThemeContext()?.config ?? null;
|
|
221
|
+
const nonce = useNonce();
|
|
176
222
|
|
|
177
223
|
return (
|
|
178
224
|
<>
|
|
179
225
|
{/* Theme script must be first to prevent FOUC */}
|
|
180
226
|
{themeConfig && (
|
|
181
227
|
<script
|
|
228
|
+
nonce={nonce}
|
|
182
229
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
183
230
|
/>
|
|
184
231
|
)}
|
|
185
232
|
{descriptors.map((descriptor, index) => {
|
|
186
233
|
if (isPromise(descriptor)) {
|
|
187
|
-
return
|
|
234
|
+
return (
|
|
235
|
+
<AsyncMetaTag
|
|
236
|
+
key={`async-${index}`}
|
|
237
|
+
promise={descriptor}
|
|
238
|
+
index={index}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
188
241
|
}
|
|
189
242
|
return renderMetaDescriptor(descriptor, index);
|
|
190
243
|
})}
|
package/src/handles/meta.ts
CHANGED
|
@@ -39,7 +39,7 @@ import type {
|
|
|
39
39
|
* Type guard for unset descriptor
|
|
40
40
|
*/
|
|
41
41
|
function isUnsetDescriptor(
|
|
42
|
-
descriptor: MetaDescriptor
|
|
42
|
+
descriptor: MetaDescriptor,
|
|
43
43
|
): descriptor is UnsetDescriptor {
|
|
44
44
|
return (
|
|
45
45
|
typeof descriptor === "object" &&
|
|
@@ -53,7 +53,7 @@ function isUnsetDescriptor(
|
|
|
53
53
|
* Type guard for title descriptor (any form)
|
|
54
54
|
*/
|
|
55
55
|
function isTitleDescriptor(
|
|
56
|
-
descriptor: MetaDescriptor
|
|
56
|
+
descriptor: MetaDescriptor,
|
|
57
57
|
): descriptor is { title: TitleDescriptor } {
|
|
58
58
|
return (
|
|
59
59
|
typeof descriptor === "object" &&
|
|
@@ -66,7 +66,7 @@ function isTitleDescriptor(
|
|
|
66
66
|
* Type guard for title template descriptor
|
|
67
67
|
*/
|
|
68
68
|
function isTitleTemplate(
|
|
69
|
-
title: TitleDescriptor
|
|
69
|
+
title: TitleDescriptor,
|
|
70
70
|
): title is { template: string; default: string } {
|
|
71
71
|
return (
|
|
72
72
|
typeof title === "object" &&
|
|
@@ -79,7 +79,9 @@ function isTitleTemplate(
|
|
|
79
79
|
/**
|
|
80
80
|
* Type guard for absolute title descriptor
|
|
81
81
|
*/
|
|
82
|
-
function isAbsoluteTitle(
|
|
82
|
+
function isAbsoluteTitle(
|
|
83
|
+
title: TitleDescriptor,
|
|
84
|
+
): title is { absolute: string } {
|
|
83
85
|
return typeof title === "object" && title !== null && "absolute" in title;
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -141,7 +143,7 @@ function addOrReplace(
|
|
|
141
143
|
result: MetaDescriptor[],
|
|
142
144
|
keyToIndex: Map<string, number>,
|
|
143
145
|
descriptor: MetaDescriptor,
|
|
144
|
-
key: string | undefined
|
|
146
|
+
key: string | undefined,
|
|
145
147
|
): void {
|
|
146
148
|
if (key !== undefined && keyToIndex.has(key)) {
|
|
147
149
|
result[keyToIndex.get(key)!] = descriptor;
|
|
@@ -158,7 +160,7 @@ function addOrReplace(
|
|
|
158
160
|
*/
|
|
159
161
|
function updateIndicesAfterRemoval(
|
|
160
162
|
keyToIndex: Map<string, number>,
|
|
161
|
-
removedIndex: number
|
|
163
|
+
removedIndex: number,
|
|
162
164
|
): void {
|
|
163
165
|
for (const [key, index] of keyToIndex) {
|
|
164
166
|
if (index > removedIndex) {
|
|
@@ -208,13 +210,23 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
208
210
|
// Store template for subsequent title descriptors in child segments
|
|
209
211
|
titleTemplate = titleValue.template;
|
|
210
212
|
// Set the default title
|
|
211
|
-
addOrReplace(
|
|
213
|
+
addOrReplace(
|
|
214
|
+
result,
|
|
215
|
+
keyToIndex,
|
|
216
|
+
{ title: titleValue.default },
|
|
217
|
+
"title",
|
|
218
|
+
);
|
|
212
219
|
continue;
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
if (isAbsoluteTitle(titleValue)) {
|
|
216
223
|
// Absolute title bypasses any template
|
|
217
|
-
addOrReplace(
|
|
224
|
+
addOrReplace(
|
|
225
|
+
result,
|
|
226
|
+
keyToIndex,
|
|
227
|
+
{ title: titleValue.absolute },
|
|
228
|
+
"title",
|
|
229
|
+
);
|
|
218
230
|
continue;
|
|
219
231
|
}
|
|
220
232
|
|
|
@@ -222,7 +234,12 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
222
234
|
const finalTitle = titleTemplate
|
|
223
235
|
? titleTemplate.replace("%s", titleValue as string)
|
|
224
236
|
: titleValue;
|
|
225
|
-
addOrReplace(
|
|
237
|
+
addOrReplace(
|
|
238
|
+
result,
|
|
239
|
+
keyToIndex,
|
|
240
|
+
{ title: finalTitle as string },
|
|
241
|
+
"title",
|
|
242
|
+
);
|
|
226
243
|
continue;
|
|
227
244
|
}
|
|
228
245
|
|
|
@@ -241,7 +258,7 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
|
|
|
241
258
|
* Use `ctx.use(Meta)` in route handlers to push meta descriptors.
|
|
242
259
|
* Use `<MetaTags />` component to render them in the document head.
|
|
243
260
|
*/
|
|
244
|
-
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
);
|
|
261
|
+
export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
|
|
262
|
+
MetaDescriptor,
|
|
263
|
+
MetaDescriptor[]
|
|
264
|
+
>(collectMeta, "__rsc_router_meta__");
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Override Handler
|
|
3
|
+
*
|
|
4
|
+
* Manages cookie-based host override for development environments.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { HostOverrideConfig } from "./types.js";
|
|
8
|
+
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
9
|
+
import { matchPattern, parseRequest } from "./pattern-matcher.js";
|
|
10
|
+
import {
|
|
11
|
+
HostOverrideNotAllowedError,
|
|
12
|
+
InvalidHostnameError,
|
|
13
|
+
HostValidationError,
|
|
14
|
+
} from "./errors.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse cookies from request
|
|
18
|
+
*/
|
|
19
|
+
export function parseCookies(request: Request): Record<string, string> {
|
|
20
|
+
const cookieHeader = request.headers.get("cookie");
|
|
21
|
+
if (!cookieHeader) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cookies: Record<string, string> = {};
|
|
26
|
+
const pairs = cookieHeader.split(";");
|
|
27
|
+
|
|
28
|
+
for (const pair of pairs) {
|
|
29
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
30
|
+
if (name && rest.length > 0) {
|
|
31
|
+
const value = rest.join("=");
|
|
32
|
+
try {
|
|
33
|
+
cookies[name] = decodeURIComponent(value);
|
|
34
|
+
} catch {
|
|
35
|
+
cookies[name] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return cookies;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get cookie value from request
|
|
45
|
+
*/
|
|
46
|
+
export function getCookie(request: Request, name: string): string | undefined {
|
|
47
|
+
const cookies = parseCookies(request);
|
|
48
|
+
return cookies[name];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create Set-Cookie header to delete a cookie
|
|
53
|
+
*/
|
|
54
|
+
export function createDeleteCookieHeader(name: string): string {
|
|
55
|
+
return `${name}=; Max-Age=0; Path=/; Secure; HttpOnly`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create error response with cookie deletion
|
|
60
|
+
*/
|
|
61
|
+
export function createCookieErrorResponse(
|
|
62
|
+
cookieName: string,
|
|
63
|
+
message: string,
|
|
64
|
+
): Response {
|
|
65
|
+
return new Response(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
error: message,
|
|
68
|
+
message: `The ${cookieName} cookie has been cleared`,
|
|
69
|
+
}),
|
|
70
|
+
{
|
|
71
|
+
status: 400,
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Set-Cookie": createDeleteCookieHeader(cookieName),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if current host is allowed to use override
|
|
82
|
+
*/
|
|
83
|
+
export function isHostAllowed(
|
|
84
|
+
request: Request,
|
|
85
|
+
allowedHosts: string[],
|
|
86
|
+
): boolean {
|
|
87
|
+
const { hostname, pathname, parts } = parseRequest(request);
|
|
88
|
+
|
|
89
|
+
for (const pattern of allowedHosts) {
|
|
90
|
+
if (matchPattern(pattern, hostname, pathname, parts)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle cookie override logic
|
|
100
|
+
*
|
|
101
|
+
* Returns overridden hostname if valid, original hostname if no override.
|
|
102
|
+
* Throws errors for invalid overrides.
|
|
103
|
+
*/
|
|
104
|
+
export function handleCookieOverride(
|
|
105
|
+
request: Request,
|
|
106
|
+
config: HostOverrideConfig | undefined,
|
|
107
|
+
input: RouterRequestInput<any>,
|
|
108
|
+
): string {
|
|
109
|
+
if (!config) {
|
|
110
|
+
const { hostname } = parseRequest(request);
|
|
111
|
+
return hostname;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { cookieName, allowedHosts, validate } = config;
|
|
115
|
+
const cookieValue = getCookie(request, cookieName);
|
|
116
|
+
const { hostname: originalHostname } = parseRequest(request);
|
|
117
|
+
|
|
118
|
+
// No cookie - return original hostname
|
|
119
|
+
if (!cookieValue) {
|
|
120
|
+
return originalHostname;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if current host is allowed
|
|
124
|
+
const allowed = isHostAllowed(request, allowedHosts);
|
|
125
|
+
|
|
126
|
+
// If not allowed, throw error
|
|
127
|
+
if (!allowed) {
|
|
128
|
+
throw new HostOverrideNotAllowedError(originalHostname, cookieName, {
|
|
129
|
+
cause: { cookieValue, currentHost: originalHostname },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If allowed and has custom validation, run it
|
|
134
|
+
if (validate) {
|
|
135
|
+
try {
|
|
136
|
+
const validatedHostname = validate(request, cookieValue, input);
|
|
137
|
+
return validatedHostname;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Wrap in HostValidationError
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
throw new HostValidationError(message, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Default validation - verify it's a valid hostname using URL constructor
|
|
146
|
+
try {
|
|
147
|
+
// Try to construct a URL with the hostname to validate it
|
|
148
|
+
const testUrl = new URL(`https://${cookieValue}`);
|
|
149
|
+
|
|
150
|
+
// Ensure the hostname matches what we provided (URL constructor normalizes it)
|
|
151
|
+
if (testUrl.hostname !== cookieValue) {
|
|
152
|
+
throw new InvalidHostnameError(cookieValue, {
|
|
153
|
+
cause: { original: cookieValue, normalized: testUrl.hostname },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// If URL constructor failed, throw InvalidHostnameError with cause
|
|
158
|
+
if (error instanceof InvalidHostnameError) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
throw new InvalidHostnameError(cookieValue, { cause: error });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return cookieValue;
|
|
165
|
+
}
|