@rshono/core 1.0.0-rc.2 → 1.0.0-rc.21

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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -158
  3. package/bin/rshono.mjs +3 -4
  4. package/dist/builder/env-shadow-loader.cjs +123 -8
  5. package/dist/builder/page-entry-loader.cjs +28 -1
  6. package/dist/builder/page-files.d.ts +0 -1
  7. package/dist/builder/page-files.js +7 -3
  8. package/dist/builder/page-files.js.map +1 -1
  9. package/dist/builder/public-env.d.ts +7 -1
  10. package/dist/builder/public-env.js +7 -0
  11. package/dist/builder/public-env.js.map +1 -1
  12. package/dist/builder/react-versions.d.ts +12 -0
  13. package/dist/builder/react-versions.js +74 -0
  14. package/dist/builder/react-versions.js.map +1 -0
  15. package/dist/builder/rspack-config.d.ts +13 -4
  16. package/dist/builder/rspack-config.js +136 -53
  17. package/dist/builder/rspack-config.js.map +1 -1
  18. package/dist/builder/server-only-imports.d.ts +32 -0
  19. package/dist/builder/server-only-imports.js +51 -0
  20. package/dist/builder/server-only-imports.js.map +1 -0
  21. package/dist/cli/build.d.ts +2 -3
  22. package/dist/cli/build.js +51 -13
  23. package/dist/cli/build.js.map +1 -1
  24. package/dist/cli/dev.d.ts +2 -3
  25. package/dist/cli/dev.js +149 -45
  26. package/dist/cli/dev.js.map +1 -1
  27. package/dist/cli/exit.d.ts +12 -0
  28. package/dist/cli/exit.js +16 -0
  29. package/dist/cli/exit.js.map +1 -0
  30. package/dist/cli/index.d.ts +0 -1
  31. package/dist/cli/index.js +91 -30
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/start.d.ts +0 -1
  34. package/dist/cli/start.js +15 -18
  35. package/dist/cli/start.js.map +1 -1
  36. package/dist/config.d.ts +72 -78
  37. package/dist/config.js +17 -1
  38. package/dist/config.js.map +1 -1
  39. package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
  40. package/dist/deploy/aws-lambda/runtime.js +5 -8
  41. package/dist/deploy/aws-lambda/runtime.js.map +1 -1
  42. package/dist/deploy/build-marker.d.ts +9 -6
  43. package/dist/deploy/build-marker.js +8 -4
  44. package/dist/deploy/build-marker.js.map +1 -1
  45. package/dist/deploy/cloudflare/build.d.ts +0 -1
  46. package/dist/deploy/cloudflare/build.js +17 -13
  47. package/dist/deploy/cloudflare/build.js.map +1 -1
  48. package/dist/deploy/cloudflare/runtime.d.ts +2 -6
  49. package/dist/deploy/cloudflare/runtime.js +73 -68
  50. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  51. package/dist/deploy/contract.d.ts +57 -41
  52. package/dist/deploy/contract.js.map +1 -1
  53. package/dist/deploy/filesystem.d.ts +3 -6
  54. package/dist/deploy/filesystem.js +23 -20
  55. package/dist/deploy/filesystem.js.map +1 -1
  56. package/dist/deploy/node/runtime.d.ts +5 -3
  57. package/dist/deploy/node/runtime.js +23 -9
  58. package/dist/deploy/node/runtime.js.map +1 -1
  59. package/dist/deploy/presets.d.ts +34 -30
  60. package/dist/deploy/presets.js +56 -44
  61. package/dist/deploy/presets.js.map +1 -1
  62. package/dist/deploy/public-paths.d.ts +42 -0
  63. package/dist/deploy/public-paths.js +66 -0
  64. package/dist/deploy/public-paths.js.map +1 -0
  65. package/dist/deploy/vercel/build.d.ts +0 -1
  66. package/dist/deploy/vercel/build.js +25 -16
  67. package/dist/deploy/vercel/build.js.map +1 -1
  68. package/dist/deploy/vercel/runtime.d.ts +10 -7
  69. package/dist/deploy/vercel/runtime.js +48 -11
  70. package/dist/deploy/vercel/runtime.js.map +1 -1
  71. package/dist/index.d.ts +19 -17
  72. package/dist/index.js +20 -10
  73. package/dist/index.js.map +1 -1
  74. package/dist/router.d.ts +212 -120
  75. package/dist/router.js +3 -8
  76. package/dist/router.js.map +1 -1
  77. package/dist/runtime/boundaries.d.ts +62 -48
  78. package/dist/runtime/boundaries.js +32 -34
  79. package/dist/runtime/boundaries.js.map +1 -1
  80. package/dist/runtime/client.d.ts +20 -11
  81. package/dist/runtime/client.js +20 -10
  82. package/dist/runtime/client.js.map +1 -1
  83. package/dist/runtime/context.d.ts +304 -123
  84. package/dist/runtime/context.js +377 -149
  85. package/dist/runtime/context.js.map +1 -1
  86. package/dist/runtime/control.d.ts +15 -1
  87. package/dist/runtime/control.js +24 -0
  88. package/dist/runtime/control.js.map +1 -1
  89. package/dist/runtime/dev-protocol.d.ts +4 -9
  90. package/dist/runtime/dev-protocol.js.map +1 -1
  91. package/dist/runtime/empty-server-app.d.ts +0 -1
  92. package/dist/runtime/entry.client.d.ts +4 -1
  93. package/dist/runtime/entry.client.js +455 -313
  94. package/dist/runtime/entry.client.js.map +1 -1
  95. package/dist/runtime/entry.rsc.d.ts +15 -7
  96. package/dist/runtime/entry.rsc.js +811 -252
  97. package/dist/runtime/entry.rsc.js.map +1 -1
  98. package/dist/runtime/entry.ssr.d.ts +27 -10
  99. package/dist/runtime/entry.ssr.js +30 -48
  100. package/dist/runtime/entry.ssr.js.map +1 -1
  101. package/dist/runtime/failure-document.d.ts +11 -0
  102. package/dist/runtime/failure-document.js +35 -0
  103. package/dist/runtime/failure-document.js.map +1 -0
  104. package/dist/runtime/flight-inject.d.ts +23 -0
  105. package/dist/runtime/flight-inject.js +403 -0
  106. package/dist/runtime/flight-inject.js.map +1 -0
  107. package/dist/runtime/hot-update.d.ts +44 -0
  108. package/dist/runtime/hot-update.js +44 -0
  109. package/dist/runtime/hot-update.js.map +1 -0
  110. package/dist/runtime/navigation.d.ts +46 -54
  111. package/dist/runtime/navigation.js +22 -63
  112. package/dist/runtime/navigation.js.map +1 -1
  113. package/dist/runtime/request.d.ts +42 -8
  114. package/dist/runtime/request.js +84 -7
  115. package/dist/runtime/request.js.map +1 -1
  116. package/dist/runtime/server.d.ts +20 -18
  117. package/dist/runtime/server.js +22 -17
  118. package/dist/runtime/server.js.map +1 -1
  119. package/dist/runtime/validate-entries.d.ts +77 -0
  120. package/dist/runtime/validate-entries.js +405 -0
  121. package/dist/runtime/validate-entries.js.map +1 -0
  122. package/dist/server/headers.d.ts +41 -15
  123. package/dist/server/headers.js +53 -24
  124. package/dist/server/headers.js.map +1 -1
  125. package/dist/server/load-config.d.ts +4 -5
  126. package/dist/server/load-config.js +22 -14
  127. package/dist/server/load-config.js.map +1 -1
  128. package/dist/server/load-env.d.ts +0 -1
  129. package/dist/server/prerendered.d.ts +88 -34
  130. package/dist/server/prerendered.js +184 -24
  131. package/dist/server/prerendered.js.map +1 -1
  132. package/dist/server/server-config.d.ts +35 -50
  133. package/dist/server/server-config.js +27 -74
  134. package/dist/server/server-config.js.map +1 -1
  135. package/dist/server/shutdown.d.ts +5 -4
  136. package/dist/server/shutdown.js +5 -3
  137. package/dist/server/shutdown.js.map +1 -1
  138. package/dist/server/ssg.d.ts +9 -7
  139. package/dist/server/ssg.js +209 -65
  140. package/dist/server/ssg.js.map +1 -1
  141. package/dist/server/static.d.ts +0 -1
  142. package/dist/server/static.js +7 -1
  143. package/dist/server/static.js.map +1 -1
  144. package/package.json +21 -23
  145. package/dist/builder/page-files.d.ts.map +0 -1
  146. package/dist/builder/public-env.d.ts.map +0 -1
  147. package/dist/builder/rspack-config.d.ts.map +0 -1
  148. package/dist/cli/build.d.ts.map +0 -1
  149. package/dist/cli/dev.d.ts.map +0 -1
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/start.d.ts.map +0 -1
  152. package/dist/config.d.ts.map +0 -1
  153. package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
  154. package/dist/deploy/build-marker.d.ts.map +0 -1
  155. package/dist/deploy/bun/runtime.d.ts +0 -11
  156. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  157. package/dist/deploy/bun/runtime.js +0 -22
  158. package/dist/deploy/bun/runtime.js.map +0 -1
  159. package/dist/deploy/cloudflare/build.d.ts.map +0 -1
  160. package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
  161. package/dist/deploy/contract.d.ts.map +0 -1
  162. package/dist/deploy/deno/runtime.d.ts +0 -11
  163. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  164. package/dist/deploy/deno/runtime.js +0 -16
  165. package/dist/deploy/deno/runtime.js.map +0 -1
  166. package/dist/deploy/filesystem.d.ts.map +0 -1
  167. package/dist/deploy/listen.d.ts +0 -20
  168. package/dist/deploy/listen.d.ts.map +0 -1
  169. package/dist/deploy/listen.js +0 -24
  170. package/dist/deploy/listen.js.map +0 -1
  171. package/dist/deploy/netlify/build.d.ts +0 -8
  172. package/dist/deploy/netlify/build.d.ts.map +0 -1
  173. package/dist/deploy/netlify/build.js +0 -52
  174. package/dist/deploy/netlify/build.js.map +0 -1
  175. package/dist/deploy/netlify/runtime.d.ts +0 -13
  176. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  177. package/dist/deploy/netlify/runtime.js +0 -24
  178. package/dist/deploy/netlify/runtime.js.map +0 -1
  179. package/dist/deploy/node/runtime.d.ts.map +0 -1
  180. package/dist/deploy/presets.d.ts.map +0 -1
  181. package/dist/deploy/vercel/build.d.ts.map +0 -1
  182. package/dist/deploy/vercel/runtime.d.ts.map +0 -1
  183. package/dist/index.d.ts.map +0 -1
  184. package/dist/router.d.ts.map +0 -1
  185. package/dist/runtime/boundaries.d.ts.map +0 -1
  186. package/dist/runtime/client.d.ts.map +0 -1
  187. package/dist/runtime/context.d.ts.map +0 -1
  188. package/dist/runtime/control.d.ts.map +0 -1
  189. package/dist/runtime/dev-protocol.d.ts.map +0 -1
  190. package/dist/runtime/empty-server-app.d.ts.map +0 -1
  191. package/dist/runtime/entry.client.d.ts.map +0 -1
  192. package/dist/runtime/entry.rsc.d.ts.map +0 -1
  193. package/dist/runtime/entry.ssr.d.ts.map +0 -1
  194. package/dist/runtime/navigation.d.ts.map +0 -1
  195. package/dist/runtime/request.d.ts.map +0 -1
  196. package/dist/runtime/server.d.ts.map +0 -1
  197. package/dist/server/compress.d.ts +0 -15
  198. package/dist/server/compress.d.ts.map +0 -1
  199. package/dist/server/compress.js +0 -76
  200. package/dist/server/compress.js.map +0 -1
  201. package/dist/server/headers.d.ts.map +0 -1
  202. package/dist/server/load-config.d.ts.map +0 -1
  203. package/dist/server/load-env.d.ts.map +0 -1
  204. package/dist/server/prerendered.d.ts.map +0 -1
  205. package/dist/server/server-config.d.ts.map +0 -1
  206. package/dist/server/shutdown.d.ts.map +0 -1
  207. package/dist/server/ssg.d.ts.map +0 -1
  208. package/dist/server/static.d.ts.map +0 -1
