@rangojs/router 0.0.0-experimental.6 → 0.0.0-experimental.64e3ae06
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 +1606 -0
- package/dist/vite/index.js +4567 -769
- package/package.json +77 -58
- 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 +167 -0
- package/skills/hooks/SKILL.md +334 -72
- 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 +89 -30
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +318 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -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 +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +282 -557
- package/src/browser/navigation-client.ts +170 -71
- 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 +292 -310
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -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 +193 -73
- 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 +32 -79
- package/src/browser/react/use-href.tsx +2 -2
- 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 +167 -55
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +118 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +13 -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 +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 +338 -0
- package/src/cache/cache-scope.ts +122 -305
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- 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 +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 +106 -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 +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +15 -29
- 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 +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 +119 -29
- package/src/index.rsc.ts +153 -19
- package/src/index.ts +211 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -147
- 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 +7 -4
- 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 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +59 -8
- 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 +374 -81
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +150 -35
- 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 +25 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- 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 +327 -369
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- 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 +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 +1242 -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 +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 +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +665 -4182
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +764 -754
- 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 +230 -0
- package/src/segment-system.tsx +26 -14
- package/src/server/context.ts +182 -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 +439 -73
- package/src/server.ts +35 -128
- package/src/ssr/index.tsx +100 -31
- 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 +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1623
- 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 -802
- 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 -782
- 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 -53
- 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 +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -16
- 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/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 -43
- 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/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Classes for Host Router
|
|
3
|
+
*
|
|
4
|
+
* All host router errors extend HostRouterError for easy instance checking.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error options with cause
|
|
9
|
+
*/
|
|
10
|
+
interface ErrorOptions {
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base error class for all host router errors
|
|
16
|
+
*/
|
|
17
|
+
export class HostRouterError extends Error {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
|
|
20
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
21
|
+
super(message);
|
|
22
|
+
if (options?.cause) {
|
|
23
|
+
this.cause = options.cause;
|
|
24
|
+
}
|
|
25
|
+
this.name = "HostRouterError";
|
|
26
|
+
Object.setPrototypeOf(this, HostRouterError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Error thrown when pattern validation fails
|
|
32
|
+
*/
|
|
33
|
+
export class InvalidPatternError extends HostRouterError {
|
|
34
|
+
constructor(pattern: string, reason: string, options?: ErrorOptions) {
|
|
35
|
+
super(`Invalid pattern "${pattern}": ${reason}`, options);
|
|
36
|
+
this.name = "InvalidPatternError";
|
|
37
|
+
Object.setPrototypeOf(this, InvalidPatternError.prototype);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error thrown when cookie override is not allowed
|
|
43
|
+
*/
|
|
44
|
+
export class HostOverrideNotAllowedError extends HostRouterError {
|
|
45
|
+
constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
|
|
46
|
+
super(
|
|
47
|
+
`Host override not allowed on "${currentHost}" (cookie: ${cookieName})`,
|
|
48
|
+
options,
|
|
49
|
+
);
|
|
50
|
+
this.name = "HostOverrideNotAllowedError";
|
|
51
|
+
Object.setPrototypeOf(this, HostOverrideNotAllowedError.prototype);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Error thrown when cookie hostname is invalid
|
|
57
|
+
*/
|
|
58
|
+
export class InvalidHostnameError extends HostRouterError {
|
|
59
|
+
constructor(hostname: string, options?: ErrorOptions) {
|
|
60
|
+
super(`Invalid hostname format: "${hostname}"`, options);
|
|
61
|
+
this.name = "InvalidHostnameError";
|
|
62
|
+
Object.setPrototypeOf(this, InvalidHostnameError.prototype);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Error thrown when custom validation fails
|
|
68
|
+
*/
|
|
69
|
+
export class HostValidationError extends HostRouterError {
|
|
70
|
+
constructor(message: string, cause?: unknown) {
|
|
71
|
+
super(message, { cause });
|
|
72
|
+
this.name = "HostValidationError";
|
|
73
|
+
Object.setPrototypeOf(this, HostValidationError.prototype);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Error thrown when no route matches
|
|
79
|
+
*/
|
|
80
|
+
export class NoRouteMatchError extends HostRouterError {
|
|
81
|
+
constructor(hostname: string, pathname: string, options?: ErrorOptions) {
|
|
82
|
+
super(`No route matched for ${hostname}${pathname}`, options);
|
|
83
|
+
this.name = "NoRouteMatchError";
|
|
84
|
+
Object.setPrototypeOf(this, NoRouteMatchError.prototype);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Error thrown when handler type is invalid
|
|
90
|
+
*/
|
|
91
|
+
export class InvalidHandlerError extends HostRouterError {
|
|
92
|
+
constructor(handler: unknown, options?: ErrorOptions) {
|
|
93
|
+
super(`Invalid handler type: ${typeof handler}`, options);
|
|
94
|
+
this.name = "InvalidHandlerError";
|
|
95
|
+
Object.setPrototypeOf(this, InvalidHandlerError.prototype);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Router
|
|
3
|
+
*
|
|
4
|
+
* A routing system for managing multi-application hosting based on
|
|
5
|
+
* domain/subdomain patterns with support for cookie-based host override
|
|
6
|
+
* for development environments.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createHostRouter } from '@rangojs/router/host';
|
|
11
|
+
*
|
|
12
|
+
* const router = createHostRouter();
|
|
13
|
+
*
|
|
14
|
+
* router.host(['.']).map(() => import('./apps/main'));
|
|
15
|
+
* router.host(['admin.*']).map(() => import('./apps/admin'));
|
|
16
|
+
*
|
|
17
|
+
* export default {
|
|
18
|
+
* fetch(request) {
|
|
19
|
+
* return router.match(request);
|
|
20
|
+
* }
|
|
21
|
+
* };
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Core router
|
|
26
|
+
export { createHostRouter } from "./router.js";
|
|
27
|
+
|
|
28
|
+
// Utilities
|
|
29
|
+
export { defineHosts } from "./utils.js";
|
|
30
|
+
|
|
31
|
+
// Errors
|
|
32
|
+
export {
|
|
33
|
+
HostRouterError,
|
|
34
|
+
InvalidPatternError,
|
|
35
|
+
HostOverrideNotAllowedError,
|
|
36
|
+
InvalidHostnameError,
|
|
37
|
+
HostValidationError,
|
|
38
|
+
NoRouteMatchError,
|
|
39
|
+
InvalidHandlerError,
|
|
40
|
+
} from "./errors.js";
|
|
41
|
+
|
|
42
|
+
// Types
|
|
43
|
+
export type {
|
|
44
|
+
HostRouter,
|
|
45
|
+
HostRouteBuilder,
|
|
46
|
+
HostRouterOptions,
|
|
47
|
+
Handler,
|
|
48
|
+
LazyHandler,
|
|
49
|
+
Middleware,
|
|
50
|
+
HostPattern,
|
|
51
|
+
HostMatchResult,
|
|
52
|
+
HostOverrideConfig,
|
|
53
|
+
} from "./types.js";
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matching Engine
|
|
3
|
+
*
|
|
4
|
+
* Handles matching of hostnames and paths against various patterns:
|
|
5
|
+
* - `.` or `*` - any apex domain
|
|
6
|
+
* - `**` - any domain (apex + subdomains)
|
|
7
|
+
* - `*.` - any single-level subdomain
|
|
8
|
+
* - `**.` - any multi-level subdomain
|
|
9
|
+
* - `example.com` - exact domain
|
|
10
|
+
* - `*.com` - any apex .com domain
|
|
11
|
+
* - `*.example.com` - subdomain of example.com
|
|
12
|
+
* - `**.example.com` - any depth subdomain
|
|
13
|
+
* - `admin.*` - admin subdomain of any apex
|
|
14
|
+
* - `example.com/admin` - specific domain with path prefix
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { InvalidPatternError } from "./errors.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a pattern by removing trailing slashes from paths
|
|
21
|
+
*/
|
|
22
|
+
export function normalizePattern(pattern: string): string {
|
|
23
|
+
// If pattern has a path component, remove trailing slash
|
|
24
|
+
const slashIndex = pattern.indexOf("/");
|
|
25
|
+
if (slashIndex !== -1) {
|
|
26
|
+
const domain = pattern.slice(0, slashIndex);
|
|
27
|
+
const path = pattern.slice(slashIndex).replace(/\/$/, "");
|
|
28
|
+
return domain + path;
|
|
29
|
+
}
|
|
30
|
+
return pattern;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse hostname and path from request URL
|
|
35
|
+
*/
|
|
36
|
+
export function parseRequest(request: Request): {
|
|
37
|
+
hostname: string;
|
|
38
|
+
pathname: string;
|
|
39
|
+
parts: string[];
|
|
40
|
+
} {
|
|
41
|
+
const url = new URL(request.url);
|
|
42
|
+
const hostname = url.hostname;
|
|
43
|
+
const pathname = url.pathname;
|
|
44
|
+
const parts = hostname.split(".");
|
|
45
|
+
|
|
46
|
+
return { hostname, pathname, parts };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Count subdomain levels (0 for apex, 1+ for subdomains)
|
|
51
|
+
*/
|
|
52
|
+
function getSubdomainLevel(parts: string[]): number {
|
|
53
|
+
// Apex domain has 2 parts (example.com)
|
|
54
|
+
// Single subdomain has 3 parts (www.example.com)
|
|
55
|
+
// Multi-level has 4+ parts (a.b.example.com)
|
|
56
|
+
return Math.max(0, parts.length - 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if hostname is an apex domain (no subdomains)
|
|
61
|
+
*/
|
|
62
|
+
function isApexDomain(parts: string[]): boolean {
|
|
63
|
+
return parts.length === 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Match a single pattern against hostname and path
|
|
68
|
+
*/
|
|
69
|
+
export function matchPattern(
|
|
70
|
+
pattern: string,
|
|
71
|
+
hostname: string,
|
|
72
|
+
pathname: string,
|
|
73
|
+
parts: string[],
|
|
74
|
+
): boolean {
|
|
75
|
+
const normalized = normalizePattern(pattern);
|
|
76
|
+
|
|
77
|
+
// Check if pattern has path component
|
|
78
|
+
const slashIndex = normalized.indexOf("/");
|
|
79
|
+
const hasPath = slashIndex !== -1;
|
|
80
|
+
const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
|
|
81
|
+
const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
|
|
82
|
+
|
|
83
|
+
// First match domain
|
|
84
|
+
const domainMatch = matchDomainPattern(domainPattern, hostname, parts);
|
|
85
|
+
if (!domainMatch) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Then match path (prefix match)
|
|
90
|
+
if (pathPattern) {
|
|
91
|
+
return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Match domain pattern against hostname
|
|
99
|
+
*/
|
|
100
|
+
function matchDomainPattern(
|
|
101
|
+
pattern: string,
|
|
102
|
+
hostname: string,
|
|
103
|
+
parts: string[],
|
|
104
|
+
): boolean {
|
|
105
|
+
// Exact match
|
|
106
|
+
if (pattern === hostname) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `.` or `*` - any apex domain
|
|
111
|
+
if (pattern === "." || pattern === "*") {
|
|
112
|
+
return isApexDomain(parts);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// `**` - any domain (apex + all subdomains)
|
|
116
|
+
if (pattern === "**") {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `*.` - any single-level subdomain
|
|
121
|
+
if (pattern === "*.") {
|
|
122
|
+
return getSubdomainLevel(parts) === 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `**.` - any multi-level subdomain (2+ levels)
|
|
126
|
+
if (pattern === "**.") {
|
|
127
|
+
return getSubdomainLevel(parts) >= 2;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// `*.tld` - any apex domain with specific TLD (e.g., *.com)
|
|
131
|
+
if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
|
|
132
|
+
const tld = pattern.slice(2);
|
|
133
|
+
return isApexDomain(parts) && hostname.endsWith("." + tld);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// `*.example.com` - single subdomain of specific domain
|
|
137
|
+
if (pattern.startsWith("*.")) {
|
|
138
|
+
const baseDomain = pattern.slice(2);
|
|
139
|
+
if (hostname.endsWith("." + baseDomain)) {
|
|
140
|
+
// Count parts: if pattern is *.example.com (3 parts),
|
|
141
|
+
// hostname should have exactly 4 parts (www.example.com)
|
|
142
|
+
const patternParts = baseDomain.split(".");
|
|
143
|
+
return parts.length === patternParts.length + 1;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// `**.example.com` - any depth subdomain of specific domain
|
|
149
|
+
if (pattern.startsWith("**.")) {
|
|
150
|
+
const baseDomain = pattern.slice(3);
|
|
151
|
+
if (hostname.endsWith("." + baseDomain)) {
|
|
152
|
+
const patternParts = baseDomain.split(".");
|
|
153
|
+
// Must have more parts than the base domain (i.e., has subdomains)
|
|
154
|
+
return parts.length > patternParts.length;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// `subdomain.*` - specific subdomain of any apex domain
|
|
160
|
+
// e.g., admin.* matches admin.example.com, admin.google.com
|
|
161
|
+
if (pattern.endsWith(".*")) {
|
|
162
|
+
const subdomain = pattern.slice(0, -2);
|
|
163
|
+
// Must be single-level subdomain (3 parts total)
|
|
164
|
+
if (parts.length === 3 && parts[0] === subdomain) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// `subdomain.**` - specific subdomain of any domain (including multi-level)
|
|
171
|
+
// e.g., admin.** matches admin.example.com, admin.sub.example.com
|
|
172
|
+
if (pattern.endsWith(".**")) {
|
|
173
|
+
const subdomain = pattern.slice(0, -3);
|
|
174
|
+
if (parts.length >= 3 && parts[0] === subdomain) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// `subdomain.` - specific subdomain of any apex domain (no wildcard)
|
|
181
|
+
// e.g., admin. matches admin.example.com, admin.google.com
|
|
182
|
+
if (pattern.endsWith(".") && !pattern.includes("*")) {
|
|
183
|
+
const subdomain = pattern.slice(0, -1);
|
|
184
|
+
// Must be exactly 3 parts (subdomain.domain.tld)
|
|
185
|
+
if (parts.length === 3 && parts[0] === subdomain) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validate pattern format
|
|
196
|
+
*/
|
|
197
|
+
export function validatePattern(pattern: string): void {
|
|
198
|
+
if (!pattern || typeof pattern !== "string") {
|
|
199
|
+
throw new InvalidPatternError(
|
|
200
|
+
pattern,
|
|
201
|
+
"Pattern must be a non-empty string",
|
|
202
|
+
{ cause: { type: typeof pattern, value: pattern } },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for invalid characters (spaces, etc.)
|
|
207
|
+
if (/\s/.test(pattern)) {
|
|
208
|
+
throw new InvalidPatternError(pattern, "contains whitespace", {
|
|
209
|
+
cause: { pattern },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Additional validation can be added here
|
|
214
|
+
}
|