@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/README.md +185 -878
- package/client.ts +218 -236
- package/core.ts +74 -175
- package/dist/client.d.ts +71 -49
- package/dist/client.js +176 -169
- package/dist/core.d.ts +57 -40
- package/dist/core.js +50 -28
- package/dist/server.d.ts +69 -66
- package/dist/server.js +178 -192
- package/dist/types.d.ts +99 -0
- package/package.json +21 -18
- package/server.ts +262 -337
- package/types.ts +125 -0
package/dist/server.js
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
// server.ts
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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(
|
|
14
|
-
return { __type: "layout",
|
|
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, {
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
113
|
-
parts.push(`<script type="application/ld+json">${JSON.stringify(
|
|
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
|
|
129
|
+
var _assetsCache = null;
|
|
123
130
|
async function resolveAssets(cfg, defaultEntry) {
|
|
124
|
-
if (
|
|
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
|
|
134
|
-
const entry = manifest[
|
|
140
|
+
const key = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
|
|
141
|
+
const entry = manifest[key];
|
|
135
142
|
if (entry) {
|
|
136
|
-
|
|
143
|
+
_assetsCache = {
|
|
137
144
|
scripts: [`/assets/${entry.file}`],
|
|
138
145
|
styles: (entry.css ?? []).map((f) => `/assets/${f}`)
|
|
139
146
|
};
|
|
140
|
-
return
|
|
147
|
+
return _assetsCache;
|
|
141
148
|
}
|
|
142
149
|
} catch {
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
|
-
|
|
152
|
+
_assetsCache = {
|
|
146
153
|
scripts: cfg.scripts ?? ["/assets/client.js"],
|
|
147
154
|
styles: cfg.styles ?? []
|
|
148
155
|
};
|
|
149
|
-
return
|
|
156
|
+
return _assetsCache;
|
|
150
157
|
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
const styleLinks =
|
|
154
|
-
const scriptTags =
|
|
155
|
-
|
|
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 ${
|
|
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(
|
|
162
|
-
|
|
163
|
-
generateHydrationScript(),
|
|
168
|
+
`<title>${esc(title)}</title>`,
|
|
169
|
+
metaHtml,
|
|
164
170
|
styleLinks,
|
|
165
171
|
"</head>",
|
|
166
172
|
"<body>",
|
|
167
|
-
|
|
173
|
+
'<div id="fnetro-app">'
|
|
174
|
+
].filter(Boolean).join("\n");
|
|
175
|
+
const tail = [
|
|
176
|
+
"</div>",
|
|
168
177
|
"<script>",
|
|
169
|
-
`window.${STATE_KEY}=${
|
|
170
|
-
`window.${PARAMS_KEY}=${
|
|
171
|
-
`window.${SEO_KEY}=${
|
|
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
|
-
].
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
238
|
-
|
|
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
|
|
251
|
-
const handlers = [...route.middleware];
|
|
272
|
+
let earlyResponse;
|
|
252
273
|
let idx = 0;
|
|
253
274
|
const runNext = async () => {
|
|
254
|
-
const mw =
|
|
275
|
+
const mw = route.middleware[idx++];
|
|
255
276
|
if (!mw) return;
|
|
256
277
|
const res = await mw(c, runNext);
|
|
257
|
-
if (res instanceof Response && !
|
|
278
|
+
if (res instanceof Response && !earlyResponse) earlyResponse = res;
|
|
258
279
|
};
|
|
259
280
|
await runNext();
|
|
260
|
-
if (
|
|
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
|
|
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,
|
|
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
|
|
275
|
-
|
|
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}]
|
|
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
|
-
|
|
367
|
+
vueOptions = {}
|
|
346
368
|
} = opts;
|
|
347
|
-
|
|
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
|
-
//
|
|
398
|
-
config(
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
|
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
|
};
|
package/dist/types.d.ts
ADDED
|
@@ -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 };
|