@rangojs/router 0.0.0-experimental.002d056c

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 (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Cookie Store — Next.js-style cookie facade backed by the response-derived model.
3
+ *
4
+ * `cookies()` returns a CookieStore scoped to the current request.
5
+ * Reads merge the original Cookie header with Set-Cookie mutations
6
+ * already queued on the response stub (last-write-wins).
7
+ * Writes append Set-Cookie to the response stub.
8
+ */
9
+
10
+ import type { CookieOptions } from "../router/middleware-types.js";
11
+ import { getRequestContext } from "./request-context.js";
12
+ import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
+
14
+ /**
15
+ * A single cookie entry returned by get() and getAll().
16
+ */
17
+ export interface Cookie {
18
+ name: string;
19
+ value: string;
20
+ }
21
+
22
+ /**
23
+ * Request-scoped cookie store.
24
+ *
25
+ * Reads see the effective merged view (original request + same-request mutations).
26
+ * Writes append Set-Cookie headers to the shared response stub.
27
+ */
28
+ export interface CookieStore {
29
+ /** Get a single cookie by name. Returns undefined if not set or deleted. */
30
+ get(name: string): Cookie | undefined;
31
+
32
+ /** Get all effective cookies, or all cookies with a given name. */
33
+ getAll(name?: string): Cookie[];
34
+
35
+ /** Check whether a cookie exists in the effective view. */
36
+ has(name: string): boolean;
37
+
38
+ /** Set a cookie (appends Set-Cookie to the response stub). */
39
+ set(name: string, value: string, options?: CookieOptions): void;
40
+
41
+ /** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
42
+ delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
43
+ }
44
+
45
+ /**
46
+ * Get the request-scoped cookie store.
47
+ *
48
+ * Must be called inside a request context (middleware, handler, loader, action).
49
+ * Throws if called outside request scope.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import { cookies } from "@rangojs/router";
54
+ *
55
+ * // In a handler, loader, or action:
56
+ * const session = cookies().get("session")?.value;
57
+ * cookies().set("session", "new-token", { httpOnly: true });
58
+ * cookies().delete("session");
59
+ * ```
60
+ */
61
+ export function cookies(): CookieStore {
62
+ const ctx = getRequestContext();
63
+ assertNotInsideCacheContext(ctx, "cookies");
64
+ return createCookieStore(ctx);
65
+ }
66
+
67
+ /**
68
+ * Read-only view of HTTP headers.
69
+ * Exposes only the read methods of the Headers API.
70
+ */
71
+ export interface ReadonlyHeaders {
72
+ get(name: string): string | null;
73
+ has(name: string): boolean;
74
+ entries(): HeadersIterator<[string, string]>;
75
+ keys(): HeadersIterator<string>;
76
+ values(): HeadersIterator<string>;
77
+ forEach(
78
+ callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
79
+ ): void;
80
+ [Symbol.iterator](): HeadersIterator<[string, string]>;
81
+ }
82
+
83
+ // Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
84
+ type HeadersIterator<T> = IterableIterator<T>;
85
+
86
+ /**
87
+ * Throw if called inside a "use cache" function.
88
+ * Reading request-scoped data (cookies, headers) inside a cached function
89
+ * produces results that vary per request but the cache key does not include
90
+ * those values, leading to one user's data being served to another.
91
+ */
92
+ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
+ if (
94
+ ctx !== null &&
95
+ ctx !== undefined &&
96
+ typeof ctx === "object" &&
97
+ (INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
98
+ ) {
99
+ throw new Error(
100
+ `${fnName}() cannot be called inside a "use cache" function. ` +
101
+ `Request-scoped data (cookies, headers) varies per request but is not ` +
102
+ `reflected in the cache key, so cached results would be served to the ` +
103
+ `wrong users. Extract the value before the cached function and pass it ` +
104
+ `as an argument:\n\n` +
105
+ ` const locale = cookies().get("locale")?.value ?? "en";\n` +
106
+ ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
+ );
108
+ }
109
+ }
110
+
111
+ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
112
+
113
+ /**
114
+ * Get the original request headers (read-only).
115
+ *
116
+ * Must be called inside a request context.
117
+ * Returns a read-only view of the incoming request's headers.
118
+ * Mutation methods (set, append, delete) throw at runtime.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { headers } from "@rangojs/router";
123
+ *
124
+ * const auth = headers().get("authorization");
125
+ * const contentType = headers().get("content-type");
126
+ * ```
127
+ */
128
+ export function headers(): ReadonlyHeaders {
129
+ const ctx = getRequestContext();
130
+ assertNotInsideCacheContext(ctx, "headers");
131
+ return new Proxy(ctx.request.headers, {
132
+ get(target, prop, receiver) {
133
+ if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
134
+ return () => {
135
+ throw new Error(
136
+ `headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
137
+ `Use ctx.header() to set response headers.`,
138
+ );
139
+ };
140
+ }
141
+ const value = Reflect.get(target, prop, receiver);
142
+ return typeof value === "function" ? value.bind(target) : value;
143
+ },
144
+ }) as unknown as ReadonlyHeaders;
145
+ }
146
+
147
+ /**
148
+ * Create a CookieStore backed by a RequestContext.
149
+ * @internal Shared between cookies() shorthand and context methods.
150
+ */
151
+ function createCookieStore(ctx: {
152
+ cookie(name: string): string | undefined;
153
+ cookies(): Record<string, string>;
154
+ setCookie(name: string, value: string, options?: CookieOptions): void;
155
+ deleteCookie(
156
+ name: string,
157
+ options?: Pick<CookieOptions, "domain" | "path">,
158
+ ): void;
159
+ }): CookieStore {
160
+ return {
161
+ get(name: string): Cookie | undefined {
162
+ const value = ctx.cookie(name);
163
+ return value !== undefined ? { name, value } : undefined;
164
+ },
165
+
166
+ getAll(name?: string): Cookie[] {
167
+ const all = ctx.cookies();
168
+ if (name !== undefined) {
169
+ const value = all[name];
170
+ return value !== undefined ? [{ name, value }] : [];
171
+ }
172
+ return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
173
+ },
174
+
175
+ has(name: string): boolean {
176
+ return ctx.cookie(name) !== undefined;
177
+ },
178
+
179
+ set(name: string, value: string, options?: CookieOptions): void {
180
+ ctx.setCookie(name, value, options);
181
+ },
182
+
183
+ delete(
184
+ name: string,
185
+ options?: Pick<CookieOptions, "domain" | "path">,
186
+ ): void {
187
+ ctx.deleteCookie(name, options);
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Fetchable loader store - internal registry for fetchable loader functions.
3
+ *
4
+ * Extracted into its own module to avoid circular dependencies between
5
+ * loader.rsc.ts and request-context.ts. This module has no imports from
6
+ * either, so both can safely import from here.
7
+ *
8
+ * Populated by createLoader() in loader.rsc.ts.
9
+ * Read by request-context.ts (for ctx.use()) and loader-registry.ts (for GET-based fetching).
10
+ */
11
+
12
+ import type { LoaderFn } from "../types.js";
13
+ import type { MiddlewareFn } from "../router/middleware.js";
14
+
15
+ export interface LoaderRegistryEntry {
16
+ fn: LoaderFn<any, any, any>;
17
+ middleware: MiddlewareFn[];
18
+ /** Whether this loader is fetchable via the _rsc_loader endpoint. */
19
+ fetchable: boolean;
20
+ }
21
+
22
+ const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
23
+
24
+ export function registerFetchableLoader(
25
+ id: string,
26
+ fn: LoaderFn<any, any, any>,
27
+ middleware: MiddlewareFn[],
28
+ fetchable: boolean,
29
+ ): void {
30
+ fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
31
+ }
32
+
33
+ export function getFetchableLoader(
34
+ id: string,
35
+ ): LoaderRegistryEntry | undefined {
36
+ return fetchableLoaderRegistry.get(id);
37
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Handle data structure: handleName -> segmentId -> entries[]
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * {
7
+ * "breadcrumbs": {
8
+ * "$root.layout": [{ label: "Home", href: "/" }],
9
+ * "shop.layout": [{ label: "Shop", href: "/shop" }],
10
+ * }
11
+ * }
12
+ * ```
13
+ */
14
+ export type HandleData = Record<string, Record<string, unknown[]>>;
15
+
16
+ function createLateHandlePushError(
17
+ handleName: string,
18
+ segmentId: string,
19
+ ): Error {
20
+ const error = new Error(
21
+ `Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
22
+ `This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
23
+ `Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
24
+ );
25
+ error.name = "LateHandlePushError";
26
+ return error;
27
+ }
28
+
29
+ /**
30
+ * Deep clone handle data to create a snapshot.
31
+ * @internal
32
+ */
33
+ function cloneHandleData(data: HandleData): HandleData {
34
+ const clone: HandleData = {};
35
+ for (const handleName in data) {
36
+ clone[handleName] = {};
37
+ for (const segmentId in data[handleName]) {
38
+ clone[handleName][segmentId] = [...data[handleName][segmentId]];
39
+ }
40
+ }
41
+ return clone;
42
+ }
43
+
44
+ /**
45
+ * HandleStore tracks pending handler promises and stores handle data.
46
+ *
47
+ * Combines two responsibilities:
48
+ * 1. Promise tracking - know when all handlers have resolved
49
+ * 2. Data storage - collect handle data pushed by handlers
50
+ * 3. Streaming - emit handle data via async iterator on each push
51
+ */
52
+ export interface HandleStore {
53
+ /**
54
+ * Track a handler promise (non-blocking).
55
+ * Returns the promise unchanged - just registers it for tracking.
56
+ */
57
+ track<T>(promise: Promise<T>): Promise<T>;
58
+
59
+ /**
60
+ * Signal that no more track() calls will be made.
61
+ * settled will not resolve until seal() is called AND all tracked
62
+ * promises have settled. Calling stream() or getData() auto-seals.
63
+ */
64
+ seal(): void;
65
+
66
+ /**
67
+ * Promise that resolves when the store is sealed AND all tracked
68
+ * handlers have settled.
69
+ */
70
+ readonly settled: Promise<void>;
71
+
72
+ /**
73
+ * Optional error callback for late streaming-handle failures.
74
+ * Called when push() throws LateHandlePushError (handle pushed after
75
+ * stream completion). Allows the router to surface these errors
76
+ * to onError and telemetry.
77
+ */
78
+ onError?: (error: Error) => void;
79
+
80
+ /**
81
+ * Push handle data for a specific handle and segment.
82
+ * Multiple pushes to the same handle/segment accumulate in an array.
83
+ * Each push triggers an emission on the stream.
84
+ */
85
+ push(handleName: string, segmentId: string, data: unknown): void;
86
+
87
+ /**
88
+ * Get all collected handle data after all handlers have settled.
89
+ * Waits for `settled`, then returns the finalized data.
90
+ */
91
+ getData(): Promise<HandleData>;
92
+
93
+ /**
94
+ * Get an async iterator that yields handle data on each push.
95
+ * The iterator completes when all handlers have settled.
96
+ * Each yield contains the full accumulated state (not just the delta).
97
+ */
98
+ stream(): AsyncGenerator<HandleData, void, unknown>;
99
+
100
+ /**
101
+ * Get handle data for a specific segment (for caching).
102
+ * Returns data in format: { handleName: [values...] }
103
+ */
104
+ getDataForSegment(segmentId: string): Record<string, unknown[]>;
105
+
106
+ /**
107
+ * Replay cached handle data back into the store (for cache hits).
108
+ * Used to restore handle data when serving cached segments.
109
+ */
110
+ replaySegmentData(
111
+ segmentId: string,
112
+ segmentHandles: Record<string, unknown[]>,
113
+ ): void;
114
+ }
115
+
116
+ /**
117
+ * Create a new HandleStore instance.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const handleStore = createHandleStore();
122
+ *
123
+ * // In router - track without awaiting
124
+ * const component = handleStore.track(entry.handler(context));
125
+ *
126
+ * // In handler - push handle data (value, promise, or async callback result)
127
+ * handleStore.push("breadcrumbs", segmentId, { label: "Home", href: "/" });
128
+ * handleStore.push("meta", segmentId, fetchMetaAsync()); // promise
129
+ *
130
+ * // Stream handle data progressively
131
+ * for await (const handles of handleStore.stream()) {
132
+ * console.log("Handle update:", handles);
133
+ * }
134
+ * ```
135
+ */
136
+ export function createHandleStore(): HandleStore {
137
+ const data: HandleData = {};
138
+
139
+ // Settlement barrier: resolved only when sealed AND inflight === 0.
140
+ // seal() signals "no more track() calls". Each track() increments
141
+ // inflightCount, each promise.finally() decrements. settled resolves
142
+ // once both conditions are met — even if tracks are added while
143
+ // earlier ones are still in flight.
144
+ let sealed = false;
145
+ let inflightCount = 0;
146
+ let drainWaiters: (() => void)[] = [];
147
+
148
+ function notifyDrain() {
149
+ if (sealed && inflightCount === 0 && drainWaiters.length > 0) {
150
+ const waiters = drainWaiters;
151
+ drainWaiters = [];
152
+ for (const resolve of waiters) resolve();
153
+ }
154
+ }
155
+
156
+ function sealInternal() {
157
+ if (sealed) return;
158
+ sealed = true;
159
+ notifyDrain();
160
+ }
161
+
162
+ // Queue for pending emissions and resolver for waiting consumer
163
+ let pendingEmissions: HandleData[] = [];
164
+ let emissionResolver: (() => void) | null = null;
165
+ let completed = false;
166
+
167
+ // Signal that a new emission is available
168
+ function signalEmission() {
169
+ if (emissionResolver) {
170
+ const resolver = emissionResolver;
171
+ emissionResolver = null;
172
+ resolver();
173
+ }
174
+ }
175
+
176
+ // Wait for the next emission or completion
177
+ function waitForEmission(): Promise<void> {
178
+ if (pendingEmissions.length > 0 || completed) {
179
+ return Promise.resolve();
180
+ }
181
+ return new Promise((resolve) => {
182
+ emissionResolver = resolve;
183
+ });
184
+ }
185
+
186
+ return {
187
+ track<T>(promise: Promise<T>): Promise<T> {
188
+ inflightCount++;
189
+ // Use .then(onSettle, onSettle) instead of .finally() to avoid
190
+ // creating an unhandled rejection branch when the tracked promise
191
+ // rejects (e.g. error route handlers). .finally() re-throws the
192
+ // rejection on a new branch that nobody catches, which can crash
193
+ // the server process.
194
+ const onSettle = () => {
195
+ inflightCount--;
196
+ notifyDrain();
197
+ };
198
+ promise.then(onSettle, onSettle);
199
+ return promise;
200
+ },
201
+
202
+ seal() {
203
+ sealInternal();
204
+ },
205
+
206
+ get settled(): Promise<void> {
207
+ if (sealed && inflightCount === 0) return Promise.resolve();
208
+ return new Promise<void>((resolve) => {
209
+ drainWaiters.push(resolve);
210
+ });
211
+ },
212
+
213
+ push(handleName: string, segmentId: string, value: unknown): void {
214
+ if (completed) {
215
+ const error = createLateHandlePushError(handleName, segmentId);
216
+ if (this.onError) this.onError(error);
217
+ throw error;
218
+ }
219
+
220
+ if (!data[handleName]) {
221
+ data[handleName] = {};
222
+ }
223
+ if (!data[handleName][segmentId]) {
224
+ data[handleName][segmentId] = [];
225
+ }
226
+ data[handleName][segmentId].push(value);
227
+
228
+ // Queue a snapshot for emission
229
+ pendingEmissions.push(cloneHandleData(data));
230
+ signalEmission();
231
+ },
232
+
233
+ getData(): Promise<HandleData> {
234
+ sealInternal();
235
+ return this.settled.then(() => cloneHandleData(data));
236
+ },
237
+
238
+ async *stream(): AsyncGenerator<HandleData, void, unknown> {
239
+ // Auto-seal: stream() is called after all track() registrations.
240
+ sealInternal();
241
+
242
+ // Set up completion handler
243
+ this.settled.then(() => {
244
+ completed = true;
245
+ signalEmission();
246
+ });
247
+
248
+ // Initial small delay to batch rapid synchronous pushes
249
+ // This allows multiple handles pushing in quick succession to be batched
250
+ await new Promise((resolve) => setTimeout(resolve, 0));
251
+
252
+ // If we already have data, yield the accumulated state
253
+ if (Object.keys(data).length > 0) {
254
+ // Clear pending emissions since we're yielding current state
255
+ pendingEmissions = [];
256
+ const snapshot = cloneHandleData(data);
257
+ yield snapshot;
258
+ }
259
+
260
+ // Continue streaming on each push
261
+ while (!completed) {
262
+ await waitForEmission();
263
+
264
+ // Yield all pending emissions (yield latest only)
265
+ if (pendingEmissions.length > 0) {
266
+ // Skip intermediate states, yield the latest
267
+ const latest = pendingEmissions[pendingEmissions.length - 1];
268
+ pendingEmissions = [];
269
+ yield latest;
270
+ }
271
+ }
272
+
273
+ // Final yield only if there are pending emissions that weren't yielded
274
+ // (handles that pushed after our last yield but before completion)
275
+ if (pendingEmissions.length > 0) {
276
+ yield cloneHandleData(data);
277
+ }
278
+ },
279
+
280
+ getDataForSegment(segmentId: string): Record<string, unknown[]> {
281
+ const result: Record<string, unknown[]> = {};
282
+ for (const handleName in data) {
283
+ if (data[handleName][segmentId]) {
284
+ result[handleName] = [...data[handleName][segmentId]];
285
+ }
286
+ }
287
+ return result;
288
+ },
289
+
290
+ replaySegmentData(
291
+ segmentId: string,
292
+ segmentHandles: Record<string, unknown[]>,
293
+ ): void {
294
+ for (const handleName in segmentHandles) {
295
+ if (!data[handleName]) {
296
+ data[handleName] = {};
297
+ }
298
+ // Replace with replayed data (not append) to avoid handle bleeding between routes.
299
+ // When a cached segment is restored, its handles should replace any existing data
300
+ // for that segment, not accumulate on top of data from a different route.
301
+ data[handleName][segmentId] = [...segmentHandles[handleName]];
302
+ }
303
+ // Trigger emission for streaming
304
+ pendingEmissions.push(cloneHandleData(data));
305
+ signalEmission();
306
+ },
307
+ };
308
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Server-side loader registry for GET-based fetching
3
+ *
4
+ * Loaders are loaded lazily via dynamic imports when first requested.
5
+ * The RSC handler looks up loaders by $$id to execute them.
6
+ */
7
+
8
+ import type { LoaderFn } from "../types.js";
9
+ import {
10
+ getFetchableLoader,
11
+ type LoaderRegistryEntry,
12
+ } from "./fetchable-loader-store.js";
13
+
14
+ // Server-side cache - maps loader $$id to function and middleware
15
+ // This is a CACHE populated by getLoaderLazy() when loaders are first accessed.
16
+ // The source of truth is fetchableLoaderRegistry in loader.ts, which is populated
17
+ // when createLoader() runs. This cache exists to:
18
+ // 1. Avoid repeated lookups/imports for the same loader
19
+ // 2. Support lazy loading in production (loaders imported on-demand)
20
+ // 3. Provide a stable reference for the RSC handler
21
+ const loaderRegistry = new Map<string, LoaderRegistryEntry>();
22
+
23
+ // Lazy import map - set by the loader manifest
24
+ // Maps loader $$id to a function that imports the loader module
25
+ type LazyLoaderImport = () => Promise<{ $$id: string }>;
26
+ let lazyLoaderImports: Map<string, LazyLoaderImport> | null = null;
27
+
28
+ /**
29
+ * Set the lazy loader imports map (called by the loader manifest)
30
+ */
31
+ export function setLoaderImports(
32
+ imports: Record<string, LazyLoaderImport>,
33
+ ): void {
34
+ lazyLoaderImports = new Map(Object.entries(imports));
35
+ }
36
+
37
+ /**
38
+ * Get a loader by $$id, loading it lazily if needed
39
+ * This is the primary method for the RSC handler to get loaders
40
+ *
41
+ * In production: IDs are hashed, looked up via the lazy import map
42
+ * In dev: IDs are "filePath#exportName", resolved via dynamic import
43
+ */
44
+ export async function getLoaderLazy(
45
+ id: string,
46
+ ): Promise<LoaderRegistryEntry | undefined> {
47
+ // Check if already cached in main registry
48
+ const existing = loaderRegistry.get(id);
49
+ if (existing) {
50
+ return existing;
51
+ }
52
+
53
+ // Check the fetchable loader registry (populated by createLoader)
54
+ const fetchable = getFetchableLoader(id);
55
+ if (fetchable) {
56
+ // Cache in main registry for future requests
57
+ loaderRegistry.set(id, fetchable);
58
+ return fetchable;
59
+ }
60
+
61
+ // Try to lazy load from the import map (production mode)
62
+ if (lazyLoaderImports && lazyLoaderImports.size > 0) {
63
+ const lazyImport = lazyLoaderImports.get(id);
64
+ if (lazyImport) {
65
+ try {
66
+ // Import the loader module - this triggers createLoader which registers fn
67
+ await lazyImport();
68
+
69
+ // Now try to get from fetchable registry (createLoader registered it)
70
+ const registered = getFetchableLoader(id);
71
+ if (registered) {
72
+ loaderRegistry.set(id, registered);
73
+ return registered;
74
+ }
75
+ } catch (error) {
76
+ console.error(`[LoaderRegistry] Failed to load loader "${id}":`, error);
77
+ }
78
+ }
79
+ }
80
+
81
+ // Dev mode fallback: parse the ID and use Vite's dynamic import
82
+ // ID format in dev: "src/path/to/file.ts#ExportName"
83
+ const hashIndex = id.indexOf("#");
84
+ if (hashIndex !== -1) {
85
+ const filePath = id.slice(0, hashIndex);
86
+
87
+ try {
88
+ // In dev mode, Vite handles dynamic imports
89
+ // Just importing the module triggers createLoader which registers the fn
90
+ await import(/* @vite-ignore */ `/${filePath}`);
91
+
92
+ // Now try to get from fetchable registry
93
+ const registered = getFetchableLoader(id);
94
+ if (registered) {
95
+ loaderRegistry.set(id, registered);
96
+ return registered;
97
+ }
98
+ } catch (error) {
99
+ console.error(`[LoaderRegistry] Failed to load loader "${id}":`, error);
100
+ }
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ /**
107
+ * Register a loader by its $$id (injected by Vite plugin)
108
+ * This is called during module loading to cache loaders
109
+ */
110
+ export function registerLoaderById(loader: {
111
+ $$id: string;
112
+ fn?: LoaderFn<any, any, any>;
113
+ }): void {
114
+ if (!loader.$$id) {
115
+ return;
116
+ }
117
+ // For fetchable loaders, fn is stored in the fetchable registry by $$id.
118
+ // Always re-check the fetchable registry so HMR picks up the new function.
119
+ const fetchable = getFetchableLoader(loader.$$id);
120
+ if (fetchable) {
121
+ loaderRegistry.set(loader.$$id, fetchable);
122
+ return;
123
+ }
124
+
125
+ // Fall back to using fn from the loader object (non-fetchable loaders)
126
+ if (loader.fn) {
127
+ loaderRegistry.set(loader.$$id, {
128
+ fn: loader.fn,
129
+ middleware: [],
130
+ fetchable: false,
131
+ });
132
+ }
133
+ }