@rangojs/router 0.0.0-experimental.10

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.
Files changed (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
package/src/handle.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Handle definition for accumulating data across route segments.
3
+ *
4
+ * Handles allow server-side route handlers to pass accumulated data to client
5
+ * components. Unlike loaders (which fetch data for specific routes), handles
6
+ * accumulate data across all matched route segments.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // Define a handle (name auto-generated from file + export)
11
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>();
12
+ *
13
+ * // Use in handler
14
+ * const push = ctx.use(Breadcrumbs);
15
+ * push({ label: "Home", href: "/" });
16
+ *
17
+ * // Consume on client
18
+ * const crumbs = useHandle(Breadcrumbs);
19
+ * ```
20
+ */
21
+ export interface Handle<TData, TAccumulated = TData[]> {
22
+ /**
23
+ * Brand to distinguish handles from loaders in ctx.use()
24
+ */
25
+ readonly __brand: "handle";
26
+
27
+ /**
28
+ * Auto-generated unique ID for this handle (used as key in storage)
29
+ * Format: "filePath#ExportName" in dev, "hash#ExportName" in production
30
+ */
31
+ readonly $$id: string;
32
+
33
+ }
34
+
35
+ /**
36
+ * Default collect function that flattens segment arrays into a single array.
37
+ */
38
+ function defaultCollect<T>(segments: T[][]): T[] {
39
+ return segments.flat();
40
+ }
41
+
42
+ // Module-level registry mapping $$id to collect functions.
43
+ // Populated when createHandle() runs (both server and client).
44
+ // Used by useHandle() to recover collect when handle is deserialized from RSC prop.
45
+ const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
46
+
47
+ /**
48
+ * Look up a collect function from the registry by handle $$id.
49
+ * Returns undefined if not registered (falls back to defaultCollect in useHandle).
50
+ */
51
+ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) | undefined {
52
+ return collectRegistry.get(id);
53
+ }
54
+
55
+ /**
56
+ * Create a handle definition for accumulating data across route segments.
57
+ *
58
+ * The $$id is auto-generated by the Vite exposeHandleId plugin based on
59
+ * file path and export name. No manual naming required.
60
+ *
61
+ * @param collect - Optional collect function (default: flatten into array)
62
+ * @param __injectedId - Auto-injected by Vite plugin, do not provide manually
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // Default: flatten into array
67
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>();
68
+ * // Result type: BreadcrumbItem[]
69
+ *
70
+ * // Custom: last value wins
71
+ * export const PageTitle = createHandle<string, string>(
72
+ * (segments) => segments.flat().at(-1) ?? "Default Title"
73
+ * );
74
+ * // Result type: string
75
+ *
76
+ * // Custom: object merge
77
+ * export const Meta = createHandle<Partial<MetaTags>, MetaTags>(
78
+ * (segments) => Object.assign({ robots: "index,follow" }, ...segments.flat())
79
+ * );
80
+ * // Result type: MetaTags
81
+ *
82
+ * // Custom: dedupe by href
83
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>(
84
+ * (segments) => {
85
+ * const all = segments.flat();
86
+ * return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
87
+ * }
88
+ * );
89
+ * ```
90
+ */
91
+ export function createHandle<TData, TAccumulated = TData[]>(
92
+ collect?: (segments: TData[][]) => TAccumulated,
93
+ __injectedId?: string
94
+ ): Handle<TData, TAccumulated> {
95
+ const handleId = __injectedId ?? "";
96
+
97
+ if (!handleId && process.env.NODE_ENV !== "production") {
98
+ console.warn(
99
+ "[rsc-router] Handle is missing $$id. " +
100
+ "Make sure the exposeHandleId Vite plugin is enabled and " +
101
+ "the handle is exported with: export const MyHandle = createHandle(...)"
102
+ );
103
+ }
104
+
105
+ const collectFn = collect ??
106
+ (defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
107
+
108
+ // Register collect in module-level registry so useHandle() can recover it
109
+ // when the handle is deserialized from RSC props (toJSON strips collect).
110
+ if (handleId) {
111
+ collectRegistry.set(handleId, collectFn as (segments: unknown[][]) => unknown);
112
+ }
113
+
114
+ return {
115
+ __brand: "handle" as const,
116
+ $$id: handleId,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Type guard to check if a value is a Handle.
122
+ */
123
+ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
124
+ return (
125
+ typeof value === "object" &&
126
+ value !== null &&
127
+ "__brand" in value &&
128
+ (value as { __brand: unknown }).__brand === "handle"
129
+ );
130
+ }
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Component to render collected meta descriptors in the document head.
5
+ *
6
+ * Supports both sync and async meta descriptors. Async descriptors
7
+ * (Promise<MetaDescriptorBase>) are resolved using React's use() hook.
8
+ *
9
+ * When theme is enabled in the router config, MetaTags also renders
10
+ * the theme initialization script to prevent FOUC (flash of unstyled content).
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * function RootLayout() {
15
+ * return (
16
+ * <html lang="en" suppressHydrationWarning>
17
+ * <head>
18
+ * <MetaTags />
19
+ * </head>
20
+ * <body>...</body>
21
+ * </html>
22
+ * );
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import { use } from "react";
28
+ import { useHandle } from "../browser/react/use-handle.js";
29
+ import { Meta } from "./meta.js";
30
+ import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
31
+ import { getSSRThemeConfig } from "../theme/theme-context.js";
32
+ import { generateThemeScript } from "../theme/theme-script.js";
33
+
34
+ // Type guards for MetaDescriptorBase variants
35
+ function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
36
+ return "charSet" in d && d.charSet === "utf-8";
37
+ }
38
+
39
+ function hasTitle(d: MetaDescriptorBase): d is { title: string } {
40
+ return "title" in d && typeof (d as { title?: unknown }).title === "string";
41
+ }
42
+
43
+ function hasNameContent(d: MetaDescriptorBase): d is { name: string; content: string } {
44
+ return "name" in d && "content" in d &&
45
+ typeof (d as { name?: unknown }).name === "string" &&
46
+ typeof (d as { content?: unknown }).content === "string";
47
+ }
48
+
49
+ function hasPropertyContent(d: MetaDescriptorBase): d is { property: string; content: string } {
50
+ return "property" in d && "content" in d &&
51
+ typeof (d as { property?: unknown }).property === "string" &&
52
+ typeof (d as { content?: unknown }).content === "string";
53
+ }
54
+
55
+ function hasHttpEquivContent(d: MetaDescriptorBase): d is { httpEquiv: string; content: string } {
56
+ return "httpEquiv" in d && "content" in d &&
57
+ typeof (d as { httpEquiv?: unknown }).httpEquiv === "string" &&
58
+ typeof (d as { content?: unknown }).content === "string";
59
+ }
60
+
61
+ function hasScriptLdJson(d: MetaDescriptorBase): d is { "script:ld+json": object } {
62
+ return "script:ld+json" in d;
63
+ }
64
+
65
+ function hasTagName(d: MetaDescriptorBase): d is { tagName: "meta" | "link"; [name: string]: string } {
66
+ return "tagName" in d && ((d as { tagName?: unknown }).tagName === "meta" || (d as { tagName?: unknown }).tagName === "link");
67
+ }
68
+
69
+ /**
70
+ * Check if a value is a Promise.
71
+ */
72
+ function isPromise(value: unknown): value is Promise<unknown> {
73
+ return value !== null && typeof value === "object" && "then" in value;
74
+ }
75
+
76
+ /**
77
+ * Render a single meta descriptor as a React element.
78
+ */
79
+ function renderMetaDescriptor(
80
+ descriptor: MetaDescriptorBase,
81
+ index: number
82
+ ): React.ReactNode {
83
+ // charset
84
+ if (hasCharSet(descriptor)) {
85
+ return <meta key="charSet" charSet={descriptor.charSet} />;
86
+ }
87
+
88
+ // title
89
+ if (hasTitle(descriptor)) {
90
+ return <title key="title">{descriptor.title}</title>;
91
+ }
92
+
93
+ // name + content (description, viewport, etc.)
94
+ if (hasNameContent(descriptor)) {
95
+ return (
96
+ <meta
97
+ key={`name-${descriptor.name}`}
98
+ name={descriptor.name}
99
+ content={descriptor.content}
100
+ />
101
+ );
102
+ }
103
+
104
+ // property + content (Open Graph, etc.)
105
+ if (hasPropertyContent(descriptor)) {
106
+ return (
107
+ <meta
108
+ key={`property-${descriptor.property}`}
109
+ property={descriptor.property}
110
+ content={descriptor.content}
111
+ />
112
+ );
113
+ }
114
+
115
+ // http-equiv + content
116
+ if (hasHttpEquivContent(descriptor)) {
117
+ return (
118
+ <meta
119
+ key={`httpEquiv-${descriptor.httpEquiv}`}
120
+ httpEquiv={descriptor.httpEquiv}
121
+ content={descriptor.content}
122
+ />
123
+ );
124
+ }
125
+
126
+ // JSON-LD structured data
127
+ if (hasScriptLdJson(descriptor)) {
128
+ const json = JSON.stringify(descriptor["script:ld+json"]);
129
+ return (
130
+ <script
131
+ key={`ld-json-${index}`}
132
+ type="application/ld+json"
133
+ dangerouslySetInnerHTML={{ __html: json }}
134
+ />
135
+ );
136
+ }
137
+
138
+ // Custom tagName (meta or link with arbitrary attributes)
139
+ if (hasTagName(descriptor)) {
140
+ const { tagName, ...rest } = descriptor;
141
+ if (tagName === "link") {
142
+ return <link key={`link-${index}`} {...(rest as React.LinkHTMLAttributes<HTMLLinkElement>)} />;
143
+ }
144
+ if (tagName === "meta") {
145
+ return <meta key={`meta-${index}`} {...(rest as React.MetaHTMLAttributes<HTMLMetaElement>)} />;
146
+ }
147
+ }
148
+
149
+ // Fallback: treat as meta attributes
150
+ return <meta key={`meta-fallback-${index}`} {...(descriptor as React.MetaHTMLAttributes<HTMLMetaElement>)} />;
151
+ }
152
+
153
+ /**
154
+ * Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
155
+ */
156
+ function AsyncMetaTag({ promise, index }: { promise: Promise<MetaDescriptorBase>; index: number }): React.ReactNode {
157
+ const resolved = use(promise);
158
+ return renderMetaDescriptor(resolved, index);
159
+ }
160
+
161
+ /**
162
+ * Renders all collected meta descriptors from route handlers.
163
+ *
164
+ * Place this component inside the `<head>` element of your document.
165
+ * It will automatically update when meta descriptors change during navigation.
166
+ *
167
+ * When theme is enabled in router config, also renders the theme initialization
168
+ * script to prevent FOUC (flash of unstyled content).
169
+ *
170
+ * Async meta descriptors (Promise<MetaDescriptorBase>) are resolved using
171
+ * React's use() hook. RSC streaming handles the Promise resolution.
172
+ */
173
+ export function MetaTags(): React.ReactNode {
174
+ const descriptors = useHandle(Meta) as MetaDescriptor[];
175
+ const themeConfig = getSSRThemeConfig();
176
+
177
+ return (
178
+ <>
179
+ {/* Theme script must be first to prevent FOUC */}
180
+ {themeConfig && (
181
+ <script
182
+ dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
183
+ />
184
+ )}
185
+ {descriptors.map((descriptor, index) => {
186
+ if (isPromise(descriptor)) {
187
+ return <AsyncMetaTag key={`async-${index}`} promise={descriptor} index={index} />;
188
+ }
189
+ return renderMetaDescriptor(descriptor, index);
190
+ })}
191
+ </>
192
+ );
193
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Built-in handles for rsc-router.
3
+ */
4
+
5
+ export { Meta } from "./meta.ts";
6
+ export { MetaTags } from "./MetaTags.tsx";
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Built-in Meta handle for managing document metadata across route segments.
3
+ *
4
+ * Provides automatic deduplication: later routes override earlier ones
5
+ * for the same meta key (title, name, property, etc.)
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // In route handler
10
+ * route("product/:id", (ctx) => {
11
+ * const meta = ctx.use(Meta);
12
+ * meta({ title: "Product Details" });
13
+ * meta({ name: "description", content: "..." });
14
+ * meta({ property: "og:title", content: "..." });
15
+ * });
16
+ *
17
+ * // In layout (renders the collected meta tags)
18
+ * function RootLayout() {
19
+ * return (
20
+ * <html>
21
+ * <head>
22
+ * <MetaTags />
23
+ * </head>
24
+ * ...
25
+ * </html>
26
+ * );
27
+ * }
28
+ * ```
29
+ */
30
+
31
+ import { createHandle, type Handle } from "../handle.js";
32
+ import type {
33
+ MetaDescriptor,
34
+ TitleDescriptor,
35
+ UnsetDescriptor,
36
+ } from "../router/types.js";
37
+
38
+ /**
39
+ * Type guard for unset descriptor
40
+ */
41
+ function isUnsetDescriptor(
42
+ descriptor: MetaDescriptor
43
+ ): descriptor is UnsetDescriptor {
44
+ return (
45
+ typeof descriptor === "object" &&
46
+ descriptor !== null &&
47
+ "unset" in descriptor &&
48
+ typeof (descriptor as UnsetDescriptor).unset === "string"
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Type guard for title descriptor (any form)
54
+ */
55
+ function isTitleDescriptor(
56
+ descriptor: MetaDescriptor
57
+ ): descriptor is { title: TitleDescriptor } {
58
+ return (
59
+ typeof descriptor === "object" &&
60
+ descriptor !== null &&
61
+ "title" in descriptor
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Type guard for title template descriptor
67
+ */
68
+ function isTitleTemplate(
69
+ title: TitleDescriptor
70
+ ): title is { template: string; default: string } {
71
+ return (
72
+ typeof title === "object" &&
73
+ title !== null &&
74
+ "template" in title &&
75
+ "default" in title
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Type guard for absolute title descriptor
81
+ */
82
+ function isAbsoluteTitle(title: TitleDescriptor): title is { absolute: string } {
83
+ return typeof title === "object" && title !== null && "absolute" in title;
84
+ }
85
+
86
+ /**
87
+ * Get a unique key for a meta descriptor for deduplication.
88
+ * Returns undefined for descriptors that shouldn't be deduplicated.
89
+ */
90
+ function getMetaKey(descriptor: MetaDescriptor): string | undefined {
91
+ // Skip unset descriptors - they are processed separately
92
+ if (isUnsetDescriptor(descriptor)) {
93
+ return undefined;
94
+ }
95
+ if ("charSet" in descriptor) {
96
+ return "charSet";
97
+ }
98
+ if ("title" in descriptor) {
99
+ return "title";
100
+ }
101
+ if ("name" in descriptor && "content" in descriptor) {
102
+ return `name:${descriptor.name}`;
103
+ }
104
+ if ("property" in descriptor && "content" in descriptor) {
105
+ return `property:${descriptor.property}`;
106
+ }
107
+ if ("httpEquiv" in descriptor && "content" in descriptor) {
108
+ return `httpEquiv:${descriptor.httpEquiv}`;
109
+ }
110
+ if ("script:ld+json" in descriptor) {
111
+ // JSON-LD scripts can have multiple, don't dedupe by default
112
+ return undefined;
113
+ }
114
+ if ("tagName" in descriptor) {
115
+ // For link tags, dedupe by rel if present
116
+ if (descriptor.tagName === "link" && "rel" in descriptor) {
117
+ // Some link rels should be unique (canonical), others not (stylesheet)
118
+ const uniqueRels = ["canonical", "icon", "apple-touch-icon"];
119
+ if (uniqueRels.includes(descriptor.rel as string)) {
120
+ return `link:${descriptor.rel}`;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ /**
129
+ * Default meta descriptors included automatically.
130
+ * These can be overridden by route handlers.
131
+ */
132
+ const defaultMetaDescriptors: MetaDescriptor[] = [
133
+ { charSet: "utf-8" },
134
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
135
+ ];
136
+
137
+ /**
138
+ * Helper to add or replace a descriptor in the result array
139
+ */
140
+ function addOrReplace(
141
+ result: MetaDescriptor[],
142
+ keyToIndex: Map<string, number>,
143
+ descriptor: MetaDescriptor,
144
+ key: string | undefined
145
+ ): void {
146
+ if (key !== undefined && keyToIndex.has(key)) {
147
+ result[keyToIndex.get(key)!] = descriptor;
148
+ } else {
149
+ if (key !== undefined) {
150
+ keyToIndex.set(key, result.length);
151
+ }
152
+ result.push(descriptor);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Helper to update indices after removing an element
158
+ */
159
+ function updateIndicesAfterRemoval(
160
+ keyToIndex: Map<string, number>,
161
+ removedIndex: number
162
+ ): void {
163
+ for (const [key, index] of keyToIndex) {
164
+ if (index > removedIndex) {
165
+ keyToIndex.set(key, index - 1);
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Collect function for Meta handle.
172
+ * Includes default meta descriptors, then deduplicates by key with later routes overriding earlier ones.
173
+ * Supports title templates, absolute titles, and unset descriptors.
174
+ */
175
+ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
176
+ const result: MetaDescriptor[] = [];
177
+ const keyToIndex = new Map<string, number>();
178
+ let titleTemplate: string | undefined;
179
+
180
+ // Add defaults first so they can be overridden
181
+ for (const descriptor of defaultMetaDescriptors) {
182
+ const key = getMetaKey(descriptor);
183
+ if (key !== undefined) {
184
+ keyToIndex.set(key, result.length);
185
+ }
186
+ result.push(descriptor);
187
+ }
188
+
189
+ for (const descriptors of segments) {
190
+ for (const descriptor of descriptors) {
191
+ // Handle unset descriptors
192
+ if (isUnsetDescriptor(descriptor)) {
193
+ const keyToRemove = descriptor.unset;
194
+ if (keyToIndex.has(keyToRemove)) {
195
+ const idx = keyToIndex.get(keyToRemove)!;
196
+ result.splice(idx, 1);
197
+ keyToIndex.delete(keyToRemove);
198
+ updateIndicesAfterRemoval(keyToIndex, idx);
199
+ }
200
+ continue;
201
+ }
202
+
203
+ // Handle title descriptors with template/absolute support
204
+ if (isTitleDescriptor(descriptor)) {
205
+ const titleValue = descriptor.title;
206
+
207
+ if (isTitleTemplate(titleValue)) {
208
+ // Store template for subsequent title descriptors in child segments
209
+ titleTemplate = titleValue.template;
210
+ // Set the default title
211
+ addOrReplace(result, keyToIndex, { title: titleValue.default }, "title");
212
+ continue;
213
+ }
214
+
215
+ if (isAbsoluteTitle(titleValue)) {
216
+ // Absolute title bypasses any template
217
+ addOrReplace(result, keyToIndex, { title: titleValue.absolute }, "title");
218
+ continue;
219
+ }
220
+
221
+ // String title - apply template if one exists
222
+ const finalTitle = titleTemplate
223
+ ? titleTemplate.replace("%s", titleValue as string)
224
+ : titleValue;
225
+ addOrReplace(result, keyToIndex, { title: finalTitle as string }, "title");
226
+ continue;
227
+ }
228
+
229
+ // Handle all other descriptors
230
+ const key = getMetaKey(descriptor);
231
+ addOrReplace(result, keyToIndex, descriptor, key);
232
+ }
233
+ }
234
+
235
+ return result;
236
+ }
237
+
238
+ /**
239
+ * Built-in handle for managing document metadata.
240
+ *
241
+ * Use `ctx.use(Meta)` in route handlers to push meta descriptors.
242
+ * Use `<MetaTags />` component to render them in the document head.
243
+ */
244
+ export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<MetaDescriptor, MetaDescriptor[]>(
245
+ collectMeta,
246
+ "__rsc_router_meta__"
247
+ );