@@ -2,48 +2,59 @@ import { type ReactNode } from 'react';
2
2
  /**
3
3
  * Imperative navigation actions, reached as `useNavigation().router`.
4
4
  *
5
- * `push` / `replace` / `refresh` are **soft** navigations: the new page's flight
6
- * payload is fetched and applied in place, so client component state outside the
7
- * changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.
5
+ * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so
6
+ * client component state outside the changed subtree survives. Off-site hrefs and a traversal that leaves
7
+ * the app fall back to a full load.
8
+ *
9
+ * Soft navigation is the browser's
10
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is
11
+ * missing, every action below is still correct and simply performs a real browser load.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const { router } = useNavigation();
16
+ * router.push('/dashboard'); // navigate, new history entry
17
+ * router.replace('/login'); // navigate, no new entry
18
+ * router.back(); // one entry back, as the browser's button does
19
+ * router.forward(); // one entry forward
20
+ * router.refresh(); // re-run this route's server components
21
+ * ```
8
22
  */
9
- export interface Router {
23
+ export interface NavigationRouter {
10
24
  /** Navigates to `href` and pushes a new history entry. */
11
25
  push(href: string): void;
12
26
  /** Navigates to `href`, replacing the current history entry instead of adding one. */
13
27
  replace(href: string): void;
14
- /** Goes back one history entry `history.back()`. */
28
+ /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */
15
29
  back(): void;
16
- /** Goes forward one history entry `history.forward()`. */
30
+ /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */
17
31
  forward(): void;
18
32
  /** Re-fetches the current route from the server, re-running its server components. */
19
33
  refresh(): void;
20
34
  /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */
21
35
  pending: boolean;
22
36
  }
23
- /** The current location plus the {@link Router}, as returned by {@link useNavigation}. */
24
- export interface Navigation {
37
+ /** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */
38
+ export interface NavigationState {
25
39
  /**
26
- * The full current {@link URL} read `url.pathname`, `url.searchParams` and the
27
- * rest off it. A fresh instance per navigation, so mutating it affects nothing
28
- * else; it is not written back to the address bar either.
40
+ * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else
41
+ * it is not written back to the address bar.
29
42
  */
30
43
  url: URL;
31
44
  /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */
32
45
  params: Record<string, string>;
33
46
  /** Imperative navigation actions and the `pending` flag. */
34
- router: Router;
47
+ router: NavigationRouter;
35
48
  }
36
49
  /**
37
- * Carries the live {@link Router} implementation from the hydration runtime down
38
- * to {@link RouterProvider}. Framework internal — read the router through
39
- * {@link useNavigation} instead.
50
+ * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.
40
51
  *
41
52
  * @internal
42
53
  */
43
- export declare const RouterContext: import("react").Context<Router>;
54
+ export declare const RouterContext: import("react").Context<NavigationRouter>;
44
55
  /**
45
- * Publishes the per-render location and params so {@link useNavigation} can read
46
- * them. Framework internal — the RSC entry wraps every page in one.
56
+ * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps
57
+ * every page in one.
47
58
  *
48
59
  * @internal
49
60
  */
@@ -53,16 +64,20 @@ export declare function RouterProvider({ href, params, children }: {
53
64
  children: ReactNode;
54
65
  }): import("react").JSX.Element;
55
66
  /**
56
- * Reactive access to the current URL and programmatic navigation, in one hook.
67
+ * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
68
+ * `'use client'` component.
69
+ *
70
+ * `url` and `params` are computed on the server and travel in the flight payload, so they are correct
71
+ * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative
72
+ * actions plus a `pending` flag, `true` while a soft navigation is in flight.
57
73
  *
58
- * Call it from a `'use client'` component. The location fields (`url` and
59
- * `params`) are computed on the server and travel in the flight payload, so they
60
- * are correct during SSR no hydration flicker and update automatically on
61
- * every navigation. The `router` sub-object holds the imperative actions plus a
62
- * `pending` flag that is `true` while a client navigation is in flight.
74
+ * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
75
+ * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
76
+ * `PageProps.url` the same value, not a live one. A page whose output depends on the query wants
77
+ * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an
78
+ * effect.
63
79
  *
64
- * Hooks can't run in a server component; read the same URL data there from
65
- * `getContext()` (`@rshono/core/server`) instead.
80
+ * Hooks can't run in a server component; read the same data there from `getRequestContext()`.
66
81
  *
67
82
  * @example
68
83
  * ```tsx
@@ -80,35 +95,12 @@ export declare function RouterProvider({ href, params, children }: {
80
95
  * }
81
96
  * ```
82
97
  *
83
- * @returns The current {@link Navigation}: `url` and `params`, plus `router`
84
- * ({@link Router}) with `push` / `replace` / `back` / `forward` / `refresh` /
85
- * `pending`.
98
+ * @returns The current {@link NavigationState}: `url` and `params`, plus `router`
99
+ * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.
86
100
  * @throws If called outside a page's React tree, where there is no navigation
87
101
  * context to read.
88
- */
89
- export declare function useNavigation(): Navigation;
90
- export interface NavigationProgressProps {
91
- /** Bar color. Defaults to a neutral blue. */
92
- color?: string;
93
- /** Bar height in pixels. Defaults to `3`. */
94
- height?: number;
95
- }
96
- /**
97
- * An opt-in top progress bar that appears while a client navigation is in
98
- * flight (driven by {@link Router.pending}). Drop one instance in your root
99
- * layout; it renders nothing on the server and stays invisible until the first
100
- * soft navigation, so there's no hydration flicker.
101
- *
102
- * @example
103
- * ```tsx
104
- * import { NavigationProgress } from '@rshono/core/client';
105
102
  *
106
- * // in your layout, once:
107
- * <body>
108
- * <NavigationProgress />
109
- * {children}
110
- * </body>
111
- * ```
103
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}
104
+ * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}
112
105
  */
113
- export declare function NavigationProgress({ color, height }?: NavigationProgressProps): ReactNode;
114
- //# sourceMappingURL=navigation.d.ts.map
106
+ export declare function useNavigation(): NavigationState;
@@ -1,20 +1,18 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext, useEffect, useMemo, useState } from 'react';
3
+ import { createContext, useContext, useMemo } from 'react';
4
4
  const noop = () => { };
5
5
  const defaultRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };
