void 0.9.2 → 0.9.3

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.
Files changed (53) hide show
  1. package/README.md +1 -0
  2. package/dist/{better-auth-shared-ChVbrq52.mjs → better-auth-shared-QAfk8CAS.mjs} +1 -1
  3. package/dist/cli/cli.mjs +12 -12
  4. package/dist/{config-BVEC0lti.mjs → config-8dLIngKW.mjs} +1 -1
  5. package/dist/{db-DiddQC2v.mjs → db-UW4IYQFw.mjs} +4 -4
  6. package/dist/{deploy-CRU9fGjE.mjs → deploy-RQXpVBL-.mjs} +27 -10
  7. package/dist/{drizzle-2fs1qTgy.mjs → drizzle-DYstYz0j.mjs} +1 -1
  8. package/dist/{env-Bvw0wMTI.mjs → env-C_ZcxxED.mjs} +2 -2
  9. package/dist/{env-types-D6qI1ThV.mjs → env-types-CaJaIRXU.mjs} +1 -1
  10. package/dist/{env-validation-DBJsxZLz.mjs → env-validation-LH6eoyW-.mjs} +3 -3
  11. package/dist/{gen-CZdNWIaA.mjs → gen-O5mXZ76K.mjs} +2 -2
  12. package/dist/{handler-xflsMwWM.d.mts → handler-hm9KldPP.d.mts} +21 -1
  13. package/dist/{headers-Y0jshugF.mjs → headers-VkAnACKX.mjs} +2 -2
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +48 -43
  16. package/dist/{init-BHupO7Fm.mjs → init-DGrLx2ls.mjs} +5 -5
  17. package/dist/{node-CHWVVjO6.mjs → node-Df8UjQ2H.mjs} +2 -2
  18. package/dist/pages/client.d.mts +1 -1
  19. package/dist/pages/index.d.mts +7 -5
  20. package/dist/pages/index.mjs +3 -3
  21. package/dist/pages/islands-plugin.d.mts +1 -1
  22. package/dist/pages/islands-plugin.mjs +1 -1
  23. package/dist/pages/protocol.d.mts +2 -2
  24. package/dist/pages/protocol.mjs +29 -11
  25. package/dist/{plugin-inference-BUBhHt_1.mjs → plugin-inference--7geW5Z2.mjs} +1 -1
  26. package/dist/{prepare-CrlAVbWS.mjs → prepare-DT-7QII_.mjs} +14 -19
  27. package/dist/{preset-CIJG2O7a.mjs → preset-2aBxMfY1.mjs} +1 -1
  28. package/dist/{project-tsconfig-HAGjfY6w.mjs → project-tsconfig-BTNuNoJ0.mjs} +18 -4
  29. package/dist/{protocol-DYca39yJ.d.mts → protocol-B9GZA4Bn.d.mts} +7 -4
  30. package/dist/{route-types-jxRfWuCb.mjs → route-types-D03ryMXz.mjs} +3 -2
  31. package/dist/runtime/better-auth-pg.mjs +1 -1
  32. package/dist/runtime/better-auth.mjs +1 -1
  33. package/dist/runtime/handler.d.mts +2 -2
  34. package/dist/runtime/handler.mjs +67 -1
  35. package/dist/runtime/live.d.mts +1 -1
  36. package/dist/runtime/validator.d.mts +1 -1
  37. package/dist/runtime/ws.d.mts +1 -1
  38. package/dist/{scan-BNC_1OsY.mjs → scan-B2H1Vo2C.mjs} +3 -3
  39. package/dist/{scan-2YmJkYAf.mjs → scan-VCAM1oh3.mjs} +55 -29
  40. package/dist/{types-Dm9kep2X.d.mts → types-DEWCqHtl.d.mts} +12 -3
  41. package/package.json +2 -2
  42. package/skills/void/docs/guide/pages-routing/layouts.md +8 -7
  43. package/skills/void/docs/guide/pages-routing/loaders.md +1 -1
  44. package/skills/void/docs/guide/pages-routing/overview.md +32 -5
  45. package/skills/void/docs/guide/server-routing.md +28 -0
  46. package/skills/void/docs/node_modules/void/AGENTS.md +15 -14
  47. package/skills/void/docs/node_modules/void/README.md +1 -0
  48. package/skills/void/docs/reference/api.md +44 -1
  49. package/skills/void/docs/reference/config.md +1 -1
  50. /package/dist/{dist-DR9sIMbM.mjs → dist-5cGIJHQQ.mjs} +0 -0
  51. /package/dist/{dotenv-Bkoqyq9r.mjs → dotenv-lS94ymhM.mjs} +0 -0
  52. /package/dist/{log-CWWZV4V1.mjs → log-BdD_Fpms.mjs} +0 -0
  53. /package/dist/{providers-uC0PJg1c.mjs → providers-BwPbdHdi.mjs} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { a as join, n as dirname, r as extname, t as basename } from "./pathe.M-eThtNZ-BrPhGF_K.mjs";
