@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.
@@ -0,0 +1,212 @@
1
+ import { Hono, MiddlewareHandler, Context } from 'hono';
2
+ import { Component } from 'vue';
3
+ import { Plugin } from 'vite';
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
+ interface AssetConfig {
169
+ scripts?: string[];
170
+ styles?: string[];
171
+ /** Directory containing the Vite-built assets and .vite/manifest.json. */
172
+ manifestDir?: string;
173
+ manifestEntry?: string;
174
+ }
175
+ interface VonoOptions extends AppConfig {
176
+ assets?: AssetConfig;
177
+ }
178
+ interface VonoApp {
179
+ /** The Hono instance — attach extra routes, error handlers, middleware. */
180
+ app: Hono;
181
+ /** WinterCG-compatible fetch handler for edge runtimes. */
182
+ handler: typeof Hono.prototype.fetch;
183
+ }
184
+ declare function createVono(config: VonoOptions): VonoApp;
185
+ type Runtime = 'node' | 'bun' | 'deno' | 'edge';
186
+ declare function detectRuntime(): Runtime;
187
+ interface ServeOptions {
188
+ app: VonoApp;
189
+ port?: number;
190
+ hostname?: string;
191
+ runtime?: Runtime;
192
+ /** Root directory that contains the built assets and public files. */
193
+ staticDir?: string;
194
+ }
195
+ declare function serve(opts: ServeOptions): Promise<void>;
196
+ interface VonoPluginOptions {
197
+ /** Server entry file. @default 'server.ts' */
198
+ serverEntry?: string;
199
+ /** Client entry file. @default 'client.ts' */
200
+ clientEntry?: string;
201
+ /** Server bundle output dir. @default 'dist/server' */
202
+ serverOutDir?: string;
203
+ /** Client assets output dir. @default 'dist/assets' */
204
+ clientOutDir?: string;
205
+ /** Extra packages external to the server bundle. */
206
+ serverExternal?: string[];
207
+ /** Options forwarded to @vitejs/plugin-vue in the client build. */
208
+ vueOptions?: Record<string, unknown>;
209
+ }
210
+ declare function vonoVitePlugin(opts?: VonoPluginOptions): Plugin;
211
+
212
+ export { type ApiRouteDef, type AppConfig, type AssetConfig, type AsyncLoader, 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 Runtime, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY, type ServeOptions, type VonoApp, type VonoOptions, type VonoPluginOptions, compilePath, createVono, defineApiRoute, defineGroup, defineLayout, definePage, detectRuntime, isAsyncLoader, matchPath, resolveRoutes, serve, toVueRouterPath, vonoVitePlugin };
package/dist/server.js ADDED
@@ -0,0 +1,451 @@
1
+ // server.ts
2
+ import { Hono } from "hono";
3
+ import { createSSRApp, defineComponent, h } from "vue";
4
+ import { createRouter, createMemoryHistory, RouterView } from "vue-router";
5
+ import { renderToString, renderToWebStream } from "@vue/server-renderer";
6
+
7
+ // types.ts
8
+ var SPA_HEADER = "x-vono-spa";
9
+ var STATE_KEY = "__VONO_STATE__";
10
+ var PARAMS_KEY = "__VONO_PARAMS__";
11
+ var SEO_KEY = "__VONO_SEO__";
12
+ var DATA_KEY = /* @__PURE__ */ Symbol.for("vono:data");
13
+
14
+ // core.ts
15
+ var VUE_BRANDS = ["__name", "__file", "__vccOpts", "setup", "render", "data", "components"];
16
+ function isAsyncLoader(c) {
17
+ if (typeof c !== "function") return false;
18
+ const f = c;
19
+ for (const brand of VUE_BRANDS) {
20
+ if (brand in f) return false;
21
+ }
22
+ return true;
23
+ }
24
+ function definePage(def) {
25
+ return { __type: "page", ...def };
26
+ }
27
+ function defineGroup(def) {
28
+ return { __type: "group", ...def };
29
+ }
30
+ function defineLayout(component) {
31
+ return { __type: "layout", component };
32
+ }
33
+ function defineApiRoute(path, register) {
34
+ return { __type: "api", path, register };
35
+ }
36
+ function compilePath(path) {
37
+ const keys = [];
38
+ const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
39
+ keys.push(k);
40
+ return "(.*)";
41
+ }).replace(/\[([^\]]+)\]/g, (_, k) => {
42
+ keys.push(k);
43
+ return "([^/]+)";
44
+ }).replace(/\*/g, "(.*)");
45
+ return { re: new RegExp(`^${src}$`), keys };
46
+ }
47
+ function matchPath(cp, pathname) {
48
+ const m = pathname.match(cp.re);
49
+ if (!m) return null;
50
+ const params = {};
51
+ cp.keys.forEach((k, i) => {
52
+ params[k] = decodeURIComponent(m[i + 1] ?? "");
53
+ });
54
+ return params;
55
+ }
56
+ function toVueRouterPath(vonoPath) {
57
+ return vonoPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1(.*)*").replace(/\[([^\]]+)\]/g, ":$1");
58
+ }
59
+ function resolveRoutes(routes, options = {}) {
60
+ const pages = [];
61
+ const apis = [];
62
+ for (const route of routes) {
63
+ if (route.__type === "api") {
64
+ apis.push({ ...route, path: (options.prefix ?? "") + route.path });
65
+ } else if (route.__type === "group") {
66
+ const prefix = (options.prefix ?? "") + route.prefix;
67
+ const mw = [...options.middleware ?? [], ...route.middleware ?? []];
68
+ const layout = route.layout !== void 0 ? route.layout : options.layout;
69
+ const sub = resolveRoutes(route.routes, {
70
+ prefix,
71
+ middleware: mw,
72
+ ...layout !== void 0 && { layout }
73
+ });
74
+ pages.push(...sub.pages);
75
+ apis.push(...sub.apis);
76
+ } else {
77
+ pages.push({
78
+ fullPath: (options.prefix ?? "") + route.path,
79
+ page: route,
80
+ layout: route.layout !== void 0 ? route.layout : options.layout,
81
+ middleware: [...options.middleware ?? [], ...route.middleware ?? []]
82
+ });
83
+ }
84
+ }
85
+ return { pages, apis };
86
+ }
87
+
88
+ // server.ts
89
+ import { build } from "vite";
90
+ function esc(s) {
91
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92
+ }
93
+ function buildHeadMeta(seo, extraHead = "") {
94
+ const m = (n, v) => v ? `<meta name="${n}" content="${esc(v)}">` : "";
95
+ const p = (pr, v) => v ? `<meta property="${pr}" content="${esc(v)}">` : "";
96
+ const lk = (rel, href) => `<link rel="${rel}" href="${esc(href)}">`;
97
+ const parts = [];
98
+ if (seo.description) parts.push(m("description", seo.description));
99
+ if (seo.keywords) parts.push(m("keywords", seo.keywords));
100
+ if (seo.author) parts.push(m("author", seo.author));
101
+ if (seo.robots) parts.push(m("robots", seo.robots));
102
+ if (seo.themeColor) parts.push(m("theme-color", seo.themeColor));
103
+ if (seo.canonical) parts.push(lk("canonical", seo.canonical));
104
+ if (seo.ogTitle) parts.push(p("og:title", seo.ogTitle));
105
+ if (seo.ogDescription) parts.push(p("og:description", seo.ogDescription));
106
+ if (seo.ogImage) parts.push(p("og:image", seo.ogImage));
107
+ if (seo.ogImageAlt) parts.push(p("og:image:alt", seo.ogImageAlt));
108
+ if (seo.ogUrl) parts.push(p("og:url", seo.ogUrl));
109
+ if (seo.ogType) parts.push(p("og:type", seo.ogType));
110
+ if (seo.ogSiteName) parts.push(p("og:site_name", seo.ogSiteName));
111
+ if (seo.twitterCard) parts.push(m("twitter:card", seo.twitterCard));
112
+ if (seo.twitterSite) parts.push(m("twitter:site", seo.twitterSite));
113
+ if (seo.twitterTitle) parts.push(m("twitter:title", seo.twitterTitle));
114
+ if (seo.twitterDescription) parts.push(m("twitter:description", seo.twitterDescription));
115
+ if (seo.twitterImage) parts.push(m("twitter:image", seo.twitterImage));
116
+ const ld = seo.jsonLd;
117
+ if (ld) {
118
+ const schemas = Array.isArray(ld) ? ld : [ld];
119
+ for (const s of schemas) {
120
+ parts.push(`<script type="application/ld+json">${JSON.stringify(s)}</script>`);
121
+ }
122
+ }
123
+ if (extraHead) parts.push(extraHead);
124
+ return parts.join("\n");
125
+ }
126
+ function mergeSEO(base, override) {
127
+ return { ...base ?? {}, ...override ?? {} };
128
+ }
129
+ var _assetsCache = null;
130
+ async function resolveAssets(cfg, defaultEntry) {
131
+ if (_assetsCache) return _assetsCache;
132
+ if (cfg.manifestDir) {
133
+ try {
134
+ const [{ readFileSync }, { join }] = await Promise.all([
135
+ import("fs"),
136
+ import("path")
137
+ ]);
138
+ const raw = readFileSync(join(cfg.manifestDir, ".vite", "manifest.json"), "utf-8");
139
+ const manifest = JSON.parse(raw);
140
+ const key = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
141
+ const entry = manifest[key];
142
+ if (entry) {
143
+ _assetsCache = {
144
+ scripts: [`/assets/${entry.file}`],
145
+ styles: (entry.css ?? []).map((f) => `/assets/${f}`)
146
+ };
147
+ return _assetsCache;
148
+ }
149
+ } catch {
150
+ }
151
+ }
152
+ _assetsCache = {
153
+ scripts: cfg.scripts ?? ["/assets/client.js"],
154
+ styles: cfg.styles ?? []
155
+ };
156
+ return _assetsCache;
157
+ }
158
+ function buildShellParts(title, metaHtml, stateJson, paramsJson, seoJson, scripts, styles, htmlAttrs) {
159
+ const attrs = Object.entries(htmlAttrs ?? { lang: "en" }).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
160
+ const styleLinks = styles.map((href) => `<link rel="stylesheet" href="${esc(href)}">`).join("\n");
161
+ const scriptTags = scripts.map((src) => `<script type="module" src="${esc(src)}"></script>`).join("\n");
162
+ const head = [
163
+ "<!DOCTYPE html>",
164
+ `<html ${attrs}>`,
165
+ "<head>",
166
+ '<meta charset="UTF-8">',
167
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
168
+ `<title>${esc(title)}</title>`,
169
+ metaHtml,
170
+ styleLinks,
171
+ "</head>",
172
+ "<body>",
173
+ '<div id="vono-app">'
174
+ ].filter(Boolean).join("\n");
175
+ const tail = [
176
+ "</div>",
177
+ "<script>",
178
+ `window.${STATE_KEY}=${stateJson};`,
179
+ `window.${PARAMS_KEY}=${paramsJson};`,
180
+ `window.${SEO_KEY}=${seoJson};`,
181
+ "</script>",
182
+ scriptTags,
183
+ "</body>",
184
+ "</html>"
185
+ ].join("\n");
186
+ return { head, tail };
187
+ }
188
+ async function resolveComponent(comp) {
189
+ if (isAsyncLoader(comp)) {
190
+ const mod = await comp();
191
+ return mod.default ?? mod;
192
+ }
193
+ return comp;
194
+ }
195
+ async function renderPage(route, data, url, params, appLayout) {
196
+ const layout = route.layout !== void 0 ? route.layout : appLayout;
197
+ const PageComp = await resolveComponent(route.page.component);
198
+ const routeComp = layout ? defineComponent({
199
+ name: "VonoRoute",
200
+ setup: () => () => h(layout.component, null, {
201
+ default: () => h(PageComp)
202
+ })
203
+ }) : PageComp;
204
+ const app = createSSRApp({ render: () => h(RouterView) });
205
+ app.provide(DATA_KEY, data);
206
+ const memHistory = createMemoryHistory();
207
+ memHistory.replace(url);
208
+ const router = createRouter({
209
+ history: memHistory,
210
+ routes: [{ path: toVueRouterPath(route.fullPath), component: routeComp }]
211
+ });
212
+ app.use(router);
213
+ await router.isReady();
214
+ return renderToWebStream(app);
215
+ }
216
+ function buildResponseStream(headHtml, bodyStream, tailHtml) {
217
+ const enc = new TextEncoder();
218
+ const { readable, writable } = new TransformStream();
219
+ (async () => {
220
+ const writer = writable.getWriter();
221
+ try {
222
+ await writer.write(enc.encode(headHtml));
223
+ const reader = bodyStream.getReader();
224
+ while (true) {
225
+ const { done, value } = await reader.read();
226
+ if (done) break;
227
+ await writer.write(value);
228
+ }
229
+ await writer.write(enc.encode(tailHtml));
230
+ await writer.close();
231
+ } catch (err) {
232
+ await writer.abort(err);
233
+ }
234
+ })();
235
+ return readable;
236
+ }
237
+ function createVono(config) {
238
+ const app = new Hono();
239
+ for (const mw of config.middleware ?? []) app.use("*", mw);
240
+ const { pages, apis } = resolveRoutes(config.routes, {
241
+ ...config.layout !== void 0 && { layout: config.layout },
242
+ middleware: []
243
+ });
244
+ const compiled = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
245
+ for (const api of apis) {
246
+ const sub = new Hono();
247
+ api.register(sub, config.middleware ?? []);
248
+ app.route(api.path, sub);
249
+ }
250
+ app.all("*", async (c) => {
251
+ const url = new URL(c.req.url);
252
+ const pathname = url.pathname;
253
+ const isSPA = c.req.header(SPA_HEADER) === "1";
254
+ const isDev = process.env["NODE_ENV"] !== "production";
255
+ let matched = null;
256
+ for (const { route: route2, cp } of compiled) {
257
+ const params2 = matchPath(cp, pathname);
258
+ if (params2 !== null) {
259
+ matched = { route: route2, params: params2 };
260
+ break;
261
+ }
262
+ }
263
+ if (!matched) {
264
+ if (config.notFound) {
265
+ const html = await renderToString(createSSRApp(config.notFound));
266
+ return c.html(`<!DOCTYPE html><html lang="en"><body>${html}</body></html>`, 404);
267
+ }
268
+ return c.text("Not Found", 404);
269
+ }
270
+ const { route, params } = matched;
271
+ const origParam = c.req.param.bind(c.req);
272
+ c.req["param"] = (key) => key != null ? params[key] ?? origParam(key) : { ...origParam(), ...params };
273
+ let earlyResponse;
274
+ let idx = 0;
275
+ const runNext = async () => {
276
+ const mw = route.middleware[idx++];
277
+ if (!mw) return;
278
+ const res = await mw(c, runNext);
279
+ if (res instanceof Response && !earlyResponse) earlyResponse = res;
280
+ };
281
+ await runNext();
282
+ if (earlyResponse) return earlyResponse;
283
+ const rawData = route.page.loader ? await route.page.loader(c) : {};
284
+ const data = rawData ?? {};
285
+ if (isSPA) {
286
+ const pageSEO2 = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
287
+ return c.json({
288
+ state: data,
289
+ params,
290
+ url: pathname,
291
+ seo: mergeSEO(config.seo, pageSEO2)
292
+ });
293
+ }
294
+ const clientEntry = config.assets?.manifestEntry ?? "client.ts";
295
+ const assets = isDev ? { scripts: [`/${clientEntry}`], styles: [] } : await resolveAssets(config.assets ?? {}, clientEntry);
296
+ const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
297
+ const seo = mergeSEO(config.seo, pageSEO);
298
+ const title = seo.title ?? "Vono";
299
+ const { head, tail } = buildShellParts(
300
+ title,
301
+ buildHeadMeta(seo, config.head),
302
+ JSON.stringify({ [pathname]: data }),
303
+ JSON.stringify(params),
304
+ JSON.stringify(seo),
305
+ assets.scripts,
306
+ assets.styles,
307
+ config.htmlAttrs
308
+ );
309
+ const bodyStream = await renderPage(route, data, pathname, params, config.layout);
310
+ const stream = buildResponseStream(head, bodyStream, tail);
311
+ return c.body(stream, 200, {
312
+ "Content-Type": "text/html; charset=UTF-8",
313
+ "Transfer-Encoding": "chunked",
314
+ "X-Content-Type-Options": "nosniff"
315
+ });
316
+ });
317
+ return { app, handler: app.fetch.bind(app) };
318
+ }
319
+ function detectRuntime() {
320
+ if (typeof globalThis["Bun"] !== "undefined") return "bun";
321
+ if (typeof globalThis["Deno"] !== "undefined") return "deno";
322
+ if (typeof process !== "undefined" && process.versions?.node) return "node";
323
+ return "edge";
324
+ }
325
+ async function serve(opts) {
326
+ const runtime = opts.runtime ?? detectRuntime();
327
+ const port = opts.port ?? Number(process?.env?.["PORT"] ?? 3e3);
328
+ const hostname = opts.hostname ?? "0.0.0.0";
329
+ const staticDir = opts.staticDir ?? "./dist";
330
+ const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
331
+ const logReady = () => console.log(`
332
+ \u{1F525} Vono [${runtime}] \u2192 http://${displayHost}:${port}
333
+ `);
334
+ switch (runtime) {
335
+ case "node": {
336
+ const [{ serve: nodeServe }, { serveStatic }] = await Promise.all([
337
+ import("@hono/node-server"),
338
+ import("@hono/node-server/serve-static")
339
+ ]);
340
+ opts.app.app.use("/assets/*", serveStatic({ root: staticDir }));
341
+ opts.app.app.use("/*", serveStatic({ root: "./public" }));
342
+ nodeServe({ fetch: opts.app.handler, port, hostname });
343
+ logReady();
344
+ break;
345
+ }
346
+ case "bun":
347
+ ;
348
+ globalThis["Bun"].serve({ fetch: opts.app.handler, port, hostname });
349
+ logReady();
350
+ break;
351
+ case "deno":
352
+ ;
353
+ globalThis["Deno"].serve({ port, hostname }, opts.app.handler);
354
+ logReady();
355
+ break;
356
+ default:
357
+ console.warn("[vono] serve() is a no-op on edge \u2014 export vono.handler instead.");
358
+ }
359
+ }
360
+ var NODE_BUILTINS = /^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)$/;
361
+ function vonoVitePlugin(opts = {}) {
362
+ const {
363
+ serverEntry = "server.ts",
364
+ clientEntry = "client.ts",
365
+ serverOutDir = "dist/server",
366
+ clientOutDir = "dist/assets",
367
+ serverExternal = [],
368
+ vueOptions = {}
369
+ } = opts;
370
+ return {
371
+ name: "vono:build",
372
+ apply: "build",
373
+ enforce: "pre",
374
+ // Server (SSR) bundle configuration.
375
+ //
376
+ // target: 'node18' is essential — it tells esbuild to emit ES2022+ syntax
377
+ // which includes top-level await. Without it, esbuild defaults to a
378
+ // browser-compatible target ("chrome87", "es2020", …) that does NOT support
379
+ // top-level await, causing the build to fail with:
380
+ // "Top-level await is not available in the configured target environment"
381
+ config() {
382
+ return {
383
+ build: {
384
+ ssr: serverEntry,
385
+ outDir: serverOutDir,
386
+ // ↓ CRITICAL — enables top-level await in the server bundle
387
+ target: "node18",
388
+ rollupOptions: {
389
+ input: serverEntry,
390
+ output: { format: "es", entryFileNames: "server.js" },
391
+ external: (id) => NODE_BUILTINS.test(id) || id === "vue" || id.startsWith("vue/") || id === "vue-router" || id === "@vue/server-renderer" || id === "@vitejs/plugin-vue" || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || serverExternal.includes(id)
392
+ }
393
+ }
394
+ };
395
+ },
396
+ // After the server bundle is written, trigger the client SPA build
397
+ async closeBundle() {
398
+ console.log("\n\u26A1 Vono: building client bundle\u2026\n");
399
+ let vuePlugin;
400
+ try {
401
+ const mod = await import("@vitejs/plugin-vue");
402
+ const factory = mod.default ?? mod;
403
+ vuePlugin = factory(vueOptions);
404
+ } catch {
405
+ throw new Error(
406
+ "[vono] @vitejs/plugin-vue is required for the client build.\n Install: npm i -D @vitejs/plugin-vue"
407
+ );
408
+ }
409
+ const plugins = Array.isArray(vuePlugin) ? vuePlugin : [vuePlugin];
410
+ await build({
411
+ configFile: false,
412
+ plugins,
413
+ build: {
414
+ outDir: clientOutDir,
415
+ // Vite 5+ writes manifest to <outDir>/.vite/manifest.json
416
+ manifest: true,
417
+ rollupOptions: {
418
+ input: clientEntry,
419
+ output: {
420
+ format: "es",
421
+ entryFileNames: "[name]-[hash].js",
422
+ chunkFileNames: "[name]-[hash].js",
423
+ assetFileNames: "[name]-[hash][extname]"
424
+ }
425
+ }
426
+ }
427
+ });
428
+ console.log("\u2705 Vono: both bundles ready\n");
429
+ }
430
+ };
431
+ }
432
+ export {
433
+ DATA_KEY,
434
+ PARAMS_KEY,
435
+ SEO_KEY,
436
+ SPA_HEADER,
437
+ STATE_KEY,
438
+ compilePath,
439
+ createVono,
440
+ defineApiRoute,
441
+ defineGroup,
442
+ defineLayout,
443
+ definePage,
444
+ detectRuntime,
445
+ isAsyncLoader,
446
+ matchPath,
447
+ resolveRoutes,
448
+ serve,
449
+ toVueRouterPath,
450
+ vonoVitePlugin
451
+ };