@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.
- package/LICENSE +21 -0
- package/README.md +348 -158
- package/bin/rshono.mjs +3 -4
- package/dist/builder/env-shadow-loader.cjs +123 -8
- package/dist/builder/page-entry-loader.cjs +28 -1
- package/dist/builder/page-files.d.ts +0 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +7 -1
- package/dist/builder/public-env.js +7 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/react-versions.d.ts +12 -0
- package/dist/builder/react-versions.js +74 -0
- package/dist/builder/react-versions.js.map +1 -0
- package/dist/builder/rspack-config.d.ts +13 -4
- package/dist/builder/rspack-config.js +136 -53
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/builder/server-only-imports.d.ts +32 -0
- package/dist/builder/server-only-imports.js +51 -0
- package/dist/builder/server-only-imports.js.map +1 -0
- package/dist/cli/build.d.ts +2 -3
- package/dist/cli/build.js +51 -13
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -3
- package/dist/cli/dev.js +149 -45
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/exit.d.ts +12 -0
- package/dist/cli/exit.js +16 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +91 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +0 -1
- package/dist/cli/start.js +15 -18
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +72 -78
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
- package/dist/deploy/aws-lambda/runtime.js +5 -8
- package/dist/deploy/aws-lambda/runtime.js.map +1 -1
- package/dist/deploy/build-marker.d.ts +9 -6
- package/dist/deploy/build-marker.js +8 -4
- package/dist/deploy/build-marker.js.map +1 -1
- package/dist/deploy/cloudflare/build.d.ts +0 -1
- package/dist/deploy/cloudflare/build.js +17 -13
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts +2 -6
- package/dist/deploy/cloudflare/runtime.js +73 -68
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +57 -41
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +3 -6
- package/dist/deploy/filesystem.js +23 -20
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +5 -3
- package/dist/deploy/node/runtime.js +23 -9
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +34 -30
- package/dist/deploy/presets.js +56 -44
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/public-paths.d.ts +42 -0
- package/dist/deploy/public-paths.js +66 -0
- package/dist/deploy/public-paths.js.map +1 -0
- package/dist/deploy/vercel/build.d.ts +0 -1
- package/dist/deploy/vercel/build.js +25 -16
- package/dist/deploy/vercel/build.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts +10 -7
- package/dist/deploy/vercel/runtime.js +48 -11
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +19 -17
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +212 -120
- package/dist/router.js +3 -8
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +62 -48
- package/dist/runtime/boundaries.js +32 -34
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +20 -11
- package/dist/runtime/client.js +20 -10
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +304 -123
- package/dist/runtime/context.js +377 -149
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts +15 -1
- package/dist/runtime/control.js +24 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts +4 -9
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/empty-server-app.d.ts +0 -1
- package/dist/runtime/entry.client.d.ts +4 -1
- package/dist/runtime/entry.client.js +455 -313
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +15 -7
- package/dist/runtime/entry.rsc.js +811 -252
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +27 -10
- package/dist/runtime/entry.ssr.js +30 -48
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/failure-document.d.ts +11 -0
- package/dist/runtime/failure-document.js +35 -0
- package/dist/runtime/failure-document.js.map +1 -0
- package/dist/runtime/flight-inject.d.ts +23 -0
- package/dist/runtime/flight-inject.js +403 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/hot-update.d.ts +44 -0
- package/dist/runtime/hot-update.js +44 -0
- package/dist/runtime/hot-update.js.map +1 -0
- package/dist/runtime/navigation.d.ts +46 -54
- package/dist/runtime/navigation.js +22 -63
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +42 -8
- package/dist/runtime/request.js +84 -7
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +20 -18
- package/dist/runtime/server.js +22 -17
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +77 -0
- package/dist/runtime/validate-entries.js +405 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/headers.d.ts +41 -15
- package/dist/server/headers.js +53 -24
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +4 -5
- package/dist/server/load-config.js +22 -14
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/load-env.d.ts +0 -1
- package/dist/server/prerendered.d.ts +88 -34
- package/dist/server/prerendered.js +184 -24
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +35 -50
- package/dist/server/server-config.js +27 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/shutdown.d.ts +5 -4
- package/dist/server/shutdown.js +5 -3
- package/dist/server/shutdown.js.map +1 -1
- package/dist/server/ssg.d.ts +9 -7
- package/dist/server/ssg.js +209 -65
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts +0 -1
- package/dist/server/static.js +7 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +21 -23
- package/dist/builder/page-files.d.ts.map +0 -1
- package/dist/builder/public-env.d.ts.map +0 -1
- package/dist/builder/rspack-config.d.ts.map +0 -1
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/start.d.ts.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
- package/dist/deploy/build-marker.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.d.ts +0 -11
- package/dist/deploy/bun/runtime.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.js +0 -22
- package/dist/deploy/bun/runtime.js.map +0 -1
- package/dist/deploy/cloudflare/build.d.ts.map +0 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
- package/dist/deploy/contract.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.d.ts +0 -11
- package/dist/deploy/deno/runtime.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.js +0 -16
- package/dist/deploy/deno/runtime.js.map +0 -1
- package/dist/deploy/filesystem.d.ts.map +0 -1
- package/dist/deploy/listen.d.ts +0 -20
- package/dist/deploy/listen.d.ts.map +0 -1
- package/dist/deploy/listen.js +0 -24
- package/dist/deploy/listen.js.map +0 -1
- package/dist/deploy/netlify/build.d.ts +0 -8
- package/dist/deploy/netlify/build.d.ts.map +0 -1
- package/dist/deploy/netlify/build.js +0 -52
- package/dist/deploy/netlify/build.js.map +0 -1
- package/dist/deploy/netlify/runtime.d.ts +0 -13
- package/dist/deploy/netlify/runtime.d.ts.map +0 -1
- package/dist/deploy/netlify/runtime.js +0 -24
- package/dist/deploy/netlify/runtime.js.map +0 -1
- package/dist/deploy/node/runtime.d.ts.map +0 -1
- package/dist/deploy/presets.d.ts.map +0 -1
- package/dist/deploy/vercel/build.d.ts.map +0 -1
- package/dist/deploy/vercel/runtime.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/runtime/boundaries.d.ts.map +0 -1
- package/dist/runtime/client.d.ts.map +0 -1
- package/dist/runtime/context.d.ts.map +0 -1
- package/dist/runtime/control.d.ts.map +0 -1
- package/dist/runtime/dev-protocol.d.ts.map +0 -1
- package/dist/runtime/empty-server-app.d.ts.map +0 -1
- package/dist/runtime/entry.client.d.ts.map +0 -1
- package/dist/runtime/entry.rsc.d.ts.map +0 -1
- package/dist/runtime/entry.ssr.d.ts.map +0 -1
- package/dist/runtime/navigation.d.ts.map +0 -1
- package/dist/runtime/request.d.ts.map +0 -1
- package/dist/runtime/server.d.ts.map +0 -1
- package/dist/server/compress.d.ts +0 -15
- package/dist/server/compress.d.ts.map +0 -1
- package/dist/server/compress.js +0 -76
- package/dist/server/compress.js.map +0 -1
- package/dist/server/headers.d.ts.map +0 -1
- package/dist/server/load-config.d.ts.map +0 -1
- package/dist/server/load-env.d.ts.map +0 -1
- package/dist/server/prerendered.d.ts.map +0 -1
- package/dist/server/server-config.d.ts.map +0 -1
- package/dist/server/shutdown.d.ts.map +0 -1
- package/dist/server/ssg.d.ts.map +0 -1
- 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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
24
|
-
export interface
|
|
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}
|
|
27
|
-
*
|
|
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:
|
|
47
|
+
router: NavigationRouter;
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
37
|
-
* Carries the live {@link
|
|
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<
|
|
54
|
+
export declare const RouterContext: import("react").Context<NavigationRouter>;
|
|
44
55
|
/**
|
|
45
|
-
* Publishes the per-render location and params
|
|
46
|
-
*
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
|
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
|
|
84
|
-
* ({@link
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
17
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
36
|
-
* `
|
|
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
|
|
55
|
-
* ({@link
|
|
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
|
|
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":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,
|
|
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
|
|
3
|
-
*
|
|
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 (`
|
|
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
|
-
/**
|
|
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
|
|
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
|
package/dist/runtime/request.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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({
|
|
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:
|
|
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
|
|
29
|
-
return request.headers.get(
|
|
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;
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/runtime/request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAExC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,GAAG,KAAK,CAAC;AACzB,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAa7B;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,MAAuC,EAAE,MAAoB;IAC/G,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAChE,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;QAClB,MAAM;KACP,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAG,+DAA+D,CAAC;AAE3F,wGAAwG;AACxG,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAO,KAAK,CAAC;IACrF,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IAC9D,OAAO,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AACtF,CAAC;AAED,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,UAAU,CAAC,KAAK,gBAAgB,CAAC;AAC9D,CAAC;AAED,mGAAmG;AACnG,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,aAA4B;IACnD,OAAO,aAAa,CAAC,IAAI,KAAK,KAAK,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY,CAAC;AAC7E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/C,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":["/**\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"]}
|
package/dist/runtime/server.d.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@rshono/core/server` — the request-scoped surface, for use
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 {
|
|
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';
|