@netrojs/fnetro 0.2.21 → 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 +213 -242
- package/core.ts +74 -175
- package/dist/client.d.ts +69 -60
- package/dist/client.js +170 -177
- 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 -199
- package/dist/types.d.ts +99 -0
- package/package.json +21 -20
- package/server.ts +263 -350
- package/types.ts +125 -0
package/dist/server.js
CHANGED
|
@@ -1,22 +1,61 @@
|
|
|
1
1
|
// server.ts
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
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");
|
|
6
13
|
|
|
7
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
|
+
}
|
|
8
24
|
function definePage(def) {
|
|
9
25
|
return { __type: "page", ...def };
|
|
10
26
|
}
|
|
11
27
|
function defineGroup(def) {
|
|
12
28
|
return { __type: "group", ...def };
|
|
13
29
|
}
|
|
14
|
-
function defineLayout(
|
|
15
|
-
return { __type: "layout",
|
|
30
|
+
function defineLayout(component) {
|
|
31
|
+
return { __type: "layout", component };
|
|
16
32
|
}
|
|
17
33
|
function defineApiRoute(path, register) {
|
|
18
34
|
return { __type: "api", path, register };
|
|
19
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
|
+
}
|
|
20
59
|
function resolveRoutes(routes, options = {}) {
|
|
21
60
|
const pages = [];
|
|
22
61
|
const apis = [];
|
|
@@ -27,7 +66,11 @@ function resolveRoutes(routes, options = {}) {
|
|
|
27
66
|
const prefix = (options.prefix ?? "") + route.prefix;
|
|
28
67
|
const mw = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
29
68
|
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
30
|
-
const sub = resolveRoutes(route.routes, {
|
|
69
|
+
const sub = resolveRoutes(route.routes, {
|
|
70
|
+
prefix,
|
|
71
|
+
middleware: mw,
|
|
72
|
+
...layout !== void 0 && { layout }
|
|
73
|
+
});
|
|
31
74
|
pages.push(...sub.pages);
|
|
32
75
|
apis.push(...sub.apis);
|
|
33
76
|
} else {
|
|
@@ -41,32 +84,9 @@ function resolveRoutes(routes, options = {}) {
|
|
|
41
84
|
}
|
|
42
85
|
return { pages, apis };
|
|
43
86
|
}
|
|
44
|
-
function compilePath(path) {
|
|
45
|
-
const keys = [];
|
|
46
|
-
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
47
|
-
keys.push(k);
|
|
48
|
-
return "(.*)";
|
|
49
|
-
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
50
|
-
keys.push(k);
|
|
51
|
-
return "([^/]+)";
|
|
52
|
-
}).replace(/\*/g, "(.*)");
|
|
53
|
-
return { re: new RegExp(`^${src}$`), keys };
|
|
54
|
-
}
|
|
55
|
-
function matchPath(compiled, pathname) {
|
|
56
|
-
const m = pathname.match(compiled.re);
|
|
57
|
-
if (!m) return null;
|
|
58
|
-
const params = {};
|
|
59
|
-
compiled.keys.forEach((k, i) => {
|
|
60
|
-
params[k] = decodeURIComponent(m[i + 1] ?? "");
|
|
61
|
-
});
|
|
62
|
-
return params;
|
|
63
|
-
}
|
|
64
|
-
var SPA_HEADER = "x-fnetro-spa";
|
|
65
|
-
var STATE_KEY = "__FNETRO_STATE__";
|
|
66
|
-
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
67
|
-
var SEO_KEY = "__FNETRO_SEO__";
|
|
68
87
|
|
|
69
88
|
// server.ts
|
|
89
|
+
import { build } from "vite";
|
|
70
90
|
function esc(s) {
|
|
71
91
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
72
92
|
}
|
|
@@ -85,33 +105,19 @@ function buildHeadMeta(seo, extraHead = "") {
|
|
|
85
105
|
if (seo.ogDescription) parts.push(p("og:description", seo.ogDescription));
|
|
86
106
|
if (seo.ogImage) parts.push(p("og:image", seo.ogImage));
|
|
87
107
|
if (seo.ogImageAlt) parts.push(p("og:image:alt", seo.ogImageAlt));
|
|
88
|
-
if (seo.ogImageWidth) parts.push(p("og:image:width", seo.ogImageWidth));
|
|
89
|
-
if (seo.ogImageHeight) parts.push(p("og:image:height", seo.ogImageHeight));
|
|
90
108
|
if (seo.ogUrl) parts.push(p("og:url", seo.ogUrl));
|
|
91
109
|
if (seo.ogType) parts.push(p("og:type", seo.ogType));
|
|
92
110
|
if (seo.ogSiteName) parts.push(p("og:site_name", seo.ogSiteName));
|
|
93
|
-
if (seo.ogLocale) parts.push(p("og:locale", seo.ogLocale));
|
|
94
111
|
if (seo.twitterCard) parts.push(m("twitter:card", seo.twitterCard));
|
|
95
112
|
if (seo.twitterSite) parts.push(m("twitter:site", seo.twitterSite));
|
|
96
|
-
if (seo.twitterCreator) parts.push(m("twitter:creator", seo.twitterCreator));
|
|
97
113
|
if (seo.twitterTitle) parts.push(m("twitter:title", seo.twitterTitle));
|
|
98
114
|
if (seo.twitterDescription) parts.push(m("twitter:description", seo.twitterDescription));
|
|
99
115
|
if (seo.twitterImage) parts.push(m("twitter:image", seo.twitterImage));
|
|
100
|
-
if (seo.twitterImageAlt) parts.push(m("twitter:image:alt", seo.twitterImageAlt));
|
|
101
|
-
for (const tag of seo.extra ?? []) {
|
|
102
|
-
const attrs = [
|
|
103
|
-
tag.name ? `name="${esc(tag.name)}"` : "",
|
|
104
|
-
tag.property ? `property="${esc(tag.property)}"` : "",
|
|
105
|
-
tag.httpEquiv ? `http-equiv="${esc(tag.httpEquiv)}"` : "",
|
|
106
|
-
`content="${esc(tag.content)}"`
|
|
107
|
-
].filter(Boolean).join(" ");
|
|
108
|
-
parts.push(`<meta ${attrs}>`);
|
|
109
|
-
}
|
|
110
116
|
const ld = seo.jsonLd;
|
|
111
117
|
if (ld) {
|
|
112
118
|
const schemas = Array.isArray(ld) ? ld : [ld];
|
|
113
|
-
for (const
|
|
114
|
-
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>`);
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
if (extraHead) parts.push(extraHead);
|
|
@@ -120,103 +126,118 @@ function buildHeadMeta(seo, extraHead = "") {
|
|
|
120
126
|
function mergeSEO(base, override) {
|
|
121
127
|
return { ...base ?? {}, ...override ?? {} };
|
|
122
128
|
}
|
|
123
|
-
var
|
|
129
|
+
var _assetsCache = null;
|
|
124
130
|
async function resolveAssets(cfg, defaultEntry) {
|
|
125
|
-
if (
|
|
131
|
+
if (_assetsCache) return _assetsCache;
|
|
126
132
|
if (cfg.manifestDir) {
|
|
127
133
|
try {
|
|
128
134
|
const [{ readFileSync }, { join }] = await Promise.all([
|
|
129
135
|
import("fs"),
|
|
130
136
|
import("path")
|
|
131
137
|
]);
|
|
132
|
-
const raw = readFileSync(join(cfg.manifestDir, "manifest.json"), "utf-8");
|
|
138
|
+
const raw = readFileSync(join(cfg.manifestDir, ".vite", "manifest.json"), "utf-8");
|
|
133
139
|
const manifest = JSON.parse(raw);
|
|
134
|
-
const
|
|
135
|
-
const entry = manifest[
|
|
140
|
+
const key = cfg.manifestEntry ?? Object.keys(manifest).find((k) => k.endsWith(defaultEntry)) ?? defaultEntry;
|
|
141
|
+
const entry = manifest[key];
|
|
136
142
|
if (entry) {
|
|
137
|
-
|
|
143
|
+
_assetsCache = {
|
|
138
144
|
scripts: [`/assets/${entry.file}`],
|
|
139
145
|
styles: (entry.css ?? []).map((f) => `/assets/${f}`)
|
|
140
146
|
};
|
|
141
|
-
return
|
|
147
|
+
return _assetsCache;
|
|
142
148
|
}
|
|
143
149
|
} catch {
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
|
-
|
|
152
|
+
_assetsCache = {
|
|
147
153
|
scripts: cfg.scripts ?? ["/assets/client.js"],
|
|
148
154
|
styles: cfg.styles ?? []
|
|
149
155
|
};
|
|
150
|
-
return
|
|
156
|
+
return _assetsCache;
|
|
151
157
|
}
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
const styleLinks =
|
|
155
|
-
const scriptTags =
|
|
156
|
-
|
|
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 = [
|
|
157
163
|
"<!DOCTYPE html>",
|
|
158
|
-
`<html ${
|
|
164
|
+
`<html ${attrs}>`,
|
|
159
165
|
"<head>",
|
|
160
166
|
'<meta charset="UTF-8">',
|
|
161
167
|
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
162
|
-
`<title>${esc(
|
|
163
|
-
|
|
164
|
-
generateHydrationScript(),
|
|
168
|
+
`<title>${esc(title)}</title>`,
|
|
169
|
+
metaHtml,
|
|
165
170
|
styleLinks,
|
|
166
171
|
"</head>",
|
|
167
172
|
"<body>",
|
|
168
|
-
|
|
173
|
+
'<div id="fnetro-app">'
|
|
174
|
+
].filter(Boolean).join("\n");
|
|
175
|
+
const tail = [
|
|
176
|
+
"</div>",
|
|
169
177
|
"<script>",
|
|
170
|
-
`window.${STATE_KEY}=${
|
|
171
|
-
`window.${PARAMS_KEY}=${
|
|
172
|
-
`window.${SEO_KEY}=${
|
|
178
|
+
`window.${STATE_KEY}=${stateJson};`,
|
|
179
|
+
`window.${PARAMS_KEY}=${paramsJson};`,
|
|
180
|
+
`window.${SEO_KEY}=${seoJson};`,
|
|
173
181
|
"</script>",
|
|
174
182
|
scriptTags,
|
|
175
183
|
"</body>",
|
|
176
184
|
"</html>"
|
|
177
|
-
].
|
|
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;
|
|
178
194
|
}
|
|
179
195
|
async function renderPage(route, data, url, params, appLayout) {
|
|
180
196
|
const layout = route.layout !== void 0 ? route.layout : appLayout;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return content;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
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 }]
|
|
196
209
|
});
|
|
210
|
+
app.use(router);
|
|
211
|
+
await router.push(url);
|
|
212
|
+
await router.isReady();
|
|
213
|
+
return renderToWebStream(app);
|
|
197
214
|
}
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
214
235
|
}
|
|
215
236
|
function createFNetro(config) {
|
|
216
237
|
const app = new Hono();
|
|
217
238
|
for (const mw of config.middleware ?? []) app.use("*", mw);
|
|
218
239
|
const { pages, apis } = resolveRoutes(config.routes, {
|
|
219
|
-
layout: config.layout,
|
|
240
|
+
...config.layout !== void 0 && { layout: config.layout },
|
|
220
241
|
middleware: []
|
|
221
242
|
});
|
|
222
243
|
const compiled = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
|
|
@@ -240,45 +261,57 @@ function createFNetro(config) {
|
|
|
240
261
|
}
|
|
241
262
|
if (!matched) {
|
|
242
263
|
if (config.notFound) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
);
|
|
246
|
-
return c.html(
|
|
247
|
-
`<!DOCTYPE html><html lang="en"><body>${html2}</body></html>`,
|
|
248
|
-
404
|
|
249
|
-
);
|
|
264
|
+
const html = await renderToString(createSSRApp(config.notFound));
|
|
265
|
+
return c.html(`<!DOCTYPE html><html lang="en"><body>${html}</body></html>`, 404);
|
|
250
266
|
}
|
|
251
267
|
return c.text("Not Found", 404);
|
|
252
268
|
}
|
|
253
269
|
const { route, params } = matched;
|
|
254
270
|
const origParam = c.req.param.bind(c.req);
|
|
255
271
|
c.req["param"] = (key) => key != null ? params[key] ?? origParam(key) : { ...origParam(), ...params };
|
|
256
|
-
let
|
|
257
|
-
const handlers = [...route.middleware];
|
|
272
|
+
let earlyResponse;
|
|
258
273
|
let idx = 0;
|
|
259
274
|
const runNext = async () => {
|
|
260
|
-
const mw =
|
|
275
|
+
const mw = route.middleware[idx++];
|
|
261
276
|
if (!mw) return;
|
|
262
277
|
const res = await mw(c, runNext);
|
|
263
|
-
if (res instanceof Response && !
|
|
278
|
+
if (res instanceof Response && !earlyResponse) earlyResponse = res;
|
|
264
279
|
};
|
|
265
280
|
await runNext();
|
|
266
|
-
if (
|
|
281
|
+
if (earlyResponse) return earlyResponse;
|
|
267
282
|
const rawData = route.page.loader ? await route.page.loader(c) : {};
|
|
268
283
|
const data = rawData ?? {};
|
|
269
284
|
if (isSPA) {
|
|
270
|
-
const
|
|
285
|
+
const pageSEO2 = typeof route.page.seo === "function" ? route.page.seo(data, params) : route.page.seo;
|
|
271
286
|
return c.json({
|
|
272
287
|
state: data,
|
|
273
288
|
params,
|
|
274
289
|
url: pathname,
|
|
275
|
-
seo: mergeSEO(config.seo,
|
|
290
|
+
seo: mergeSEO(config.seo, pageSEO2)
|
|
276
291
|
});
|
|
277
292
|
}
|
|
278
293
|
const clientEntry = config.assets?.manifestEntry ?? "client.ts";
|
|
279
294
|
const assets = isDev ? { scripts: [`/${clientEntry}`], styles: [] } : await resolveAssets(config.assets ?? {}, clientEntry);
|
|
280
|
-
const
|
|
281
|
-
|
|
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
|
+
});
|
|
282
315
|
});
|
|
283
316
|
return { app, handler: app.fetch.bind(app) };
|
|
284
317
|
}
|
|
@@ -295,7 +328,7 @@ async function serve(opts) {
|
|
|
295
328
|
const staticDir = opts.staticDir ?? "./dist";
|
|
296
329
|
const displayHost = hostname === "0.0.0.0" ? "localhost" : hostname;
|
|
297
330
|
const logReady = () => console.log(`
|
|
298
|
-
\u{1F525} FNetro [${runtime}]
|
|
331
|
+
\u{1F525} FNetro [${runtime}] \u2192 http://${displayHost}:${port}
|
|
299
332
|
`);
|
|
300
333
|
switch (runtime) {
|
|
301
334
|
case "node": {
|
|
@@ -309,38 +342,21 @@ async function serve(opts) {
|
|
|
309
342
|
logReady();
|
|
310
343
|
break;
|
|
311
344
|
}
|
|
312
|
-
case "bun":
|
|
345
|
+
case "bun":
|
|
313
346
|
;
|
|
314
347
|
globalThis["Bun"].serve({ fetch: opts.app.handler, port, hostname });
|
|
315
348
|
logReady();
|
|
316
349
|
break;
|
|
317
|
-
|
|
318
|
-
case "deno": {
|
|
350
|
+
case "deno":
|
|
319
351
|
;
|
|
320
352
|
globalThis["Deno"].serve({ port, hostname }, opts.app.handler);
|
|
321
353
|
logReady();
|
|
322
354
|
break;
|
|
323
|
-
}
|
|
324
355
|
default:
|
|
325
|
-
console.warn(
|
|
326
|
-
"[fnetro] serve() is a no-op on edge runtimes \u2014 export `fnetro.handler` instead."
|
|
327
|
-
);
|
|
356
|
+
console.warn("[fnetro] serve() is a no-op on edge \u2014 export fnetro.handler instead.");
|
|
328
357
|
}
|
|
329
358
|
}
|
|
330
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)$/;
|
|
331
|
-
async function loadSolid() {
|
|
332
|
-
try {
|
|
333
|
-
const mod = await import("vite-plugin-solid");
|
|
334
|
-
return mod.default ?? mod;
|
|
335
|
-
} catch {
|
|
336
|
-
throw new Error(
|
|
337
|
-
"[fnetro] vite-plugin-solid is required.\n Install it: npm i -D vite-plugin-solid"
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
function toPlugins(v) {
|
|
342
|
-
return Array.isArray(v) ? v : [v];
|
|
343
|
-
}
|
|
344
360
|
function fnetroVitePlugin(opts = {}) {
|
|
345
361
|
const {
|
|
346
362
|
serverEntry = "server.ts",
|
|
@@ -348,85 +364,46 @@ function fnetroVitePlugin(opts = {}) {
|
|
|
348
364
|
serverOutDir = "dist/server",
|
|
349
365
|
clientOutDir = "dist/assets",
|
|
350
366
|
serverExternal = [],
|
|
351
|
-
|
|
367
|
+
vueOptions = {}
|
|
352
368
|
} = opts;
|
|
353
|
-
|
|
354
|
-
let _solidPlugins = [];
|
|
355
|
-
const jsxPlugin = {
|
|
356
|
-
name: "fnetro:jsx",
|
|
357
|
-
enforce: "pre",
|
|
358
|
-
// Sync config hook — must return Omit<UserConfig, 'plugins'> | null
|
|
359
|
-
// Note: Vite 6+ deprecated `esbuild.jsx`; Vite 8 uses `oxc` instead.
|
|
360
|
-
config(_cfg, env) {
|
|
361
|
-
return {
|
|
362
|
-
oxc: {
|
|
363
|
-
jsx: "automatic",
|
|
364
|
-
jsxImportSource: "solid-js"
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
},
|
|
368
|
-
async buildStart() {
|
|
369
|
-
if (!_solid) {
|
|
370
|
-
_solid = await loadSolid();
|
|
371
|
-
_solidPlugins = toPlugins(_solid({ ssr: true, ...solidOptions }));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
const solidProxy = {
|
|
376
|
-
name: "fnetro:solid-proxy",
|
|
377
|
-
enforce: "pre",
|
|
378
|
-
async transform(code, id, options) {
|
|
379
|
-
if (!_solidPlugins[0]?.transform) return null;
|
|
380
|
-
const hook = _solidPlugins[0].transform;
|
|
381
|
-
const fn = typeof hook === "function" ? hook : hook.handler;
|
|
382
|
-
if (!fn) return null;
|
|
383
|
-
return fn.call(this, code, id, options);
|
|
384
|
-
},
|
|
385
|
-
async resolveId(id) {
|
|
386
|
-
if (!_solidPlugins[0]?.resolveId) return null;
|
|
387
|
-
const hook = _solidPlugins[0].resolveId;
|
|
388
|
-
const fn = typeof hook === "function" ? hook : hook.handler;
|
|
389
|
-
if (!fn) return null;
|
|
390
|
-
return fn.call(this, id, void 0, {});
|
|
391
|
-
},
|
|
392
|
-
async load(id) {
|
|
393
|
-
if (!_solidPlugins[0]?.load) return null;
|
|
394
|
-
const hook = _solidPlugins[0].load;
|
|
395
|
-
const fn = typeof hook === "function" ? hook : hook.handler;
|
|
396
|
-
if (!fn) return null;
|
|
397
|
-
return fn.call(this, id, {});
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
const buildPlugin = {
|
|
369
|
+
return {
|
|
401
370
|
name: "fnetro:build",
|
|
402
371
|
apply: "build",
|
|
403
372
|
enforce: "pre",
|
|
404
|
-
//
|
|
405
|
-
config(
|
|
373
|
+
// Server (SSR) bundle configuration
|
|
374
|
+
config() {
|
|
406
375
|
return {
|
|
407
376
|
build: {
|
|
408
377
|
ssr: serverEntry,
|
|
409
378
|
outDir: serverOutDir,
|
|
410
379
|
rollupOptions: {
|
|
411
380
|
input: serverEntry,
|
|
412
|
-
output: {
|
|
413
|
-
|
|
414
|
-
entryFileNames: "server.js"
|
|
415
|
-
},
|
|
416
|
-
external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || id === "@solidjs/router" || id.startsWith("@solidjs/router/") || 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)
|
|
417
383
|
}
|
|
418
384
|
}
|
|
419
385
|
};
|
|
420
386
|
},
|
|
387
|
+
// After the server bundle is written, trigger the client SPA build
|
|
421
388
|
async closeBundle() {
|
|
422
389
|
console.log("\n\u26A1 FNetro: building client bundle\u2026\n");
|
|
423
|
-
|
|
424
|
-
|
|
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];
|
|
425
401
|
await build({
|
|
426
402
|
configFile: false,
|
|
427
|
-
plugins
|
|
403
|
+
plugins,
|
|
428
404
|
build: {
|
|
429
405
|
outDir: clientOutDir,
|
|
406
|
+
// Vite 5+ writes manifest to <outDir>/.vite/manifest.json
|
|
430
407
|
manifest: true,
|
|
431
408
|
rollupOptions: {
|
|
432
409
|
input: clientEntry,
|
|
@@ -442,9 +419,9 @@ function fnetroVitePlugin(opts = {}) {
|
|
|
442
419
|
console.log("\u2705 FNetro: both bundles ready\n");
|
|
443
420
|
}
|
|
444
421
|
};
|
|
445
|
-
return [jsxPlugin, solidProxy, buildPlugin];
|
|
446
422
|
}
|
|
447
423
|
export {
|
|
424
|
+
DATA_KEY,
|
|
448
425
|
PARAMS_KEY,
|
|
449
426
|
SEO_KEY,
|
|
450
427
|
SPA_HEADER,
|
|
@@ -457,7 +434,9 @@ export {
|
|
|
457
434
|
definePage,
|
|
458
435
|
detectRuntime,
|
|
459
436
|
fnetroVitePlugin,
|
|
437
|
+
isAsyncLoader,
|
|
460
438
|
matchPath,
|
|
461
439
|
resolveRoutes,
|
|
462
|
-
serve
|
|
440
|
+
serve,
|
|
441
|
+
toVueRouterPath
|
|
463
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 };
|