@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/server.ts ADDED
@@ -0,0 +1,590 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Vono · server.ts
3
+ // Hono app factory · Vue 3 streaming SSR · SEO head · asset manifest
4
+ // Vite plugin (dual-bundle: server SSR + client SPA)
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ import { Hono } from 'hono'
8
+ import { createSSRApp, defineComponent, h, type Component } from 'vue'
9
+ import { createRouter, createMemoryHistory, RouterView } from 'vue-router'
10
+ import { renderToString, renderToWebStream } from '@vue/server-renderer'
11
+ import {
12
+ resolveRoutes, compilePath, matchPath, toVueRouterPath, isAsyncLoader,
13
+ SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY, DATA_KEY,
14
+ type AppConfig, type ResolvedRoute, type LayoutDef, type SEOMeta,
15
+ } from './core'
16
+ import { build, type Plugin, type InlineConfig, type UserConfig } from 'vite'
17
+
18
+ // ── HTML helpers ──────────────────────────────────────────────────────────────
19
+
20
+ function esc(s: string): string {
21
+ return s
22
+ .replace(/&/g, '&')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;')
26
+ }
27
+
28
+ // ── SEO → <head> HTML ─────────────────────────────────────────────────────────
29
+
30
+ function buildHeadMeta(seo: SEOMeta, extraHead = ''): string {
31
+ const m = (n: string, v?: string) => v ? `<meta name="${n}" content="${esc(v)}">` : ''
32
+ const p = (pr: string, v?: string) => v ? `<meta property="${pr}" content="${esc(v)}">` : ''
33
+ const lk = (rel: string, href: string) => `<link rel="${rel}" href="${esc(href)}">`
34
+ const parts: string[] = []
35
+
36
+ if (seo.description) parts.push(m('description', seo.description))
37
+ if (seo.keywords) parts.push(m('keywords', seo.keywords))
38
+ if (seo.author) parts.push(m('author', seo.author))
39
+ if (seo.robots) parts.push(m('robots', seo.robots))
40
+ if (seo.themeColor) parts.push(m('theme-color', seo.themeColor))
41
+ if (seo.canonical) parts.push(lk('canonical', seo.canonical))
42
+
43
+ if (seo.ogTitle) parts.push(p('og:title', seo.ogTitle))
44
+ if (seo.ogDescription) parts.push(p('og:description', seo.ogDescription))
45
+ if (seo.ogImage) parts.push(p('og:image', seo.ogImage))
46
+ if (seo.ogImageAlt) parts.push(p('og:image:alt', seo.ogImageAlt))
47
+ if (seo.ogUrl) parts.push(p('og:url', seo.ogUrl))
48
+ if (seo.ogType) parts.push(p('og:type', seo.ogType))
49
+ if (seo.ogSiteName) parts.push(p('og:site_name', seo.ogSiteName))
50
+
51
+ if (seo.twitterCard) parts.push(m('twitter:card', seo.twitterCard))
52
+ if (seo.twitterSite) parts.push(m('twitter:site', seo.twitterSite))
53
+ if (seo.twitterTitle) parts.push(m('twitter:title', seo.twitterTitle))
54
+ if (seo.twitterDescription) parts.push(m('twitter:description', seo.twitterDescription))
55
+ if (seo.twitterImage) parts.push(m('twitter:image', seo.twitterImage))
56
+
57
+ const ld = seo.jsonLd
58
+ if (ld) {
59
+ const schemas = Array.isArray(ld) ? ld : [ld]
60
+ for (const s of schemas) {
61
+ parts.push(`<script type="application/ld+json">${JSON.stringify(s)}</script>`)
62
+ }
63
+ }
64
+
65
+ if (extraHead) parts.push(extraHead)
66
+ return parts.join('\n')
67
+ }
68
+
69
+ function mergeSEO(base?: SEOMeta, override?: SEOMeta): SEOMeta {
70
+ return { ...(base ?? {}), ...(override ?? {}) }
71
+ }
72
+
73
+ // ── Asset resolution ──────────────────────────────────────────────────────────
74
+
75
+ export interface AssetConfig {
76
+ scripts?: string[]
77
+ styles?: string[]
78
+ /** Directory containing the Vite-built assets and .vite/manifest.json. */
79
+ manifestDir?: string
80
+ manifestEntry?: string
81
+ }
82
+
83
+ interface ResolvedAssets { scripts: string[]; styles: string[] }
84
+
85
+ // Process-level cache — resolved once on first production request.
86
+ let _assetsCache: ResolvedAssets | null = null
87
+
88
+ async function resolveAssets(cfg: AssetConfig, defaultEntry: string): Promise<ResolvedAssets> {
89
+ if (_assetsCache) return _assetsCache
90
+
91
+ if (cfg.manifestDir) {
92
+ try {
93
+ const [{ readFileSync }, { join }] = await Promise.all([
94
+ import('node:fs'),
95
+ import('node:path'),
96
+ ])
97
+ // Vite 5+ writes manifest to <outDir>/.vite/manifest.json
98
+ const raw = readFileSync(join(cfg.manifestDir, '.vite', 'manifest.json'), 'utf-8')
99
+ const manifest = JSON.parse(raw) as Record<string, { file: string; css?: string[] }>
100
+ const key = cfg.manifestEntry
101
+ ?? Object.keys(manifest).find(k => k.endsWith(defaultEntry))
102
+ ?? defaultEntry
103
+ const entry = manifest[key]
104
+ if (entry) {
105
+ _assetsCache = {
106
+ scripts: [`/assets/${entry.file}`],
107
+ styles: (entry.css ?? []).map((f: string) => `/assets/${f}`),
108
+ }
109
+ return _assetsCache
110
+ }
111
+ } catch { /* manifest missing or malformed — fall through */ }
112
+ }
113
+
114
+ _assetsCache = {
115
+ scripts: cfg.scripts ?? ['/assets/client.js'],
116
+ styles: cfg.styles ?? [],
117
+ }
118
+ return _assetsCache
119
+ }
120
+
121
+ // ── HTML shell parts ──────────────────────────────────────────────────────────
122
+
123
+ interface ShellParts {
124
+ head: string // everything up to and including the opening <div id="vono-app">
125
+ tail: string // everything after the closing </div>
126
+ }
127
+
128
+ function buildShellParts(
129
+ title: string,
130
+ metaHtml: string,
131
+ stateJson: string,
132
+ paramsJson: string,
133
+ seoJson: string,
134
+ scripts: string[],
135
+ styles: string[],
136
+ htmlAttrs?: Record<string, string>,
137
+ ): ShellParts {
138
+ const attrs = Object.entries(htmlAttrs ?? { lang: 'en' })
139
+ .map(([k, v]) => `${k}="${esc(v)}"`)
140
+ .join(' ')
141
+ const styleLinks = styles.map(href => `<link rel="stylesheet" href="${esc(href)}">`).join('\n')
142
+ const scriptTags = scripts.map(src => `<script type="module" src="${esc(src)}"></script>`).join('\n')
143
+
144
+ const head = [
145
+ '<!DOCTYPE html>',
146
+ `<html ${attrs}>`,
147
+ '<head>',
148
+ '<meta charset="UTF-8">',
149
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
150
+ `<title>${esc(title)}</title>`,
151
+ metaHtml,
152
+ styleLinks,
153
+ '</head>',
154
+ '<body>',
155
+ '<div id="vono-app">',
156
+ ].filter(Boolean).join('\n')
157
+
158
+ const tail = [
159
+ '</div>',
160
+ '<script>',
161
+ `window.${STATE_KEY}=${stateJson};`,
162
+ `window.${PARAMS_KEY}=${paramsJson};`,
163
+ `window.${SEO_KEY}=${seoJson};`,
164
+ '</script>',
165
+ scriptTags,
166
+ '</body>',
167
+ '</html>',
168
+ ].join('\n')
169
+
170
+ return { head, tail }
171
+ }
172
+
173
+ // ── Async component resolution ────────────────────────────────────────────────
174
+
175
+ /** On the server: await the loader to get the real component before rendering. */
176
+ async function resolveComponent(comp: Component | ((...a: unknown[]) => unknown)): Promise<Component> {
177
+ if (isAsyncLoader(comp)) {
178
+ const mod = await (comp as () => Promise<unknown>)()
179
+ return ((mod as any).default ?? mod) as Component
180
+ }
181
+ return comp as Component
182
+ }
183
+
184
+ // ── Vue SSR renderer (streaming) ──────────────────────────────────────────────
185
+
186
+ /**
187
+ * Builds a fresh Vue SSR app + router per request (required — no shared state
188
+ * across requests) and streams HTML output.
189
+ *
190
+ * The memory history is initialised at the request URL *before* the router is
191
+ * created. This ensures the router's internal startup navigation resolves
192
+ * against the correct route and never emits a spurious
193
+ * "[Vue Router warn]: No match found for location with path '/'" warning.
194
+ */
195
+ async function renderPage(
196
+ route: ResolvedRoute,
197
+ data: object,
198
+ url: string,
199
+ params: Record<string, string>,
200
+ appLayout: LayoutDef | undefined,
201
+ ): Promise<ReadableStream<Uint8Array>> {
202
+ const layout = route.layout !== undefined ? route.layout : appLayout
203
+
204
+ // Resolve async component loaders — critical for SSR correctness
205
+ const PageComp = await resolveComponent(route.page.component)
206
+
207
+ const routeComp: Component = layout
208
+ ? defineComponent({
209
+ name: 'VonoRoute',
210
+ setup: () => () => h(layout.component as Component, null, {
211
+ default: () => h(PageComp),
212
+ }),
213
+ })
214
+ : PageComp
215
+
216
+ // Create a fresh app + router per request (SSR safety — no shared state)
217
+ const app = createSSRApp({ render: () => h(RouterView) })
218
+ app.provide(DATA_KEY, data)
219
+
220
+ // ── Vue Router warning fix ────────────────────────────────────────────────
221
+ // createMemoryHistory() initialises its location to '/'. When the router
222
+ // is constructed it performs an internal navigation to that initial location.
223
+ // If the only registered route is e.g. '/about', no match is found and
224
+ // Vue Router emits a warning even though the subsequent router.push('/about')
225
+ // succeeds perfectly.
226
+ //
227
+ // Fix: call history.replace(url) BEFORE constructing the router. The router
228
+ // then sees the correct initial location and its startup navigation succeeds
229
+ // without warnings. No separate router.push() is required.
230
+ const memHistory = createMemoryHistory()
231
+ memHistory.replace(url)
232
+
233
+ const router = createRouter({
234
+ history: memHistory,
235
+ routes: [{ path: toVueRouterPath(route.fullPath), component: routeComp }],
236
+ })
237
+ app.use(router)
238
+
239
+ // router.isReady() resolves once the initial navigation (to `url`) completes.
240
+ await router.isReady()
241
+
242
+ // renderToWebStream streams body chunks as Uint8Array — lower TTFB vs
243
+ // renderToString (which buffers the entire body before responding).
244
+ return renderToWebStream(app)
245
+ }
246
+
247
+ /** Prepend `head` and append `tail` around Vue's streaming body. */
248
+ function buildResponseStream(
249
+ headHtml: string,
250
+ bodyStream: ReadableStream<Uint8Array>,
251
+ tailHtml: string,
252
+ ): ReadableStream<Uint8Array> {
253
+ const enc = new TextEncoder()
254
+ const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()
255
+
256
+ ;(async () => {
257
+ const writer = writable.getWriter()
258
+ try {
259
+ await writer.write(enc.encode(headHtml))
260
+ const reader = bodyStream.getReader()
261
+ while (true) {
262
+ const { done, value } = await reader.read()
263
+ if (done) break
264
+ await writer.write(value)
265
+ }
266
+ await writer.write(enc.encode(tailHtml))
267
+ await writer.close()
268
+ } catch (err) {
269
+ await writer.abort(err)
270
+ }
271
+ })()
272
+
273
+ return readable
274
+ }
275
+
276
+ // ── createVono ──────────────────────────────────────────────────────────────
277
+
278
+ export interface VonoOptions extends AppConfig {
279
+ assets?: AssetConfig
280
+ }
281
+
282
+ export interface VonoApp {
283
+ /** The Hono instance — attach extra routes, error handlers, middleware. */
284
+ app: Hono
285
+ /** WinterCG-compatible fetch handler for edge runtimes. */
286
+ handler: typeof Hono.prototype.fetch
287
+ }
288
+
289
+ export function createVono(config: VonoOptions): VonoApp {
290
+ const app = new Hono()
291
+
292
+ // Global middleware (runs before every route)
293
+ for (const mw of config.middleware ?? []) app.use('*', mw)
294
+
295
+ const { pages, apis } = resolveRoutes(config.routes, {
296
+ ...(config.layout !== undefined && { layout: config.layout }),
297
+ middleware: [],
298
+ })
299
+
300
+ // Pre-compile path patterns — avoids recompiling on every request
301
+ const compiled = pages.map(r => ({ route: r, cp: compilePath(r.fullPath) }))
302
+
303
+ // Register API sub-apps before the catch-all page handler
304
+ for (const api of apis) {
305
+ const sub = new Hono()
306
+ api.register(sub, config.middleware ?? [])
307
+ app.route(api.path, sub)
308
+ }
309
+
310
+ app.all('*', async (c) => {
311
+ const url = new URL(c.req.url)
312
+ const pathname = url.pathname
313
+ const isSPA = c.req.header(SPA_HEADER) === '1'
314
+ const isDev = process.env['NODE_ENV'] !== 'production'
315
+
316
+ // Route matching
317
+ let matched: { route: ResolvedRoute; params: Record<string, string> } | null = null
318
+ for (const { route, cp } of compiled) {
319
+ const params = matchPath(cp, pathname)
320
+ if (params !== null) { matched = { route, params }; break }
321
+ }
322
+
323
+ if (!matched) {
324
+ if (config.notFound) {
325
+ const html = await renderToString(createSSRApp(config.notFound))
326
+ return c.html(`<!DOCTYPE html><html lang="en"><body>${html}</body></html>`, 404)
327
+ }
328
+ return c.text('Not Found', 404)
329
+ }
330
+
331
+ const { route, params } = matched
332
+
333
+ // Expose dynamic params through c.req.param()
334
+ const origParam = c.req.param.bind(c.req);
335
+ (c.req as any)['param'] = (key?: string) =>
336
+ key != null
337
+ ? (params[key] ?? origParam(key))
338
+ : { ...origParam(), ...params }
339
+
340
+ // Route-level middleware chain (run in order, short-circuit on early response)
341
+ let earlyResponse: Response | undefined
342
+ let idx = 0
343
+ const runNext = async (): Promise<void> => {
344
+ const mw = route.middleware[idx++]
345
+ if (!mw) return
346
+ const res = await mw(c, runNext)
347
+ if (res instanceof Response && !earlyResponse) earlyResponse = res
348
+ }
349
+ await runNext()
350
+ if (earlyResponse) return earlyResponse
351
+
352
+ // Run loader
353
+ const rawData = route.page.loader ? await route.page.loader(c) : {}
354
+ const data = (rawData ?? {}) as object
355
+
356
+ // ── SPA navigation: return JSON only ─────────────────────────────────────
357
+ if (isSPA) {
358
+ const pageSEO = typeof route.page.seo === 'function'
359
+ ? route.page.seo(data as any, params)
360
+ : route.page.seo
361
+ return c.json({
362
+ state: data,
363
+ params,
364
+ url: pathname,
365
+ seo: mergeSEO(config.seo, pageSEO),
366
+ })
367
+ }
368
+
369
+ // ── Full SSR: stream HTML response ────────────────────────────────────────
370
+ const clientEntry = config.assets?.manifestEntry ?? 'client.ts'
371
+ const assets = isDev
372
+ ? { scripts: [`/${clientEntry}`], styles: [] as string[] }
373
+ : await resolveAssets(config.assets ?? {}, clientEntry)
374
+
375
+ const pageSEO = typeof route.page.seo === 'function'
376
+ ? route.page.seo(data as any, params)
377
+ : route.page.seo
378
+ const seo = mergeSEO(config.seo, pageSEO)
379
+ const title = seo.title ?? 'Vono'
380
+
381
+ const { head, tail } = buildShellParts(
382
+ title,
383
+ buildHeadMeta(seo, config.head),
384
+ JSON.stringify({ [pathname]: data }),
385
+ JSON.stringify(params),
386
+ JSON.stringify(seo),
387
+ assets.scripts,
388
+ assets.styles,
389
+ config.htmlAttrs,
390
+ )
391
+
392
+ // Render the body asynchronously while the head is already on the wire
393
+ const bodyStream = await renderPage(route, data, pathname, params, config.layout)
394
+ const stream = buildResponseStream(head, bodyStream, tail)
395
+
396
+ return c.body(stream, 200, {
397
+ 'Content-Type': 'text/html; charset=UTF-8',
398
+ 'Transfer-Encoding': 'chunked',
399
+ 'X-Content-Type-Options': 'nosniff',
400
+ })
401
+ })
402
+
403
+ return { app, handler: app.fetch.bind(app) }
404
+ }
405
+
406
+ // ── serve() ───────────────────────────────────────────────────────────────────
407
+
408
+ export type Runtime = 'node' | 'bun' | 'deno' | 'edge'
409
+
410
+ export function detectRuntime(): Runtime {
411
+ if (typeof (globalThis as any)['Bun'] !== 'undefined') return 'bun'
412
+ if (typeof (globalThis as any)['Deno'] !== 'undefined') return 'deno'
413
+ if (typeof process !== 'undefined' && process.versions?.node) return 'node'
414
+ return 'edge'
415
+ }
416
+
417
+ export interface ServeOptions {
418
+ app: VonoApp
419
+ port?: number
420
+ hostname?: string
421
+ runtime?: Runtime
422
+ /** Root directory that contains the built assets and public files. */
423
+ staticDir?: string
424
+ }
425
+
426
+ export async function serve(opts: ServeOptions): Promise<void> {
427
+ const runtime = opts.runtime ?? detectRuntime()
428
+ const port = opts.port ?? Number(process?.env?.['PORT'] ?? 3000)
429
+ const hostname = opts.hostname ?? '0.0.0.0'
430
+ const staticDir = opts.staticDir ?? './dist'
431
+ const displayHost = hostname === '0.0.0.0' ? 'localhost' : hostname
432
+ const logReady = () => console.log(`\n🔥 Vono [${runtime}] → http://${displayHost}:${port}\n`)
433
+
434
+ switch (runtime) {
435
+ case 'node': {
436
+ const [{ serve: nodeServe }, { serveStatic }] = await Promise.all([
437
+ import('@hono/node-server'),
438
+ import('@hono/node-server/serve-static'),
439
+ ])
440
+ opts.app.app.use('/assets/*', serveStatic({ root: staticDir }))
441
+ opts.app.app.use('/*', serveStatic({ root: './public' }))
442
+ nodeServe({ fetch: opts.app.handler, port, hostname })
443
+ logReady()
444
+ break
445
+ }
446
+ case 'bun':
447
+ ;(globalThis as any)['Bun'].serve({ fetch: opts.app.handler, port, hostname })
448
+ logReady()
449
+ break
450
+ case 'deno':
451
+ ;(globalThis as any)['Deno'].serve({ port, hostname }, opts.app.handler)
452
+ logReady()
453
+ break
454
+ default:
455
+ console.warn('[vono] serve() is a no-op on edge — export vono.handler instead.')
456
+ }
457
+ }
458
+
459
+ // ── Vite plugin ───────────────────────────────────────────────────────────────
460
+ //
461
+ // Design:
462
+ // • The user's vite.config.ts already includes vue() from @vitejs/plugin-vue.
463
+ // That plugin handles .vue transforms in both dev mode and the server build.
464
+ // • vonoVitePlugin() only handles build orchestration:
465
+ // - `vite build` → server SSR bundle (dist/server/server.js)
466
+ // - `closeBundle` → client SPA bundle (dist/assets/… + .vite/manifest.json)
467
+ //
468
+ // This keeps the plugin simple and avoids fragile hook-proxying.
469
+
470
+ const NODE_BUILTINS =
471
+ /^node:|^(assert|buffer|child_process|cluster|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|worker_threads|zlib)$/
472
+
473
+ export interface VonoPluginOptions {
474
+ /** Server entry file. @default 'server.ts' */
475
+ serverEntry?: string
476
+ /** Client entry file. @default 'client.ts' */
477
+ clientEntry?: string
478
+ /** Server bundle output dir. @default 'dist/server' */
479
+ serverOutDir?: string
480
+ /** Client assets output dir. @default 'dist/assets' */
481
+ clientOutDir?: string
482
+ /** Extra packages external to the server bundle. */
483
+ serverExternal?: string[]
484
+ /** Options forwarded to @vitejs/plugin-vue in the client build. */
485
+ vueOptions?: Record<string, unknown>
486
+ }
487
+
488
+ export function vonoVitePlugin(opts: VonoPluginOptions = {}): Plugin {
489
+ const {
490
+ serverEntry = 'server.ts',
491
+ clientEntry = 'client.ts',
492
+ serverOutDir = 'dist/server',
493
+ clientOutDir = 'dist/assets',
494
+ serverExternal = [],
495
+ vueOptions = {},
496
+ } = opts
497
+
498
+ return {
499
+ name: 'vono:build',
500
+ apply: 'build',
501
+ enforce: 'pre',
502
+
503
+ // Server (SSR) bundle configuration.
504
+ //
505
+ // target: 'node18' is essential — it tells esbuild to emit ES2022+ syntax
506
+ // which includes top-level await. Without it, esbuild defaults to a
507
+ // browser-compatible target ("chrome87", "es2020", …) that does NOT support
508
+ // top-level await, causing the build to fail with:
509
+ // "Top-level await is not available in the configured target environment"
510
+ config(): Omit<UserConfig, 'plugins'> {
511
+ return {
512
+ build: {
513
+ ssr: serverEntry,
514
+ outDir: serverOutDir,
515
+ // ↓ CRITICAL — enables top-level await in the server bundle
516
+ target: 'node18',
517
+ rollupOptions: {
518
+ input: serverEntry,
519
+ output: { format: 'es', entryFileNames: 'server.js' },
520
+ external: (id: string) =>
521
+ NODE_BUILTINS.test(id)
522
+ || id === 'vue' || id.startsWith('vue/')
523
+ || id === 'vue-router'
524
+ || id === '@vue/server-renderer'
525
+ || id === '@vitejs/plugin-vue'
526
+ || id === '@hono/node-server'
527
+ || id === '@hono/node-server/serve-static'
528
+ || serverExternal.includes(id),
529
+ },
530
+ },
531
+ }
532
+ },
533
+
534
+ // After the server bundle is written, trigger the client SPA build
535
+ async closeBundle() {
536
+ console.log('\n⚡ Vono: building client bundle…\n')
537
+
538
+ let vuePlugin: Plugin | Plugin[]
539
+ try {
540
+ const mod = await import('@vitejs/plugin-vue' as string)
541
+ const factory = (mod.default ?? mod) as (opts?: Record<string, unknown>) => Plugin | Plugin[]
542
+ vuePlugin = factory(vueOptions)
543
+ } catch {
544
+ throw new Error(
545
+ '[vono] @vitejs/plugin-vue is required for the client build.\n' +
546
+ ' Install: npm i -D @vitejs/plugin-vue',
547
+ )
548
+ }
549
+
550
+ const plugins = (
551
+ Array.isArray(vuePlugin) ? vuePlugin : [vuePlugin]
552
+ ) as NonNullable<InlineConfig['plugins']>
553
+
554
+ await build({
555
+ configFile: false as const,
556
+ plugins,
557
+ build: {
558
+ outDir: clientOutDir,
559
+ // Vite 5+ writes manifest to <outDir>/.vite/manifest.json
560
+ manifest: true,
561
+ rollupOptions: {
562
+ input: clientEntry,
563
+ output: {
564
+ format: 'es',
565
+ entryFileNames: '[name]-[hash].js',
566
+ chunkFileNames: '[name]-[hash].js',
567
+ assetFileNames: '[name]-[hash][extname]',
568
+ },
569
+ },
570
+ },
571
+ })
572
+
573
+ console.log('✅ Vono: both bundles ready\n')
574
+ },
575
+ }
576
+ }
577
+
578
+ // ── Re-exports ────────────────────────────────────────────────────────────────
579
+
580
+ export {
581
+ definePage, defineGroup, defineLayout, defineApiRoute, isAsyncLoader,
582
+ resolveRoutes, compilePath, matchPath, toVueRouterPath,
583
+ SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY, DATA_KEY,
584
+ } from './core'
585
+
586
+ export type {
587
+ AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef, Route,
588
+ SEOMeta, HonoMiddleware, LoaderCtx, ResolvedRoute, CompiledPath,
589
+ ClientMiddleware, AsyncLoader, InferPageData,
590
+ } from './core'