Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/scripts/packed-consumer-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
const fullSuitePaths = [
/^(?:package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|\.node-version|\.npmrc|\.pnpmfile\.cjs|vite\.config\.[jt]s)$/,
/^packages\/[^/]+\/(?:package\.json|(?:vite|tsdown)\.config\.[cm]?[jt]s|tsconfig[^/]*\.json)$/,
/^packages\/(?:create-void|react|vue|svelte|solid|md)\/(?:src|bin)\//,
/^packages\/(?:create-void|react|vue|svelte|solid|hono|md)\/(?:src|bin)\//,
/^packages\/void\/src\/cli\/(?:init|template|package|agents)(?:[-./])/,
/^\.github\/scripts\/(?:packed-|release-policy)/,
/^\.github\/workflows\/(?:packed-consumers|prepublish-test|publish)\.yml$/,
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/packed-consumer-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ test('runs the full matrix for dependencies, adapters, scaffolding, and packagin
'packages/void/package.json',
'packages/void/vite.config.ts',
'packages/solid/src/plugin.ts',
'packages/hono/src/plugin.ts',
'packages/vue/src/runtime/index.ts',
'packages/create-void/src/pm.ts',
'packages/void/src/cli/init.ts',
Expand Down
12 changes: 10 additions & 2 deletions .github/scripts/packed-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,11 @@ assert(managers.every((manager) => ['npm', 'pnpm'].includes(manager)));
const frameworks = process.env.VOID_PACKED_FRAMEWORK
? [process.env.VOID_PACKED_FRAMEWORK]
: full
? ['react', 'vue', 'svelte', 'solid']
? ['react', 'vue', 'svelte', 'solid', 'hono']
: ['react'];
assert(frameworks.every((framework) => ['react', 'vue', 'svelte', 'solid'].includes(framework)));
assert(
frameworks.every((framework) => ['react', 'vue', 'svelte', 'solid', 'hono'].includes(framework)),
);
const pages = {
react: `import { useState } from 'react';
import { Link } from '@void/react';
Expand All @@ -175,6 +177,12 @@ import { Link } from '@void/solid';
export default function Page(props: { message: string }) {
const [count, setCount] = createSignal(0);
return <><h1>{props.message}</h1><button onClick={() => setCount(count() + 1)}>Count {count()}</button><Link href="/next">Next page</Link></>;
}`,
hono: `import { useState } from 'hono/jsx';
import { Link } from '@void/hono';
export default function Page({ message }: { message: string }) {
const [count, setCount] = useState(0);
return <><h1>{message}</h1><button onClick={() => setCount(count + 1)}>Count {count}</button><Link href="/next">Next page</Link></>;
}`,
};

Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/packed-init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function initializePackedProject(
version,
toolchain = 'vite',
) {
const frameworks = ['react', 'vue', 'svelte', 'solid'];
const frameworks = ['react', 'vue', 'svelte', 'solid', 'hono'];
assert(frameworks.includes(framework), 'Unknown Pages framework');
assert(['npm', 'pnpm'].includes(manager), 'Unknown package manager');
assert(['vite', 'vite-plus'].includes(toolchain), 'Unknown toolchain');
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pnpm workspaces managed through Vite+. Shared dependency versions use the **pnpm
| [`packages/vue`](./packages/vue/README.md) | `@void/vue` | Vue framework adapter for Pages mode (SSR, Link, useForm) |
| [`packages/svelte`](./packages/svelte/README.md) | `@void/svelte` | Svelte 5 framework adapter for Pages mode (SSR, Link, useForm) |
| [`packages/solid`](./packages/solid/README.md) | `@void/solid` | SolidJS framework adapter for Pages mode (SSR, Link, useForm) |
| [`packages/hono`](./packages/hono/README.md) | `@void/hono` | Hono JSX framework adapter for Pages mode (SSR, Link, useForm) |
| [`docs`](./docs) | (private) | Documentation site (VitePress) |
| [`platform`](./platform/README.md) | (private) | Self-hosted platform source, routing, proxy, logs, and dashboard |
| `playground/*` | (private) | local apps for testing |
Expand All @@ -85,7 +86,7 @@ Platform packages live under `platform/packages/*` and may depend on public/shar

Read [the core package instructions](./packages/void/AGENTS.md) for plugin,
runtime, code-generation and CLI invariants. Each framework adapter has its
own README under `packages/{react,vue,svelte,solid}`.
own README under `packages/{react,vue,svelte,solid,hono}`.
The [API reference](./docs/reference/api.md) and
[CLI reference](./docs/reference/cli.md) describe the supported public surface.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Void brings together three open-source tools for you and your coding agents:
- **Void CLI:** Build your app, provision the Cloudflare resources it uses, apply migrations, and deploy with one command. Manage resources, inspect logs, and roll back through the same CLI.
- **Void Platform:** Install a shared deployment environment in your Cloudflare account for your team or customers, with access controls, resource limits, usage reporting, and [connected applications](./docs/guide/platform/administration/connected-apps.md) that request user-approved deployments.

The SDK's features are optional and composable. Use React, Vue, Svelte, or Solid for Void pages, or keep a [supported application framework](https://void.cloud/integrations/frameworks/overview) for routing and rendering while Void handles resource APIs, migrations, and deployment.
The SDK's features are optional and composable. Use React, Vue, Svelte, Solid, or Hono for Void pages, or keep a [supported application framework](https://void.cloud/integrations/frameworks/overview) for routing and rendering while Void handles resource APIs, migrations, and deployment.

## Getting Started

Expand Down
5 changes: 3 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ vp run test:integration
vp run test:metaframeworks
vp run test:pages:react-vue
vp run test:pages:svelte-solid
vp run test:pages:hono
vp run test:platform
```

Expand Down Expand Up @@ -213,7 +214,7 @@ workspace links can hide. They do not publish packages or deploy applications.

Smoke coverage checks package contents and exports, real initialization, CLI help, a frozen
reinstall, public Vite builds, TypeScript, and development HTTP requests for pages, assets, and
KV. It uses no browser. Full coverage adds Vue, Svelte, Solid, Markdown, production preview,
KV. It uses no browser. Full coverage adds Vue, Svelte, Solid, Hono, Markdown, production preview,
and browser hydration/navigation. Superseded PR/main runs are cancelled; release runs are not.

The selector reads all PR file pages or the complete pushed commit range. Renames consider both
Expand All @@ -228,7 +229,7 @@ VOID_PACKED_SUITE=full node .github/scripts/packed-consumer.mjs
node --test --test-timeout=10000 .github/scripts/packed-*.test.mjs
```

`VOID_PACKED_PM=npm|pnpm` and `VOID_PACKED_FRAMEWORK=react|vue|svelte|solid` select a focused
`VOID_PACKED_PM=npm|pnpm` and `VOID_PACKED_FRAMEWORK=react|vue|svelte|solid|hono` select a focused
local run. `VOID_PACKED_TOOLCHAIN=vite-plus` verifies Vite+ initialization, the workspace's
tested Vite+ version, and the generated `vp` build/dev/preview commands. CI checks a React
app with Vite+ for both package managers alongside the plain Vite coverage.
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export default extendConfig(
},

vite: {
plugins: [groupIconVitePlugin()],
plugins: [groupIconVitePlugin({ customIcon: { hono: 'logos:hono' } })],
},

themeConfig: {
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/app-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The type is auto-detected from your project structure, or you can set it explici

## Void Apps

In a Void app, Void handles server routing and deployment. Use React, Vue, Svelte, or Solid for server-rendered pages, or build a frontend with any library that works with Vite.
In a Void app, Void handles server routing and deployment. Use React, Vue, Svelte, Solid, or Hono for server-rendered pages, or build a frontend with any library that works with Vite.

A Void app can be **API-only** (just `routes/`), a **SPA + API** (frontend in `src/` with API routes), or **full-stack with pages mode** (server-rendered pages in `pages/` with co-located data loading):

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# What is Void?

Void is a full-stack framework for Cloudflare Workers, powered by Vite. Build your UI with React, Vue, Svelte, or Solid, and write server code in the same project. Void connects routing, data access, and deployment while keeping types shared across your app.
Void is a full-stack framework for Cloudflare Workers, powered by Vite. Build your UI with React, Vue, Svelte, Solid, or Hono, and write server code in the same project. Void connects routing, data access, and deployment while keeping types shared across your app.

Start with the [Quickstart](./quickstart.md) to create and deploy an app. If you already use a meta framework, follow its [integration guide](../integrations/frameworks/overview.md).

Expand Down
49 changes: 44 additions & 5 deletions docs/guide/pages-routing/actions-and-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ export default function CreateUser() {
}
```

```tsx [Hono]
// pages/users/index.tsx
import { Form, useForm } from '@void/hono';

export default function CreateUser() {
const form = useForm('/users', { name: '', email: '' });
const value = (e: Event) => (e.target as HTMLInputElement).value;

return (
<Form form={form}>
<input value={form.data.name} onInput={(e) => form.setData('name', value(e))} />
{form.errors.name && <span>{form.errors.name}</span>}

<input value={form.data.email} onInput={(e) => form.setData('email', value(e))} />
{form.errors.email && <span>{form.errors.email}</span>}

<button disabled={form.pending}>Create</button>
</Form>
);
}
```

:::

The types are inferred from your action's `withValidator()` schema in the companion `.server.ts` file. If no validator is defined, the body type falls back to `Record<string, unknown>`, and you still get URL autocomplete.
Expand All @@ -167,7 +189,7 @@ The types are inferred from your action's `withValidator()` schema in the compan

Use `setError()` for client-side validation. Editing a field clears its error automatically.

For a native React form, use `form.post`, `form.put`, `form.patch`, or `form.delete` as a native form action. For an awaitable mutation, use `action()`. In Vue, Svelte, and Solid, form submissions return `Promise<void>`; catch thrown errors or use your framework's error handling.
For a native React form, use `form.post`, `form.put`, `form.patch`, or `form.delete` as a native form action. For an awaitable mutation, use `action()`. In Vue, Svelte, Solid, and Hono, form submissions return `Promise<void>`; catch thrown errors or use your framework's error handling.

For dynamic routes, pass `params` in the options:

Expand Down Expand Up @@ -200,7 +222,7 @@ const form = useForm(

If a callback awaits `router.visit()` or `router.refresh()`, pass `{ resolveOn: 'data' }` so it can finish before React commits the navigation. See [React navigation completion](../../reference/api/pages.md#react-navigation-completion).

Vue, Svelte, and Solid submissions return a promise instead; use `await form.post()` to run work after a submission.
Vue, Svelte, Solid, and Hono submissions return a promise instead; use `await form.post()` to run work after a submission.

### Scroll after submission

Expand All @@ -216,7 +238,7 @@ await action('/users/:id?update', {
});
```

In React, set `preserveScroll` in the third argument to `useForm()`. In Vue, Svelte, and Solid, pass it to the submit method, such as `form.post({ preserveScroll: false })`.
In React, set `preserveScroll` in the third argument to `useForm()`. In Vue, Svelte, Solid, and Hono, pass it to the submit method, such as `form.post({ preserveScroll: false })`.

## Named Actions

Expand Down Expand Up @@ -277,7 +299,7 @@ Each named action has its own validator schema, which supplies types for its URL
Use `action()` for a button or other mutation that does not need form state. It updates the page after a successful request:

```ts
import { action } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid"
import { action } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid", "@void/hono"

const result = await action('/users/:id?delete', {
params: { id: '42' },
Expand All @@ -303,7 +325,7 @@ Authentication errors (`401`, `403`), server errors (`500`, `502`), and unexpect
Use `isVoidActionError()` to tell a failed action apart from other errors. It's `true` for a `VoidActionError` at any status, and narrows the error so you can read `status` and `message`. `isCallSiteActionError()` is `true` only for the expected errors that `action()` returns.

```ts
import { isVoidActionError } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid"
import { isVoidActionError } from '@void/react'; // or "@void/vue", "@void/svelte", "@void/solid", "@void/hono"

function describeError(error: unknown) {
if (isVoidActionError(error)) {
Expand Down Expand Up @@ -410,6 +432,23 @@ export default function Upload() {
}
```

```tsx [Hono]
import { Form, useForm } from '@void/hono';

export default function Upload() {
const form = useForm('/photos', { title: '', photo: null as File | null });
const input = (e: Event) => e.target as HTMLInputElement;

return (
<Form form={form}>
<input value={form.data.title} onInput={(e) => form.setData('title', input(e).value)} />
<input type="file" onChange={(e) => form.setData('photo', input(e).files?.[0] ?? null)} />
<button disabled={form.pending}>Upload</button>
</Form>
);
}
```

:::

On the server, use `c.req.parseBody()` to access the uploaded file:
Expand Down
56 changes: 56 additions & 0 deletions docs/guide/pages-routing/islands.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ export default function BlogIndex(props) {
}
```

```tsx [Hono]
// pages/blog/index.island.tsx
import Counter from './_Counter' with { island: 'load' };
import PostForm from './_PostForm' with { island: 'visible' };

export default function BlogIndex(props) {
return (
<div>
<h1>Blog</h1>
<Counter />
{props.posts.map((post) => (
<a key={post.slug} href={`/blog/${post.slug}`}>
{post.title}
</a>
))}
<PostForm />
</div>
);
}
```

:::

Components imported _without_ the `island` attribute are rendered as static HTML only. No JavaScript is sent to the browser for them.
Expand Down Expand Up @@ -342,6 +363,41 @@ export default function PostForm() {
}
```

```tsx [Hono]
import { useIslandForm } from '@void/hono';

export default function PostForm() {
const form = useIslandForm({ title: '', body: '' });

return (
<form
onSubmit={(e) => {
e.preventDefault();
return form.post('/blog');
}}
>
<label for="title">Title:</label>
<input
id="title"
value={form.data.title}
onInput={(e) => form.setData('title', (e.target as HTMLInputElement).value)}
/>
{form.errors.title && <span>{form.errors.title}</span>}

<label for="body">Body:</label>
<textarea
id="body"
value={form.data.body}
onInput={(e) => form.setData('body', (e.target as HTMLTextAreaElement).value)}
/>
{form.errors.body && <span>{form.errors.body}</span>}

<button disabled={form.pending}>{form.pending ? 'Saving...' : 'Add Post'}</button>
</form>
);
}
```

:::

`useIslandForm` provides the same form state as [`useForm`](./actions-and-forms#useform). Pass the action URL to `post`, `put`, `patch`, or `delete` to submit.
Expand Down
30 changes: 29 additions & 1 deletion docs/guide/pages-routing/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,29 @@ export default function Layout(props: { children: JSX.Element }) {
}
```

```tsx [Hono]
// pages/layout.tsx
import { useShared, Link } from '@void/hono';
import type { Child } from 'hono/jsx';

export default function Layout(props: { children?: Child }) {
const shared = useShared();
return (
<>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
{shared.auth?.user && <span>{shared.auth.user.name}</span>}
</nav>
<main>{props.children}</main>
</>
);
}
```

:::

When a page renders, the layout wraps it. The page component is injected as the layout's children in React, Solid, and Svelte, or as a slot in Vue:
When a page renders, the layout wraps it. The page component is injected as the layout's children in React, Solid, Svelte, and Hono, or as a slot in Vue:

<img src="./layout-nesting.svg" alt="Layout nesting diagram: pages/layout wraps pages/users/layout wraps pages/users/[id] page component" style="max-width: 520px; width: 100%;" />

Expand Down Expand Up @@ -192,6 +212,14 @@ export default function Page() {
}
```

```tsx [Hono]
export const layout = 'landing';

export default function Page() {
return <div>This page uses the landing layout</div>;
}
```

:::

For markdown pages, use frontmatter:
Expand Down
Loading
Loading