6
6
  /**
7
- * Carries the live {@link Router} implementation from the hydration runtime down
8
- * to {@link RouterProvider}. Framework internal — read the router through
9
- * {@link useNavigation} instead.
7
+ * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.
10
8
  *
11
9
  * @internal
12
10
  */
13
11
  export const RouterContext = createContext(defaultRouter);
14
12
  const NavigationContext = createContext(null);
15
13
  /**
16
- * Publishes the per-render location and params so {@link useNavigation} can read
17
- * them. Framework internal — the RSC entry wraps every page in one.
14
+ * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps
15
+ * every page in one.
18
16
  *
19
17
  * @internal
20
18
  */
@@ -24,16 +22,20 @@ export function RouterProvider({ href, params, children }) {
24
22
  return _jsx(NavigationContext.Provider, { value: value, children: children });
25
23
  }
26
24
  /**
27
- * Reactive access to the current URL and programmatic navigation, in one hook.
25
+ * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
26
+ * `'use client'` component.
28
27
  *
29
- * Call it from a `'use client'` component. The location fields (`url` and
30
- * `params`) are computed on the server and travel in the flight payload, so they
31
- * are correct during SSR no hydration flicker and update automatically on
32
- * every navigation. The `router` sub-object holds the imperative actions plus a
33
- * `pending` flag that is `true` while a client navigation is in flight.
28
+ * `url` and `params` are computed on the server and travel in the flight payload, so they are correct
29
+ * during SSR no hydration flicker and update on every navigation. `router` holds the imperative
30
+ * actions plus a `pending` flag, `true` while a soft navigation is in flight.
34
31
  *
35
- * Hooks can't run in a server component; read the same URL data there from
36
- * `getContext()` (`@rshono/core/server`) instead.
32
+ * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The
33
+ * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own
34
+ * `PageProps.url` — the same value, not a live one. A page whose output depends on the query wants
35
+ * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an
36
+ * effect.
37
+ *
38
+ * Hooks can't run in a server component; read the same data there from `getRequestContext()`.
37
39
  *
38
40
  * @example
39
41
  * ```tsx
@@ -51,62 +53,19 @@ export function RouterProvider({ href, params, children }) {
51
53
  * }
52
54
  * ```
53
55
  *
54
- * @returns The current {@link Navigation}: `url` and `params`, plus `router`
55
- * ({@link Router}) with `push` / `replace` / `back` / `forward` / `refresh` /
56
- * `pending`.
56
+ * @returns The current {@link NavigationState}: `url` and `params`, plus `router`
57
+ * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.
57
58
  * @throws If called outside a page's React tree, where there is no navigation
58
59
  * context to read.
60
+ *
61
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}
62
+ * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}
59
63
  */
