Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/guide/pages-routing/actions-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export const action = defineHandler.withValidator({
});
```

| Action returns | Behavior |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
| `c.redirect(url)` | Navigates to another page. Build `url` with `routeUrl()`, or annotate a full path with `satisfies RouteUrl`. |
| Action returns | Behavior |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
| `c.redirect(url)` | Navigates to another page. With [`routing.typedRoutes`](../type-safety.md#checked-url-strings), the location is checked the same way as a link. Cast a string TypeScript cannot check with `as Href`. |

Middleware does not run again after the action. To change [shared data](./layouts.md#shared-data), update it in the action or redirect.

Expand Down
50 changes: 29 additions & 21 deletions docs/guide/type-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export default defineConfig({

### Checked URL strings

[`routing.typedRoutes`](../reference/config.md#routing-typedroutes) checks plain URL strings in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, `c.rewrite()`, `revalidate({ paths })`, `connectEventStream()`, `connectLiveStream()`, and `useIslandForm()`'s `post()`, `put()`, `patch()`, `delete()`, and `submit()`. Apps created with `void init` have it on. To turn it on in an existing app:
[`routing.typedRoutes`](../reference/config.md#routing-typedroutes) checks plain URL strings in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, `c.rewrite()`, `c.redirect()`, `revalidate({ paths })`, `connectEventStream()`, `connectLiveStream()`, and `useIslandForm()`'s `post()`, `put()`, `patch()`, `delete()`, and `submit()`. Apps created with `void init` have it on. To turn it on in an existing app:

```ts
// void.config.ts
Expand Down Expand Up @@ -330,20 +330,28 @@ Links can also point to the current page (`?page=2`, `#comments`) and to `http:/

The [SSE](./sse.md#client) and [live stream](./live.md#subscribe-from-the-browser) clients, `connectEventStream()` and `connectLiveStream()`, take one of your routes or an `http://` or `https://` URL for a stream on another origin. They reject same-page, `mailto:`, `tel:`, and `ws://` URLs. Cast a stream URL that TypeScript can't check with `as RouteUrl`; the cast turns off the check for that URL.

`c.redirect()` is not part of that check. It still accepts any string. Build an internal path with `routeUrl()`, or annotate a full path with `satisfies RouteUrl`. With `routing.typedRoutes` on, a path that matches no route is a type error. An external URL stays a plain string:
`c.redirect()` uses the same check as links. With `routing.typedRoutes` on, the location is a route URL, a same-page query or hash (`?page=2`, `#comments`), or an `http://`, `https://`, `mailto:`, or `tel:` URL. A path that matches no route is a type error. An external `https://` literal needs no cast. A `URL` or a destination object is accepted too. Cast a string TypeScript cannot check, such as a URL from a provider SDK, your CMS, or a file in `public/`, with `as Href`:

```ts
import { routeUrl, type RouteUrl } from 'void/routes';
import { routeUrl, type Href } from 'void/routes';

declare const id: string;
declare const dynamic: string;

c.redirect('/about');
c.redirect(routeUrl('/users/:id', { params: { id } }));
c.redirect('/users/42' satisfies RouteUrl);
c.redirect({ pathname: '/users/:id', params: { id } });
c.redirect('https://example.com/login');
c.redirect(new URL('https://example.com/login'));
c.redirect(dynamic as Href);
```