2
2
  import { n as readProjectPaths } from "./project-paths-CCMrHYQm.mjs";
3
- import { t as glob } from "./dist-DR9sIMbM.mjs";
3
+ import { t as glob } from "./dist-5cGIJHQQ.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  //#region src/router/parse-filename.ts
6
6
  const EXTENSIONS = [
@@ -83,30 +83,29 @@ function toLayoutId(layout) {
83
83
  if (isNamedLayout(layout)) return layout.directory ? `${layout.directory}/_layouts/${layout.name}` : `_layouts/${layout.name}`;
84
84
  return layout.directory ? `${layout.directory}/layout` : "layout";
85
85
  }
86
- /** Resolve the layout chain for a component (innermost last) */
87
- function resolveLayoutChain(componentId, layouts) {
86
+ function resolveLayoutDirectories(componentId, componentPath) {
87
+ if (componentPath) {
88
+ const segments = componentPath.split("/");
89
+ segments.pop();
90
+ return segments.map((_, index) => segments.slice(0, index + 1).join("/"));
91
+ }
88
92
  const parts = componentId.split("/");
93
+ return parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join("/"));
94
+ }
95
+ /** Resolve the layout chain for a component (innermost last) */
96
+ function resolveLayoutChain(componentId, layouts, componentPath) {
89
97
  const chain = [];
90
98
  const root = layouts.find((layout) => layout.directory === "");
91
99
  if (root) chain.push(root);
92
- let dir = "";
93
- for (let partIndex = 0; partIndex < parts.length - 1; partIndex++) {
94
- dir = dir ? `${dir}/${parts[partIndex]}` : parts[partIndex];
100
+ for (const dir of resolveLayoutDirectories(componentId, componentPath)) {
95
101
  const layout = layouts.find((candidate) => candidate.directory === dir);
96
102
  if (layout) chain.push(layout);
97
103
  }
98
104
  return chain;
99
105
  }
100
106
  /** Walk up directory tree to find closest _layouts/<name> */
101
- function resolveNamedLayout(name, componentId, namedLayouts) {
102
- const parts = componentId.split("/");
103
- const dirs = [];
104
- let dir = "";
105
- for (let partIndex = 0; partIndex < parts.length - 1; partIndex++) {
106
- dir = dir ? `${dir}/${parts[partIndex]}` : parts[partIndex];
107
- dirs.push(dir);
108
- }
109
- dirs.reverse();
107
+ function resolveNamedLayout(name, componentId, namedLayouts, componentPath) {
108
+ const dirs = resolveLayoutDirectories(componentId, componentPath).reverse();
110
109
  dirs.push("");
111
110
  for (const directory of dirs) {
112
111
  const found = namedLayouts.find((namedLayout) => namedLayout.name === name && namedLayout.directory === directory);
@@ -116,7 +115,7 @@ function resolveNamedLayout(name, componentId, namedLayouts) {
116
115
  }
117
116
  function resolveEffectiveLayoutEntries(page, layouts, namedLayouts) {
118
117
  if (page.layout === false) return [];
119
- const defaultChain = resolveLayoutChain(page.componentId, layouts).map((layout) => ({
118
+ const defaultChain = resolveLayoutChain(page.componentId, layouts, page.componentPath).map((layout) => ({
120
119
  id: toLayoutId(layout),
121
120
  definition: layout
122
121
  }));
@@ -124,7 +123,7 @@ function resolveEffectiveLayoutEntries(page, layouts, namedLayouts) {
124
123
  const isExclusive = page.layout.startsWith("!");
125
124
  const name = isExclusive ? page.layout.slice(1) : page.layout;
126
125
  if (name.startsWith(".") || name.startsWith("/")) return defaultChain;
127
- const resolved = resolveNamedLayout(name, page.componentId, namedLayouts);
126
+ const resolved = resolveNamedLayout(name, page.componentId, namedLayouts, page.componentPath);
128
127
  if (!resolved) return defaultChain;
129
128
  const namedEntry = {
130
129
  id: toLayoutId(resolved),
@@ -160,13 +159,31 @@ const SERVER_EXTENSIONS = [
160
159
  "mts"
161
160
  ];
162
161
  const PAGES_EXPORT_RE = /export\s+(?:const|function|async\s+function)\s+(loader|actions?)\b/g;
163
- const REVALIDATE_RE = /export\s+const\s+revalidate\s*=\s*(\d+)\s*;/;
164
- const PRERENDER_RE = /export\s+const\s+prerender\s*=\s*true\s*;/;
165
- const PRERENDER_FALSE_RE = /export\s+const\s+prerender\s*=\s*false\s*;/;
162
+ const REVALIDATE_RE = /export\s+const\s+revalidate\s*=\s*(\d+)\s*;?/;
163
+ const PRERENDER_RE = /export\s+const\s+prerender\s*=\s*true\s*;?/;
164
+ const PRERENDER_FALSE_RE = /export\s+const\s+prerender\s*=\s*false\s*;?/;
165
+ const SSR_TRUE_RE = /export\s+const\s+ssr\s*=\s*true\s*;?/;
166
+ const SSR_FALSE_RE = /export\s+const\s+ssr\s*=\s*false\s*;?/;
166
167
  const GET_PRERENDER_PATHS_RE = /export\s+(?:async\s+)?function\s+getPrerenderPaths\b/;
167
168
  const LAYOUT_STRING_RE = /export\s+const\s+layout\s*=\s*["']([^"']+)["']\s*;?/;
168
169
  const LAYOUT_FALSE_RE = /export\s+const\s+layout\s*=\s*false\s*;?/;
169
170
  const MAX_REVALIDATE = 31536e3;
171
+ function inferAutoPrerender(page, options) {
172
+ const isDynamic = page.params.length > 0 || page.catchAll;
173
+ if (options?.output === "static") return !isDynamic || page.hasGetPrerenderPaths === true;
174
+ return page.island === true && !page.methods.includes("loader") && !isDynamic;
175
+ }
176
+ function resolvePageRendering(page, options) {
177
+ if (page.island && page.ssr === false) throw new Error(`pages: ssr = false is not valid for island page '${page.componentPath}'.`);
178
+ const ssrMode = page.ssr === true ? "server" : page.ssr === false ? "client" : "auto";
179
+ const prerenderMode = page.prerender === true ? "force" : page.prerender === false ? "disabled" : "auto";
180
+ return {
181
+ ssrMode,
182
+ prerenderMode,
183
+ renderMode: page.island === true ? "island" : ssrMode === "client" ? "client" : "server",
184
+ shouldPrerender: prerenderMode === "force" || prerenderMode === "auto" && inferAutoPrerender(page, options)
185
+ };
186
+ }
170
187
  async function scanPages(root, options) {
171
188
  const pagesDir = options?.pagesDir ?? options?.paths?.pagesDir ?? readProjectPaths(root).pagesDir;
172
189
  if (!existsSync(pagesDir)) return {
@@ -219,7 +236,7 @@ async function scanPages(root, options) {
219
236
  const methods = [];
220
237
  let revalidate;
221
238
  let prerender;
222
- let prerenderExplicitlyFalse = false;
239
+ let ssr;
223
240
  let hasGetPrerenderPaths;
224
241
  const serverBase = hasIslandSuffix ? file.replace(`.island${ext}`, "") : file.replace(ext, "");
225
242
  for (const serverExt of SERVER_EXTENSIONS) {
@@ -231,17 +248,24 @@ async function scanPages(root, options) {
231
248
  const revalidateMatch = content.match(REVALIDATE_RE);
232
249
  if (revalidateMatch) revalidate = Number(revalidateMatch[1]);
233
250
  if (PRERENDER_RE.test(content)) prerender = true;
234
- if (PRERENDER_FALSE_RE.test(content)) prerenderExplicitlyFalse = true;
251
+ else if (PRERENDER_FALSE_RE.test(content)) prerender = false;
252
+ if (SSR_TRUE_RE.test(content)) ssr = true;
253
+ else if (SSR_FALSE_RE.test(content)) ssr = false;
235
254
  if (GET_PRERENDER_PATHS_RE.test(content)) hasGetPrerenderPaths = true;
236
255
  break;
237
256
  }
238
257
  }
239
- if (options?.output === "static") {
240
- if (!prerenderExplicitlyFalse && prerender !== true) {
241
- if (!(route.params.length > 0 || route.catchAll) || hasGetPrerenderPaths) prerender = true;
242
- }
243
- } else if (island && !methods.includes("loader") && route.params.length === 0 && !route.catchAll && !prerenderExplicitlyFalse) prerender = true;
244
- if (prerender && revalidate === void 0) revalidate = MAX_REVALIDATE;
258
+ const rendering = resolvePageRendering({
259
+ componentPath: file,
260
+ island,
261
+ ssr,
262
+ prerender,
263
+ methods,
264
+ params: route.params,
265
+ catchAll: route.catchAll,
266
+ hasGetPrerenderPaths
267
+ }, options);
268
+ if (rendering.shouldPrerender && revalidate === void 0) revalidate = MAX_REVALIDATE;
245
269
  let metadata;
246
270
  let layout;
247
271
  if (ext === ".md") {
@@ -264,6 +288,8 @@ async function scanPages(root, options) {
264
288
  componentId,
265
289
  revalidate,
266
290
  prerender,
291
+ ssr,
292
+ ...rendering,
267
293
  hasGetPrerenderPaths,
268
294
  island,
269
295
  metadata,
@@ -291,7 +317,7 @@ async function scanPages(root, options) {
291
317
  if (page.layout === void 0 || page.layout === false) continue;
292
318
  const name = page.layout.startsWith("!") ? page.layout.slice(1) : page.layout;
293
319
  if (name.startsWith(".") || name.startsWith("/")) continue;
294
- if (!resolveNamedLayout(name, page.componentId, namedLayouts)) throw new Error(`pages: Page '${page.componentPath}' specifies layout '${page.layout}', but _layouts/${name}.vue was not found in any ancestor directory.`);
320
+ if (!resolveNamedLayout(name, page.componentId, namedLayouts, page.componentPath)) throw new Error(`pages: Page '${page.componentPath}' specifies layout '${page.layout}', but _layouts/${name}.vue was not found in any ancestor directory.`);
295
321
  }
296
322
  return {
297
323
  pages,
@@ -1,4 +1,8 @@
1
1
  //#region src/pages/types.d.ts
2
+ type ExplicitBoolean = true | false | undefined;
3
+ type SsrMode = "auto" | "server" | "client";
4
+ type PrerenderMode = "auto" | "force" | "disabled";
5
+ type PageRenderMode = "server" | "client" | "island";
2
6
  type PageDefinition = {
3
7
  /** URL pattern, e.g. "/users/:id" */pattern: string; /** Route params, e.g. ["id"] */
4
8
  params: Array<string>; /** Whether this is a catch-all route */
@@ -7,8 +11,13 @@ type PageDefinition = {
7
11
  serverPath: string | null; /** HTTP methods exported by server handler */
8
12
  methods: Array<string>; /** Component ID used in page object, e.g. "users/[id]" */
9
13
  componentId: string; /** Edge caching revalidate TTL in seconds, from .server.ts export */
10
- revalidate?: number; /** Whether `export const prerender = true` is set in server handler */
11
- prerender?: boolean; /** Whether `export function getPrerenderPaths` is defined in server handler */
14
+ revalidate?: number; /** Whether `export const prerender = true/false` is set in server handler */
15
+ prerender?: ExplicitBoolean; /** Whether `export const ssr = true/false` is set in server handler */
16
+ ssr?: ExplicitBoolean; /** Normalized SSR mode from the public `ssr` export */
17
+ ssrMode: SsrMode; /** Normalized prerender mode from the public `prerender` export */
18
+ prerenderMode: PrerenderMode; /** Normalized page render mode */
19
+ renderMode: PageRenderMode; /** Whether this page should participate in prerender collection */
20
+ shouldPrerender: boolean; /** Whether `export function getPrerenderPaths` is defined in server handler */
12
21
  hasGetPrerenderPaths?: boolean; /** Whether this is an island page (.island.tsx/.island.vue) */
13
22
  island?: boolean; /** Frontmatter metadata extracted from .md files (title, description) */
14
23
  metadata?: {
@@ -39,4 +48,4 @@ type DevCssAsset = {
39
48
  css: string;
40
49
  };
41
50
  //#endregion
42
- export { PageScanResult as a, PageDefinition as i, LayoutDefinition as n, NamedLayoutDefinition as r, DevCssAsset as t };
51
+ export { PageRenderMode as a, PageDefinition as i, LayoutDefinition as n, PageScanResult as o, NamedLayoutDefinition as r, DevCssAsset as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -353,7 +353,7 @@
353
353
  "valibot": ">=1.0.0-beta.7",
354
354
  "vite": "^8.0.0",
355
355
  "zod": "^3.25.0 || ^4.0.0",
356
- "@void/md": "0.9.2"
356
+ "@void/md": "0.9.3"
357
357
  },
358
358
  "peerDependenciesMeta": {
359
359
  "@void/md": {
@@ -149,7 +149,7 @@ import { useShared, Link } from '@void/solid';
149
149
  import type { JSX } from 'solid-js';
150
150
 
151
151
  export default function Layout(props: { children: JSX.Element }) {
152
- const shared = useShared<{ auth: { user: { name: string } | null } }>();
152
+ const shared = useShared();
153
153
  return (
154
154
  <>
155
155
  <nav>
@@ -307,15 +307,16 @@ Middleware can inject data available on every page via `c.set("shared", {...})`.
307
307
  ```ts
308
308
  // middleware/01.auth.ts
309
309
  import { defineMiddleware } from 'void';
310
+ import { getUser, type AuthUser } from 'void/auth';
310
311
 
311
312
  declare module 'void' {
312
313
  interface CloudContextVariables {
313
- shared: { auth: { user: { name: string } | null } };
314
+ shared: { auth: { user: AuthUser | null } };
314
315
  }
315
316
  }
316
317
 
317
318
  export default defineMiddleware(async (c, next) => {
318
- const user = await getSessionUser(c);
319
+ const user = getUser();
319
320
  c.set('shared', { auth: { user } });
320
321
  await next();
321
322
  });
@@ -329,7 +330,7 @@ Access it on the client with `useShared()`. The return type is inferred from you
329
330
  import { useShared } from '@void/react';
330
331
 
331
332
  export default function Page() {
332
- const { auth } = useShared(); // { auth: { user: { name: string } | null } }
333
+ const { auth } = useShared(); // { auth: { user: AuthUser | null } }
333
334
  return <p>Hello, {auth?.user?.name}</p>;
334
335
  }
335
336
  ```
@@ -337,14 +338,14 @@ export default function Page() {
337
338
  ```vue [Vue]
338
339
  <script setup lang="ts">
339
340
  import { useShared } from '@void/vue';
340
- const { auth } = useShared(); // { auth: { user: { name: string } | null } }
341
+ const { auth } = useShared(); // { auth: { user: AuthUser | null } }
341
342
  </script>
342
343
  ```
343
344
 
344
345
  ```svelte [Svelte]
345
346
  <script>
346
347
  import { useShared } from "@void/svelte";
347
- const { auth } = useShared(); // { auth: { user: { name: string } | null } }
348
+ const { auth } = useShared(); // { auth: { user: AuthUser | null } }
348
349
  </script>
349
350
  ```
350
351
 
@@ -352,7 +353,7 @@ const { auth } = useShared(); // { auth: { user: { name: string } | null } }
352
353
  import { useShared } from '@void/solid';
353
354
 
354
355
  export default function Page() {
355
- const shared = useShared(); // { auth: { user: { name: string } | null } }
356
+ const shared = useShared(); // { auth: { user: AuthUser | null } }
356
357
  return <p>Hello, {shared.auth?.user?.name}</p>;
357
358
  }
358
359
  ```
@@ -237,7 +237,7 @@ export default function Dashboard(props: Props) {
237
237
 
238
238
  ### How Streaming Works
239
239
 
240
- On the initial page load (SSR), React uses React 19 streaming SSR and renders the nearest Suspense fallback for deferred props; the other adapters render their loading state. As each deferred function resolves, the server streams an inline `<script>` tag that delivers the data, so no extra HTTP request is needed. On SPA navigation, deferred data streams via NDJSON over the same response.
240
+ On the initial page load (SSR), React uses React 19 streaming SSR and renders the nearest Suspense fallback for deferred props; the other adapters render their loading state. As each deferred function resolves, the server streams an inline `<script>` tag that delivers the data, so no extra HTTP request is needed. Routes with `export const ssr = false` skip server-rendered component HTML but still stream deferred resolution scripts after the client-mounted shell. On SPA navigation, deferred data streams via NDJSON over the same response.
241
241
 
242
242
  ### Deferred Props After Mutations
243
243
 
@@ -100,6 +100,7 @@ Each page can have a companion `.server.ts` file that runs exclusively on the se
100
100
 
101
101
  - A [**loader**](./loaders), which runs on `GET` and returns the data that becomes the page component's props
102
102
  - [**Actions**](./actions-and-forms), which handle mutations from forms and programmatic calls. Export a single `action` or multiple [named actions](./actions-and-forms#named-actions) when a page has several mutations
103
+ - `ssr = false` to opt a route out of server-rendered component HTML while keeping server loaders and client-side routing
103
104
 
104
105
  File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups.
105
106
 
@@ -107,14 +108,40 @@ File-based routing rules are the same as [server routing](../server-routing.md):
107
108
 
108
109
  Pages uses an Inertia-style protocol under the hood:
109
110
 
110
- | Request | Response |
111
- | --------------------- | --------------------------------------------------------------------- |
112
- | Initial page load | Full SSR HTML. Client hydrates automatically. |
113
- | Subsequent navigation | JSON with component name + props. Client component swap or re-render. |
114
- | Form submission | Runs action, then returns fresh props or a redirect. |
111
+ | Request | Response |
112
+ | --------------------- | -------------------------------------------------------------------------------------------------------------- |
113
+ | Initial page load | Full SSR HTML. Client hydrates automatically. Routes with `ssr = false` return a client-mounted shell instead. |
114
+ | Subsequent navigation | JSON with component name + props. Client component swap or re-render. |
115
+ | Form submission | Runs action, then returns fresh props or a redirect. |
115
116
 
116
117
  This means the first page load is server-rendered for SEO and performance, while later navigations stay fast without full page reloads.
117
118
 
119
+ To opt a specific route out of server-rendered component HTML, export `ssr = false` from its companion `.server.ts` file:
120
+
121
+ ```ts
122
+ // pages/dashboard.server.ts
123
+ import { defineHandler } from 'void';
124
+
125
+ export const ssr = false;
126
+
127
+ export const loader = defineHandler(async () => {
128
+ return { title: 'Dashboard' };
129
+ });
130
+ ```
131
+
132
+ The loader still runs on the first request, and its props are embedded in the HTML shell. The page component mounts in the browser instead of hydrating server-rendered markup.
133
+
134
+ Render and prerender flags combine like this:
135
+
136
+ | Page exports | Behavior |
137
+ | -------------------------------------- | ------------------------------------------------------------------------- |
138
+ | `ssr` unset or `true` | Server-render component HTML on request. |
139
+ | `ssr = false` | Return a client-mounted shell on request. |
140
+ | `ssr = false` + `prerender = true` | Prerender a client-mounted shell with embedded loader data. |
141
+ | `ssr = false` + `prerender = false` | Return the client-mounted shell only on request; never prerender it. |
142
+ | Island page + `ssr = false` | Invalid. Island pages already use the island renderer. |
143
+ | `output: "static"` + `prerender` unset | Auto-prerender pages that have known paths, including client-only shells. |
144
+
118
145
  Use the `Link` component for SPA navigation between pages. It renders an `<a>` tag that intercepts clicks and navigates without a full page reload:
119
146
 
120
147
  ::: code-group
@@ -191,6 +191,34 @@ export default defineMiddleware(async (c, next) => {
191
191
 
192
192
  `defineMiddleware` uses Hono middleware semantics: `(c, next) => Promise<void> | void`.
193
193
 
194
+ For a temporary full-site gate, use the built-in `basicAuth()` middleware with credentials from `void/env`. Void internal endpoints under `/__void` are excluded automatically so deploy migrations and dev tooling continue to work. Wrap `void/env` reads in functions so they are resolved per request after Void has bound the runtime env.
195
+
196
+ ```ts
197
+ // env.ts
198
+ import { defineEnv, string } from 'void/env';
199
+
200
+ export default defineEnv({
201
+ BASIC_AUTH_USERNAME: string(),
202
+ BASIC_AUTH_PASSWORD: string(),
203
+ });
204
+ ```
205
+
206
+ ```ts
207
+ // middleware/01.basic-auth.ts
208
+ import { basicAuth } from 'void';
209
+ import { env } from 'void/env';
210
+
211
+ export default basicAuth({
212
+ username: () => env.BASIC_AUTH_USERNAME,
213
+ password: () => env.BASIC_AUTH_PASSWORD,
214
+ realm: 'Preview',
215
+ });
216
+ ```
217
+
218
+ Set `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` as local environment variables for development and production secrets before deploy.
219
+
220
+ For app-specific bypasses such as health checks or public webhooks, compose that logic in your own middleware before calling `basicAuth()`.
221
+
194
222
  Middleware can set typed context variables using `c.set()`. Augment the `CloudContextVariables` interface so downstream handlers get full type safety:
195
223
 
196
224
  ```ts
@@ -23,6 +23,7 @@ Non-CF targets disable CF-only bindings (`void/db`, `void/kv`, `void/auth`, `voi
23
23
  - `defineHandler(fn)` — wraps a route handler `(c: CloudContext) => R`, returns `TypedHandler<{}, R>`
24
24
  - `defineHandler.withValidator(validators)(fn)` — validates body/query/params via Standard Schema before calling handler, returns `TypedHandler<V, R>` with phantom types for codegen
25
25
  - `defineMiddleware(fn)` — wraps Hono middleware `(c, next) => void`
26
+ - `basicAuth(options)` — built-in Basic Auth middleware for temporary site gates; excludes Void internals under `/__void`
26
27
  - Handlers can return plain objects/strings (auto-converted via `convertReturnValue`) or use Hono's `c.json()` / `c.text()` directly.
27
28
  - Route files use **named HTTP method exports** (`export const GET`, `export const POST`, etc.) — one file per path, multiple methods per file.
28
29
 
@@ -90,20 +91,20 @@ src/
90
91
 
91
92
  ### Package Exports
92
93
 
93
- | Subpath | What |
94
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
95
- | `void` | `voidPlugin()` named export + `defineHandler` + `defineMiddleware` + `defineHead` + `defineQueue` + `CloudContext` type |
96
- | `void/handler` | `defineHandler()`, `defineHandler.withValidator()`, `defineMiddleware()`, `defineHead()`, types |
97
- | `void/response` | `convertReturnValue()` |
98
- | `void/validator` | `runValidation()`, `ValidatorSlots`, `HandlerInput` types |
99
- | `void/routes` | Empty `RouteMap` + `WebSocketRouteMap` stubs (augmented by generated `routes.d.ts`) |
100
- | `void/client` | Typed `fetch()` client + `fetchStream()` SSE consumer + `FetchError` |
101
- | `void/ws` | `defineRoom()`, `defineWebSocket()`, typed `connect()`, and WebSocket context types |
102
- | `void/db` | `db` Drizzle D1 instance (auto-wired with user schema) + `createDb()` for custom D1 bindings |
103
- | `void/queues` | `queues` typed proxy + `QueueMap` stub interface (augmented by generated `queues.d.ts`) |
104
- | `void/ai` | `ai` proxy — Cloudflare-native `ai.run()`/`ai.stream()`, provider-native `ai.provider().fetch()`, `ai.models()` |
105
- | `void/log` | `logger.error/warn/info(msg, fields?)` — emits stringified JSON to `console.*` so Cloudflare Tail captures level + msg |
106
- | `void/env` | `defineEnv()`, typed `env` proxy, built-in schema helpers (`string`, `number`, `oneOf`, …) + global Cloudflare types |
94
+ | Subpath | What |
95
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
96
+ | `void` | `voidPlugin()` named export + `defineHandler` + `defineMiddleware` + `basicAuth` + `defineHead` + `defineQueue` + `CloudContext` type |
97
+ | `void/handler` | `defineHandler()`, `defineHandler.withValidator()`, `defineMiddleware()`, `basicAuth()`, `defineHead()`, types |
98
+ | `void/response` | `convertReturnValue()` |
99
+ | `void/validator` | `runValidation()`, `ValidatorSlots`, `HandlerInput` types |
100
+ | `void/routes` | Empty `RouteMap` + `WebSocketRouteMap` stubs (augmented by generated `routes.d.ts`) |
101
+ | `void/client` | Typed `fetch()` client + `fetchStream()` SSE consumer + `FetchError` |
102
+ | `void/ws` | `defineRoom()`, `defineWebSocket()`, typed `connect()`, and WebSocket context types |
103
+ | `void/db` | `db` Drizzle D1 instance (auto-wired with user schema) + `createDb()` for custom D1 bindings |
104
+ | `void/queues` | `queues` typed proxy + `QueueMap` stub interface (augmented by generated `queues.d.ts`) |
105
+ | `void/ai` | `ai` proxy — `ai.run()`, `ai.stream()`, `ai.models()` with auto-detected backend (service binding / HTTPS / direct) |
106
+ | `void/log` | `logger.error/warn/info(msg, fields?)` — emits stringified JSON to `console.*` so Cloudflare Tail captures level + msg |
107
+ | `void/env` | `defineEnv()`, typed `env` proxy, built-in schema helpers (`string`, `number`, `oneOf`, …) + global Cloudflare types |
107
108
 
108
109
  The CLI binary is at `dist/cli/cli.mjs` (declared in `bin` field of package.json).
109
110
 
@@ -48,6 +48,7 @@ Runtime helpers include:
48
48
 
49
49
  - `defineHandler`
50
50
  - `defineMiddleware`
51
+ - `basicAuth`
51
52
  - `defineScheduled`
52
53
  - `defineQueue`
53
54
  - `void/db`
@@ -118,6 +118,49 @@ export default defineMiddleware(async (c, next) => {
118
118
  function defineMiddleware(handler: MiddlewareHandler<CloudEnv>): MiddlewareHandler<CloudEnv>;
119
119
  ```
120
120
 
121
+ ### `basicAuth(options)`
122
+
123
+ Built-in Basic authentication middleware for temporary site gates and pre-launch protection. Use it from `middleware/` with credentials from `void/env` to protect the whole app, or pass it to `defineHandler()` for a single route.
124
+
125
+ Void internal endpoints under `/__void` are always excluded so deploy-time migrations, prerendering, cron/queue dispatch, and remote binding helpers still reach Void's own internal-token checks.
126
+
127
+ When credentials come from `void/env`, wrap the reads in functions so they are resolved per request after Void has bound the runtime env.
128
+
129
+ For app-specific bypasses such as health checks or public webhooks, compose that logic in your own middleware before calling `basicAuth()`.
130
+
131
+ ```ts
132
+ // env.ts
133
+ import { defineEnv, string } from 'void/env';
134
+
135
+ export default defineEnv({
136
+ BASIC_AUTH_USERNAME: string(),
137
+ BASIC_AUTH_PASSWORD: string(),
138
+ });
139
+ ```
140
+
141
+ ```ts
142
+ // middleware/01.basic-auth.ts
143
+ import { basicAuth } from 'void';
144
+ import { env } from 'void/env';
145
+
146
+ export default basicAuth({
147
+ username: () => env.BASIC_AUTH_USERNAME,
148
+ password: () => env.BASIC_AUTH_PASSWORD,
149
+ realm: 'Preview',
150
+ });
151
+ ```
152
+
153
+ **Signature:**
154
+
155
+ ```ts
156
+ function basicAuth(options: {
157
+ username: string | (() => string);
158
+ password: string | (() => string);
159
+ realm?: string | (() => string);
160
+ message?: string | (() => string);
161
+ }): MiddlewareHandler<CloudEnv>;
162
+ ```
163
+
121
164
  ### `defineScheduled(handler)`
122
165
 
123
166
  Wraps a Cloudflare [Scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) with type inference.
@@ -1105,7 +1148,7 @@ This table lists app-facing imports. Exported implementation subpaths such as `v
1105
1148
  | Import path | Contents |
1106
1149
  | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1107
1150
  | `void` | `voidPlugin`, handler/type re-exports, `defer`, `Deferred`, `DeferredState`, `InferProps`, `HeadDescriptor` |
1108
- | `void/handler` | `defineHandler`, `defineMiddleware`, `defineScheduled`, `defineQueue`, `defineRender`, `defineHead`, types |
1151
+ | `void/handler` | `defineHandler`, `defineMiddleware`, `basicAuth`, `defineScheduled`, `defineQueue`, `defineRender`, `defineHead`, types |
1109
1152
  | `void/auth` | `defineAuth`, `getUser`, `getSession`, `requireAuth`, `AuthUser`, `AuthSession`, `AuthState` |
1110
1153
  | `void/client` | `fetch`, `fetchStream`, `FetchError`, `auth`, `createAuthClient`, `AuthUser`, `AuthSession`, `AuthState` |
1111
1154
  | `void/client/{framework}` | Same as `void/client`, with framework-specific Better Auth clients for `react`, `vue`, `svelte`, and `solid` |
@@ -172,7 +172,7 @@ Output mode. Controls the default rendering strategy for pages.
172
172
 
173
173
  When set to `"static"`, all pages are prerendered at build time as static HTML files written to `dist/client/`. Individual pages can opt out with `export const prerender = false`. Dynamic pages (with route params) without a `getPrerenderPaths()` export are implicitly not prerendered.
174
174
 
175
- When omitted or set to `"server"`, pages are server-rendered on request. Individual pages can opt into deploy-time prerendering with `export const prerender = true`.
175
+ When omitted or set to `"server"`, pages are server-rendered on request. Individual pages can opt into deploy-time prerendering with `export const prerender = true`, or opt out of server-rendered component HTML with `export const ssr = false`.
176
176
 
177
177
  ```json
178
178
  { "output": "static" }
File without changes
File without changes