60
64
  export function useNavigation() {
61
65
  const value = useContext(NavigationContext);
62
66
  if (!value) {
63
- throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getContext() instead.");
67
+ throw new Error("[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.");
64
68
  }
65
69
  return value;
66
70
  }
67
- /**
68
- * An opt-in top progress bar that appears while a client navigation is in
69
- * flight (driven by {@link Router.pending}). Drop one instance in your root
70
- * layout; it renders nothing on the server and stays invisible until the first
71
- * soft navigation, so there's no hydration flicker.
72
- *
73
- * @example
74
- * ```tsx
75
- * import { NavigationProgress } from '@rshono/core/client';
76
- *
77
- * // in your layout, once:
78
- * <body>
79
- * <NavigationProgress />
80
- * {children}
81
- * </body>
82
- * ```
83
- */
84
- export function NavigationProgress({ color = '#3b82f6', height = 3 } = {}) {
85
- const { router } = useNavigation();
86
- const [bar, setBar] = useState({ width: 0, opacity: 0 });
87
- useEffect(() => {
88
- if (router.pending) {
89
- // Jump in, then creep toward — but never reach — the end while we wait.
90
- setBar({ width: 15, opacity: 1 });
91
- const ramp = setTimeout(() => setBar({ width: 85, opacity: 1 }), 80);
92
- return () => clearTimeout(ramp);
93
- }
94
- // Done: snap to full, then fade out. (No-op if it was never shown.)
95
- setBar((b) => (b.opacity === 0 ? b : { width: 100, opacity: 1 }));
96
- const hide = setTimeout(() => setBar({ width: 0, opacity: 0 }), 220);
97
- return () => clearTimeout(hide);
98
- }, [router.pending]);
99
- return (_jsx("div", { "data-rshono-progress": "", "aria-hidden": "true", style: {
100
- position: 'fixed',
101
- top: 0,
102
- left: 0,
103
- height,
104
- width: `${bar.width}%`,
105
- opacity: bar.opacity,
106
- background: color,
107
- zIndex: 2147483647,
108
- pointerEvents: 'none',
109
- transition: 'width 200ms ease-out, opacity 200ms ease-out',
110
- } }));
111
- }
112
71
  //# sourceMappingURL=navigation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * `push` / `replace` / `refresh` are **soft** navigations: the new page's flight\n * payload is fetched and applied in place, so client component state outside the\n * changed subtree survives. Off-site or non-HTTP hrefs fall back to a full load.\n */\nexport interface Router {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Goes back one history entry `history.back()`. */\n back(): void;\n /** Goes forward one history entry `history.forward()`. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link Router}, as returned by {@link useNavigation}. */\nexport interface Navigation {\n /**\n * The full current {@link URL} — read `url.pathname`, `url.searchParams` and the\n * rest off it. A fresh instance per navigation, so mutating it affects nothing\n * else; it is not written back to the address bar either.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: Router;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: Router = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link Router} implementation from the hydration runtime down\n * to {@link RouterProvider}. Framework internal — read the router through\n * {@link useNavigation} instead.\n *\n * @internal\n */\nexport const RouterContext = createContext<Router>(defaultRouter);\n\nconst NavigationContext = createContext<Navigation | null>(null);\n\n/**\n * Publishes the per-render location and params so {@link useNavigation} can read\n * them. Framework internal — the RSC entry wraps every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n const value = useMemo<Navigation>(() => ({ url: new URL(href), params, router }), [href, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook.\n *\n * Call it from a `'use client'` component. The location fields (`url` and\n * `params`) are computed on the server and travel in the flight payload, so they\n * are correct during SSR — no hydration flicker — and update automatically on\n * every navigation. The `router` sub-object holds the imperative actions plus a\n * `pending` flag that is `true` while a client navigation is in flight.\n *\n * Hooks can't run in a server component; read the same URL data there from\n * `getContext()` (`@rshono/core/server`) instead.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link Navigation}: `url` and `params`, plus `router`\n * ({@link Router}) with `push` / `replace` / `back` / `forward` / `refresh` /\n * `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n */\nexport function useNavigation(): Navigation {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getContext() instead.\",\n );\n }\n return value;\n}\n\nexport interface NavigationProgressProps {\n /** Bar color. Defaults to a neutral blue. */\n color?: string;\n /** Bar height in pixels. Defaults to `3`. */\n height?: number;\n}\n\n/**\n * An opt-in top progress bar that appears while a client navigation is in\n * flight (driven by {@link Router.pending}). Drop one instance in your root\n * layout; it renders nothing on the server and stays invisible until the first\n * soft navigation, so there's no hydration flicker.\n *\n * @example\n * ```tsx\n * import { NavigationProgress } from '@rshono/core/client';\n *\n * // in your layout, once:\n * <body>\n * <NavigationProgress />\n * {children}\n * </body>\n * ```\n */\nexport function NavigationProgress({ color = '#3b82f6', height = 3 }: NavigationProgressProps = {}): ReactNode {\n const { router } = useNavigation();\n const [bar, setBar] = useState({ width: 0, opacity: 0 });\n\n useEffect(() => {\n if (router.pending) {\n // Jump in, then creep toward — but never reach — the end while we wait.\n setBar({ width: 15, opacity: 1 });\n const ramp = setTimeout(() => setBar({ width: 85, opacity: 1 }), 80);\n return () => clearTimeout(ramp);\n }\n // Done: snap to full, then fade out. (No-op if it was never shown.)\n setBar((b) => (b.opacity === 0 ? b : { width: 100, opacity: 1 }));\n const hide = setTimeout(() => setBar({ width: 0, opacity: 0 }), 220);\n return () => clearTimeout(hide);\n }, [router.pending]);\n\n return (\n <div\n data-rshono-progress=\"\"\n aria-hidden=\"true\"\n style={{\n position: 'fixed',\n top: 0,\n left: 0,\n height,\n width: `${bar.width}%`,\n opacity: bar.opacity,\n background: color,\n zIndex: 2147483647,\n pointerEvents: 'none',\n transition: 'width 200ms ease-out, opacity 200ms ease-out',\n }}\n />\n );\n}\n"]}
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAkB,MAAM,OAAO,CAAC;AAmD3E,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAE/G,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * it is not written back to the address bar.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n const value = useMemo<NavigationState>(() => ({ url: new URL(href), params, router }), [href, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The\n * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own\n * `PageProps.url` — the same value, not a live one. A page whose output depends on the query wants\n * `render: 'dynamic'`; a component that only needs it after hydration can read `location.search` in an\n * effect.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
@@ -1,10 +1,9 @@
1
1
  /**
2
- * The four distinct shapes an incoming render request can take, modeled as a
3
- * discriminated union so illegal combinations (e.g. an action id on a GET) are
4
- * unrepresentable and every consumer dispatches exhaustively on `kind`.
2
+ * The four shapes an incoming render request can take, as a discriminated union so that illegal
3
+ * combinations an action id on a GET are unrepresentable.
5
4
  *
6
5
  * - `document` — a normal navigation; respond with a full SSR HTML document.
7
- * - `rsc` — a soft-navigation flight fetch (`Accept: text/x-component`).
6
+ * - `rsc` — a soft-navigation flight fetch (`RSC: 1`).
8
7
  * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).
9
8
  * - `rsc-action` — a client-initiated server-action call carrying an action id.
10
9
  */
@@ -18,18 +17,53 @@ export type RenderRequest = {
18
17
  kind: 'rsc-action';
19
18
  actionId: string;
20
19
  };
21
- /** Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server action. */
20
+ /**
21
+ * Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server
22
+ * action.
23
+ *
24
+ * `signal` is for a navigation, which a later one supersedes — an action is never abandoned that way, so its
25
+ * caller passes none.
26
+ */
22
27
  export declare function createRscRequest(urlString: string, action?: {
23
28
  id: string;
24
29
  body: BodyInit;
25
- }): Request;
30
+ }, signal?: AbortSignal): Request;
31
+ /**
32
+ * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`
33
+ * values, and no header of its own to need a preflight for.
34
+ *
35
+ * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,
36
+ * because what makes a form post forgeable from another site is its shape and not what happens to be in the
37
+ * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`
38
+ * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form
39
+ * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost
40
+ * was a forced authenticated page render, and a claim about the boundary that was not true as written.
41
+ *
42
+ * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not
43
+ * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.
44
+ *
45
+ * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and
46
+ * that decides what is *decoded*, so a header the two read differently is a request that takes the form path
47
+ * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said "the
48
+ * client-initiated shape, already covered" while `get` was falsy, so the request fell through to
49
+ * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a
50
+ * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a
51
+ * predicate that fails open on a shape nothing produces today is one bad day from being wrong.
52
+ */
53
+ export declare function isBrowserFormPost(request: Request): boolean;
26
54
  export declare function parseRenderRequest(request: Request): RenderRequest;
