@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,162 @@
1
+ /**
2
+ * Shared location state utilities - works in both RSC and client contexts
3
+ * No "use client" directive so it can be imported from RSC
4
+ */
5
+
6
+ /**
7
+ * Internal entry representing a state value with its unique key.
8
+ * When __rsc_ls_lazy is true, __rsc_ls_value holds a getter function
9
+ * that is called at navigation time (not at entry creation time).
10
+ */
11
+ export interface LocationStateEntry {
12
+ readonly __rsc_ls_key: string;
13
+ readonly __rsc_ls_value: unknown;
14
+ readonly __rsc_ls_lazy?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Options for createLocationState
19
+ */
20
+ export interface LocationStateOptions {
21
+ /** When true, the state is cleared from history after first read (flash message pattern) */
22
+ flash?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Type-safe location state definition
27
+ *
28
+ * Created via createLocationState(), used with Link's state prop
29
+ * and useLocationState() hook.
30
+ */
31
+ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
32
+ (...args: TArgs): LocationStateEntry;
33
+ /** Injected by Vite plugin - do not set manually */
34
+ __rsc_ls_key: string;
35
+ /** Whether this state auto-clears after first read */
36
+ readonly __rsc_ls_flash: boolean;
37
+ /** Read the current value from history.state (client-side only, undefined during SSR) */
38
+ read(): TState | undefined;
39
+ }
40
+
41
+ /**
42
+ * Create a type-safe location state definition
43
+ *
44
+ * The key is auto-injected by the Vite exposeInternalIds plugin as a property
45
+ * based on file path and export name. No manual key required.
46
+ *
47
+ * @param options Optional configuration
48
+ * @returns A typed state definition for use with Link and useLocationState
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * // Persistent state (survives back/forward)
53
+ * export const ProductState = createLocationState<{ name: string; price: number }>();
54
+ *
55
+ * // Flash state (cleared after first read)
56
+ * export const FlashMessage = createLocationState<{ text: string }>({ flash: true });
57
+ *
58
+ * // Use in Link
59
+ * <Link to="/product/123" state={[ProductState({ name: "Widget", price: 9.99 })]}>
60
+ *
61
+ * // Just-in-time typed state (getter called at click time, not render time).
62
+ * // Must be in a client component — the getter function can't cross the RSC boundary.
63
+ * <Link
64
+ * to="/product/123"
65
+ * state={[ProductState(() => ({ name: product.name, price: product.price }))]}
66
+ * >
67
+ *
68
+ * // Read with hook (reactive)
69
+ * const product = useLocationState(ProductState);
70
+ *
71
+ * // Read without hook (snapshot, client-side only)
72
+ * const snap = ProductState.read();
73
+ * ```
74
+ */
75
+ export function createLocationState<TState>(
76
+ options?: LocationStateOptions,
77
+ ): LocationStateDefinition<[TState | (() => TState)], TState> {
78
+ const flash = options?.flash ?? false;
79
+ let _key: string | undefined;
80
+
81
+ function getKey(): string {
82
+ if (!_key && process.env.NODE_ENV === "development") {
83
+ throw new Error(
84
+ "[rsc-router] createLocationState key not set. " +
85
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
86
+ "the state is exported with: export const MyState = createLocationState(...)",
87
+ );
88
+ }
89
+ return _key!;
90
+ }
91
+
92
+ const fn = (stateOrGetter: TState | (() => TState)): LocationStateEntry => {
93
+ if (typeof stateOrGetter === "function") {
94
+ // Store getter as-is; resolved at navigation time by resolveLocationStateEntries()
95
+ return {
96
+ __rsc_ls_key: getKey(),
97
+ __rsc_ls_value: stateOrGetter,
98
+ __rsc_ls_lazy: true,
99
+ };
100
+ }
101
+ return {
102
+ __rsc_ls_key: getKey(),
103
+ __rsc_ls_value: stateOrGetter,
104
+ };
105
+ };
106
+
107
+ // Use defineProperty for __rsc_ls_key to avoid Object.assign evaluating
108
+ // the getter during construction (before the Vite plugin sets the key).
109
+ Object.defineProperty(fn, "__rsc_ls_key", {
110
+ get: () => getKey(),
111
+ set: (k: string) => {
112
+ _key = k;
113
+ },
114
+ enumerable: true,
115
+ configurable: true,
116
+ });
117
+
118
+ Object.defineProperty(fn, "__rsc_ls_flash", {
119
+ value: flash,
120
+ enumerable: true,
121
+ });
122
+
123
+ Object.defineProperty(fn, "read", {
124
+ value: (): TState | undefined => {
125
+ if (typeof window === "undefined") return undefined;
126
+ return window.history.state?.[getKey()] as TState | undefined;
127
+ },
128
+ enumerable: true,
129
+ });
130
+
131
+ return fn as LocationStateDefinition<[TState | (() => TState)], TState>;
132
+ }
133
+
134
+ /**
135
+ * Check if a value is a LocationStateEntry
136
+ */
137
+ export function isLocationStateEntry(
138
+ value: unknown,
139
+ ): value is LocationStateEntry {
140
+ return (
141
+ value !== null &&
142
+ typeof value === "object" &&
143
+ "__rsc_ls_key" in value &&
144
+ "__rsc_ls_value" in value &&
145
+ typeof (value as LocationStateEntry).__rsc_ls_key === "string"
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Resolve state entries into a flat object for history.state
151
+ */
152
+ export function resolveLocationStateEntries(
153
+ entries: LocationStateEntry[],
154
+ ): Record<string, unknown> {
155
+ const result: Record<string, unknown> = {};
156
+ for (const entry of entries) {
157
+ result[entry.__rsc_ls_key] = entry.__rsc_ls_lazy
158
+ ? (entry.__rsc_ls_value as () => unknown)()
159
+ : entry.__rsc_ls_value;
160
+ }
161
+ return result;
162
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import type { LocationStateDefinition } from "./location-state-shared.js";
5
+
6
+ // Re-export shared utilities and types
7
+ export {
8
+ createLocationState,
9
+ isLocationStateEntry,
10
+ resolveLocationStateEntries,
11
+ type LocationStateEntry,
12
+ type LocationStateDefinition,
13
+ type LocationStateOptions,
14
+ } from "./location-state-shared.js";
15
+
16
+ /**
17
+ * Hook to read location state from history.state
18
+ *
19
+ * Behavior depends on the definition:
20
+ * - Normal state: persists across navigations, reactive to popstate
21
+ * - Flash state (created with { flash: true }): read once, cleared after paint
22
+ *
23
+ * Overloaded:
24
+ * - With definition: Returns typed state from the specific key
25
+ * - With type param only: Returns plain state from history.state.state
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * // Persistent state
30
+ * const ProductState = createLocationState<{ name: string }>();
31
+ * const state = useLocationState(ProductState);
32
+ *
33
+ * // Flash state (auto-clears after paint)
34
+ * const FlashMsg = createLocationState<{ text: string }>({ flash: true });
35
+ * const flash = useLocationState(FlashMsg);
36
+ *
37
+ * // Plain state access (reads from history.state.state)
38
+ * const state = useLocationState<{ from?: string }>();
39
+ * ```
40
+ */
41
+ export function useLocationState<TArgs extends unknown[], TState>(
42
+ definition: LocationStateDefinition<TArgs, TState>,
43
+ ): TState | undefined;
44
+ export function useLocationState<T = unknown>(): T | undefined;
45
+ export function useLocationState<TArgs extends unknown[], TState>(
46
+ definition?: LocationStateDefinition<TArgs, TState>,
47
+ ): TState | undefined {
48
+ const key = definition?.__rsc_ls_key;
49
+ const isFlash = definition?.__rsc_ls_flash ?? false;
50
+
51
+ const [state, setState] = useState<TState | undefined>(() => {
52
+ if (typeof window === "undefined") return undefined;
53
+ if (key) {
54
+ return window.history.state?.[key] as TState | undefined;
55
+ }
56
+ // Plain state: stored under history.state.state
57
+ return window.history.state?.state as TState | undefined;
58
+ });
59
+
60
+ // Subscribe to popstate and programmatic state changes
61
+ useEffect(() => {
62
+ const handlePopstate = () => {
63
+ if (key) {
64
+ setState(window.history.state?.[key] as TState | undefined);
65
+ } else {
66
+ setState(window.history.state?.state as TState | undefined);
67
+ }
68
+ };
69
+
70
+ // Handle programmatic state changes (same-page navigation with
71
+ // ctx.setLocationState where components don't remount)
72
+ const handleLocationState = () => {
73
+ if (key) {
74
+ const val = window.history.state?.[key] as TState | undefined;
75
+ if (isFlash) {
76
+ // For flash state, only update if there's a new value
77
+ if (val !== undefined) {
78
+ setState(val);
79
+ }
80
+ } else {
81
+ setState(val);
82
+ }
83
+ } else {
84
+ setState(window.history.state?.state as TState | undefined);
85
+ }
86
+ };
87
+
88
+ window.addEventListener("popstate", handlePopstate);
89
+ window.addEventListener("__rsc_locationstate", handleLocationState);
90
+ return () => {
91
+ window.removeEventListener("popstate", handlePopstate);
92
+ window.removeEventListener("__rsc_locationstate", handleLocationState);
93
+ };
94
+ }, [key, isFlash]);
95
+
96
+ // Flash: clear from history.state after paint so subsequent navigations don't see it.
97
+ // Depends on `state` so it re-runs when state is set via the event listener.
98
+ useEffect(() => {
99
+ if (isFlash && key && state !== undefined) {
100
+ const cleaned = { ...window.history.state };
101
+ delete cleaned[key];
102
+ window.history.replaceState(cleaned, "", window.location.href);
103
+ }
104
+ }, [isFlash, key, state]);
105
+
106
+ return state;
107
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ createElement,
6
+ type Context,
7
+ type ReactNode,
8
+ } from "react";
9
+
10
+ /**
11
+ * Context for the current include() mount path.
12
+ *
13
+ * Each include() wraps its subtree with a MountContext.Provider
14
+ * carrying the URL prefix. Nested includes override the context,
15
+ * so useMount() returns the nearest mount path.
16
+ *
17
+ * Default value "/" means root-level (no include wrapping).
18
+ */
19
+ export const MountContext: Context<string> = createContext<string>("/");
20
+
21
+ /**
22
+ * Provider wrapper for MountContext.
23
+ *
24
+ * RSC server components cannot use MountContext.Provider directly because
25
+ * .Provider is a property on the context object, not a named export.
26
+ * Client reference proxies on the RSC server return undefined for property
27
+ * access. This wrapper is a proper "use client" export that RSC can reference.
28
+ */
29
+ export function MountContextProvider({
30
+ value,
31
+ children,
32
+ }: {
33
+ value: string;
34
+ children: ReactNode;
35
+ }): ReactNode {
36
+ return createElement(MountContext, { value }, children);
37
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Context for CSP nonce propagation to client components during SSR.
5
+ *
6
+ * The SSR renderer wraps the tree with NonceContext.Provider so that
7
+ * client components (e.g. MetaTags) can apply nonces to inline scripts.
8
+ * On the browser side, no provider is needed — the default undefined
9
+ * is correct since CSP nonces are a server-side HTML concern.
10
+ */
11
+
12
+ import { createContext, useContext, type Context } from "react";
13
+
14
+ export const NonceContext: Context<string | undefined> = createContext<
15
+ string | undefined
16
+ >(undefined);
17
+
18
+ /**
19
+ * Read the CSP nonce during SSR. Returns undefined on the client.
20
+ */
21
+ export function useNonce(): string | undefined {
22
+ return useContext(NonceContext);
23
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shallow equality check for selector results.
3
+ * Uses Object.is for value comparison (handles NaN and +-0 correctly).
4
+ */
5
+ export function shallowEqual<T>(a: T, b: T): boolean {
6
+ if (Object.is(a, b)) return true;
7
+ if (
8
+ typeof a !== "object" ||
9
+ a === null ||
10
+ typeof b !== "object" ||
11
+ b === null
12
+ ) {
13
+ return false;
14
+ }
15
+ const keysA = Object.keys(a);
16
+ const keysB = Object.keys(b);
17
+ if (keysA.length !== keysB.length) return false;
18
+ for (const key of keysA) {
19
+ if (
20
+ !Object.hasOwn(b, key) ||
21
+ !Object.is((a as any)[key], (b as any)[key])
22
+ ) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
@@ -0,0 +1,218 @@
1
+ "use client";
2
+
3
+ import {
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useRef,
8
+ useOptimistic,
9
+ startTransition,
10
+ } from "react";
11
+ import { NavigationStoreContext } from "./context.js";
12
+ import { shallowEqual } from "./shallow-equal.js";
13
+ import type { TrackedActionState } from "../types.js";
14
+ import { invariant } from "../../errors.js";
15
+
16
+ /**
17
+ * Default action state (idle with no payload)
18
+ */
19
+ const DEFAULT_ACTION_STATE: TrackedActionState = {
20
+ state: "idle",
21
+ actionId: null,
22
+ payload: null,
23
+ error: null,
24
+ result: null,
25
+ };
26
+
27
+ /**
28
+ * Normalize action ID - returns the ID as-is
29
+ *
30
+ * Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
31
+ * When using function references, we use the full ID for exact matching.
32
+ * When using strings, the event controller supports suffix matching
33
+ * (e.g., "addToCart" matches "hash#addToCart").
34
+ */
35
+ function normalizeActionId(actionId: string): string {
36
+ return actionId;
37
+ }
38
+
39
+ /**
40
+ * Extract action ID from a server action function or string.
41
+ *
42
+ * Actions passed as props from server components lose their metadata
43
+ * during RSC serialization - use a string action name instead.
44
+ */
45
+ export function getActionId(action: ServerActionFunction | string): string {
46
+ invariant(
47
+ typeof action === "function" || typeof action === "string",
48
+ `useAction: action must be a function or string, got ${typeof action}`,
49
+ );
50
+ const actionId = (action as any)?.$$id;
51
+ if (actionId) {
52
+ return normalizeActionId(actionId);
53
+ }
54
+
55
+ // If action is a string, use it directly
56
+ if (typeof action === "string") {
57
+ return action;
58
+ }
59
+
60
+ // If we get here, this is likely an action passed as prop from a server component
61
+ // These lose their metadata during RSC serialization
62
+ throw new Error(
63
+ `useAction: Cannot extract action ID from function reference.
64
+
65
+ This typically happens when an action is passed as a prop from a server component.
66
+ Actions passed through RSC lose their metadata during serialization.
67
+
68
+ Solutions:
69
+ 1. Import the action directly in your client component:
70
+ import { myAction } from './actions';
71
+ const state = useAction(myAction);
72
+
73
+ 2. Use the action name as a string:
74
+ const state = useAction("myAction");
75
+
76
+ The string must match the exported function name from your "use server" file.`,
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Server action function type
82
+ * Server actions have a $$id property added by the RSC compiler
83
+ */
84
+ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
85
+ $$id?: string;
86
+ };
87
+
88
+ /**
89
+ * Hook to track the lifecycle of a specific server action
90
+ *
91
+ * Unlike useNavigation which tracks global navigation state, useAction
92
+ * tracks the state of individual server action invocations.
93
+ *
94
+ * Uses the event controller for reactive state management.
95
+ * State is derived from the inflight actions tracked by the controller.
96
+ *
97
+ * Features:
98
+ * - Tracks action lifecycle: idle → loading → streaming → idle
99
+ * - Captures result/error locally (React handles cleanup)
100
+ * - If multiple actions fire, tracks only the last one
101
+ * - Supports selector pattern like useNavigation
102
+ *
103
+ * Matching behavior:
104
+ * - **Function reference**: Uses full $$id for exact matching. This is precise
105
+ * and distinguishes between actions with the same name in different files.
106
+ * - **String**: Matches by suffix (action name after #). This is convenient
107
+ * but may be ambiguous if multiple files export the same action name.
108
+ *
109
+ * @param action - Either a server action function or a string action name.
110
+ * - **Function**: Must be directly imported in the client component.
111
+ * Actions passed as props from server components will throw an error.
112
+ * - **String**: The exported function name from your "use server" file.
113
+ * Matches any action ending with "#actionName" (suffix match).
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * // Option 1: Direct import (precise matching)
118
+ * import { addToCart } from './actions';
119
+ * const actionState = useAction(addToCart);
120
+ *
121
+ * // Option 2: String-based (suffix matching)
122
+ * // Matches "hash#addToCart" or "src/actions.ts#addToCart"
123
+ * const actionState = useAction('addToCart');
124
+ *
125
+ * // With selector for specific values
126
+ * const isLoading = useAction(addToCart, state => state.state === 'loading');
127
+ * const error = useAction(addToCart, state => state.error);
128
+ * ```
129
+ *
130
+ * @note The selector is expected to be stable for a given hook instance.
131
+ * This hook tracks one projection of one action. Changing selector semantics
132
+ * for the same action ID without a new action event is not a supported pattern;
133
+ * use separate useAction() subscriptions if you need different projections.
134
+ *
135
+ * @note Actions passed as props from server components lose their metadata
136
+ * during RSC serialization. Use a string action name or import directly.
137
+ */
138
+ export function useAction(
139
+ action: ServerActionFunction | string,
140
+ ): TrackedActionState;
141
+ export function useAction<T>(
142
+ action: ServerActionFunction | string,
143
+ selector: (state: TrackedActionState) => T,
144
+ ): T;
145
+ export function useAction<T>(
146
+ action: ServerActionFunction | string,
147
+ selector?: (state: TrackedActionState) => T,
148
+ ): T | TrackedActionState {
149
+ const ctx = useContext(NavigationStoreContext);
150
+ const actionId =
151
+ typeof window !== "undefined" && typeof document !== "undefined"
152
+ ? getActionId(action)
153
+ : "";
154
+
155
+ // Base state for useOptimistic
156
+ const [baseState, setBaseState] = useState<T | TrackedActionState>(() => {
157
+ if (!ctx) {
158
+ return selector ? selector(DEFAULT_ACTION_STATE) : DEFAULT_ACTION_STATE;
159
+ }
160
+ const state = ctx.eventController.getActionState(actionId);
161
+ return selector ? selector(state) : state;
162
+ });
163
+ const prevSelected = useRef(baseState);
164
+ prevSelected.current = baseState;
165
+ // useOptimistic allows immediate updates during transitions/actions
166
+ const [optimisticState, setOptimisticState] = useOptimistic<
167
+ T | TrackedActionState
168
+ >(null!);
169
+
170
+ // Ref keeps the latest selector for subscription callbacks without
171
+ // re-subscribing on every render. Selector changes themselves are not
172
+ // treated as a reactive input; this hook expects a stable selector and
173
+ // represents one subscription/projection for one action.
174
+ const selectorRef = useRef(selector);
175
+ selectorRef.current = selector;
176
+
177
+ // Subscribe to action state changes from event controller
178
+ useEffect(() => {
179
+ if (!ctx) return;
180
+
181
+ // Sync current state for the (possibly new) actionId so that switching
182
+ // actions on an idle page doesn't leave stale data from the old action.
183
+ const currentState = ctx.eventController.getActionState(actionId);
184
+ const currentSelected = selectorRef.current
185
+ ? selectorRef.current(currentState)
186
+ : currentState;
187
+ if (!shallowEqual(currentSelected, prevSelected.current)) {
188
+ prevSelected.current = currentSelected;
189
+ setBaseState(currentSelected);
190
+ }
191
+
192
+ // Subscribe to action-specific updates
193
+ const unsubscribe = ctx.eventController.subscribeToAction(
194
+ actionId,
195
+ (state) => {
196
+ const selectedState = selectorRef.current
197
+ ? selectorRef.current(state)
198
+ : state;
199
+
200
+ if (!shallowEqual(selectedState, prevSelected.current)) {
201
+ prevSelected.current = selectedState;
202
+ setBaseState(selectedState);
203
+ startTransition(() => {
204
+ setOptimisticState(selectedState);
205
+ });
206
+ }
207
+ },
208
+ );
209
+
210
+ return () => {
211
+ unsubscribe();
212
+ };
213
+ }, [actionId]);
214
+
215
+ return (optimisticState ?? baseState) as T | TrackedActionState;
216
+ }
217
+
218
+ export type { TrackedActionState };
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { useContext, useCallback } from "react";
4
+ import { NavigationStoreContext } from "./context.js";
5
+
6
+ /**
7
+ * Return type for useClientCache hook
8
+ */
9
+ export interface ClientCacheControls {
10
+ /**
11
+ * Clear the client-side navigation cache.
12
+ * Call this after data changes that happen outside of server actions
13
+ * (e.g., REST API calls, WebSocket updates, etc.)
14
+ *
15
+ * This will also broadcast to other tabs to clear their caches.
16
+ */
17
+ clear: () => void;
18
+ }
19
+
20
+ /**
21
+ * Hook to access client-side cache controls
22
+ *
23
+ * Use this when you need to manually invalidate the navigation cache
24
+ * after data changes that happen outside of server actions.
25
+ *
26
+ * Server actions automatically clear the cache, so you only need this for:
27
+ * - REST API mutations
28
+ * - WebSocket-driven updates
29
+ * - Other non-RSC data changes
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * function DataEditor() {
34
+ * const { clear } = useClientCache();
35
+ *
36
+ * async function handleSave() {
37
+ * await fetch('/api/data', { method: 'POST', body: JSON.stringify(data) });
38
+ * // Clear cache so back/forward navigation shows fresh data
39
+ * clear();
40
+ * }
41
+ *
42
+ * return <button onClick={handleSave}>Save</button>;
43
+ * }
44
+ * ```
45
+ */
46
+ export function useClientCache(): ClientCacheControls {
47
+ const ctx = useContext(NavigationStoreContext);
48
+
49
+ if (!ctx) {
50
+ throw new Error("useClientCache must be used within NavigationProvider");
51
+ }
52
+
53
+ const clear = useCallback(() => {
54
+ ctx.store.clearHistoryCache();
55
+ }, [ctx]);
56
+
57
+ return { clear };
58
+ }