diff --git a/docs/guide/edge/revalidation.md b/docs/guide/edge/revalidation.md index 78c75085d..ee3cfe025 100644 --- a/docs/guide/edge/revalidation.md +++ b/docs/guide/edge/revalidation.md @@ -121,13 +121,12 @@ 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 }); @@ -135,3 +134,5 @@ export const POST = defineHandler(async (c) => { 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. diff --git a/docs/guide/type-safety.md b/docs/guide/type-safety.md index fa60dc198..8ef61aaae 100644 --- a/docs/guide/type-safety.md +++ b/docs/guide/type-safety.md @@ -247,17 +247,21 @@ export async function getPrerenderPaths(): Promise[]> In custom SSR mode the function returns full path strings, so annotate it as `Promise`. 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 @@ -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 @@ -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` | diff --git a/docs/reference/api/resources.md b/docs/reference/api/resources.md index def494cd7..ea3eb01c9 100644 --- a/docs/reference/api/resources.md +++ b/docs/reference/api/resources.md @@ -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; +function revalidate(options: { paths?: RouteUrl[]; all?: boolean }): Promise; ``` ## Logging {#logging} diff --git a/docs/reference/config.md b/docs/reference/config.md index a19cb1f2f..5bea9c943 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -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: { diff --git a/packages/void/src/runtime/isr.ts b/packages/void/src/runtime/isr.ts index 2deaccf5f..1c7385216 100644 --- a/packages/void/src/runtime/isr.ts +++ b/packages/void/src/runtime/isr.ts @@ -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; + /** Paths to purge. Checked against the app's routes when `routing.typedRoutes` is on. */ + paths?: Array; all?: boolean; }; diff --git a/packages/void/test/integration/route-url-builder.test.ts b/packages/void/test/integration/route-url-builder.test.ts index 287c419e7..315fd1435 100644 --- a/packages/void/test/integration/route-url-builder.test.ts +++ b/packages/void/test/integration/route-url-builder.test.ts @@ -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'; @@ -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); diff --git a/packages/void/test/unit/typed-navigation.typecheck.ts b/packages/void/test/unit/typed-navigation.typecheck.ts index 1b54df8ae..ef41f83bb 100644 --- a/packages/void/test/unit/typed-navigation.typecheck.ts +++ b/packages/void/test/unit/typed-navigation.typecheck.ts @@ -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'; @@ -83,6 +84,12 @@ export function checkUntypedRouteStrings(): void { expectTypeOf(routeUrl('/typed-navigation')).toEqualTypeOf(); } +// Without routing.typedRoutes, revalidated paths are plain strings. +export async function checkUntypedRevalidatePaths(paths: Array): Promise { + await revalidate({ paths }); + await revalidate({ paths: ['/unknown'] }); +} + export function checkUntypedStreamUrls(url: string): void { expectTypeOf[0]>().toEqualTypeOf(); expectTypeOf[0]>().toEqualTypeOf();