27
55
  /** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */
28
- export declare function acceptsRsc(request: Request): boolean;
56
+ export declare function asksForRsc(request: Request): boolean;
57
+ /** The header a response has to `Vary` on, since one page URL answers as either representation. */
58
+ export declare const RSC_VARY_HEADER = "RSC";
29
59
  /** True when the response should be a flight payload rather than an HTML document. */
30
60
  export declare function wantsRsc(renderRequest: RenderRequest): boolean;
61
+ /**
62
+ * {@link wantsRsc} straight off the request, for the error and control-signal paths — reached from Hono's
63
+ * `onError` / `notFound`, so they have no parsed {@link RenderRequest} to hand.
64
+ */
65
+ export declare function requestWantsRsc(request: Request): boolean;
31
66
  /** True when the request carries a server action to run before rendering. */
32
67
  export declare function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, {
33
68
  kind: 'form-action' | 'rsc-action';
34
69
  }>;
35
- //# sourceMappingURL=request.d.ts.map
@@ -1,18 +1,86 @@
1
+ /**
2
+ * The header that names the server action a client-initiated call wants to run, and so the header that
3
+ * decides which of the two action branches a POST takes.
4
+ *
5
+ * That choice is a security boundary, not just a dispatch detail. It is deliberately *not* a CORS-safelisted
6
+ * header: a page on another origin cannot send one without a successful preflight, and the framework answers
7
+ * no preflight, so the `rsc-action` branch is unreachable cross-origin and carries no origin check of its
8
+ * own. The `form-action` branch is the forgeable one — a form post needs no preflight — and that is the one
9
+ * `refusesCrossSiteForm` stands in front of. See `SECURITY.md`.
10
+ */
1
11
  const HEADER_ACTION_ID = 'x-rsc-action';
