@netrojs/vono 0.0.1

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.
package/client.ts ADDED
@@ -0,0 +1,309 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Vono · client.ts
3
+ // Vue 3 SSR hydration · Vue Router SPA · reactive page data · SEO sync
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ import {
7
+ createSSRApp,
8
+ defineAsyncComponent,
9
+ defineComponent,
10
+ h,
11
+ inject,
12
+ reactive,
13
+ readonly,
14
+ type Component,
15
+ type InjectionKey,
16
+ } from 'vue'
17
+ import {
18
+ createRouter,
19
+ createWebHistory,
20
+ RouterView,
21
+ } from 'vue-router'
22
+ import {
23
+ isAsyncLoader,
24
+ resolveRoutes,
25
+ toVueRouterPath,
26
+ compilePath,
27
+ matchPath,
28
+ SPA_HEADER,
29
+ STATE_KEY,
30
+ PARAMS_KEY,
31
+ SEO_KEY,
32
+ DATA_KEY,
33
+ type AppConfig,
34
+ type LayoutDef,
35
+ type SEOMeta,
36
+ type ClientMiddleware,
37
+ } from './core'
38
+
39
+ // ── SEO ───────────────────────────────────────────────────────────────────────
40
+
41
+ function setMeta(selector: string, attr: string, val?: string): void {
42
+ if (!val) { document.querySelector(selector)?.remove(); return }
43
+ let el = document.querySelector<HTMLMetaElement>(selector)
44
+ if (!el) {
45
+ el = document.createElement('meta')
46
+ // Destructuring with defaults avoids string|undefined from noUncheckedIndexedAccess
47
+ const [, attrName = '', attrVal = ''] = /\[([^=]+)="([^"]+)"\]/.exec(selector) ?? []
48
+ if (attrName) el.setAttribute(attrName, attrVal)
49
+ document.head.appendChild(el)
50
+ }
51
+ el.setAttribute(attr, val)
52
+ }
53
+
54
+ export function syncSEO(seo: SEOMeta): void {
55
+ if (seo.title) document.title = seo.title
56
+ setMeta('[name="description"]', 'content', seo.description)
57
+ setMeta('[name="keywords"]', 'content', seo.keywords)
58
+ setMeta('[name="robots"]', 'content', seo.robots)
59
+ setMeta('[name="theme-color"]', 'content', seo.themeColor)
60
+ setMeta('[property="og:title"]', 'content', seo.ogTitle)
61
+ setMeta('[property="og:description"]', 'content', seo.ogDescription)
62
+ setMeta('[property="og:image"]', 'content', seo.ogImage)
63
+ setMeta('[property="og:url"]', 'content', seo.ogUrl)
64
+ setMeta('[property="og:type"]', 'content', seo.ogType)
65
+ setMeta('[name="twitter:card"]', 'content', seo.twitterCard)
66
+ setMeta('[name="twitter:title"]', 'content', seo.twitterTitle)
67
+ setMeta('[name="twitter:description"]','content', seo.twitterDescription)
68
+ setMeta('[name="twitter:image"]', 'content', seo.twitterImage)
69
+
70
+ let link = document.querySelector<HTMLLinkElement>('link[rel="canonical"]')
71
+ if (seo.canonical) {
72
+ if (!link) {
73
+ link = document.createElement('link')
74
+ link.rel = 'canonical'
75
+ document.head.appendChild(link)
76
+ }
77
+ link.href = seo.canonical
78
+ } else {
79
+ link?.remove()
80
+ }
81
+ }
82
+
83
+ // ── SPA data fetch + prefetch cache ──────────────────────────────────────────
84
+
85
+ interface SpaPayload {
86
+ state: Record<string, unknown>
87
+ params: Record<string, string>
88
+ seo: SEOMeta
89
+ }
90
+
91
+ // Module-level cache so repeated visits to the same URL don't re-fetch
92
+ const _fetchCache = new Map<string, Promise<SpaPayload>>()
93
+
94
+ function fetchSPA(href: string): Promise<SpaPayload> {
95
+ if (!_fetchCache.has(href)) {
96
+ _fetchCache.set(
97
+ href,
98
+ fetch(href, { headers: { [SPA_HEADER]: '1' } }).then(r => {
99
+ if (!r.ok) throw new Error(`[vono] ${r.status} ${r.statusText} — ${href}`)
100
+ return r.json() as Promise<SpaPayload>
101
+ }),
102
+ )
103
+ }
104
+ return _fetchCache.get(href)!
105
+ }
106
+
107
+ export function prefetch(url: string): void {
108
+ try {
109
+ const u = new URL(url, location.origin)
110
+ if (u.origin === location.origin) fetchSPA(u.toString())
111
+ } catch { /* ignore malformed URLs */ }
112
+ }
113
+
114
+ // ── Client middleware ─────────────────────────────────────────────────────────
115
+
116
+ const _mw: ClientMiddleware[] = []
117
+
118
+ /**
119
+ * Register a client-side navigation middleware.
120
+ * Must be called **before** `boot()`.
121
+ *
122
+ * @example
123
+ * useClientMiddleware(async (url, next) => {
124
+ * if (!isLoggedIn() && url.startsWith('/dashboard')) {
125
+ * await navigate('/login')
126
+ * return
127
+ * }
128
+ * await next()
129
+ * })
130
+ */
131
+ export function useClientMiddleware(mw: ClientMiddleware): void {
132
+ _mw.push(mw)
133
+ }
134
+
135
+ async function runMw(url: string, done: () => Promise<void>): Promise<void> {
136
+ const chain: ClientMiddleware[] = [
137
+ ..._mw,
138
+ async (_: string, next: () => Promise<void>) => { await done(); await next() },
139
+ ]
140
+ let i = 0
141
+ const run = async (): Promise<void> => {
142
+ const fn = chain[i++]
143
+ if (fn) await fn(url, run)
144
+ }
145
+ await run()
146
+ }
147
+
148
+ // ── Reactive page data ────────────────────────────────────────────────────────
149
+ //
150
+ // A single module-level reactive object that lives for the app's lifetime.
151
+ // On SPA navigation it is updated in-place so page components re-render
152
+ // reactively without being unmounted.
153
+ //
154
+ // The app provides it as readonly via DATA_KEY so page components cannot
155
+ // mutate it directly.
156
+
157
+ const _pageData = reactive<Record<string, unknown>>({})
158
+
159
+ function updatePageData(newData: Record<string, unknown>): void {
160
+ // Delete keys that are no longer present in the new data
161
+ for (const k of Object.keys(_pageData)) {
162
+ if (!(k in newData)) delete _pageData[k]
163
+ }
164
+ Object.assign(_pageData, newData)
165
+ }
166
+
167
+ /**
168
+ * Access the current page's loader data inside any Vue component.
169
+ * The returned object is reactive — it updates automatically on navigation.
170
+ *
171
+ * @example
172
+ * const data = usePageData<{ title: string; posts: Post[] }>()
173
+ * // data.title is typed and reactive
174
+ */
175
+ export function usePageData<T extends Record<string, unknown> = Record<string, unknown>>(): T {
176
+ // DATA_KEY is typed as symbol; cast to InjectionKey for strong inference
177
+ const data = inject(DATA_KEY as InjectionKey<T>)
178
+ if (data === undefined) {
179
+ throw new Error('[vono] usePageData() must be called inside a component setup().')
180
+ }
181
+ return data
182
+ }
183
+
184
+ // ── boot() ────────────────────────────────────────────────────────────────────
185
+
186
+ export interface BootOptions extends AppConfig {
187
+ /** Warm fetch cache on link hover. @default true */
188
+ prefetchOnHover?: boolean
189
+ }
190
+
191
+ export async function boot(options: BootOptions): Promise<void> {
192
+ const container = document.getElementById('vono-app')
193
+ if (!container) {
194
+ console.error('[vono] #vono-app not found — aborting hydration.')
195
+ return
196
+ }
197
+
198
+ const { pages } = resolveRoutes(options.routes, {
199
+ ...(options.layout !== undefined && { layout: options.layout }),
200
+ middleware: [],
201
+ })
202
+
203
+ // Read server-injected bootstrap data
204
+ const stateMap = (window as any)[STATE_KEY] as Record<string, Record<string, unknown>> ?? {}
205
+ const seoData = (window as any)[SEO_KEY] as SEOMeta ?? {}
206
+ const pathname = location.pathname
207
+
208
+ // Seed reactive store and sync SEO from server data (no network request)
209
+ updatePageData(stateMap[pathname] ?? {})
210
+ syncSEO(seoData)
211
+
212
+ // Build Vue Router route table
213
+ // Async loaders are wrapped with defineAsyncComponent for code splitting.
214
+ const vueRoutes = pages.map(r => {
215
+ const layout = r.layout !== undefined ? r.layout : options.layout
216
+ const comp = r.page.component
217
+
218
+ const PageComp: Component = isAsyncLoader(comp)
219
+ ? defineAsyncComponent(comp)
220
+ : comp as Component
221
+
222
+ const routeComp: Component = layout
223
+ ? defineComponent({
224
+ name: 'VonoRoute',
225
+ setup: () => () => h((layout as LayoutDef).component as Component, null, {
226
+ default: () => h(PageComp),
227
+ }),
228
+ })
229
+ : PageComp
230
+
231
+ return { path: toVueRouterPath(r.fullPath), component: routeComp }
232
+ })
233
+
234
+ // Pre-load the current route's async chunk BEFORE hydrating to guarantee the
235
+ // client VDOM matches the SSR HTML (avoids hydration mismatch on first load).
236
+ const currentRoute = pages.find(r => matchPath(compilePath(r.fullPath), pathname) !== null)
237
+ if (currentRoute && isAsyncLoader(currentRoute.page.component)) {
238
+ await currentRoute.page.component()
239
+ }
240
+
241
+ // createSSRApp: tells Vue to hydrate existing DOM instead of re-rendering
242
+ const app = createSSRApp({ name: 'VonoApp', render: () => h(RouterView) })
243
+ app.provide(DATA_KEY as InjectionKey<typeof _pageData>, readonly(_pageData))
244
+
245
+ const router = createRouter({ history: createWebHistory(), routes: vueRoutes })
246
+
247
+ // Track whether this is the initial (server-hydrated) navigation.
248
+ // We skip data fetching for the first navigation — the server already
249
+ // injected the data into window.__VONO_STATE__.
250
+ let isInitialNav = true
251
+
252
+ router.beforeEach(async (to, _from, next) => {
253
+ if (isInitialNav) {
254
+ isInitialNav = false
255
+ return next()
256
+ }
257
+
258
+ const href = new URL(to.fullPath, location.origin).toString()
259
+
260
+ try {
261
+ await runMw(to.fullPath, async () => {
262
+ const payload = await fetchSPA(href)
263
+ updatePageData(payload.state ?? {})
264
+ syncSEO(payload.seo ?? {})
265
+ window.scrollTo(0, 0)
266
+ })
267
+ next()
268
+ } catch (err) {
269
+ console.error('[vono] Navigation error:', err)
270
+ // Hard navigate as fallback — the server will handle the request
271
+ location.href = to.fullPath
272
+ }
273
+ })
274
+
275
+ app.use(router)
276
+ await router.isReady()
277
+ app.mount(container)
278
+
279
+ // Hover prefetch — warm the fetch cache before the user clicks
280
+ if (options.prefetchOnHover !== false) {
281
+ document.addEventListener('mouseover', (e) => {
282
+ const a = (e as MouseEvent).composedPath()
283
+ .find((el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement)
284
+ if (a?.href) prefetch(a.href)
285
+ })
286
+ }
287
+ }
288
+
289
+ // ── Re-exports ────────────────────────────────────────────────────────────────
290
+
291
+ export {
292
+ definePage, defineGroup, defineLayout, defineApiRoute, isAsyncLoader,
293
+ resolveRoutes, compilePath, matchPath, toVueRouterPath,
294
+ SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY, DATA_KEY,
295
+ } from './core'
296
+
297
+ export type {
298
+ AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef, Route,
299
+ SEOMeta, HonoMiddleware, LoaderCtx, ResolvedRoute, CompiledPath,
300
+ ClientMiddleware, AsyncLoader, InferPageData,
301
+ } from './core'
302
+
303
+ // Vue Router composables re-exported for convenience
304
+ export {
305
+ useRoute,
306
+ useRouter,
307
+ RouterLink,
308
+ RouterView,
309
+ } from 'vue-router'
package/core.ts ADDED
@@ -0,0 +1,151 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Vono · core.ts
3
+ // Route builders · path matching · route resolution · async-loader detection
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ import type { Component } from 'vue'
7
+ import type {
8
+ PageDef, GroupDef, LayoutDef, ApiRouteDef, Route,
9
+ ResolvedRoute, CompiledPath, HonoMiddleware, AsyncLoader, LoaderCtx,
10
+ } from './types'
11
+
12
+ // ── Async-loader detection ────────────────────────────────────────────────────
13
+ //
14
+ // A Vue component (SFC compiled by vite-plugin-vue) always carries one or more
15
+ // of these brand properties. A plain () => import('./Page.vue') factory has
16
+ // none of them, so checking for their absence is sufficient for real-world use.
17
+
18
+ const VUE_BRANDS = ['__name', '__file', '__vccOpts', 'setup', 'render', 'data', 'components'] as const
19
+
20
+ /**
21
+ * Returns true when `c` is an async factory function (i.e. `() => import(...)`)
22
+ * rather than a resolved Vue component object.
23
+ *
24
+ * Used by both server.ts (to resolve the import before SSR) and client.ts
25
+ * (to wrap with defineAsyncComponent for lazy hydration).
26
+ */
27
+ export function isAsyncLoader(c: unknown): c is AsyncLoader {
28
+ if (typeof c !== 'function') return false
29
+ const f = c as unknown as Record<string, unknown>
30
+ for (const brand of VUE_BRANDS) {
31
+ if (brand in f) return false
32
+ }
33
+ return true
34
+ }
35
+
36
+ // ── Builder functions ─────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Define a page route with full type inference.
40
+ *
41
+ * TypeScript infers `TData` automatically from the `loader` return type, so
42
+ * you rarely need to supply the generic manually. Export the page constant and
43
+ * use `InferPageData<typeof myPage>` in your component for a single source of
44
+ * truth.
45
+ *
46
+ * @example
47
+ * export const postPage = definePage({
48
+ * path: '/post/[slug]',
49
+ * loader: async (c) => fetchPost(c.req.param('slug')),
50
+ * component: () => import('./pages/post.vue'),
51
+ * })
52
+ * export type PostData = InferPageData<typeof postPage>
53
+ */
54
+ export function definePage<TData extends object = Record<string, never>>(
55
+ def: Omit<PageDef<TData>, '__type'>,
56
+ ): PageDef<TData> {
57
+ return { __type: 'page', ...def }
58
+ }
59
+
60
+ export function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef {
61
+ return { __type: 'group', ...def }
62
+ }
63
+
64
+ /** Wrap a Vue layout component (must render <slot />) as a Vono layout. */
65
+ export function defineLayout(component: Component): LayoutDef {
66
+ return { __type: 'layout', component }
67
+ }
68
+
69
+ export function defineApiRoute(
70
+ path: string,
71
+ register: ApiRouteDef['register'],
72
+ ): ApiRouteDef {
73
+ return { __type: 'api', path, register }
74
+ }
75
+
76
+ // ── Path matching (Vono [param] syntax → RegExp) ────────────────────────────
77
+
78
+ export function compilePath(path: string): CompiledPath {
79
+ const keys: string[] = []
80
+ const src = path
81
+ .replace(/\[\.\.\.([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '(.*)' })
82
+ .replace(/\[([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '([^/]+)' })
83
+ .replace(/\*/g, '(.*)')
84
+ return { re: new RegExp(`^${src}$`), keys }
85
+ }
86
+
87
+ export function matchPath(
88
+ cp: CompiledPath,
89
+ pathname: string,
90
+ ): Record<string, string> | null {
91
+ const m = pathname.match(cp.re)
92
+ if (!m) return null
93
+ const params: Record<string, string> = {}
94
+ cp.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1] ?? '') })
95
+ return params
96
+ }
97
+
98
+ /**
99
+ * Convert Vono `[param]` syntax to Vue Router `:param` syntax.
100
+ *
101
+ * `/posts/[slug]` → `/posts/:slug`
102
+ * `/files/[...path]` → `/files/:path(.*)*`
103
+ */
104
+ export function toVueRouterPath(vonoPath: string): string {
105
+ return vonoPath
106
+ .replace(/\[\.\.\.([^\]]+)\]/g, ':$1(.*)*')
107
+ .replace(/\[([^\]]+)\]/g, ':$1')
108
+ }
109
+
110
+ // ── Route resolution ──────────────────────────────────────────────────────────
111
+
112
+ export function resolveRoutes(
113
+ routes: Route[],
114
+ options: {
115
+ prefix?: string
116
+ middleware?: HonoMiddleware[]
117
+ layout?: LayoutDef | false
118
+ } = {},
119
+ ): { pages: ResolvedRoute[]; apis: ApiRouteDef[] } {
120
+ const pages: ResolvedRoute[] = []
121
+ const apis: ApiRouteDef[] = []
122
+
123
+ for (const route of routes) {
124
+ if (route.__type === 'api') {
125
+ apis.push({ ...route, path: (options.prefix ?? '') + route.path })
126
+ } else if (route.__type === 'group') {
127
+ const prefix = (options.prefix ?? '') + route.prefix
128
+ const mw = [...(options.middleware ?? []), ...(route.middleware ?? [])]
129
+ const layout = route.layout !== undefined ? route.layout : options.layout
130
+ const sub = resolveRoutes(route.routes, {
131
+ prefix,
132
+ middleware: mw,
133
+ ...(layout !== undefined && { layout }),
134
+ })
135
+ pages.push(...sub.pages)
136
+ apis.push(...sub.apis)
137
+ } else {
138
+ pages.push({
139
+ fullPath: (options.prefix ?? '') + route.path,
140
+ page: route,
141
+ layout: route.layout !== undefined ? route.layout : options.layout,
142
+ middleware: [...(options.middleware ?? []), ...(route.middleware ?? [])],
143
+ })
144
+ }
145
+ }
146
+
147
+ return { pages, apis }
148
+ }
149
+
150
+ // Re-export all types so `import from '@netrojs/vono'` (root export) works
151
+ export * from './types'
@@ -0,0 +1,199 @@
1
+ import { Component } from 'vue';
2
+ import { Hono, MiddlewareHandler, Context } from 'hono';
3
+ export { RouterLink, RouterView, useRoute, useRouter } from 'vue-router';
4
+
5
+ type HonoMiddleware = MiddlewareHandler;
6
+ type LoaderCtx = Context;
7
+ interface SEOMeta {
8
+ title?: string;
9
+ description?: string;
10
+ keywords?: string;
11
+ author?: string;
12
+ robots?: string;
13
+ canonical?: string;
14
+ themeColor?: string;
15
+ ogTitle?: string;
16
+ ogDescription?: string;
17
+ ogImage?: string;
18
+ ogImageAlt?: string;
19
+ ogUrl?: string;
20
+ ogType?: string;
21
+ ogSiteName?: string;
22
+ twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
23
+ twitterSite?: string;
24
+ twitterTitle?: string;
25
+ twitterDescription?: string;
26
+ twitterImage?: string;
27
+ /** Structured data injected as <script type="application/ld+json">. */
28
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
29
+ }
30
+ type AsyncLoader = () => Promise<{
31
+ default: Component;
32
+ } | Component>;
33
+ interface PageDef<TData extends object = Record<string, never>> {
34
+ readonly __type: 'page';
35
+ path: string;
36
+ middleware?: HonoMiddleware[];
37
+ loader?: (c: LoaderCtx) => TData | Promise<TData>;
38
+ seo?: SEOMeta | ((data: TData, params: Record<string, string>) => SEOMeta);
39
+ /** Override or disable the app-level layout for this route. */
40
+ layout?: LayoutDef | false;
41
+ /**
42
+ * The Vue component to render for this route.
43
+ * Use () => import('./Page.vue') for automatic code splitting.
44
+ */
45
+ component: Component | AsyncLoader;
46
+ }
47
+ interface GroupDef {
48
+ readonly __type: 'group';
49
+ prefix: string;
50
+ layout?: LayoutDef | false;
51
+ middleware?: HonoMiddleware[];
52
+ routes: Route[];
53
+ }
54
+ interface LayoutDef {
55
+ readonly __type: 'layout';
56
+ /** Vue layout component — must contain <slot /> for page content. */
57
+ component: Component;
58
+ }
59
+ interface ApiRouteDef {
60
+ readonly __type: 'api';
61
+ path: string;
62
+ register: (app: Hono, globalMiddleware: HonoMiddleware[]) => void;
63
+ }
64
+ type Route = PageDef<any> | GroupDef | ApiRouteDef;
65
+ interface AppConfig {
66
+ layout?: LayoutDef;
67
+ seo?: SEOMeta;
68
+ middleware?: HonoMiddleware[];
69
+ routes: Route[];
70
+ notFound?: Component;
71
+ htmlAttrs?: Record<string, string>;
72
+ /** Extra HTML injected into <head> (e.g. font preloads). */
73
+ head?: string;
74
+ }
75
+ interface ResolvedRoute {
76
+ fullPath: string;
77
+ page: PageDef<any>;
78
+ layout: LayoutDef | false | undefined;
79
+ middleware: HonoMiddleware[];
80
+ }
81
+ interface CompiledPath {
82
+ re: RegExp;
83
+ keys: string[];
84
+ }
85
+ type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
86
+ /** Custom request header that identifies an SPA navigation (JSON payload). */
87
+ declare const SPA_HEADER = "x-vono-spa";
88
+ /** window key for SSR-injected per-page loader data. */
89
+ declare const STATE_KEY = "__VONO_STATE__";
90
+ /** window key for SSR-injected URL params. */
91
+ declare const PARAMS_KEY = "__VONO_PARAMS__";
92
+ /** window key for SSR-injected SEO meta. */
93
+ declare const SEO_KEY = "__VONO_SEO__";
94
+ /**
95
+ * Vue provide/inject key for the reactive page-data object.
96
+ * Symbol.for() ensures the same reference across module instances (SSR safe).
97
+ */
98
+ declare const DATA_KEY: unique symbol;
99
+ /**
100
+ * Extract the loader data type from a `PageDef` returned by `definePage()`.
101
+ *
102
+ * This enables you to define the data type exactly once — inferred from the
103
+ * loader — and import it into page components for `usePageData<T>()`.
104
+ *
105
+ * @example
106
+ * // app/routes.ts
107
+ * export const homePage = definePage({
108
+ * path: '/',
109
+ * loader: async () => ({ title: 'Hello', count: 42 }),
110
+ * component: () => import('./pages/home.vue'),
111
+ * })
112
+ * export type HomeData = InferPageData<typeof homePage>
113
+ * // HomeData = { title: string; count: number }
114
+ *
115
+ * // app/pages/home.vue
116
+ * import type { HomeData } from '../routes'
117
+ * const data = usePageData<HomeData>()
118
+ */
119
+ type InferPageData<T> = T extends PageDef<infer D> ? D : never;
120
+
121
+ /**
122
+ * Returns true when `c` is an async factory function (i.e. `() => import(...)`)
123
+ * rather than a resolved Vue component object.
124
+ *
125
+ * Used by both server.ts (to resolve the import before SSR) and client.ts
126
+ * (to wrap with defineAsyncComponent for lazy hydration).
127
+ */
128
+ declare function isAsyncLoader(c: unknown): c is AsyncLoader;
129
+ /**
130
+ * Define a page route with full type inference.
131
+ *
132
+ * TypeScript infers `TData` automatically from the `loader` return type, so
133
+ * you rarely need to supply the generic manually. Export the page constant and
134
+ * use `InferPageData<typeof myPage>` in your component for a single source of
135
+ * truth.
136
+ *
137
+ * @example
138
+ * export const postPage = definePage({
139
+ * path: '/post/[slug]',
140
+ * loader: async (c) => fetchPost(c.req.param('slug')),
141
+ * component: () => import('./pages/post.vue'),
142
+ * })
143
+ * export type PostData = InferPageData<typeof postPage>
144
+ */
145
+ declare function definePage<TData extends object = Record<string, never>>(def: Omit<PageDef<TData>, '__type'>): PageDef<TData>;
146
+ declare function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef;
147
+ /** Wrap a Vue layout component (must render <slot />) as a Vono layout. */
148
+ declare function defineLayout(component: Component): LayoutDef;
149
+ declare function defineApiRoute(path: string, register: ApiRouteDef['register']): ApiRouteDef;
150
+ declare function compilePath(path: string): CompiledPath;
151
+ declare function matchPath(cp: CompiledPath, pathname: string): Record<string, string> | null;
152
+ /**
153
+ * Convert Vono `[param]` syntax to Vue Router `:param` syntax.
154
+ *
155
+ * `/posts/[slug]` → `/posts/:slug`
156
+ * `/files/[...path]` → `/files/:path(.*)*`
157
+ */
158
+ declare function toVueRouterPath(vonoPath: string): string;
159
+ declare function resolveRoutes(routes: Route[], options?: {
160
+ prefix?: string;
161
+ middleware?: HonoMiddleware[];
162
+ layout?: LayoutDef | false;
163
+ }): {
164
+ pages: ResolvedRoute[];
165
+ apis: ApiRouteDef[];
166
+ };
167
+
168
+ declare function syncSEO(seo: SEOMeta): void;
169
+ declare function prefetch(url: string): void;
170
+ /**
171
+ * Register a client-side navigation middleware.
172
+ * Must be called **before** `boot()`.
173
+ *
174
+ * @example
175
+ * useClientMiddleware(async (url, next) => {
176
+ * if (!isLoggedIn() && url.startsWith('/dashboard')) {
177
+ * await navigate('/login')
178
+ * return
179
+ * }
180
+ * await next()
181
+ * })
182
+ */
183
+ declare function useClientMiddleware(mw: ClientMiddleware): void;
184
+ /**
185
+ * Access the current page's loader data inside any Vue component.
186
+ * The returned object is reactive — it updates automatically on navigation.
187
+ *
188
+ * @example
189
+ * const data = usePageData<{ title: string; posts: Post[] }>()
190
+ * // data.title is typed and reactive
191
+ */
192
+ declare function usePageData<T extends Record<string, unknown> = Record<string, unknown>>(): T;
193
+ interface BootOptions extends AppConfig {
194
+ /** Warm fetch cache on link hover. @default true */
195
+ prefetchOnHover?: boolean;
196
+ }
197
+ declare function boot(options: BootOptions): Promise<void>;
198
+
199
+ export { type ApiRouteDef, type AppConfig, type AsyncLoader, type BootOptions, type ClientMiddleware, type CompiledPath, DATA_KEY, type GroupDef, type HonoMiddleware, type InferPageData, type LayoutDef, type LoaderCtx, PARAMS_KEY, type PageDef, type ResolvedRoute, type Route, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY, boot, compilePath, defineApiRoute, defineGroup, defineLayout, definePage, isAsyncLoader, matchPath, prefetch, resolveRoutes, syncSEO, toVueRouterPath, useClientMiddleware, usePageData };