The cast turns the check off, so cast only URLs you trust. Check a URL from the request, such as a `?redirectTo=` parameter, before redirecting to it. See [`c.redirect()`](../reference/api/rewrites.md#c-rewrite-destination).

`void/routes` exports two types for these strings:

- `RouteUrl`: the URL of one of your routes. `routeUrl()` returns it, and `c.rewrite()`, `useIslandForm()`, and `revalidate({ paths })` accept it. Annotate a `c.redirect()` path or a `routing.prerender` list with it.
- `Href`: anything a link can point to: a `RouteUrl`, a same-page query or hash, or an external URL.
- `RouteUrl`: the URL of one of your routes. `routeUrl()` returns it, and `c.rewrite()`, `useIslandForm()`, and `revalidate({ paths })` accept it. Annotate a `routing.prerender` list with it.
- `Href`: anything a link or `c.redirect()` can point to: a `RouteUrl`, a same-page query or hash, or an external URL.

Cast a string that TypeScript can't check, such as a path from your CMS or a file in `public/`. The cast turns the check off for that string:

Expand Down Expand Up @@ -401,21 +409,21 @@ Route types update with the dev server. For CI or standalone typechecking, run `

## What Gets Checked

| Layer | What's type-checked |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Database | Table columns, value types, insert/update shapes from Drizzle schema |
| Handlers | `c.env` bindings, validator input, return type |
| Fetch client | Route paths, HTTP methods, `body`/`query`/`params`, response type |
| Navigation | Route patterns and required parameter names and values in `routeUrl()` and destination objects |
| Links | With `routing.typedRoutes`, URL strings in `Link`, router methods, `c.rewrite()`, `useIslandForm()`, and SSE and live stream clients |
| `c.redirect` | An internal path built with `routeUrl()`, or a full path annotated with `satisfies RouteUrl` |
| `useParams` | Page patterns and the names of the params they return |
| `c.req.param` | Page param names, when the result is annotated with `PageParams` |
| `getPrerenderPaths` | Page param names, when the return type is annotated with `PageParams` |
| `revalidate` | With `routing.typedRoutes`, URL strings in `paths` |
| `routing.prerender` | Paths, when the array is annotated with `satisfies RouteUrl[]` |
| `useForm` | Action URLs, `data` fields, `errors` keys, `reset()` args (including `{ initial: true }`), `params`, `resetOnSuccess` |
| `action()` | Action URLs, `data` payload, `params` |
| Layer | What's type-checked |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Database | Table columns, value types, insert/update shapes from Drizzle schema |
| Handlers | `c.env` bindings, validator input, return type |
| Fetch client | Route paths, HTTP methods, `body`/`query`/`params`, response type |
| Navigation | Route patterns and required parameter names and values in `routeUrl()` and destination objects |
| Links | With `routing.typedRoutes`, URL strings in `Link`, router methods, `c.rewrite()`, `useIslandForm()`, and SSE and live stream clients |
| `c.redirect` | With `routing.typedRoutes`, route URLs, same-page queries and hashes, destination objects, `URL` values, and `http://`, `https://`, `mailto:`, and `tel:` literals. A string TypeScript cannot check needs `as Href`. |
| `useParams` | Page patterns and the names of the params they return |
| `c.req.param` | Page param names, when the result is annotated with `PageParams` |
| `getPrerenderPaths` | Page param names, when the return type is annotated with `PageParams` |
| `revalidate` | With `routing.typedRoutes`, URL strings in `paths` |
| `routing.prerender` | Paths, when the array is annotated with `satisfies RouteUrl[]` |
| `useForm` | Action URLs, `data` fields, `errors` keys, `reset()` args (including `{ initial: true }`), `params`, `resetOnSuccess` |
| `action()` | Action URLs, `data` payload, `params` |

## Serialization

Expand Down
16 changes: 13 additions & 3 deletions docs/reference/api/rewrites.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,24 @@ return c.rewrite({

With [`routing.typedRoutes`](../config.md#routing-typedroutes), a string destination must be a [`RouteUrl`](./types.md#route-types): a URL that matches one of your routes. Cast a path built at runtime, such as ``c.rewrite(`/${locale}${path}` as RouteUrl)``.

For a typed redirect, build the URL with [`routeUrl()`](./types.md#routeurl-path-options). `c.redirect()` still accepts any string, so a full path is checked only when you annotate it with [`RouteUrl`](./types.md#route-types). That check applies when [`routing.typedRoutes`](../config.md#routing-typedroutes) is on. An external URL stays a plain string:
With [`routing.typedRoutes`](../config.md#routing-typedroutes), `c.redirect()` checks its location the same way as a link. A string must be a route URL, a same-page query or hash, or an `http://`, `https://`, `mailto:`, or `tel:` URL. An external `https://` literal needs no cast. A `URL`, a [`routeUrl()`](./types.md#routeurl-path-options) string, or a destination object is accepted. Cast a string TypeScript cannot check, such as a checkout URL from a payment provider's SDK, with `as Href`:

```ts
import { routeUrl, type RouteUrl } from 'void/routes';
import { routeUrl, type Href } from 'void/routes';

declare const checkoutUrl: string;

return c.redirect(routeUrl('/users/:id', { params: { id: 42 } }));
return c.redirect('/users/42' satisfies RouteUrl);
return c.redirect({ pathname: '/users/:id', params: { id: 42 } });
return c.redirect('https://example.com/login');
return c.redirect(checkoutUrl as Href);
```

The cast turns the check off for that value, and Void does not check the location at runtime, so cast only URLs you trust. A URL from the request, such as a `?redirectTo=` parameter, can point to another site. Resolve it against the request URL and redirect only when the origin matches:

```ts
const requested = new URL(c.req.query('redirectTo') ?? '/', c.req.url);
return c.redirect(requested.origin === new URL(c.req.url).origin ? requested : '/');
```

`c.redirect()` sends the location it is given. The request query is not added to an internal path, a protocol-relative URL, or an absolute URL, so `c.redirect('/notes')` from `/sign-in?redirectTo=/notes` goes to `/notes`. [`routing.redirects`](../../guide/edge/redirects.md) still merge the request query into the destination.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Import these types from `void/routes`:
| `RouteDestination<P>` | `{ pathname: P }` with `RouteOptions<P>`. Defaults to all known routes when `P` is omitted. |
| `RouteUrl` | The URL of one of your routes, returned by `routeUrl()`. `string` unless `typedRoutes` is on. |
| `Href` | A `RouteUrl`, a same-page query or hash, or an external URL. `string` unless `typedRoutes` is on. |
| `RouteUrlInput` | `Href`, `URL`, or `RouteDestination`; accepted by router navigation methods. |
| `RouteUrlInput` | `Href`, `URL`, or `RouteDestination`; accepted by router navigation methods and `c.redirect()`. |
| `RouteQuery` | Query keys mapped to `RouteQueryValue` or readonly arrays of those values. |
| `RouteQueryValue` | `string`, `number`, `boolean`, `null`, or `undefined`. |

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ routing: {

#### `routing.typedRoutes`

Type-check URL strings against your app's routes in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, `c.rewrite()`, `useIslandForm()`, `revalidate({ paths })`, `connectEventStream()`, and `connectLiveStream()`. Omitted or `false` means off; `void init` turns it on for new apps. It changes types only, not navigation at runtime. Link and router strings may also be same-page queries or hashes, or `http://`, `https://`, `mailto:`, and `tel:` URLs. `useIslandForm()` and `revalidate({ paths })` take route URLs; `connectEventStream()` and `connectLiveStream()` also take `http://` and `https://` URLs. `c.redirect()` and `routing.prerender` stay plain strings until you annotate them. See [Checked URL strings](../guide/type-safety#checked-url-strings).
Type-check URL strings against your app's routes in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()`, `c.rewrite()`, `c.redirect()`, `useIslandForm()`, `revalidate({ paths })`, `connectEventStream()`, and `connectLiveStream()`. Omitted or `false` means off; `void init` turns it on for new apps. It changes types only, not navigation at runtime. Link and router strings may also be same-page queries or hashes, or `http://`, `https://`, `mailto:`, and `tel:` URLs. `useIslandForm()` and `revalidate({ paths })` take route URLs; `connectEventStream()` and `connectLiveStream()` also take `http://` and `https://` URLs. `c.redirect()` accepts those strings, a `URL`, or a destination object. An external `https://` literal needs no cast; cast a string TypeScript cannot check with `as Href`. `routing.prerender` takes plain strings unless you annotate them. See [Checked URL strings](../guide/type-safety#checked-url-strings).

```ts
routing: {
Expand Down
52 changes: 52 additions & 0 deletions packages/void/src/pages/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Context } from 'hono';
import { executeComposedHandler, getHandlerComposition } from '../runtime/handler-composition.ts';
import type { CloudBindings, CloudContext } from '../runtime/handler.ts';
import { isConvertedResponse } from '../runtime/response.ts';
Expand Down Expand Up @@ -60,6 +61,7 @@ type PageHandler = ((c: CloudContext) => unknown | Promise<unknown>) | undefined
type PageHeadHandler =
| ((c: CloudContext, props: Record<string, unknown>) => HeadDescriptor | undefined)
| undefined;
type PageRequestContext = Context | CloudContext;
type PageRenderOptions = {
renderMode?: PageRenderMode;
/** Dev server build: rejected deferred props keep their raw error message. */
Expand Down Expand Up @@ -455,8 +457,34 @@ function streamDeferredResponse(
return renderedResponse(c, readable, contentType, assetTags);
}

function asCloudContext(c: PageRequestContext): CloudContext {
return c as unknown as CloudContext;
}

/** Handle GET page request — run handler, return page object or SSR HTML */
export async function handlePageGet(
handler: PageHandler,
headFn: PageHeadHandler,
c: PageRequestContext,
componentId: string,
renderFn: RenderPage,
getAssetTags: GetAssetTags,
configHead?: HeadConfig,
options?: PageRenderOptions,
): Promise<Response> {
return runPageGet(
handler,
headFn,
asCloudContext(c),
componentId,
renderFn,
getAssetTags,
configHead,
options,
);
}

async function runPageGet(
handler: PageHandler,
headFn: PageHeadHandler,
c: CloudContext,
Expand Down Expand Up @@ -615,6 +643,30 @@ function isSameOriginAction(request: Request): boolean {

/** Handle POST/PUT/PATCH/DELETE page request — run mutation, then re-run GET or redirect */
export async function handlePageMutation(
mutationHandler: PageHandler,
getHandler: PageHandler,
headFn: PageHeadHandler,
c: PageRequestContext,
componentId: string,
renderFn: RenderPage,
getAssetTags: GetAssetTags,
configHead?: HeadConfig,
options?: PageRenderOptions,
): Promise<Response> {
return runPageMutation(
mutationHandler,
getHandler,
headFn,
asCloudContext(c),
componentId,
renderFn,
getAssetTags,
configHead,
options,
);
}

async function runPageMutation(
mutationHandler: PageHandler,
getHandler: PageHandler,
headFn: PageHeadHandler,
Expand Down
Loading
Loading