2
- const RSC_CONTENT_TYPE = 'text/x-component';
3
- /** Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server action. */
4
- export function createRscRequest(urlString, action) {
12
+ /**
13
+ * The header that asks a page URL for its flight payload rather than its HTML document, and the value it
14
+ * carries. One URL answers as either, so this is what the response `Vary`s on.
15
+ *
16
+ * A header of its own rather than `Accept: text/x-component`, which is what this used to be. Content
17
+ * negotiation was the more standards-shaped mechanism and it cost too much: a prerendered page is served
18
+ * `public, max-age=300`, and `Vary: Accept` on a publicly cacheable response is close to a cache-disabling
19
+ * header — real browsers send long `Accept` strings that differ by vendor and version, so a CDN keyed on it
20
+ * stores a copy per variant of the same bytes, and some shared caches decline to store a `Vary` they consider
21
+ * high-cardinality at all. This one has exactly two states.
22
+ */
23
+ const HEADER_RSC = 'rsc';
24
+ const HEADER_RSC_VALUE = '1';
25
+ /**
26
+ * Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server
27
+ * action.
28
+ *
29
+ * `signal` is for a navigation, which a later one supersedes — an action is never abandoned that way, so its
30
+ * caller passes none.
31
+ */
32
+ export function createRscRequest(urlString, action, signal) {
5
33
  const url = new URL(urlString, location.origin);
6
- const headers = new Headers({ Accept: RSC_CONTENT_TYPE });
34
+ const headers = new Headers({ [HEADER_RSC]: HEADER_RSC_VALUE });
7
35
  if (action)
8
36
  headers.set(HEADER_ACTION_ID, action.id);
9
37
  return new Request(url, {
10
38
  method: action ? 'POST' : 'GET',
11
39
  headers,
12
40
  body: action?.body,
41
+ signal,
13
42
  });
14
43
  }
44
+ /**
45
+ * The two content types React writes a form action as, and so the two a POST must carry to be *decoded* as
46
+ * one.
47
+ *
48
+ * Deliberately narrower than the set {@link isBrowserFormPost} refuses on. `decodeAction` reaches the body
49
+ * through `request.formData()`, which throws on a `text/plain` one — so classifying that enctype as a
50
+ * `form-action` would turn every same-origin `text/plain` POST into a 400 where the page used to render.
51
+ * What the framework decodes and what it refuses are two different questions.
52
+ */
15
53
  const FORM_CONTENT_TYPES = /^(?:multipart\/form-data|application\/x-www-form-urlencoded)/i;
54
+ /** The third `enctype` a browser `<form>` can send, which React never writes and this never decodes. */
55
+ const PLAIN_TEXT_ENCTYPE = /^text\/plain/i;
56
+ /**
57
+ * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`
58
+ * values, and no header of its own to need a preflight for.
59
+ *
60
+ * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,
61
+ * because what makes a form post forgeable from another site is its shape and not what happens to be in the
62
+ * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`
63
+ * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form
64
+ * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost
65
+ * was a forced authenticated page render, and a claim about the boundary that was not true as written.
66
+ *
67
+ * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not
68
+ * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.
69
+ *
70
+ * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and
71
+ * that decides what is *decoded*, so a header the two read differently is a request that takes the form path
72
+ * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said "the
73
+ * client-initiated shape, already covered" while `get` was falsy, so the request fell through to
74
+ * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a
75
+ * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a
76
+ * predicate that fails open on a shape nothing produces today is one bad day from being wrong.
77
+ */
78
+ export function isBrowserFormPost(request) {
79
+ if (request.method !== 'POST' || request.headers.get(HEADER_ACTION_ID))
80
+ return false;
81
+ const contentType = request.headers.get('content-type') ?? '';
82
+ return FORM_CONTENT_TYPES.test(contentType) || PLAIN_TEXT_ENCTYPE.test(contentType);
83
+ }
16
84
  export function parseRenderRequest(request) {
17
85
  if (request.method === 'POST') {
18
86
  const actionId = request.headers.get(HEADER_ACTION_ID);
@@ -22,16 +90,25 @@ export function parseRenderRequest(request) {
22
90
  return { kind: 'form-action' };
23
91
  return { kind: 'document' };
24
92
  }
25
- return { kind: acceptsRsc(request) ? 'rsc' : 'document' };
93
+ return { kind: asksForRsc(request) ? 'rsc' : 'document' };
26
94
  }
27
95
  /** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */
28
- export function acceptsRsc(request) {
29
- return request.headers.get('accept')?.includes(RSC_CONTENT_TYPE) ?? false;
96
+ export function asksForRsc(request) {
97
+ return request.headers.get(HEADER_RSC) === HEADER_RSC_VALUE;
30
98
  }
99
+ /** The header a response has to `Vary` on, since one page URL answers as either representation. */
100
+ export const RSC_VARY_HEADER = 'RSC';
31
101
  /** True when the response should be a flight payload rather than an HTML document. */
32
102
  export function wantsRsc(renderRequest) {
33
103
  return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';
34
104
  }
105
+ /**
106
+ * {@link wantsRsc} straight off the request, for the error and control-signal paths — reached from Hono's
107
+ * `onError` / `notFound`, so they have no parsed {@link RenderRequest} to hand.
108
+ */
109
+ export function requestWantsRsc(request) {
110
+ return wantsRsc(parseRenderRequest(request));
111
+ }
35
112
  /** True when the request carries a server action to run before rendering. */
36
113
  export function isActionRequest(renderRequest) {
37
114
  return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';
@@ -1 +1 @@
1
- {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,cAAc,CAAC;AACxC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAc5C,uHAAuH;AACvH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,MAAuC;IACzF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC1D,IAAI,MAAM;QAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK;QAC/B,OAAO;QACP,IAAI,EAAE,MAAM,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,kBAAkB,GAAG,+DAA+D,CAAC;AAE3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QACvD,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QACtD,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACvG,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC9B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;AAC5D,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC;AAC5E,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,aAA4B;IACnD,OAAO,aAAa,CAAC,IAAI,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AAC7E,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,eAAe,CAAC,aAA4B;IAC1D,OAAO,aAAa,CAAC,IAAI,KAAK,aAAa,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AACrF,CAAC","sourcesContent":["const HEADER_ACTION_ID = 'x-rsc-action';\nconst RSC_CONTENT_TYPE = 'text/x-component';\n\n/**\n * The four distinct shapes an incoming render request can take, modeled as a\n * discriminated union so illegal combinations (e.g. an action id on a GET) are\n * unrepresentable and every consumer dispatches exhaustively on `kind`.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`Accept: text/x-component`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/** Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server action. */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ Accept: RSC_CONTENT_TYPE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n });\n}\n\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: acceptsRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function acceptsRsc(request: Request): boolean {\n return request.headers.get('accept')?.includes(RSC_CONTENT_TYPE) ?? false;\n}\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The header that names the server action a client-initiated call wants to run, and so the header that\n * decides which of the two action branches a POST takes.\n *\n * That choice is a security boundary, not just a dispatch detail. It is deliberately *not* a CORS-safelisted\n * header: a page on another origin cannot send one without a successful preflight, and the framework answers\n * no preflight, so the `rsc-action` branch is unreachable cross-origin and carries no origin check of its\n * own. The `form-action` branch is the forgeable one — a form post needs no preflight — and that is the one\n * `refusesCrossSiteForm` stands in front of. See `SECURITY.md`.\n */\nconst HEADER_ACTION_ID = 'x-rsc-action';\n\n/**\n * The header that asks a page URL for its flight payload rather than its HTML document, and the value it\n * carries. One URL answers as either, so this is what the response `Vary`s on.\n *\n * A header of its own rather than `Accept: text/x-component`, which is what this used to be. Content\n * negotiation was the more standards-shaped mechanism and it cost too much: a prerendered page is served\n * `public, max-age=300`, and `Vary: Accept` on a publicly cacheable response is close to a cache-disabling\n * header — real browsers send long `Accept` strings that differ by vendor and version, so a CDN keyed on it\n * stores a copy per variant of the same bytes, and some shared caches decline to store a `Vary` they consider\n * high-cardinality at all. This one has exactly two states.\n */\nconst HEADER_RSC = 'rsc';\nconst HEADER_RSC_VALUE = '1';\n\n/**\n * The four shapes an incoming render request can take, as a discriminated union so that illegal\n * combinations an action id on a GET are unrepresentable.\n *\n * - `document` — a normal navigation; respond with a full SSR HTML document.\n * - `rsc` — a soft-navigation flight fetch (`RSC: 1`).\n * - `form-action` — a progressive-enhancement `<form>` POST (no JavaScript).\n * - `rsc-action` — a client-initiated server-action call carrying an action id.\n */\nexport type RenderRequest = { kind: 'document' } | { kind: 'rsc' } | { kind: 'form-action' } | { kind: 'rsc-action'; actionId: string };\n\n/**\n * Builds the `Request` the client sends to ask a page for its flight payload, optionally carrying a server\n * action.\n *\n * `signal` is for a navigation, which a later one supersedes — an action is never abandoned that way, so its\n * caller passes none.\n */\nexport function createRscRequest(urlString: string, action?: { id: string; body: BodyInit }, signal?: AbortSignal): Request {\n const url = new URL(urlString, location.origin);\n const headers = new Headers({ [HEADER_RSC]: HEADER_RSC_VALUE });\n if (action) headers.set(HEADER_ACTION_ID, action.id);\n return new Request(url, {\n method: action ? 'POST' : 'GET',\n headers,\n body: action?.body,\n signal,\n });\n}\n\n/**\n * The two content types React writes a form action as, and so the two a POST must carry to be *decoded* as\n * one.\n *\n * Deliberately narrower than the set {@link isBrowserFormPost} refuses on. `decodeAction` reaches the body\n * through `request.formData()`, which throws on a `text/plain` one — so classifying that enctype as a\n * `form-action` would turn every same-origin `text/plain` POST into a 400 where the page used to render.\n * What the framework decodes and what it refuses are two different questions.\n */\nconst FORM_CONTENT_TYPES = /^(?:multipart\\/form-data|application\\/x-www-form-urlencoded)/i;\n\n/** The third `enctype` a browser `<form>` can send, which React never writes and this never decodes. */\nconst PLAIN_TEXT_ENCTYPE = /^text\\/plain/i;\n\n/**\n * Whether a POST arrived in a shape a browser `<form>` could have produced: one of the three `enctype`\n * values, and no header of its own to need a preflight for.\n *\n * The framework's cross-site refusal runs on this rather than on the {@link RenderRequest} classification,\n * because what makes a form post forgeable from another site is its shape and not what happens to be in the\n * body. Keyed the other way, `text/plain` — the one enctype React never writes — was classified `document`\n * and never reached the refusal, while the README promised that a page route refuses *every* cross-site form\n * post. No action could run through it, since `decodeAction` is only called for the two above; what it cost\n * was a forced authenticated page render, and a claim about the boundary that was not true as written.\n *\n * `x-rsc-action` excludes the client-initiated shape, which needs no check of its own: that header is not\n * CORS-safelisted, so a cross-origin caller needs a preflight the framework never answers.\n *\n * **The same test {@link parseRenderRequest} makes**, and it has to be: this decides what is *refused* and\n * that decides what is *decoded*, so a header the two read differently is a request that takes the form path\n * without passing the form check. A present-but-empty `x-rsc-action` was exactly that — `has` said \"the\n * client-initiated shape, already covered\" while `get` was falsy, so the request fell through to\n * `form-action` and ran the action it carried with the cross-site refusal skipped. Not reachable from a\n * browser, since an empty header is still a header and still needs the preflight; refused anyway, because a\n * predicate that fails open on a shape nothing produces today is one bad day from being wrong.\n */\nexport function isBrowserFormPost(request: Request): boolean {\n if (request.method !== 'POST' || request.headers.get(HEADER_ACTION_ID)) return false;\n const contentType = request.headers.get('content-type') ?? '';\n return FORM_CONTENT_TYPES.test(contentType) || PLAIN_TEXT_ENCTYPE.test(contentType);\n}\n\nexport function parseRenderRequest(request: Request): RenderRequest {\n if (request.method === 'POST') {\n const actionId = request.headers.get(HEADER_ACTION_ID);\n if (actionId) return { kind: 'rsc-action', actionId };\n if (FORM_CONTENT_TYPES.test(request.headers.get('content-type') ?? '')) return { kind: 'form-action' };\n return { kind: 'document' };\n }\n return { kind: asksForRsc(request) ? 'rsc' : 'document' };\n}\n\n/** Whether the client asked for a flight payload. For GET paths that only need the boolean, without parsing. */\nexport function asksForRsc(request: Request): boolean {\n return request.headers.get(HEADER_RSC) === HEADER_RSC_VALUE;\n}\n\n/** The header a response has to `Vary` on, since one page URL answers as either representation. */\nexport const RSC_VARY_HEADER = 'RSC';\n\n/** True when the response should be a flight payload rather than an HTML document. */\nexport function wantsRsc(renderRequest: RenderRequest): boolean {\n return renderRequest.kind === 'rsc' || renderRequest.kind === 'rsc-action';\n}\n\n/**\n * {@link wantsRsc} straight off the request, for the error and control-signal paths — reached from Hono's\n * `onError` / `notFound`, so they have no parsed {@link RenderRequest} to hand.\n */\nexport function requestWantsRsc(request: Request): boolean {\n return wantsRsc(parseRenderRequest(request));\n}\n\n/** True when the request carries a server action to run before rendering. */\nexport function isActionRequest(renderRequest: RenderRequest): renderRequest is Extract<RenderRequest, { kind: 'form-action' | 'rsc-action' }> {\n return renderRequest.kind === 'form-action' || renderRequest.kind === 'rsc-action';\n}\n"]}
@@ -1,24 +1,26 @@
1
1
  /**
2
- * `@rshono/core/server` — the request-scoped surface, for use inside server components
3
- * and `'use server'` action modules: {@link getContext} for the URL, cookies,
4
- * params, env and middleware variables, the {@link redirect} and {@link notFound}
5
- * control-flow helpers, and {@link onServerError} for reporting the errors the
6
- * framework catches.
2
+ * `@rshono/core/server` — the request-scoped surface, for server components and `'use server'` action
3
+ * modules: `getRequestContext()` for the URL, cookies, params, env and middleware variables, the
4
+ * `redirect()` and `notFound()` control-flow helpers, and `onServerError()` for reporting the errors
5
+ * the framework catches. Plus `publicUrl(c)` for middleware, which is handed Hono's `c` rather than a
6
+ * request context.
7
7
  *
8
- * Server-only. Importing this from a `'use client'` module is a mistake those
9
- * run in the browser (and are SSR'd without a bound context). Read what you need
10
- * on the server and pass it down as props, or use `useNavigation()` from
11
- * `@rshono/core/client` for URL data.
8
+ * Server-only: a `'use client'` module runs in the browser, with no bound context. Read what you need
9
+ * on the server and pass it down as props, or use `useNavigation()` from `@rshono/core/client`.
12
10
  *
13
- * A barrel rather than `./context.js` itself, which is where all of this is
14
- * implemented: that module also exports the plumbing the framework's own entry
15
- * points need (`runWithContext`, `readParams`, `publicUrl`, `reportServerError`),
16
- * and pointing the `./server` subpath straight at it published every one of them —
17
- * `@internal` in the docs, but present in a consumer's autocomplete and pinned by
18
- * semver all the same. They stay reachable by relative import, which is the only
19
- * way the framework itself ever reaches them.
11
+ * @example
12
+ * ```ts
13
+ * 'use server';
14
+ * import { getRequestContext, redirect } from '@rshono/core/server';
15
+ *
16
+ * export async function logout() {
17
+ * getRequestContext().cookies.delete('session', { path: '/' });
18
+ * redirect('/');
19
+ * }
20
+ * ```
21
+ *
22
+ * @see {@link https://www.rshono.com/docs/api#rshonocoreserver | Docs — `@rshono/core/server`}
20
23
  *
21
24
  * @packageDocumentation
22
25
  */
23
- export { getContext, notFound, onServerError, redirect, type Ctx, type EnvVars, type RedirectStatus, type ServerErrorContext, type ServerErrorHandler, type ServerErrorSource, } from './context.js';
24
- //# sourceMappingURL=server.d.ts.map
26
+ export { getRequestContext, notFound, onServerError, publicUrl, redirect, type EnvVars, type RedirectStatus, type RequestContext, type ServerErrorContext, type ServerErrorHandler, type ServerErrorSource, } from './context.js';