@rshono/core 1.0.0-rc.28 → 1.0.0-rc.29
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/README.md +5 -4
- package/dist/router.d.ts +9 -8
- package/dist/router.js.map +1 -1
- package/dist/runtime/client.d.ts +1 -1
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/control.js +12 -1
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/entry.client.js +414 -115
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.js +41 -9
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/navigation-preparation.d.ts +52 -0
- package/dist/runtime/navigation-preparation.js +155 -0
- package/dist/runtime/navigation-preparation.js.map +1 -0
- package/dist/runtime/navigation-transition.d.ts +6 -0
- package/dist/runtime/navigation-transition.js +62 -0
- package/dist/runtime/navigation-transition.js.map +1 -0
- package/dist/runtime/navigation.d.ts +28 -26
- package/dist/runtime/navigation.js +50 -47
- package/dist/runtime/navigation.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,10 +102,11 @@ export default async function Profile({ params, ctx }: PageProps<'/profile/:id'>
|
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
- Pages receive `{ url, params, ctx }` — `PageProps<'/profile/:id'>` types `params.id`, and `url` is a real
|
|
105
|
-
`URL`.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
`URL`. `params`, and `url` during SSR and hydration, reach a `'use client'` component from
|
|
106
|
+
`useNavigation()`; after hydration that hook's `url` follows the browser address — in-page links,
|
|
107
|
+
Back/Forward, the live query — while on a `render: 'static'` page `PageProps.url` stays the build-time
|
|
108
|
+
URL. The fragment is the one part a browser never sends to the server: it is never in `PageProps.url`,
|
|
109
|
+
only in the hook's `url.hash` after hydration.
|
|
109
110
|
- **`ctx` is the request context** — `ctx.req`, cookies, env, middleware variables, the proxy-aware URL. It is
|
|
110
111
|
the same object `getRequestContext()` returns from `@rshono/core/server`, handed over so a page needs no
|
|
111
112
|
import. Reading it on a `render: 'static'` page throws: a page rendered once at build time has no request.
|
package/dist/router.d.ts
CHANGED
|
@@ -28,8 +28,9 @@ export type PathParams<P extends string> = ParamKeys<P> extends never ? Record<s
|
|
|
28
28
|
* typed key-by-key; without it `params` falls back to an open `Record<string, string>`.
|
|
29
29
|
*
|
|
30
30
|
* `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is
|
|
31
|
-
* a type error at the route definition. `
|
|
32
|
-
* `'use client'` component
|
|
31
|
+
* a type error at the route definition. `params`, and `url` during SSR and hydration, mirror what
|
|
32
|
+
* `useNavigation()` gives a `'use client'` component; after hydration that hook's `url` follows the
|
|
33
|
+
* browser address instead.
|
|
33
34
|
*
|
|
34
35
|
* @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.
|
|
35
36
|
* @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and
|
|
@@ -56,12 +57,12 @@ export interface PageProps<Path extends string = string, E extends Env = Env> {
|
|
|
56
57
|
*
|
|
57
58
|
* On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the
|
|
58
59
|
* origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft
|
|
59
|
-
* navigation alike
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* navigation alike: the payload carries one build-time `href`. **`useNavigation()` reads that payload
|
|
61
|
+
* during SSR and hydration and then follows the browser address**, so a `'use client'` component can
|
|
62
|
+
* read the live query with no effect of its own. The fragment is the one part no payload can carry —
|
|
63
|
+
* a browser never sends `#…` with the request — so this prop never has one, while the hook's `url.hash`
|
|
64
|
+
* arrives from the address bar after hydration. Mark the route `render: 'dynamic'` if the page's server
|
|
65
|
+
* render depends on the query.
|
|
65
66
|
*/
|
|
66
67
|
url: URL;
|
|
67
68
|
/** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAoQA;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AA6LD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so importing `@rshono/core` pulls in none of `context.ts`'s runtime machinery.\nimport type { RequestContext } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key per `:param`\n * segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,\n * `:id{[0-9]+}` and `*` all work.\n *\n * You rarely name this directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n *\n * @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type argument to get `params`\n * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is\n * a type error at the route definition. `url` and `params` mirror what `useNavigation()` gives a\n * `'use client'` component, so a read moves across the server/client line unchanged.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and\n * {@link RequestContext.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /\n * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not\n * serializable, so hand a `'use client'` component `url.href` rather than `url`.\n *\n * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the\n * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft\n * navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload\n * carries one `href` and both readings come from it. The fragment is the one part no payload can carry\n * — a browser never sends `#…` with the request — so this prop never has one, while\n * `useNavigation().url.hash` is read from the browser after hydration. Mark the route\n * `render: 'dynamic'` if the page depends on the query; a `'use client'` component that only wants it\n * after hydration can read `location.search` in an effect.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the object `getRequestContext()` returns, handed to the page so cookies,\n * headers, env and middleware variables are reachable without an import.\n *\n * Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never\n * serialized. Passing it to a `'use client'` component fails the render, because it wraps the live\n * request — read what you need here and pass plain values down.\n *\n * Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an\n * enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only\n * serialization of a server component's props, which walks own enumerable properties. So `<Child\n * {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.\n * Nested server components are meant to call `getRequestContext()` for the same object rather than\n * receive it, which is also the fix if a spread has already cost you an afternoon.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.\n * Mark the route `render: 'dynamic'`, or use the `url` / `params` props.\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: RequestContext<E>;\n}\n\n/**\n * A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via\n * a shared layout. It may be `async` and await data directly.\n *\n * Each page module default-exports exactly one. Interactive parts belong in `'use client'` components\n * the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n *\n * @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}\n * @see {@link https://www.rshono.com/docs/pages | Docs — pages}\n */\n// `any`, not `unknown`: this default is what an unparameterised `PageComponent` means in a user's own\n// annotation, and `unknown` props would reject every component that declares the props it actually takes.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`\n * export. It only ever loads on the server, so importing a database client or reading secrets from it\n * is safe.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from 'hono';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointServerModule {\n /**\n * A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the\n * request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all\n * reached through it.\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /**\n * Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in\n * declaration order.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the {@link PageComponent}.\n *\n * Write it inline as `() => import('…')`: the framework detects that exact form and injects the\n * `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up\n * any other way — a variable, a barrel re-export, a computed specifier — and you have to put\n * `'use server-entry'` on the page module's first line yourself.\n *\n * @example\n * ```ts\n * component: () => import('./components/profile')\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at\n * build time on the server, so it may hit a database or read the filesystem.\n *\n * A parameterised static route without this falls back to rendering per request, with a build\n * warning. Wildcard (`*`), optional and regex params cannot be prerendered.\n *\n * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,\n * and the build fails naming the value rather than writing a page nothing will serve. So: no\n * `\\ / : * ? \" < > |` or control characters, no trailing `.` or space, and not a reserved Windows device\n * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is\n * not one that fails in CI on Windows.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /**\n * Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * HTTP method to match, or a list of them. Defaults to `'all'` — every method.\n *\n * There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a\n * `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`\n * alone would never be reached.\n *\n * A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the\n * whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }\n * ```\n */\n method?: HTTPMethod | readonly HTTPMethod[];\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/**\n * HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.\n *\n * No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.\n */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not\n * explicitly `'endpoint'` is one.\n *\n * @internal\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and `error` in\n * {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production — a generic\n * `'Internal Server Error'` and no `stack`; in dev, the real message and stack.\n */\nexport interface ErrorPageInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /**\n * The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than\n * because some errors lack one, so a page that renders it should guard on it, not on a mode flag.\n */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus\n * the redaction-aware {@link ErrorPageInfo}.\n *\n * In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so\n * the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return (\n * <html>\n * <body>\n * <h1>Something went wrong</h1>\n * <p>{error.message}</p>\n * {error.stack && <pre>{error.stack}</pre>}\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {\n /** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */\n error: ErrorPageInfo;\n};\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n *\n * @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /**\n * Page rendered with a 500 status when a request throws — a page component, a page module that will not\n * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.\n *\n * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.\n * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself\n * throws, that is reported too and the framework's document answers.\n */\n error?: FallbackPage;\n}\n\n/**\n * The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not\n * is otherwise a build-time throw from `interpolatePath` rather than a type error.\n *\n * Keys only, not full assignability, because the declared field type is `Record<string, string>` and a\n * `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no\n * key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for\n * such a route at all and an error there would be about the wrong thing.\n */\ntype ValidateStaticPaths<R, P extends string> =\n ParamKeys<P> extends never\n ? R\n : R extends { staticPaths: () => infer Sets }\n ? Awaited<Sets> extends ReadonlyArray<infer Set>\n ? [keyof PathParams<P>] extends [keyof Set]\n ? R\n : R & { staticPaths: `every param set staticPaths returns needs the params of '${P}'` }\n : R\n : R;\n\n// `PageProps<P, any>`, not `PageProps<P>`: only the *path* is being checked, and pinning the Env would\n// fail every page that declares its own (`PageProps<'/x', MyEnv>`, to type `ctx.var`).\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Env is deliberately unpinned; see above.\n [PageProps<P, any>] extends [CP]\n ? ValidateStaticPaths<R, P>\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page\n * that never renders, and nothing at runtime looks for one.\n *\n * Excess-property checking did this while the object form was an overload of its own with a concrete\n * parameter type. It cannot once the parameter is generic and inferred from the argument, because the\n * inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.\n */\ntype ValidateConfigKeys<T> =\n Exclude<keyof T, keyof RouteConfig> extends never\n ? T\n : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;\n\n/** The check for whichever of the two accepted shapes was passed. */\ntype ValidateInput<T> = T extends readonly Route[]\n ? ValidateRoutes<T>\n : T extends { routes: infer R extends readonly Route[] }\n ? ValidateConfigKeys<T> & { routes: ValidateRoutes<R> }\n : T;\n\n/** The route tuple inside either shape, so the return type carries the literals through both. */\ntype RoutesOf<T> = T extends readonly Route[] ? T : T extends { routes: infer R extends readonly Route[] } ? R : readonly Route[];\n\n/**\n * Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file\n * rshono requires. It only ever runs on the server, so importing server-only modules from it (inside\n * `staticPaths`, say) is safe.\n *\n * Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a\n * bare {@link Route} array as shorthand for an app with neither.\n *\n * Beyond typing the config, every page is cross-checked against its own path: props not satisfied by\n * `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do\n * not fill the path makes that field one.\n *\n * **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant\n * a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the\n * object form's complaint — \"Property 'routes' is missing\" — pointing at a change the author should not make.\n * The real message was on line 8. A single signature reports the argument once, at the field that is wrong.\n *\n * @param input - A {@link RouteConfig}, or the bare `routes` array.\n * @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example\n * ```ts\n * // The shorthand, for an app with no notFound or error page.\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing | Docs — routing}\n */\nexport function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAqQA;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC;AACnC,CAAC;AA6LD,MAAM,UAAU,YAAY,CAAC,KAAqC;IAChE,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAE,KAAqB,CAAC;AAC3E,CAAC","sourcesContent":["import type { Env, Handler } from 'hono';\nimport type { ParamKeys, ParamKeyToRecord } from 'hono/types';\nimport type { ReactNode } from 'react';\n// Type-only, so importing `@rshono/core` pulls in none of `context.ts`'s runtime machinery.\nimport type { RequestContext } from './runtime/context.js';\n\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\ntype UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;\n\n/**\n * The `params` record implied by a route path pattern — one required `string` key per `:param`\n * segment, `Record<string, never>` for a path with none. Paths use Hono's syntax, so `:id`,\n * `:id{[0-9]+}` and `*` all work.\n *\n * You rarely name this directly; {@link PageProps} applies it for you.\n *\n * @typeParam P - The literal route path, e.g. `'/users/:id/posts/:postId'`.\n *\n * @example\n * ```ts\n * type P = PathParams<'/users/:id/posts/:postId'>; // { id: string; postId: string }\n * ```\n *\n * @see {@link https://hono.dev/docs/api/routing#path-parameter | Hono — path parameters}\n */\nexport type PathParams<P extends string> =\n ParamKeys<P> extends never ? Record<string, never> : Simplify<UnionToIntersection<ParamKeyToRecord<ParamKeys<P>>>>;\n\n/**\n * Props every page component receives. Pass the route's path as the type argument to get `params`\n * typed key-by-key; without it `params` falls back to an open `Record<string, string>`.\n *\n * `defineRoutes` checks each page's props against `PageProps<path>`, so a mismatched path literal is\n * a type error at the route definition. `params`, and `url` during SSR and hydration, mirror what\n * `useNavigation()` gives a `'use client'` component; after hydration that hook's `url` follows the\n * browser address instead.\n *\n * @typeParam Path - The literal path this page is mounted at, e.g. `'/profile/:id'`.\n * @typeParam E - The app's Hono {@link Env}, to type {@link RequestContext.var} and\n * {@link RequestContext.env} on {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { PageProps } from '@rshono/core';\n *\n * export default async function Profile({ params, url }: PageProps<'/profile/:id'>) {\n * const user = await db.getUser(params.id); // params.id is string\n * const tab = url.searchParams.get('tab') ?? 'overview';\n * return <Layout>{user.name} — {tab}</Layout>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#page-props | Docs — page props}\n */\nexport interface PageProps<Path extends string = string, E extends Env = Env> {\n /**\n * The absolute browser-facing request {@link URL}, proxy-header aware (`X-Forwarded-Host` /\n * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not\n * serializable, so hand a `'use client'` component `url.href` rather than `url`.\n *\n * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the\n * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft\n * navigation alike: the payload carries one build-time `href`. **`useNavigation()` reads that payload\n * during SSR and hydration and then follows the browser address**, so a `'use client'` component can\n * read the live query with no effect of its own. The fragment is the one part no payload can carry —\n * a browser never sends `#…` with the request — so this prop never has one, while the hook's `url.hash`\n * arrives from the address bar after hydration. Mark the route `render: 'dynamic'` if the page's server\n * render depends on the query.\n */\n url: URL;\n /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: string extends Path ? Record<string, string> : PathParams<Path>;\n /**\n * The request context — the object `getRequestContext()` returns, handed to the page so cookies,\n * headers, env and middleware variables are reachable without an import.\n *\n * Server-only, non-enumerable (a `{...props}` spread and `JSON.stringify` both skip it) and never\n * serialized. Passing it to a `'use client'` component fails the render, because it wraps the live\n * request — read what you need here and pass plain values down.\n *\n * Non-enumerable is the one place this API breaks a JavaScript expectation, and it is unavoidable: an\n * enumerable `ctx` would put `ctx.hono.env` — every binding and secret — into React's dev-only\n * serialization of a server component's props, which walks own enumerable properties. So `<Child\n * {...props} />` hands a **server** child `ctx: undefined` with no error, while the type says otherwise.\n * Nested server components are meant to call `getRequestContext()` for the same object rather than\n * receive it, which is also the fix if a spread has already cost you an afternoon.\n *\n * Reading it on a `render: 'static'` route throws: a prerendered page has no per-request context.\n * Mark the route `render: 'dynamic'`, or use the `url` / `params` props.\n *\n * @example\n * ```tsx\n * export default function Dashboard({ ctx }: PageProps) {\n * const session = ctx.cookies.get('session');\n * if (!session) redirect('/login');\n * return <Layout>Signed in as {session}</Layout>;\n * }\n * ```\n */\n ctx: RequestContext<E>;\n}\n\n/**\n * A page: a React **server component** rendering the entire document (`<html>…</html>`), usually via\n * a shared layout. It may be `async` and await data directly.\n *\n * Each page module default-exports exactly one. Interactive parts belong in `'use client'` components\n * the page imports — only those ship JS.\n *\n * @typeParam P - The component's props; for a page these are {@link PageProps}.\n *\n * @see {@link https://react.dev/reference/rsc/server-components | React — Server Components}\n * @see {@link https://www.rshono.com/docs/pages | Docs — pages}\n */\n// `any`, not `unknown`: this default is what an unparameterised `PageComponent` means in a user's own\n// annotation, and `unknown` props would reject every component that declares the props it actually takes.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PageComponent<P = any> = (props: P) => ReactNode | Promise<ReactNode>;\n\n/**\n * The shape an `{ type: 'endpoint' }` route's server module must have: a single named `handler`\n * export. It only ever loads on the server, so importing a database client or reading secrets from it\n * is safe.\n *\n * @example\n * ```ts\n * // src/health.ts\n * import type { Handler } from 'hono';\n *\n * export const handler: Handler = (c) => c.json({ ok: true });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointServerModule {\n /**\n * A Hono {@link Handler} for every request the route matches. It is passed Hono's `Context`, so the\n * request, the response builders (`c.json`, `c.text`, `c.body`) and middleware variables are all\n * reached through it.\n *\n * @see {@link https://hono.dev/docs/api/context | Hono — Context}\n */\n handler: Handler;\n}\n\n/**\n * A page route — a path rendered by a server component. This is the default\n * route kind, so `type` can be omitted.\n *\n * @example\n * ```ts\n * { path: '/profile/:id', component: () => import('./components/profile') }\n * ```\n */\nexport interface PageRoute {\n /** Discriminates a page from an endpoint; optional because `'page'` is the default. */\n type?: 'page';\n /**\n * Hono-style path pattern, e.g. `/`, `/profile/:id`, `/files/*`. Routes are matched in\n * declaration order.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * Dynamic import of the page module, whose default export is the {@link PageComponent}.\n *\n * Write it inline as `() => import('…')`: the framework detects that exact form and injects the\n * `'use server-entry'` directive that attaches the page's client JS and CSS. Wire the component up\n * any other way — a variable, a barrel re-export, a computed specifier — and you have to put\n * `'use server-entry'` on the page module's first line yourself.\n *\n * @example\n * ```ts\n * component: () => import('./components/profile')\n * ```\n *\n * @see {@link https://www.rshono.com/docs/pages#the-use-server-entry-directive | Docs — the `'use server-entry'` directive}\n */\n component: () => Promise<{ default: PageComponent }>;\n /** `'static'` prerenders the route at build time; `'dynamic'` (the default) renders per request. */\n render?: 'static' | 'dynamic';\n /**\n * For a `render: 'static'` route with params: the param sets to prerender, one page each. Runs at\n * build time on the server, so it may hit a database or read the filesystem.\n *\n * A parameterised static route without this falls back to rendering per request, with a build\n * warning. Wildcard (`*`), optional and regex params cannot be prerendered.\n *\n * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,\n * and the build fails naming the value rather than writing a page nothing will serve. So: no\n * `\\ / : * ? \" < > |` or control characters, no trailing `.` or space, and not a reserved Windows device\n * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is\n * not one that fails in CI on Windows.\n *\n * @example\n * ```ts\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#static-rendering | Docs — static rendering}\n */\n staticPaths?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;\n}\n\n/**\n * An endpoint route — a path served by a raw Hono handler instead of a React\n * component. Use it for JSON APIs, webhooks, redirects, feeds, or anything that\n * isn't an HTML page.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing#endpoint-routes | Docs — endpoint routes}\n */\nexport interface EndpointRoute {\n /** Marks this route as an endpoint rather than a page. Required. */\n type: 'endpoint';\n /**\n * Hono-style path pattern, e.g. `/api/health`, `/api/users/:id`.\n *\n * @see {@link https://hono.dev/docs/api/routing | Hono — routing}\n */\n path: string;\n /**\n * HTTP method to match, or a list of them. Defaults to `'all'` — every method.\n *\n * There is no `'head'`: Hono dispatches a `HEAD` as a `GET` and strips the body off the response, so a\n * `HEAD` is already answered by the `'get'` handler (and by `'all'`), and a route registered for `HEAD`\n * alone would never be reached.\n *\n * A list is how a two-method endpoint says so; `'all'` inside one is refused, since it is either the\n * whole thing or a mistake. A method the route does not name gets Hono's 404 rather than the handler.\n *\n * @example\n * ```ts\n * { type: 'endpoint', path: '/api/session', method: ['get', 'delete'], server: () => import('./session') }\n * ```\n */\n method?: HTTPMethod | readonly HTTPMethod[];\n /** Dynamic import of the {@link EndpointServerModule} exporting `handler`. */\n server: () => Promise<EndpointServerModule>;\n}\n\n/**\n * HTTP methods an {@link EndpointRoute} can match. `'all'` matches every method.\n *\n * No `'head'`, deliberately — see {@link EndpointRoute.method}. A `HEAD` reaches the `'get'` handler.\n */\nexport type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'all';\n\n/** Any entry in the `routes` array: a {@link PageRoute} or an {@link EndpointRoute}. */\nexport type Route = PageRoute | EndpointRoute;\n\n/**\n * Narrows a {@link Route} to a {@link PageRoute} — `type` is optional on page routes, so anything not\n * explicitly `'endpoint'` is one.\n *\n * @internal\n */\nexport function isPageRoute(route: Route): route is PageRoute {\n return route.type !== 'endpoint';\n}\n\n/**\n * A page the framework falls back to rather than routes to — `notFound` and `error` in\n * {@link RouteConfig}. Same contract as a {@link PageRoute} `component`, without a path of its own.\n */\nexport interface FallbackPage {\n /** Dynamic import of the page module; its default export is the {@link PageComponent}. */\n component: () => Promise<{ default: PageComponent }>;\n}\n\n/**\n * The error detail handed to the `error` page. Redacted in production — a generic\n * `'Internal Server Error'` and no `stack`; in dev, the real message and stack.\n */\nexport interface ErrorPageInfo {\n /** The thrown error's message in dev; `'Internal Server Error'` in production. */\n message: string;\n /**\n * The stack trace — **dev only**, and `undefined` in every build. Optional for that reason rather than\n * because some errors lack one, so a page that renders it should guard on it, not on a mode flag.\n */\n stack?: string;\n}\n\n/**\n * Props for the `error` page declared in {@link RouteConfig.error} — the usual {@link PageProps} plus\n * the redaction-aware {@link ErrorPageInfo}.\n *\n * In a build `error.message` is the generic `'Internal Server Error'` and `error.stack` is `undefined`, so\n * the page below guards on the stack rather than on a mode flag — there is no mode flag to guard on.\n *\n * @typeParam E - The app's Hono {@link Env}, forwarded to {@link PageProps.ctx}.\n *\n * @example\n * ```tsx\n * import type { ErrorPageProps } from '@rshono/core';\n *\n * export default function ServerError({ error }: ErrorPageProps) {\n * return (\n * <html>\n * <body>\n * <h1>Something went wrong</h1>\n * <p>{error.message}</p>\n * {error.stack && <pre>{error.stack}</pre>}\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport type ErrorPageProps<E extends Env = Env> = PageProps<string, E> & {\n /** The error that failed the request, redacted in production — see {@link ErrorPageInfo}. */\n error: ErrorPageInfo;\n};\n\n/**\n * The object form accepted by {@link defineRoutes}: the route table plus the two\n * optional framework-owned pages.\n *\n * @typeParam TRoutes - Inferred tuple of route literals, which is what makes the\n * per-route `path` → props check possible.\n *\n * @see {@link https://www.rshono.com/docs/routing#notfound-and-error | Docs — notFound and error pages}\n */\nexport interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]> {\n /** Every page and endpoint in the app, matched in order. */\n routes: TRoutes;\n /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */\n notFound?: FallbackPage;\n /**\n * Page rendered with a 500 status when a request throws — a page component, a page module that will not\n * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.\n *\n * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.\n * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself\n * throws, that is reported too and the framework's document answers.\n */\n error?: FallbackPage;\n}\n\n/**\n * The same check for `staticPaths`, whose param sets have to fill the route's own path: a key that does not\n * is otherwise a build-time throw from `interpolatePath` rather than a type error.\n *\n * Keys only, not full assignability, because the declared field type is `Record<string, string>` and a\n * `staticPaths` annotated as returning exactly that has to stay accepted — an index signature carries no\n * key to check, so it passes. Skipped where the path has no params, because `staticPaths` is not called for\n * such a route at all and an error there would be about the wrong thing.\n */\ntype ValidateStaticPaths<R, P extends string> =\n ParamKeys<P> extends never\n ? R\n : R extends { staticPaths: () => infer Sets }\n ? Awaited<Sets> extends ReadonlyArray<infer Set>\n ? [keyof PathParams<P>] extends [keyof Set]\n ? R\n : R & { staticPaths: `every param set staticPaths returns needs the params of '${P}'` }\n : R\n : R;\n\n// `PageProps<P, any>`, not `PageProps<P>`: only the *path* is being checked, and pinning the Env would\n// fail every page that declares its own (`PageProps<'/x', MyEnv>`, to type `ctx.var`).\ntype ValidateRoute<R> = R extends {\n path: infer P extends string;\n component: () => Promise<{ default: PageComponent<infer CP> }>;\n}\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the Env is deliberately unpinned; see above.\n [PageProps<P, any>] extends [CP]\n ? ValidateStaticPaths<R, P>\n : R & { component: `component props are not satisfied by PageProps<'${P}'>` }\n : R;\n\ntype ValidateRoutes<TRoutes extends readonly Route[]> = { [K in keyof TRoutes]: ValidateRoute<TRoutes[K]> };\n\n/**\n * Refuses a key that is not a {@link RouteConfig} field — a typo'd `notfound` is otherwise a fallback page\n * that never renders, and nothing at runtime looks for one.\n *\n * Excess-property checking did this while the object form was an overload of its own with a concrete\n * parameter type. It cannot once the parameter is generic and inferred from the argument, because the\n * inferred type *has* the extra key. The message here is the better one anyway: it lands on the field.\n */\ntype ValidateConfigKeys<T> =\n Exclude<keyof T, keyof RouteConfig> extends never\n ? T\n : T & Record<Exclude<keyof T, keyof RouteConfig>, 'not a defineRoutes field — the fields are routes, notFound and error'>;\n\n/** The check for whichever of the two accepted shapes was passed. */\ntype ValidateInput<T> = T extends readonly Route[]\n ? ValidateRoutes<T>\n : T extends { routes: infer R extends readonly Route[] }\n ? ValidateConfigKeys<T> & { routes: ValidateRoutes<R> }\n : T;\n\n/** The route tuple inside either shape, so the return type carries the literals through both. */\ntype RoutesOf<T> = T extends readonly Route[] ? T : T extends { routes: infer R extends readonly Route[] } ? R : readonly Route[];\n\n/**\n * Declares the app's route table. Export the result as `routes` from `src/routes.ts` — the one file\n * rshono requires. It only ever runs on the server, so importing server-only modules from it (inside\n * `staticPaths`, say) is safe.\n *\n * Takes a {@link RouteConfig} — the `routes` array plus the optional `notFound` and `error` pages — or a\n * bare {@link Route} array as shorthand for an app with neither.\n *\n * Beyond typing the config, every page is cross-checked against its own path: props not satisfied by\n * `PageProps<'<its path>'>` make the `component` field a type error, and a `staticPaths` whose param sets do\n * not fill the path makes that field one.\n *\n * **One signature over both shapes, deliberately.** The array form used to be a second overload, which meant\n * a mistake inside a bare array was reported as an overload-resolution failure whose *first* line was the\n * object form's complaint — \"Property 'routes' is missing\" — pointing at a change the author should not make.\n * The real message was on line 8. A single signature reports the argument once, at the field that is wrong.\n *\n * @param input - A {@link RouteConfig}, or the bare `routes` array.\n * @returns The config, unchanged and fully typed; an array is wrapped as `{ routes }`.\n *\n * @example\n * ```ts\n * // src/routes.ts\n * import { defineRoutes } from '@rshono/core';\n *\n * export const routes = defineRoutes({\n * routes: [\n * { path: '/', component: () => import('./components/home') },\n * { path: '/profile/:id', component: () => import('./components/profile') },\n * {\n * path: '/docs/:slug',\n * render: 'static',\n * component: () => import('./components/documentation'),\n * staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],\n * },\n * { type: 'endpoint', path: '/api/health', server: () => import('./health') },\n * ],\n * notFound: { component: () => import('./components/404') },\n * error: { component: () => import('./components/500') },\n * });\n * ```\n *\n * @example\n * ```ts\n * // The shorthand, for an app with no notFound or error page.\n * export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);\n * ```\n *\n * @see {@link https://www.rshono.com/docs/routing | Docs — routing}\n */\nexport function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;\nexport function defineRoutes(input: readonly Route[] | RouteConfig): RouteConfig {\n return Array.isArray(input) ? { routes: input } : (input as RouteConfig);\n}\n"]}
|
package/dist/runtime/client.d.ts
CHANGED
|
@@ -21,5 +21,5 @@
|
|
|
21
21
|
*
|
|
22
22
|
* @packageDocumentation
|
|
23
23
|
*/
|
|
24
|
-
export { useNavigation, type NavigationRouter, type NavigationState } from './navigation.js';
|
|
24
|
+
export { useNavigation, type NavigationOutcome, type NavigationRouter, type NavigationState } from './navigation.js';
|
|
25
25
|
export { AsyncBoundary, CatchBoundary, type AsyncBoundaryProps, type CatchBoundaryProps, type ErrorFallback } from './boundaries.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,aAAa,EAAuE,MAAM,iBAAiB,CAAC;AACrH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAwE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * `@rshono/core/client` — the browser-side surface: `useNavigation()` for the current URL and soft\n * navigation, plus the `<AsyncBoundary>` and `<CatchBoundary>` components.\n *\n * Every export is itself a `'use client'` module, so a server component can render `<AsyncBoundary>`\n * directly — but the hook needs a client component. In a server component, read the same request data\n * from `getRequestContext()` in `@rshono/core/server`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function Tab() {\n * const { url, router } = useNavigation();\n * return <button onClick={() => router.push(`${url.pathname}?tab=details`)}>Details</button>;\n * }\n * ```\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n *\n * @packageDocumentation\n */\n\nexport { useNavigation, type NavigationOutcome, type NavigationRouter, type NavigationState } from './navigation.js';\nexport { AsyncBoundary, CatchBoundary, type AsyncBoundaryProps, type CatchBoundaryProps, type ErrorFallback } from './boundaries.js';\n"]}
|
package/dist/runtime/control.js
CHANGED
|
@@ -57,7 +57,18 @@ export function parseRedirectDigest(digest) {
|
|
|
57
57
|
if (sep === -1)
|
|
58
58
|
return null;
|
|
59
59
|
const status = Number(rest.slice(0, sep));
|
|
60
|
-
|
|
60
|
+
// A malformed escape is corruption between `RedirectSignal` and here — every target it encodes round
|
|
61
|
+
// trips — and letting `decodeURIComponent` throw would take the redirect off the wire entirely: this runs
|
|
62
|
+
// from React's error callbacks and from a rejected fetch, where a throw escapes as an unhandled rejection
|
|
63
|
+
// or out of the callback. The literal text is the better guess at the target, the same fallback
|
|
64
|
+
// `jumpToAnchor` uses for a malformed fragment; refusing the digest instead would read as a `notFound()`.
|
|
65
|
+
let location;
|
|
66
|
+
try {
|
|
67
|
+
location = decodeURIComponent(rest.slice(sep + 1));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
location = rest.slice(sep + 1);
|
|
71
|
+
}
|
|
61
72
|
return { location, status: Number.isFinite(status) ? status : 307 };
|
|
62
73
|
}
|
|
63
74
|
//# sourceMappingURL=control.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/runtime/control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAE5C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B,QAAQ;IACR,MAAM;IAHR,MAAM,CAAS;IACxB,YACW,QAAgB,EAChB,MAAc;QAEvB,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;wBAHjC,QAAQ;sBACR,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,GAAG,eAAe,GAAG,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9E,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,GAAG,gBAAgB,CAAC;IACnC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAID,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,cAAc,IAAI,KAAK,YAAY,cAAc,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,gBAAgB,IAAI,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAQ,KAAqC,EAAE,MAAM,KAAK,QAAQ,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/runtime/control.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAC3C,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAE5C,MAAM,OAAO,cAAe,SAAQ,KAAK;IAG5B,QAAQ;IACR,MAAM;IAHR,MAAM,CAAS;IACxB,YACW,QAAgB,EAChB,MAAc;QAEvB,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;wBAHjC,QAAQ;sBACR,MAAM;QAGf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,GAAG,eAAe,GAAG,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9E,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,GAAG,gBAAgB,CAAC;IACnC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAID,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,cAAc,IAAI,KAAK,YAAY,cAAc,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,gBAAgB,IAAI,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,OAAQ,KAAqC,EAAE,MAAM,KAAK,QAAQ,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1C,qGAAqG;IACrG,0GAA0G;IAC1G,0GAA0G;IAC1G,gGAAgG;IAChG,0GAA0G;IAC1G,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AACtE,CAAC","sourcesContent":["/**\n * `redirect()` and `notFound()` as thrown errors, and the `digest` string that carries them.\n *\n * The digest is the only part that survives React — a server component's error reaches the browser as a\n * digest with no message — so the redirect target is encoded into it. The server matches the class, the\n * client matches the string.\n */\nconst REDIRECT_PREFIX = 'RSHONO_REDIRECT;';\nconst NOT_FOUND_DIGEST = 'RSHONO_NOT_FOUND';\n\nexport class RedirectSignal extends Error {\n readonly digest: string;\n constructor(\n readonly location: string,\n readonly status: number,\n ) {\n super(`[rshono] redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.digest = `${REDIRECT_PREFIX}${status};${encodeURIComponent(location)}`;\n }\n}\n\nexport class NotFoundSignal extends Error {\n readonly digest = NOT_FOUND_DIGEST;\n constructor() {\n super('[rshono] notFound');\n this.name = 'NotFoundSignal';\n }\n}\n\nexport type ControlSignal = RedirectSignal | NotFoundSignal;\n\nexport function isControlSignal(error: unknown): error is ControlSignal {\n return error instanceof RedirectSignal || error instanceof NotFoundSignal;\n}\n\nexport function isControlDigest(digest: unknown): digest is string {\n return typeof digest === 'string' && (digest === NOT_FOUND_DIGEST || digest.startsWith(REDIRECT_PREFIX));\n}\n\n/**\n * Whether an error is React's stand-in for one that came out of a flight payload, rather than one that\n * started life where it was caught. Here, beside {@link isControlDigest}, because both are the same\n * question — what a `digest` tells you about where an error has been.\n *\n * A `digest` is what React puts on the far side of a payload boundary in place of the real error, so its\n * presence *is* the provenance: the layer that wrote the payload met the original and reported it in full.\n * Reporting the stand-in as well tags one fault with a second `source`, and in a build the copy carries no\n * message at all — React redacts it — so the second line says nothing the first did not.\n *\n * `reportServerError` cannot see this for itself: it de-duplicates on object identity, and a stand-in is a\n * different object. Every place a payload error can be reported a second time makes this test — the SSR\n * shell path, the top-level handler, and the `error` page's own catch.\n */\nexport function cameFromPayload(error: unknown): boolean {\n return typeof (error as { digest?: unknown } | null)?.digest === 'string';\n}\n\nexport function parseRedirectDigest(digest: string): { location: string; status: number } | null {\n if (!digest.startsWith(REDIRECT_PREFIX)) return null;\n const rest = digest.slice(REDIRECT_PREFIX.length);\n const sep = rest.indexOf(';');\n if (sep === -1) return null;\n const status = Number(rest.slice(0, sep));\n // A malformed escape is corruption between `RedirectSignal` and here — every target it encodes round\n // trips — and letting `decodeURIComponent` throw would take the redirect off the wire entirely: this runs\n // from React's error callbacks and from a rejected fetch, where a throw escapes as an unhandled rejection\n // or out of the callback. The literal text is the better guess at the target, the same fallback\n // `jumpToAnchor` uses for a malformed fragment; refusing the digest instead would read as a `notFound()`.\n let location: string;\n try {\n location = decodeURIComponent(rest.slice(sep + 1));\n } catch {\n location = rest.slice(sep + 1);\n }\n return { location, status: Number.isFinite(status) ? status : 307 };\n}\n"]}
|