@netrojs/fnetro 0.2.20 → 0.3.0

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/dist/server.js CHANGED
@@ -1,21 +1,61 @@
1
1
  // server.ts
2
2
  import { Hono } from "hono";
3
- import { createComponent } from "solid-js";
4
- import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
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-fnetro-spa";
9
+ var STATE_KEY = "__FNETRO_STATE__";
10
+ var PARAMS_KEY = "__FNETRO_PARAMS__";
11
+ var SEO_KEY = "__FNETRO_SEO__";
12
+ var DATA_KEY = /* @__PURE__ */ Symbol.for("fnetro:data");
5
13
 
6
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
+ }
7
24
  function definePage(def) {
8
25
  return { __type: "page", ...def };
9
26
  }
10
27
  function defineGroup(def) {
11
28
  return { __type: "group", ...def };
12
29
  }
13
- function defineLayout(Component) {
14
- return { __type: "layout", Component };
30
+ function defineLayout(component) {
31
+ return { __type: "layout", component };
15
32
  }
16
33
  function defineApiRoute(path, register) {
17
34
  return { __type: "api", path, register };
18
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(fnetroPath) {
57
+ return fnetroPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1(.*)*").replace(/\[([^\]]+)\]/g, ":$1");
58
+ }
19
59
  function resolveRoutes(routes, options = {}) {
20
60
  const pages = [];
21
61
  const apis = [];
@@ -26,7 +66,11 @@ function resolveRoutes(routes, options = {}) {
26
66
  const prefix = (options.prefix ?? "") + route.prefix;
27
67
  const mw = [...options.middleware ?? [], ...route.middleware ?? []];
28
68
  const layout = route.layout !== void 0 ? route.layout : options.layout;
29
- const sub = resolveRoutes(route.routes, { prefix, middleware: mw, layout });
69
+ const sub = resolveRoutes(route.routes, {
70
+ prefix,
71
+ middleware: mw,
72
+ ...layout !== void 0 && { layout }
73
+ });
30
74
  pages.push(...sub.pages);
31
75
  apis.push(...sub.apis);
32
76
  } else {
@@ -40,32 +84,9 @@ function resolveRoutes(routes, options = {}) {
40
84
  }
41
85
  return { pages, apis };
42
86
  }
43
- function compilePath(path) {
44
- const keys = [];
45
- const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
46
- keys.push(k);
47
- return "(.*)";
48
- }).replace(/\[([^\]]+)\]/g, (_, k) => {
49
- keys.push(k);
50
- return "([^/]+)";
51
- }).replace(/\*/g, "(.*)");
52
- return { re: new RegExp(`^${src}$`), keys };
53
- }
54
- function matchPath(compiled, pathname) {
55
- const m = pathname.match(compiled.re);
56
- if (!m) return null;
57
- const params = {};
58
- compiled.keys.forEach((k, i) => {
59
- params[k] = decodeURIComponent(m[i + 1] ?? "");
60
- });
61
- return params;
62
- }
63
- var SPA_HEADER = "x-fnetro-spa";
64
- var STATE_KEY = "__FNETRO_STATE__";
65
- var PARAMS_KEY = "__FNETRO_PARAMS__";
66
- var SEO_KEY = "__FNETRO_SEO__";
67
87
 
68
88
  // server.ts
89
+ import { build } from "vite";
69
90
  function esc(s) {
70
91
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
71
92
  }
