@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- 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 +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -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 +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -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 +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -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/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- 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/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Breadcrumbs handle for accumulating breadcrumb items across route segments.
|
|
3
|
+
*
|
|
4
|
+
* Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`.
|
|
5
|
+
* Items are collected in parent-to-child order with automatic deduplication
|
|
6
|
+
* by `href` (last item for each href wins).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // In route handler
|
|
11
|
+
* route("/blog/:slug", (ctx) => {
|
|
12
|
+
* const breadcrumb = ctx.use(Breadcrumbs);
|
|
13
|
+
* breadcrumb({ label: "Blog", href: "/blog" });
|
|
14
|
+
* breadcrumb({ label: post.title, href: `/blog/${ctx.params.slug}` });
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // In client component (consume with useHandle)
|
|
18
|
+
* const crumbs = useHandle(Breadcrumbs);
|
|
19
|
+
* crumbs.map((c) => <a href={c.href}>{c.label}</a>);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { createHandle, type Handle } from "../handle.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single breadcrumb item.
|
|
28
|
+
*
|
|
29
|
+
* @property label - Display text for the breadcrumb
|
|
30
|
+
* @property href - URL the breadcrumb links to
|
|
31
|
+
* @property content - Optional extra content (sync or async) rendered alongside the label
|
|
32
|
+
*/
|
|
33
|
+
export interface BreadcrumbItem {
|
|
34
|
+
label: string;
|
|
35
|
+
href: string;
|
|
36
|
+
content?: ReactNode | Promise<ReactNode>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Collect function for Breadcrumbs handle.
|
|
41
|
+
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
+
* (last item for each href wins).
|
|
43
|
+
*/
|
|
44
|
+
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
|
+
const all = segments.flat();
|
|
46
|
+
const seen = new Map<string, number>();
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < all.length; i++) {
|
|
49
|
+
seen.set(all[i].href, i);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Return items in order, keeping only the last occurrence per href
|
|
53
|
+
return all.filter((item, index) => seen.get(item.href) === index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Built-in handle for accumulating breadcrumb navigation items.
|
|
58
|
+
*
|
|
59
|
+
* Use `ctx.use(Breadcrumbs)` in route handlers to push breadcrumb items.
|
|
60
|
+
* Use `useHandle(Breadcrumbs)` in client components to consume them.
|
|
61
|
+
*/
|
|
62
|
+
export const Breadcrumbs: Handle<BreadcrumbItem, BreadcrumbItem[]> =
|
|
63
|
+
createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
64
|
+
collectBreadcrumbs,
|
|
65
|
+
"__rsc_router_breadcrumbs__",
|
|
66
|
+
);
|
package/src/handles/index.ts
CHANGED
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__");
|
|
@@ -4,30 +4,36 @@
|
|
|
4
4
|
* Manages cookie-based host override for development environments.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { HostOverrideConfig } from
|
|
8
|
-
import {
|
|
7
|
+
import type { HostOverrideConfig } from "./types.js";
|
|
8
|
+
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
9
|
+
import { matchPattern, parseRequest } from "./pattern-matcher.js";
|
|
9
10
|
import {
|
|
10
11
|
HostOverrideNotAllowedError,
|
|
11
12
|
InvalidHostnameError,
|
|
12
13
|
HostValidationError,
|
|
13
|
-
} from
|
|
14
|
+
} from "./errors.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Parse cookies from request
|
|
17
18
|
*/
|
|
18
19
|
export function parseCookies(request: Request): Record<string, string> {
|
|
19
|
-
const cookieHeader = request.headers.get(
|
|
20
|
+
const cookieHeader = request.headers.get("cookie");
|
|
20
21
|
if (!cookieHeader) {
|
|
21
22
|
return {};
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const cookies: Record<string, string> = {};
|
|
25
|
-
const pairs = cookieHeader.split(
|
|
26
|
+
const pairs = cookieHeader.split(";");
|
|
26
27
|
|
|
27
28
|
for (const pair of pairs) {
|
|
28
|
-
const [
|
|
29
|
-
if (
|
|
30
|
-
|
|
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
|
+
}
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -54,7 +60,7 @@ export function createDeleteCookieHeader(name: string): string {
|
|
|
54
60
|
*/
|
|
55
61
|
export function createCookieErrorResponse(
|
|
56
62
|
cookieName: string,
|
|
57
|
-
message: string
|
|
63
|
+
message: string,
|
|
58
64
|
): Response {
|
|
59
65
|
return new Response(
|
|
60
66
|
JSON.stringify({
|
|
@@ -64,10 +70,10 @@ export function createCookieErrorResponse(
|
|
|
64
70
|
{
|
|
65
71
|
status: 400,
|
|
66
72
|
headers: {
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Set-Cookie": createDeleteCookieHeader(cookieName),
|
|
69
75
|
},
|
|
70
|
-
}
|
|
76
|
+
},
|
|
71
77
|
);
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -76,7 +82,7 @@ export function createCookieErrorResponse(
|
|
|
76
82
|
*/
|
|
77
83
|
export function isHostAllowed(
|
|
78
84
|
request: Request,
|
|
79
|
-
allowedHosts: string[]
|
|
85
|
+
allowedHosts: string[],
|
|
80
86
|
): boolean {
|
|
81
87
|
const { hostname, pathname, parts } = parseRequest(request);
|
|
82
88
|
|
|
@@ -98,7 +104,7 @@ export function isHostAllowed(
|
|
|
98
104
|
export function handleCookieOverride(
|
|
99
105
|
request: Request,
|
|
100
106
|
config: HostOverrideConfig | undefined,
|
|
101
|
-
|
|
107
|
+
input: RouterRequestInput<any>,
|
|
102
108
|
): string {
|
|
103
109
|
if (!config) {
|
|
104
110
|
const { hostname } = parseRequest(request);
|
|
@@ -127,7 +133,7 @@ export function handleCookieOverride(
|
|
|
127
133
|
// If allowed and has custom validation, run it
|
|
128
134
|
if (validate) {
|
|
129
135
|
try {
|
|
130
|
-
const validatedHostname = validate(request, cookieValue,
|
|
136
|
+
const validatedHostname = validate(request, cookieValue, input);
|
|
131
137
|
return validatedHostname;
|
|
132
138
|
} catch (error) {
|
|
133
139
|
// Wrap in HostValidationError
|
package/src/host/errors.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class HostRouterError extends Error {
|
|
|
22
22
|
if (options?.cause) {
|
|
23
23
|
this.cause = options.cause;
|
|
24
24
|
}
|
|
25
|
-
this.name =
|
|
25
|
+
this.name = "HostRouterError";
|
|
26
26
|
Object.setPrototypeOf(this, HostRouterError.prototype);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -33,7 +33,7 @@ export class HostRouterError extends Error {
|
|
|
33
33
|
export class InvalidPatternError extends HostRouterError {
|
|
34
34
|
constructor(pattern: string, reason: string, options?: ErrorOptions) {
|
|
35
35
|
super(`Invalid pattern "${pattern}": ${reason}`, options);
|
|
36
|
-
this.name =
|
|
36
|
+
this.name = "InvalidPatternError";
|
|
37
37
|
Object.setPrototypeOf(this, InvalidPatternError.prototype);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -45,9 +45,9 @@ export class HostOverrideNotAllowedError extends HostRouterError {
|
|
|
45
45
|
constructor(currentHost: string, cookieName: string, options?: ErrorOptions) {
|
|
46
46
|
super(
|
|
47
47
|
`Host override not allowed on "${currentHost}" (cookie: ${cookieName})`,
|
|
48
|
-
options
|
|
48
|
+
options,
|
|
49
49
|
);
|
|
50
|
-
this.name =
|
|
50
|
+
this.name = "HostOverrideNotAllowedError";
|
|
51
51
|
Object.setPrototypeOf(this, HostOverrideNotAllowedError.prototype);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -58,7 +58,7 @@ export class HostOverrideNotAllowedError extends HostRouterError {
|
|
|
58
58
|
export class InvalidHostnameError extends HostRouterError {
|
|
59
59
|
constructor(hostname: string, options?: ErrorOptions) {
|
|
60
60
|
super(`Invalid hostname format: "${hostname}"`, options);
|
|
61
|
-
this.name =
|
|
61
|
+
this.name = "InvalidHostnameError";
|
|
62
62
|
Object.setPrototypeOf(this, InvalidHostnameError.prototype);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -69,7 +69,7 @@ export class InvalidHostnameError extends HostRouterError {
|
|
|
69
69
|
export class HostValidationError extends HostRouterError {
|
|
70
70
|
constructor(message: string, cause?: unknown) {
|
|
71
71
|
super(message, { cause });
|
|
72
|
-
this.name =
|
|
72
|
+
this.name = "HostValidationError";
|
|
73
73
|
Object.setPrototypeOf(this, HostValidationError.prototype);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -80,7 +80,7 @@ export class HostValidationError extends HostRouterError {
|
|
|
80
80
|
export class NoRouteMatchError extends HostRouterError {
|
|
81
81
|
constructor(hostname: string, pathname: string, options?: ErrorOptions) {
|
|
82
82
|
super(`No route matched for ${hostname}${pathname}`, options);
|
|
83
|
-
this.name =
|
|
83
|
+
this.name = "NoRouteMatchError";
|
|
84
84
|
Object.setPrototypeOf(this, NoRouteMatchError.prototype);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -91,7 +91,7 @@ export class NoRouteMatchError extends HostRouterError {
|
|
|
91
91
|
export class InvalidHandlerError extends HostRouterError {
|
|
92
92
|
constructor(handler: unknown, options?: ErrorOptions) {
|
|
93
93
|
super(`Invalid handler type: ${typeof handler}`, options);
|
|
94
|
-
this.name =
|
|
94
|
+
this.name = "InvalidHandlerError";
|
|
95
95
|
Object.setPrototypeOf(this, InvalidHandlerError.prototype);
|
|
96
96
|
}
|
|
97
97
|
}
|
package/src/host/index.ts
CHANGED
|
@@ -23,13 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
// Core router
|
|
26
|
-
export { createHostRouter } from
|
|
27
|
-
|
|
28
|
-
// Host router registry for build-time discovery
|
|
29
|
-
export { HostRouterRegistry, type HostRouterRegistryEntry } from './router.js';
|
|
26
|
+
export { createHostRouter } from "./router.js";
|
|
30
27
|
|
|
31
28
|
// Utilities
|
|
32
|
-
export { defineHosts } from
|
|
29
|
+
export { defineHosts } from "./utils.js";
|
|
33
30
|
|
|
34
31
|
// Errors
|
|
35
32
|
export {
|
|
@@ -40,7 +37,7 @@ export {
|
|
|
40
37
|
HostValidationError,
|
|
41
38
|
NoRouteMatchError,
|
|
42
39
|
InvalidHandlerError,
|
|
43
|
-
} from
|
|
40
|
+
} from "./errors.js";
|
|
44
41
|
|
|
45
42
|
// Types
|
|
46
43
|
export type {
|
|
@@ -53,4 +50,4 @@ export type {
|
|
|
53
50
|
HostPattern,
|
|
54
51
|
HostMatchResult,
|
|
55
52
|
HostOverrideConfig,
|
|
56
|
-
} from
|
|
53
|
+
} from "./types.js";
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
* - `example.com/admin` - specific domain with path prefix
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { InvalidPatternError } from
|
|
17
|
+
import { InvalidPatternError } from "./errors.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Normalize a pattern by removing trailing slashes from paths
|
|
21
21
|
*/
|
|
22
22
|
export function normalizePattern(pattern: string): string {
|
|
23
23
|
// If pattern has a path component, remove trailing slash
|
|
24
|
-
const slashIndex = pattern.indexOf(
|
|
24
|
+
const slashIndex = pattern.indexOf("/");
|
|
25
25
|
if (slashIndex !== -1) {
|
|
26
26
|
const domain = pattern.slice(0, slashIndex);
|
|
27
|
-
const path = pattern.slice(slashIndex).replace(/\/$/,
|
|
27
|
+
const path = pattern.slice(slashIndex).replace(/\/$/, "");
|
|
28
28
|
return domain + path;
|
|
29
29
|
}
|
|
30
30
|
return pattern;
|
|
@@ -41,7 +41,7 @@ export function parseRequest(request: Request): {
|
|
|
41
41
|
const url = new URL(request.url);
|
|
42
42
|
const hostname = url.hostname;
|
|
43
43
|
const pathname = url.pathname;
|
|
44
|
-
const parts = hostname.split(
|
|
44
|
+
const parts = hostname.split(".");
|
|
45
45
|
|
|
46
46
|
return { hostname, pathname, parts };
|
|
47
47
|
}
|
|
@@ -70,12 +70,12 @@ export function matchPattern(
|
|
|
70
70
|
pattern: string,
|
|
71
71
|
hostname: string,
|
|
72
72
|
pathname: string,
|
|
73
|
-
parts: string[]
|
|
73
|
+
parts: string[],
|
|
74
74
|
): boolean {
|
|
75
75
|
const normalized = normalizePattern(pattern);
|
|
76
76
|
|
|
77
77
|
// Check if pattern has path component
|
|
78
|
-
const slashIndex = normalized.indexOf(
|
|
78
|
+
const slashIndex = normalized.indexOf("/");
|
|
79
79
|
const hasPath = slashIndex !== -1;
|
|
80
80
|
const domainPattern = hasPath ? normalized.slice(0, slashIndex) : normalized;
|
|
81
81
|
const pathPattern = hasPath ? normalized.slice(slashIndex) : null;
|
|
@@ -88,7 +88,7 @@ export function matchPattern(
|
|
|
88
88
|
|
|
89
89
|
// Then match path (prefix match)
|
|
90
90
|
if (pathPattern) {
|
|
91
|
-
return pathname === pathPattern || pathname.startsWith(pathPattern +
|
|
91
|
+
return pathname === pathPattern || pathname.startsWith(pathPattern + "/");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
return true;
|
|
@@ -100,7 +100,7 @@ export function matchPattern(
|
|
|
100
100
|
function matchDomainPattern(
|
|
101
101
|
pattern: string,
|
|
102
102
|
hostname: string,
|
|
103
|
-
parts: string[]
|
|
103
|
+
parts: string[],
|
|
104
104
|
): boolean {
|
|
105
105
|
// Exact match
|
|
106
106
|
if (pattern === hostname) {
|
|
@@ -108,48 +108,48 @@ function matchDomainPattern(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// `.` or `*` - any apex domain
|
|
111
|
-
if (pattern ===
|
|
111
|
+
if (pattern === "." || pattern === "*") {
|
|
112
112
|
return isApexDomain(parts);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// `**` - any domain (apex + all subdomains)
|
|
116
|
-
if (pattern ===
|
|
116
|
+
if (pattern === "**") {
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// `*.` - any single-level subdomain
|
|
121
|
-
if (pattern ===
|
|
121
|
+
if (pattern === "*.") {
|
|
122
122
|
return getSubdomainLevel(parts) === 1;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// `**.` - any multi-level subdomain (2+ levels)
|
|
126
|
-
if (pattern ===
|
|
126
|
+
if (pattern === "**.") {
|
|
127
127
|
return getSubdomainLevel(parts) >= 2;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// `*.tld` - any apex domain with specific TLD (e.g., *.com)
|
|
131
|
-
if (pattern.startsWith(
|
|
131
|
+
if (pattern.startsWith("*.") && !pattern.includes(".", 2)) {
|
|
132
132
|
const tld = pattern.slice(2);
|
|
133
|
-
return isApexDomain(parts) && hostname.endsWith(
|
|
133
|
+
return isApexDomain(parts) && hostname.endsWith("." + tld);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// `*.example.com` - single subdomain of specific domain
|
|
137
|
-
if (pattern.startsWith(
|
|
137
|
+
if (pattern.startsWith("*.")) {
|
|
138
138
|
const baseDomain = pattern.slice(2);
|
|
139
|
-
if (hostname.endsWith(
|
|
139
|
+
if (hostname.endsWith("." + baseDomain)) {
|
|
140
140
|
// Count parts: if pattern is *.example.com (3 parts),
|
|
141
141
|
// hostname should have exactly 4 parts (www.example.com)
|
|
142
|
-
const patternParts = baseDomain.split(
|
|
142
|
+
const patternParts = baseDomain.split(".");
|
|
143
143
|
return parts.length === patternParts.length + 1;
|
|
144
144
|
}
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// `**.example.com` - any depth subdomain of specific domain
|
|
149
|
-
if (pattern.startsWith(
|
|
149
|
+
if (pattern.startsWith("**.")) {
|
|
150
150
|
const baseDomain = pattern.slice(3);
|
|
151
|
-
if (hostname.endsWith(
|
|
152
|
-
const patternParts = baseDomain.split(
|
|
151
|
+
if (hostname.endsWith("." + baseDomain)) {
|
|
152
|
+
const patternParts = baseDomain.split(".");
|
|
153
153
|
// Must have more parts than the base domain (i.e., has subdomains)
|
|
154
154
|
return parts.length > patternParts.length;
|
|
155
155
|
}
|
|
@@ -158,7 +158,7 @@ function matchDomainPattern(
|
|
|
158
158
|
|
|
159
159
|
// `subdomain.*` - specific subdomain of any apex domain
|
|
160
160
|
// e.g., admin.* matches admin.example.com, admin.google.com
|
|
161
|
-
if (pattern.endsWith(
|
|
161
|
+
if (pattern.endsWith(".*")) {
|
|
162
162
|
const subdomain = pattern.slice(0, -2);
|
|
163
163
|
// Must be single-level subdomain (3 parts total)
|
|
164
164
|
if (parts.length === 3 && parts[0] === subdomain) {
|
|
@@ -169,7 +169,7 @@ function matchDomainPattern(
|
|
|
169
169
|
|
|
170
170
|
// `subdomain.**` - specific subdomain of any domain (including multi-level)
|
|
171
171
|
// e.g., admin.** matches admin.example.com, admin.sub.example.com
|
|
172
|
-
if (pattern.endsWith(
|
|
172
|
+
if (pattern.endsWith(".**")) {
|
|
173
173
|
const subdomain = pattern.slice(0, -3);
|
|
174
174
|
if (parts.length >= 3 && parts[0] === subdomain) {
|
|
175
175
|
return true;
|
|
@@ -179,7 +179,7 @@ function matchDomainPattern(
|
|
|
179
179
|
|
|
180
180
|
// `subdomain.` - specific subdomain of any apex domain (no wildcard)
|
|
181
181
|
// e.g., admin. matches admin.example.com, admin.google.com
|
|
182
|
-
if (pattern.endsWith(
|
|
182
|
+
if (pattern.endsWith(".") && !pattern.includes("*")) {
|
|
183
183
|
const subdomain = pattern.slice(0, -1);
|
|
184
184
|
// Must be exactly 3 parts (subdomain.domain.tld)
|
|
185
185
|
if (parts.length === 3 && parts[0] === subdomain) {
|
|
@@ -195,17 +195,17 @@ function matchDomainPattern(
|
|
|
195
195
|
* Validate pattern format
|
|
196
196
|
*/
|
|
197
197
|
export function validatePattern(pattern: string): void {
|
|
198
|
-
if (!pattern || typeof pattern !==
|
|
198
|
+
if (!pattern || typeof pattern !== "string") {
|
|
199
199
|
throw new InvalidPatternError(
|
|
200
200
|
pattern,
|
|
201
|
-
|
|
202
|
-
{ cause: { type: typeof pattern, value: pattern } }
|
|
201
|
+
"Pattern must be a non-empty string",
|
|
202
|
+
{ cause: { type: typeof pattern, value: pattern } },
|
|
203
203
|
);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// Check for invalid characters (spaces, etc.)
|
|
207
207
|
if (/\s/.test(pattern)) {
|
|
208
|
-
throw new InvalidPatternError(pattern,
|
|
208
|
+
throw new InvalidPatternError(pattern, "contains whitespace", {
|
|
209
209
|
cause: { pattern },
|
|
210
210
|
});
|
|
211
211
|
}
|