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
7 changes: 4 additions & 3 deletions docs/guide/edge/revalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,18 @@ You can purge ISR cache entries programmatically from a route handler:
```ts
import { defineHandler } from 'void';
import { revalidate } from 'void/isr';
import type { RouteUrl } from 'void/routes';

export const POST = defineHandler(async (c) => {
// ... update content in database ...

// Purge specific pages. `satisfies RouteUrl[]` checks the paths when routing.typedRoutes is on.
await revalidate({ paths: ['/', '/blog/hello-world'] satisfies RouteUrl[] });
// Purge specific pages
await revalidate({ paths: ['/', '/blog/hello-world'] });

// Or purge all cached pages
await revalidate({ all: true });

return c.json({ ok: true });
});
```

With [`routing.typedRoutes`](../../reference/config.md#routing-typedroutes) on, each path must match one of your routes. Cast a path that TypeScript can't check, such as one from a webhook payload, with `as RouteUrl` from `void/routes`. The cast turns the check off for that path.
17 changes: 11 additions & 6 deletions docs/guide/type-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,21 @@ export async function getPrerenderPaths(): Promise<PageParams<'/blog/:slug'>[]>

In custom SSR mode the function returns full path strings, so annotate it as `Promise<string[]>`. See [Edge Prerendering](./edge/prerendering.md#dynamic-pages-with-params) and [Static Site Generation](./ssg.md#per-page-overrides).

`revalidate({ paths })` and `routing.prerender` are lists of plain strings. Annotate the array with `satisfies RouteUrl[]` so each path has to match a route. The check applies when [`routing.typedRoutes`](../reference/config.md#routing-typedroutes) is on; otherwise `RouteUrl` is `string`:
`routing.prerender` is a list of plain strings. Annotate the array with `satisfies RouteUrl[]` so each path has to match a route. The check applies when [`routing.typedRoutes`](../reference/config.md#routing-typedroutes) is on; otherwise `RouteUrl` is `string`:

```ts
// void.config.ts
import { defineConfig } from 'void/config';
import type { RouteUrl } from 'void/routes';

await revalidate({ paths: ['/', '/blog/hello-world'] satisfies RouteUrl[] });
export default defineConfig({
routing: { prerender: ['/', '/blog/hello-world'] satisfies RouteUrl[] },
});
```

### 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()`, `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()`, `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 @@ -338,10 +342,10 @@ c.redirect('https://example.com/login');

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

- `RouteUrl`: the URL of one of your routes. `routeUrl()` returns it, and `c.rewrite()` and `useIslandForm()` accept it. Annotate a `c.redirect()` path or a prerender or revalidate path list with it.
- `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.

Cast a string that TypeScript can't check, such as a path from your CMS or a file in `public/`:
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:

::: code-group

Expand Down Expand Up @@ -408,7 +412,8 @@ Route types update with the dev server. For CI or standalone typechecking, run `
| `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` | `paths`, and `routing.prerender`, when the array is annotated with `satisfies RouteUrl[]` |
| `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` |

Expand Down
7 changes: 3 additions & 4 deletions docs/reference/api/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,17 @@ Revalidate ISR-cached pages on demand. In local development this is a no-op beca

```ts
import { revalidate } from 'void/isr';
import type { RouteUrl } from 'void/routes';

await revalidate({ paths: ['/', '/blog/hello'] satisfies RouteUrl[] });
await revalidate({ paths: ['/', '/blog/hello'] });
await revalidate({ all: true });
```

`paths` is `string[]`. Annotate the array with `satisfies RouteUrl[]` so TypeScript checks each path when [`routing.typedRoutes`](../config.md#routing-typedroutes) is on.
With [`routing.typedRoutes`](../config.md#routing-typedroutes) on, each path in `paths` must match one of your routes; otherwise `RouteUrl` is `string`. Cast a path that TypeScript can't check with `as RouteUrl`. The cast turns the check off for that path.

**Signature:**

```ts
function revalidate(options: { paths?: string[]; all?: boolean }): Promise<void>;
function revalidate(options: { paths?: RouteUrl[]; all?: boolean }): Promise<void>;
```

## Logging {#logging}
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()`, `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()` takes a route URL; `connectEventStream()` and `connectLiveStream()` also take `http://` and `https://` URLs. `c.redirect()`, `revalidate({ paths })`, 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()`, `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).

```ts
routing: {
Expand Down
4 changes: 3 additions & 1 deletion packages/void/src/runtime/isr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
isrVariantPrefix,
listIsrHosts,
} from '@void/isr/runtime';
import type { RouteUrl } from 'void/routes';
import { getRawRuntimeEnv } from './env-raw.ts';

export type RevalidateOptions = {
paths?: Array<string>;
/** Paths to purge. Checked against the app's routes when `routing.typedRoutes` is on. */
paths?: Array<RouteUrl>;
all?: boolean;
};

Expand Down
8 changes: 8 additions & 0 deletions packages/void/test/integration/route-url-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const typedCheck = `import { routeUrl, type Href, type RouteUrl } from 'void/rou
import { defineConfig } from 'void/config';
import type { VoidRouter } from 'void/pages-client';
import { defineMiddleware, type CloudContext } from 'void/handler';
import { revalidate } from 'void/isr';
import { connectLiveStream } from 'void/live/client';
import { connectEventStream } from 'void/sse/client';
import type { IslandFormState } from 'void/pages-client';
Expand Down Expand Up @@ -95,6 +96,13 @@ void context.rewrite('https://example.com');
// @ts-expect-error rewrites do not take same-page hashes
void context.rewrite('#details');
context.redirect('https://example.com/elsewhere');
void revalidate({ paths: ['/', '/about', \`/users/\${id}\`, user] });
// @ts-expect-error revalidated paths are checked
void revalidate({ paths: ['/unknown'] });
declare const stored: string[];
// @ts-expect-error unchecked string arrays are rejected
void revalidate({ paths: stored });
void revalidate({ paths: stored as RouteUrl[] });
connectEventStream('/api/legacy');
connectEventStream(\`/api/users/\${id}\`);
connectEventStream(user);
Expand Down
7 changes: 7 additions & 0 deletions packages/void/test/unit/typed-navigation.typecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type CloudContext,
type CloudEnv,
} from '../../src/runtime/handler.ts';
import { revalidate } from '../../src/runtime/isr.ts';
import { connectLiveStream } from '../../src/runtime/live-client.ts';
import { connectEventStream } from '../../src/runtime/sse-client.ts';

Expand Down Expand Up @@ -83,6 +84,12 @@ export function checkUntypedRouteStrings(): void {
expectTypeOf(routeUrl('/typed-navigation')).toEqualTypeOf<string>();
}

// Without routing.typedRoutes, revalidated paths are plain strings.
export async function checkUntypedRevalidatePaths(paths: Array<string>): Promise<void> {
await revalidate({ paths });
await revalidate({ paths: ['/unknown'] });
}

export function checkUntypedStreamUrls(url: string): void {
expectTypeOf<Parameters<typeof connectEventStream>[0]>().toEqualTypeOf<string | URL>();
expectTypeOf<Parameters<typeof connectLiveStream>[0]>().toEqualTypeOf<string | URL>();
Expand Down
Loading