@@ -84,33 +105,19 @@ function buildHeadMeta(seo, extraHead = "") {
84
105
  if (seo.ogDescription) parts.push(p("og:description", seo.ogDescription));
85
106
  if (seo.ogImage) parts.push(p("og:image", seo.ogImage));
86
107
  if (seo.ogImageAlt) parts.push(p("og:image:alt", seo.ogImageAlt));
87
- if (seo.ogImageWidth) parts.push(p("og:image:width", seo.ogImageWidth));
88
- if (seo.ogImageHeight) parts.push(p("og:image:height", seo.ogImageHeight));
89
108
  if (seo.ogUrl) parts.push(p("og:url", seo.ogUrl));
90
109
  if (seo.ogType) parts.push(p("og:type", seo.ogType));
91
110
  if (seo.ogSiteName) parts.push(p("og:site_name", seo.ogSiteName));
92
- if (seo.ogLocale) parts.push(p("og:locale", seo.ogLocale));
93
111
  if (seo.twitterCard) parts.push(m("twitter:card", seo.twitterCard));
94
112
  if (seo.twitterSite) parts.push(m("twitter:site", seo.twitterSite));
95
- if (seo.twitterCreator) parts.push(m("twitter:creator", seo.twitterCreator));
96
113
  if (seo.twitterTitle) parts.push(m("twitter:title", seo.twitterTitle));
97
114
  if (seo.twitterDescription) parts.push(m("twitter:description", seo.twitterDescription));
98
115
  if (seo.twitterImage) parts.push(m("twitter:image", seo.twitterImage));
99
- if (seo.twitterImageAlt) parts.push(m("twitter:image:alt", seo.twitterImageAlt));
100
- for (const tag of seo.extra ?? []) {
101
- const attrs = [
102
- tag.name ? `name="${esc(tag.name)}"` : "",
103
- tag.property ? `property="${esc(tag.property)}"` : "",
104
- tag.httpEquiv ? `http-equiv="${esc(tag.httpEquiv)}"` : "",
105
- `content="${esc(tag.content)}"`
106
- ].filter(Boolean).join(" ");
107
- parts.push(`<meta ${attrs}>`);
108
- }
109
116
  const ld = seo.jsonLd;
110
117
  if (ld) {
111
118
  const schemas = Array.isArray(ld) ? ld : [ld];
112
- for (const schema of schemas) {
113
- parts.push(`<script type="application/ld+json">${JSON.stringify(schema)}</script>`);
119
+ for (const s of schemas) {
120
+ parts.push(`<script type="application/ld+json">${JSON.stringify(s)}</script>`);
114
121
  }
115
122
  }
116
123
  if (extraHead) parts.push(extraHead);
@@ -119,98 +126,118 @@ function buildHeadMeta(seo, extraHead = "") {
119
126
  function mergeSEO(base, override) {
120
127
  return { ...base ?? {}, ...override ?? {} };
121
128
  }
122
- var _assets = null;
129
+ var _assetsCache = null;
123
130
  async function resolveAssets(cfg, defaultEntry) {
124
- if (_assets) return _assets;
131
+ if (_assetsCache) return _assetsCache;
125
132
  if (cfg.manifestDir) {
126
133
  try {
127
134
  const [{ readFileSync }, { join }] = await Promise.all([
128
135
  import("fs"),
129
136
  import("path")
130
137
  ]);
131
- const raw = readFileSync(join(cfg.manifestDir, "manifest.json"), "utf-8");
138
+ const raw = readFileSync(join(cfg.manifestDir, ".vite", "manifest.json"), "utf-8");
132
139
  const manifest = JSON.parse(raw);
133
- const entryKey = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
134
- const entry = manifest[entryKey];
140
+ const key = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
141
+ const entry = manifest[key];
135
142
  if (entry) {
136
- _assets = {
143
+ _assetsCache = {
137
144
  scripts: [`/assets/${entry.file}`],
138
145
  styles: (entry.css ?? []).map((f) => `/assets/${f}`)
139
146
  };
140
- return _assets;
147
+ return _assetsCache;
141
148
  }
142
149
  } catch {
143
150
  }
144
151
  }
145
- _assets = {
152
+ _assetsCache = {
146
153
  scripts: cfg.scripts ?? ["/assets/client.js"],
147
154
  styles: cfg.styles ?? []
148
155
  };
149
- return _assets;
156
+ return _assetsCache;
150
157
  }
151
- function buildShell(o) {
152
- const htmlAttrStr = Object.entries(o.htmlAttrs ?? { lang: "en" }).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
153
- const styleLinks = o.styles.map((href) => `<link rel="stylesheet" href="${esc(href)}">`).join("\n");
154
- const scriptTags = o.scripts.map((src) => `<script type="module" src="${esc(src)}"></script>`).join("\n");
155
- return [
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 = [
156
163
  "<!DOCTYPE html>",
157
- `<html ${htmlAttrStr}>`,
164
+ `<html ${attrs}>`,
158
165
  "<head>",
159
166
  '<meta charset="UTF-8">',
160
167
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
161
- `<title>${esc(o.title)}</title>`,
162
- o.metaHtml,
163
- generateHydrationScript(),
168
+ `<title>${esc(title)}</title>`,
169
+ metaHtml,
164
170
  styleLinks,
165
171
  "</head>",
166
172
  "<body>",
167
- `<div id="fnetro-app">${o.bodyHtml}</div>`,
173
+ '<div id="fnetro-app">'
174
+ ].filter(Boolean).join("\n");
175
+ const tail = [
176
+ "</div>",
168
177
  "<script>",
169
- `window.${STATE_KEY}=${o.stateJson};`,
170
- `window.${PARAMS_KEY}=${o.paramsJson};`,
171
- `window.${SEO_KEY}=${o.seoJson};`,
178
+ `window.${STATE_KEY}=${stateJson};`,
179
+ `window.${PARAMS_KEY}=${paramsJson};`,
180
+ `window.${SEO_KEY}=${seoJson};`,
172
181
  "</script>",
173
182
  scriptTags,
174
183
  "</body>",
175
184
  "</html>"
176
- ].filter(Boolean).join("\n");
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;
177
194
  }
178
195
  async function renderPage(route, data, url, params, appLayout) {
179
196
  const layout = route.layout !== void 0 ? route.layout : appLayout;
180
- return renderToStringAsync(() => {
181
- const pageEl = createComponent(route.page.Page, { ...data, url, params });
182
- if (!layout) return pageEl;
183
- return createComponent(layout.Component, {
184
- url,
185
- params,
186
- get children() {
187
- return pageEl;
188
- }
189
- });
197
+ const PageComp = await resolveComponent(route.page.component);
198
+ const routeComp = layout ? defineComponent({
199
+ name: "FNetroRoute",
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 router = createRouter({
207
+ history: createMemoryHistory(),
208
+ routes: [{ path: toVueRouterPath(route.fullPath), component: routeComp }]
190
209
  });
210
+ app.use(router);
211
+ await router.push(url);
212
+ await router.isReady();
213
+ return renderToWebStream(app);
191
214
  }
192
- async function renderFullPage(route, data, url, params, config, assets) {
193
- const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
194
- const seo = mergeSEO(config.seo, pageSEO);
195
- const title = seo.title ?? "FNetro";
196
- const bodyHtml = await renderPage(route, data, url, params, config.layout);
197
- return buildShell({
198
- title,
199
- metaHtml: buildHeadMeta(seo, config.head),
200
- bodyHtml,
201
- stateJson: JSON.stringify({ [url]: data }),
202
- paramsJson: JSON.stringify(params),
203
- seoJson: JSON.stringify(seo),
204
- scripts: assets.scripts,
205
- styles: assets.styles,
206
- htmlAttrs: config.htmlAttrs
207
- });
215
+ function buildResponseStream(headHtml, bodyStream, tailHtml) {
216
+ const enc = new TextEncoder();
217
+ const { readable, writable } = new TransformStream();
218
+ (async () => {
219
+ const writer = writable.getWriter();
220
+ try {
221
+ await writer.write(enc.encode(headHtml));
222
+ const reader = bodyStream.getReader();
223
+ while (true) {
224
+ const { done, value } = await reader.read();
225
+ if (done) break;
226
+ await writer.write(value);
227
+ }
228
+ await writer.write(enc.encode(tailHtml));
229
+ await writer.close();
230
+ } catch (err) {
231
+ await writer.abort(err);
232
+ }
233
+ })();
234
+ return readable;
208
235
  }
209
236
  function createFNetro(config) {
210
237
  const app = new Hono();
211
238
  for (const mw of config.middleware ?? []) app.use("*", mw);
212
239
  const { pages, apis } = resolveRoutes(config.routes, {
213
- layout: config.layout,
240
+ ...config.layout !== void 0 && { layout: config.layout },
214
241
  middleware: []
215
242
  });
216
243
  const compiled = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
@@ -234,45 +261,57 @@ function createFNetro(config) {
234
261
  }
235
262
  if (!matched) {
236
263
  if (config.notFound) {
237
- const html2 = await renderToStringAsync(
238
- () => createComponent(config.notFound, {})
239
- );
240
- return c.html(
241
- `<!DOCTYPE html><html lang="en"><body>${html2}</body></html>`,
242
- 404
243
- );
264
+ const html = await renderToString(createSSRApp(config.notFound));
265
+ return c.html(`<!DOCTYPE html><html lang="en"><body>${html}</body></html>`, 404);
244
266
  }
245
267
  return c.text("Not Found", 404);
246
268
  }
247
269
  const { route, params } = matched;
248
270
  const origParam = c.req.param.bind(c.req);
249
271
  c.req["param"] = (key) => key != null ? params[key] ?? origParam(key) : { ...origParam(), ...params };
250
- let early;
251
- const handlers = [...route.middleware];
272
+ let earlyResponse;
252
273
  let idx = 0;
253
274
  const runNext = async () => {
254
- const mw = handlers[idx++];
275
+ const mw = route.middleware[idx++];
255
276
  if (!mw) return;
256
277
  const res = await mw(c, runNext);
257
- if (res instanceof Response && !early) early = res;
278
+ if (res instanceof Response && !earlyResponse) earlyResponse = res;
258
279
  };
259
280
  await runNext();
260
- if (early) return early;
281
+ if (earlyResponse) return earlyResponse;
261
282
  const rawData = route.page.loader ? await route.page.loader(c) : {};
262
283
  const data = rawData ?? {};
263
284
  if (isSPA) {
264
- const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
285
+ const pageSEO2 = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
265
286
  return c.json({
266
287
  state: data,
267
288
  params,
268
289
  url: pathname,
269
- seo: mergeSEO(config.seo, pageSEO)
290
+ seo: mergeSEO(config.seo, pageSEO2)
270
291
  });
271
292
  }
272
293
  const clientEntry = config.assets?.manifestEntry ?? "client.ts";
273
294
  const assets = isDev ? { scripts: [`/${clientEntry}`], styles: [] } : await resolveAssets(config.assets ?? {}, clientEntry);
274
- const html = await renderFullPage(route, data, pathname, params, config, assets);
275
- return c.html(html);
295
+ const pageSEO = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
296
+ const seo = mergeSEO(config.seo, pageSEO);
297
+ const title = seo.title ?? "FNetro";
298
+ const { head, tail } = buildShellParts(
299
+ title,
300
+ buildHeadMeta(seo, config.head),
301
+ JSON.stringify({ [pathname]: data }),
302
+ JSON.stringify(params),
303
+ JSON.stringify(seo),
304
+ assets.scripts,
305
+ assets.styles,
306
+ config.htmlAttrs
307
+ );
308
+ const bodyStream = await renderPage(route, data, pathname, params, config.layout);
309
+ const stream = buildResponseStream(head, bodyStream, tail);
310
+ return c.body(stream, 200, {
311
+ "Content-Type": "text/html; charset=UTF-8",
312
+ "Transfer-Encoding": "chunked",
313
+ "X-Content-Type-Options": "nosniff"
314
+ });
276
315
  });
277
316
  return { app, handler: app.fetch.bind(app) };
278
317
  }
@@ -289,7 +328,7 @@ async function serve(opts) {
289
328
  const staticDir = opts.staticDir ?? "./dist";
290
329
  const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
291
330
  const logReady = () => console.log(`
292
- \u{1F525} FNetro [${runtime}] ready \u2192 http://${displayHost}:${port}
331
+ \u{1F525} FNetro [${runtime}] \u2192 http://${displayHost}:${port}
293
332
  `);
294
333
  switch (runtime) {
295
334
  case "node": {
@@ -303,38 +342,21 @@ async function serve(opts) {
303
342
  logReady();
304
343
  break;
305
344
  }
306
- case "bun": {
345
+ case "bun":
307
346
  ;
308
347
  globalThis["Bun"].serve({ fetch: opts.app.handler, port, hostname });
309
348
  logReady();
310
349
  break;
311
- }
312
- case "deno": {
350
+ case "deno":
313
351
  ;
314
352
  globalThis["Deno"].serve({ port, hostname }, opts.app.handler);
315
353
  logReady();
316
354
  break;
317
- }
318
355
  default:
319
- console.warn(
320
- "[fnetro] serve() is a no-op on edge runtimes \u2014 export `fnetro.handler` instead."
321
- );
356
+ console.warn("[fnetro] serve() is a no-op on edge \u2014 export fnetro.handler instead.");
322
357
  }
323
358
  }
324
359
  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)$/;
325
- async function loadSolid() {
326
- try {
327
- const mod = await import("vite-plugin-solid");
328
- return mod.default ?? mod;
329
- } catch {
330
- throw new Error(
331
- "[fnetro] vite-plugin-solid is required.\n Install it: npm i -D vite-plugin-solid"
332
- );
333
- }
334
- }
335
- function toPlugins(v) {
336
- return Array.isArray(v) ? v : [v];
337
- }
338
360
  function fnetroVitePlugin(opts = {}) {
339
361
  const {
340
362
  serverEntry = "server.ts",
@@ -342,84 +364,46 @@ function fnetroVitePlugin(opts = {}) {
342
364
  serverOutDir = "dist/server",
343
365
  clientOutDir = "dist/assets",
344
366
  serverExternal = [],
345
- solidOptions = {}
367
+ vueOptions = {}
346
368
  } = opts;
347
- let _solid = null;
348
- let _solidPlugins = [];
349
- const jsxPlugin = {
350
- name: "fnetro:jsx",
351
- enforce: "pre",
352
- // Sync config hook — must return Omit<UserConfig, 'plugins'> | null
353
- config(_cfg, _env) {
354
- return {
355
- esbuild: {
356
- jsx: "automatic",
357
- jsxImportSource: "solid-js"
358
- }
359
- };
360
- },
361
- async buildStart() {
362
- if (!_solid) {
363
- _solid = await loadSolid();
364
- _solidPlugins = toPlugins(_solid({ ssr: true, ...solidOptions }));
365
- }
366
- }
367
- };
368
- const solidProxy = {
369
- name: "fnetro:solid-proxy",
370
- enforce: "pre",
371
- async transform(code, id, options) {
372
- if (!_solidPlugins[0]?.transform) return null;
373
- const hook = _solidPlugins[0].transform;
374
- const fn = typeof hook === "function" ? hook : hook.handler;
375
- if (!fn) return null;
376
- return fn.call(this, code, id, options);
377
- },
378
- async resolveId(id) {
379
- if (!_solidPlugins[0]?.resolveId) return null;
380
- const hook = _solidPlugins[0].resolveId;
381
- const fn = typeof hook === "function" ? hook : hook.handler;
382
- if (!fn) return null;
383
- return fn.call(this, id, void 0, {});
384
- },
385
- async load(id) {
386
- if (!_solidPlugins[0]?.load) return null;
387
- const hook = _solidPlugins[0].load;
388
- const fn = typeof hook === "function" ? hook : hook.handler;
389
- if (!fn) return null;
390
- return fn.call(this, id, {});
391
- }
392
- };
393
- const buildPlugin = {
369
+ return {
394
370
  name: "fnetro:build",
395
371
  apply: "build",
396
372
  enforce: "pre",
397
- // Sync config hook — Omit<UserConfig, 'plugins'> satisfies the ObjectHook constraint
398
- config(_cfg, _env) {
373
+ // Server (SSR) bundle configuration
374
+ config() {
399
375
  return {
400
376
  build: {
401
377
  ssr: serverEntry,
402
378
  outDir: serverOutDir,
403
379
  rollupOptions: {
404
380
  input: serverEntry,
405
- output: {
406
- format: "es",
407
- entryFileNames: "server.js"
408
- },
409
- external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || serverExternal.includes(id)
381
+ output: { format: "es", entryFileNames: "server.js" },
382
+ 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)
410
383
  }
411
384
  }
412
385
  };
413
386
  },
387
+ // After the server bundle is written, trigger the client SPA build
414
388
  async closeBundle() {
415
389
  console.log("\n\u26A1 FNetro: building client bundle\u2026\n");
416
- const solid = _solid ?? await loadSolid();
417
- const { build } = await import("vite");
390
+ let vuePlugin;
391
+ try {
392
+ const mod = await import("@vitejs/plugin-vue");
393
+ const factory = mod.default ?? mod;
394
+ vuePlugin = factory(vueOptions);
395
+ } catch {
396
+ throw new Error(
397
+ "[fnetro] @vitejs/plugin-vue is required for the client build.\n Install: npm i -D @vitejs/plugin-vue"
398
+ );
399
+ }
400
+ const plugins = Array.isArray(vuePlugin) ? vuePlugin : [vuePlugin];
418
401
  await build({
419
402
  configFile: false,
420
- plugins: toPlugins(solid({ ...solidOptions })),
403
+ plugins,
421
404
  build: {
422
405
  outDir: clientOutDir,
406
+ // Vite 5+ writes manifest to <outDir>/.vite/manifest.json
423
407
  manifest: true,
424
408
  rollupOptions: {
425
409
  input: clientEntry,
@@ -435,9 +419,9 @@ function fnetroVitePlugin(opts = {}) {
435
419
  console.log("\u2705 FNetro: both bundles ready\n");
436
420
  }
437
421
  };
438
- return [jsxPlugin, solidProxy, buildPlugin];
439
422
  }
440
423
  export {
424
+ DATA_KEY,
441
425
  PARAMS_KEY,
442
426
  SEO_KEY,
443
427
  SPA_HEADER,
@@ -450,7 +434,9 @@ export {
450
434
  definePage,
451
435
  detectRuntime,
452
436
  fnetroVitePlugin,
437
+ isAsyncLoader,
453
438
  matchPath,
454
439
  resolveRoutes,
455
- serve
440
+ serve,
441
+ toVueRouterPath
456
442
  };
@@ -0,0 +1,99 @@
1
+ import { Component } from 'vue';
2
+ import { Hono, MiddlewareHandler, Context } from 'hono';
3
+
4
+ type HonoMiddleware = MiddlewareHandler;
5
+ type LoaderCtx = Context;
6
+ interface SEOMeta {
7
+ title?: string;
8
+ description?: string;
9
+ keywords?: string;
10
+ author?: string;
11
+ robots?: string;
12
+ canonical?: string;
13
+ themeColor?: string;
14
+ ogTitle?: string;
15
+ ogDescription?: string;
16
+ ogImage?: string;
17
+ ogImageAlt?: string;
18
+ ogUrl?: string;
19
+ ogType?: string;
20
+ ogSiteName?: string;
21
+ twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
22
+ twitterSite?: string;
23
+ twitterTitle?: string;
24
+ twitterDescription?: string;
25
+ twitterImage?: string;
26
+ /** Structured data injected as <script type="application/ld+json">. */
27
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
28
+ }
29
+ type AsyncLoader = () => Promise<{
30
+ default: Component;
31
+ } | Component>;
32
+ interface PageDef<TData extends object = Record<string, never>> {
33
+ readonly __type: 'page';
34
+ path: string;
35
+ middleware?: HonoMiddleware[];
36
+ loader?: (c: LoaderCtx) => TData | Promise<TData>;
37
+ seo?: SEOMeta | ((data: TData, params: Record<string, string>) => SEOMeta);
38
+ /** Override or disable the app-level layout for this route. */
39
+ layout?: LayoutDef | false;
40
+ /**
41
+ * The Vue component to render for this route.
42
+ * Use () => import('./Page.vue') for automatic code splitting.
43
+ */
44
+ component: Component | AsyncLoader;
45
+ }
46
+ interface GroupDef {
47
+ readonly __type: 'group';
48
+ prefix: string;
49
+ layout?: LayoutDef | false;
50
+ middleware?: HonoMiddleware[];
51
+ routes: Route[];
52
+ }
53
+ interface LayoutDef {
54
+ readonly __type: 'layout';
55
+ /** Vue layout component — must contain <slot /> for page content. */
56
+ component: Component;
57
+ }
58
+ interface ApiRouteDef {
59
+ readonly __type: 'api';
60
+ path: string;
61
+ register: (app: Hono, globalMiddleware: HonoMiddleware[]) => void;
62
+ }
63
+ type Route = PageDef<any> | GroupDef | ApiRouteDef;
64
+ interface AppConfig {
65
+ layout?: LayoutDef;
66
+ seo?: SEOMeta;
67
+ middleware?: HonoMiddleware[];
68
+ routes: Route[];
69
+ notFound?: Component;
70
+ htmlAttrs?: Record<string, string>;
71
+ /** Extra HTML injected into <head> (e.g. font preloads). */
72
+ head?: string;
73
+ }
74
+ interface ResolvedRoute {
75
+ fullPath: string;
76
+ page: PageDef<any>;
77
+ layout: LayoutDef | false | undefined;
78
+ middleware: HonoMiddleware[];
79
+ }
80
+ interface CompiledPath {
81
+ re: RegExp;
82
+ keys: string[];
83
+ }
84
+ type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
85
+ /** Custom request header that identifies an SPA navigation (JSON payload). */
86
+ declare const SPA_HEADER = "x-fnetro-spa";
87
+ /** window key for SSR-injected per-page loader data. */
88
+ declare const STATE_KEY = "__FNETRO_STATE__";
89
+ /** window key for SSR-injected URL params. */
90
+ declare const PARAMS_KEY = "__FNETRO_PARAMS__";
91
+ /** window key for SSR-injected SEO meta. */
92
+ declare const SEO_KEY = "__FNETRO_SEO__";
93
+ /**
94
+ * Vue provide/inject key for the reactive page-data object.
95
+ * Symbol.for() ensures the same reference across module instances (SSR safe).
96
+ */
97
+ declare const DATA_KEY: unique symbol;
98
+
99
+ export { type ApiRouteDef, type AppConfig, type AsyncLoader, type ClientMiddleware, type CompiledPath, DATA_KEY, type GroupDef, type HonoMiddleware, type LayoutDef, type LoaderCtx, PARAMS_KEY, type PageDef, type ResolvedRoute, type Route, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY };