@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/client.js
CHANGED
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
// client.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
createSSRApp,
|
|
4
|
+
defineAsyncComponent,
|
|
5
|
+
defineComponent,
|
|
6
|
+
h,
|
|
7
|
+
inject,
|
|
8
|
+
reactive,
|
|
9
|
+
readonly
|
|
10
|
+
} from "vue";
|
|
11
|
+
import {
|
|
12
|
+
createRouter,
|
|
13
|
+
createWebHistory,
|
|
14
|
+
RouterView
|
|
15
|
+
} from "vue-router";
|
|
16
|
+
|
|
17
|
+
// types.ts
|
|
18
|
+
var SPA_HEADER = "x-fnetro-spa";
|
|
19
|
+
var STATE_KEY = "__FNETRO_STATE__";
|
|
20
|
+
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
21
|
+
var SEO_KEY = "__FNETRO_SEO__";
|
|
22
|
+
var DATA_KEY = /* @__PURE__ */ Symbol.for("fnetro:data");
|
|
4
23
|
|
|
5
24
|
// core.ts
|
|
25
|
+
var VUE_BRANDS = ["__name", "__file", "__vccOpts", "setup", "render", "data", "components"];
|
|
26
|
+
function isAsyncLoader(c) {
|
|
27
|
+
if (typeof c !== "function") return false;
|
|
28
|
+
const f = c;
|
|
29
|
+
for (const brand of VUE_BRANDS) {
|
|
30
|
+
if (brand in f) return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
6
34
|
function definePage(def) {
|
|
7
35
|
return { __type: "page", ...def };
|
|
8
36
|
}
|
|
9
37
|
function defineGroup(def) {
|
|
10
38
|
return { __type: "group", ...def };
|
|
11
39
|
}
|
|
12
|
-
function defineLayout(
|
|
13
|
-
return { __type: "layout",
|
|
40
|
+
function defineLayout(component) {
|
|
41
|
+
return { __type: "layout", component };
|
|
14
42
|
}
|
|
15
43
|
function defineApiRoute(path, register) {
|
|
16
44
|
return { __type: "api", path, register };
|
|
17
45
|
}
|
|
46
|
+
function compilePath(path) {
|
|
47
|
+
const keys = [];
|
|
48
|
+
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
49
|
+
keys.push(k);
|
|
50
|
+
return "(.*)";
|
|
51
|
+
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
52
|
+
keys.push(k);
|
|
53
|
+
return "([^/]+)";
|
|
54
|
+
}).replace(/\*/g, "(.*)");
|
|
55
|
+
return { re: new RegExp(`^${src}$`), keys };
|
|
56
|
+
}
|
|
57
|
+
function matchPath(cp, pathname) {
|
|
58
|
+
const m = pathname.match(cp.re);
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
const params = {};
|
|
61
|
+
cp.keys.forEach((k, i) => {
|
|
62
|
+
params[k] = decodeURIComponent(m[i + 1] ?? "");
|
|
63
|
+
});
|
|
64
|
+
return params;
|
|
65
|
+
}
|
|
66
|
+
function toVueRouterPath(fnetroPath) {
|
|
67
|
+
return fnetroPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1(.*)*").replace(/\[([^\]]+)\]/g, ":$1");
|
|
68
|
+
}
|
|
18
69
|
function resolveRoutes(routes, options = {}) {
|
|
19
70
|
const pages = [];
|
|
20
71
|
const apis = [];
|
|
@@ -25,7 +76,11 @@ function resolveRoutes(routes, options = {}) {
|
|
|
25
76
|
const prefix = (options.prefix ?? "") + route.prefix;
|
|
26
77
|
const mw = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
27
78
|
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
28
|
-
const sub = resolveRoutes(route.routes, {
|
|
79
|
+
const sub = resolveRoutes(route.routes, {
|
|
80
|
+
prefix,
|
|
81
|
+
middleware: mw,
|
|
82
|
+
...layout !== void 0 && { layout }
|
|
83
|
+
});
|
|
29
84
|
pages.push(...sub.pages);
|
|
30
85
|
apis.push(...sub.apis);
|
|
31
86
|
} else {
|
|
@@ -39,58 +94,14 @@ function resolveRoutes(routes, options = {}) {
|
|
|
39
94
|
}
|
|
40
95
|
return { pages, apis };
|
|
41
96
|
}
|
|
42
|
-
function compilePath(path) {
|
|
43
|
-
const keys = [];
|
|
44
|
-
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
45
|
-
keys.push(k);
|
|
46
|
-
return "(.*)";
|
|
47
|
-
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
48
|
-
keys.push(k);
|
|
49
|
-
return "([^/]+)";
|
|
50
|
-
}).replace(/\*/g, "(.*)");
|
|
51
|
-
return { re: new RegExp(`^${src}$`), keys };
|
|
52
|
-
}
|
|
53
|
-
function matchPath(compiled, pathname) {
|
|
54
|
-
const m = pathname.match(compiled.re);
|
|
55
|
-
if (!m) return null;
|
|
56
|
-
const params = {};
|
|
57
|
-
compiled.keys.forEach((k, i) => {
|
|
58
|
-
params[k] = decodeURIComponent(m[i + 1] ?? "");
|
|
59
|
-
});
|
|
60
|
-
return params;
|
|
61
|
-
}
|
|
62
|
-
var SPA_HEADER = "x-fnetro-spa";
|
|
63
|
-
var STATE_KEY = "__FNETRO_STATE__";
|
|
64
|
-
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
65
|
-
var SEO_KEY = "__FNETRO_SEO__";
|
|
66
97
|
|
|
67
98
|
// client.ts
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
var _setNav = null;
|
|
78
|
-
var _mw = [];
|
|
79
|
-
function useClientMiddleware(mw) {
|
|
80
|
-
_mw.push(mw);
|
|
81
|
-
}
|
|
82
|
-
async function runMiddleware(url, done) {
|
|
83
|
-
const chain = [..._mw, async (_u, next) => {
|
|
84
|
-
await done();
|
|
85
|
-
await next();
|
|
86
|
-
}];
|
|
87
|
-
let i = 0;
|
|
88
|
-
const run = async () => {
|
|
89
|
-
const fn = chain[i++];
|
|
90
|
-
if (fn) await fn(url, run);
|
|
91
|
-
};
|
|
92
|
-
await run();
|
|
93
|
-
}
|
|
99
|
+
import {
|
|
100
|
+
useRoute,
|
|
101
|
+
useRouter,
|
|
102
|
+
RouterLink,
|
|
103
|
+
RouterView as RouterView2
|
|
104
|
+
} from "vue-router";
|
|
94
105
|
function setMeta(selector, attr, val) {
|
|
95
106
|
if (!val) {
|
|
96
107
|
document.querySelector(selector)?.remove();
|
|
@@ -99,8 +110,8 @@ function setMeta(selector, attr, val) {
|
|
|
99
110
|
let el = document.querySelector(selector);
|
|
100
111
|
if (!el) {
|
|
101
112
|
el = document.createElement("meta");
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
113
|
+
const [, attrName = "", attrVal = ""] = /\[([^=]+)="([^"]+)"\]/.exec(selector) ?? [];
|
|
114
|
+
if (attrName) el.setAttribute(attrName, attrVal);
|
|
104
115
|
document.head.appendChild(el);
|
|
105
116
|
}
|
|
106
117
|
el.setAttribute(attr, val);
|
|
@@ -120,149 +131,140 @@ function syncSEO(seo) {
|
|
|
120
131
|
setMeta('[name="twitter:title"]', "content", seo.twitterTitle);
|
|
121
132
|
setMeta('[name="twitter:description"]', "content", seo.twitterDescription);
|
|
122
133
|
setMeta('[name="twitter:image"]', "content", seo.twitterImage);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
document.head.appendChild(linkEl);
|
|
134
|
+
let link = document.querySelector('link[rel="canonical"]');
|
|
135
|
+
if (seo.canonical) {
|
|
136
|
+
if (!link) {
|
|
137
|
+
link = document.createElement("link");
|
|
138
|
+
link.rel = "canonical";
|
|
139
|
+
document.head.appendChild(link);
|
|
130
140
|
}
|
|
131
|
-
|
|
141
|
+
link.href = seo.canonical;
|
|
132
142
|
} else {
|
|
133
|
-
|
|
143
|
+
link?.remove();
|
|
134
144
|
}
|
|
135
145
|
}
|
|
136
|
-
var
|
|
137
|
-
function
|
|
138
|
-
if (!
|
|
139
|
-
|
|
146
|
+
var _fetchCache = /* @__PURE__ */ new Map();
|
|
147
|
+
function fetchSPA(href) {
|
|
148
|
+
if (!_fetchCache.has(href)) {
|
|
149
|
+
_fetchCache.set(
|
|
140
150
|
href,
|
|
141
151
|
fetch(href, { headers: { [SPA_HEADER]: "1" } }).then((r) => {
|
|
142
|
-
if (!r.ok) throw new Error(
|
|
152
|
+
if (!r.ok) throw new Error(`[fnetro] ${r.status} ${r.statusText} \u2014 ${href}`);
|
|
143
153
|
return r.json();
|
|
144
154
|
})
|
|
145
155
|
);
|
|
146
156
|
}
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
async function navigate(to, opts = {}) {
|
|
150
|
-
const u = new URL(to, location.origin);
|
|
151
|
-
if (u.origin !== location.origin) {
|
|
152
|
-
location.href = to;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (!findRoute(u.pathname)) {
|
|
156
|
-
location.href = to;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
await runMiddleware(u.pathname, async () => {
|
|
160
|
-
try {
|
|
161
|
-
const payload = await fetchPayload(u.toString());
|
|
162
|
-
history[opts.replace ? "replaceState" : "pushState"](
|
|
163
|
-
{ url: u.pathname },
|
|
164
|
-
"",
|
|
165
|
-
u.pathname
|
|
166
|
-
);
|
|
167
|
-
if (opts.scroll !== false) window.scrollTo(0, 0);
|
|
168
|
-
_setNav?.({ path: u.pathname, data: payload.state ?? {}, params: payload.params ?? {} });
|
|
169
|
-
syncSEO(payload.seo ?? {});
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.error("[fnetro] Navigation error:", err);
|
|
172
|
-
location.href = to;
|
|
173
|
-
}
|
|
174
|
-
});
|
|
157
|
+
return _fetchCache.get(href);
|
|
175
158
|
}
|
|
176
159
|
function prefetch(url) {
|
|
177
160
|
try {
|
|
178
161
|
const u = new URL(url, location.origin);
|
|
179
|
-
if (u.origin
|
|
180
|
-
fetchPayload(u.toString());
|
|
162
|
+
if (u.origin === location.origin) fetchSPA(u.toString());
|
|
181
163
|
} catch {
|
|
182
164
|
}
|
|
183
165
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
(el) => el instanceof HTMLAnchorElement
|
|
188
|
-
);
|
|
189
|
-
if (!a?.href) return;
|
|
190
|
-
if (a.target && a.target !== "_self") return;
|
|
191
|
-
if (a.hasAttribute("data-no-spa") || a.rel?.includes("external")) return;
|
|
192
|
-
const u = new URL(a.href);
|
|
193
|
-
if (u.origin !== location.origin) return;
|
|
194
|
-
e.preventDefault();
|
|
195
|
-
navigate(a.href);
|
|
166
|
+
var _mw = [];
|
|
167
|
+
function useClientMiddleware(mw) {
|
|
168
|
+
_mw.push(mw);
|
|
196
169
|
}
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
170
|
+
async function runMw(url, done) {
|
|
171
|
+
const chain = [
|
|
172
|
+
..._mw,
|
|
173
|
+
async (_, next) => {
|
|
174
|
+
await done();
|
|
175
|
+
await next();
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
let i = 0;
|
|
179
|
+
const run = async () => {
|
|
180
|
+
const fn = chain[i++];
|
|
181
|
+
if (fn) await fn(url, run);
|
|
182
|
+
};
|
|
183
|
+
await run();
|
|
202
184
|
}
|
|
203
|
-
|
|
204
|
-
|
|
185
|
+
var _pageData = reactive({});
|
|
186
|
+
function updatePageData(newData) {
|
|
187
|
+
for (const k of Object.keys(_pageData)) {
|
|
188
|
+
if (!(k in newData)) delete _pageData[k];
|
|
189
|
+
}
|
|
190
|
+
Object.assign(_pageData, newData);
|
|
205
191
|
}
|
|
206
|
-
function
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!m) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const layout = m.route.layout !== void 0 ? m.route.layout : props.appLayout;
|
|
216
|
-
const pageEl = createComponent(m.route.page.Page, { ...data, url: path, params });
|
|
217
|
-
if (!layout) return pageEl;
|
|
218
|
-
return createComponent(layout.Component, {
|
|
219
|
-
url: path,
|
|
220
|
-
params,
|
|
221
|
-
get children() {
|
|
222
|
-
return pageEl;
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
return view;
|
|
192
|
+
function usePageData() {
|
|
193
|
+
const data = inject(DATA_KEY);
|
|
194
|
+
if (data === void 0) {
|
|
195
|
+
throw new Error("[fnetro] usePageData() must be called inside a component setup().");
|
|
196
|
+
}
|
|
197
|
+
return data;
|
|
227
198
|
}
|
|
228
199
|
async function boot(options) {
|
|
200
|
+
const container = document.getElementById("fnetro-app");
|
|
201
|
+
if (!container) {
|
|
202
|
+
console.error("[fnetro] #fnetro-app not found \u2014 aborting hydration.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
229
205
|
const { pages } = resolveRoutes(options.routes, {
|
|
230
|
-
layout: options.layout,
|
|
206
|
+
...options.layout !== void 0 && { layout: options.layout },
|
|
231
207
|
middleware: []
|
|
232
208
|
});
|
|
233
|
-
_routes = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
|
|
234
|
-
_appLayout = options.layout;
|
|
235
|
-
const pathname = location.pathname;
|
|
236
|
-
if (!findRoute(pathname)) {
|
|
237
|
-
console.warn(`[fnetro] No route matched "${pathname}" \u2014 skipping hydration`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
209
|
const stateMap = window[STATE_KEY] ?? {};
|
|
241
|
-
const paramsMap = window[PARAMS_KEY] ?? {};
|
|
242
210
|
const seoData = window[SEO_KEY] ?? {};
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
data: stateMap[pathname] ?? {},
|
|
246
|
-
params: paramsMap
|
|
247
|
-
};
|
|
248
|
-
const container = document.getElementById("fnetro-app");
|
|
249
|
-
if (!container) {
|
|
250
|
-
console.error("[fnetro] #fnetro-app not found \u2014 aborting hydration");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
211
|
+
const pathname = location.pathname;
|
|
212
|
+
updatePageData(stateMap[pathname] ?? {});
|
|
253
213
|
syncSEO(seoData);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
214
|
+
const vueRoutes = pages.map((r) => {
|
|
215
|
+
const layout = r.layout !== void 0 ? r.layout : options.layout;
|
|
216
|
+
const comp = r.page.component;
|
|
217
|
+
const PageComp = isAsyncLoader(comp) ? defineAsyncComponent(comp) : comp;
|
|
218
|
+
const routeComp = layout ? defineComponent({
|
|
219
|
+
name: "FNetroRoute",
|
|
220
|
+
setup: () => () => h(layout.component, null, {
|
|
221
|
+
default: () => h(PageComp)
|
|
222
|
+
})
|
|
223
|
+
}) : PageComp;
|
|
224
|
+
return { path: toVueRouterPath(r.fullPath), component: routeComp };
|
|
225
|
+
});
|
|
226
|
+
const currentRoute = pages.find((r) => matchPath(compilePath(r.fullPath), pathname) !== null);
|
|
227
|
+
if (currentRoute && isAsyncLoader(currentRoute.page.component)) {
|
|
228
|
+
await currentRoute.page.component();
|
|
229
|
+
}
|
|
230
|
+
const app = createSSRApp({ name: "FNetroApp", render: () => h(RouterView) });
|
|
231
|
+
app.provide(DATA_KEY, readonly(_pageData));
|
|
232
|
+
const router = createRouter({ history: createWebHistory(), routes: vueRoutes });
|
|
233
|
+
let isInitialNav = true;
|
|
234
|
+
router.beforeEach(async (to, _from, next) => {
|
|
235
|
+
if (isInitialNav) {
|
|
236
|
+
isInitialNav = false;
|
|
237
|
+
return next();
|
|
238
|
+
}
|
|
239
|
+
const href = new URL(to.fullPath, location.origin).toString();
|
|
240
|
+
try {
|
|
241
|
+
await runMw(to.fullPath, async () => {
|
|
242
|
+
const payload = await fetchSPA(href);
|
|
243
|
+
updatePageData(payload.state ?? {});
|
|
244
|
+
syncSEO(payload.seo ?? {});
|
|
245
|
+
window.scrollTo(0, 0);
|
|
246
|
+
});
|
|
247
|
+
next();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error("[fnetro] Navigation error:", err);
|
|
250
|
+
location.href = to.fullPath;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
app.use(router);
|
|
254
|
+
await router.isReady();
|
|
255
|
+
app.mount(container);
|
|
259
256
|
if (options.prefetchOnHover !== false) {
|
|
260
|
-
document.addEventListener("mouseover",
|
|
257
|
+
document.addEventListener("mouseover", (e) => {
|
|
258
|
+
const a = e.composedPath().find((el) => el instanceof HTMLAnchorElement);
|
|
259
|
+
if (a?.href) prefetch(a.href);
|
|
260
|
+
});
|
|
261
261
|
}
|
|
262
|
-
window.addEventListener("popstate", onPopState);
|
|
263
262
|
}
|
|
264
263
|
export {
|
|
264
|
+
DATA_KEY,
|
|
265
265
|
PARAMS_KEY,
|
|
266
|
+
RouterLink,
|
|
267
|
+
RouterView2 as RouterView,
|
|
266
268
|
SEO_KEY,
|
|
267
269
|
SPA_HEADER,
|
|
268
270
|
STATE_KEY,
|
|
@@ -272,9 +274,14 @@ export {
|
|
|
272
274
|
defineGroup,
|
|
273
275
|
defineLayout,
|
|
274
276
|
definePage,
|
|
277
|
+
isAsyncLoader,
|
|
275
278
|
matchPath,
|
|
276
|
-
navigate,
|
|
277
279
|
prefetch,
|
|
278
280
|
resolveRoutes,
|
|
279
|
-
|
|
281
|
+
syncSEO,
|
|
282
|
+
toVueRouterPath,
|
|
283
|
+
useClientMiddleware,
|
|
284
|
+
usePageData,
|
|
285
|
+
useRoute,
|
|
286
|
+
useRouter
|
|
280
287
|
};
|
package/dist/core.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { Component } from 'vue';
|
|
1
2
|
import { Hono, MiddlewareHandler, Context } from 'hono';
|
|
2
|
-
import { Component, JSX } from 'solid-js';
|
|
3
3
|
|
|
4
4
|
type HonoMiddleware = MiddlewareHandler;
|
|
5
5
|
type LoaderCtx = Context;
|
|
@@ -15,44 +15,33 @@ interface SEOMeta {
|
|
|
15
15
|
ogDescription?: string;
|
|
16
16
|
ogImage?: string;
|
|
17
17
|
ogImageAlt?: string;
|
|
18
|
-
ogImageWidth?: string;
|
|
19
|
-
ogImageHeight?: string;
|
|
20
18
|
ogUrl?: string;
|
|
21
19
|
ogType?: string;
|
|
22
20
|
ogSiteName?: string;
|
|
23
|
-
ogLocale?: string;
|
|
24
21
|
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
|
|
25
22
|
twitterSite?: string;
|
|
26
|
-
twitterCreator?: string;
|
|
27
23
|
twitterTitle?: string;
|
|
28
24
|
twitterDescription?: string;
|
|
29
25
|
twitterImage?: string;
|
|
30
|
-
|
|
26
|
+
/** Structured data injected as <script type="application/ld+json">. */
|
|
31
27
|
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
32
|
-
extra?: Array<{
|
|
33
|
-
name?: string;
|
|
34
|
-
property?: string;
|
|
35
|
-
httpEquiv?: string;
|
|
36
|
-
content: string;
|
|
37
|
-
}>;
|
|
38
28
|
}
|
|
39
|
-
type
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
interface LayoutProps {
|
|
44
|
-
children: JSX.Element;
|
|
45
|
-
url: string;
|
|
46
|
-
params: Record<string, string>;
|
|
47
|
-
}
|
|
48
|
-
interface PageDef<TData extends object = {}> {
|
|
29
|
+
type AsyncLoader = () => Promise<{
|
|
30
|
+
default: Component;
|
|
31
|
+
} | Component>;
|
|
32
|
+
interface PageDef<TData extends object = Record<string, never>> {
|
|
49
33
|
readonly __type: 'page';
|
|
50
34
|
path: string;
|
|
51
35
|
middleware?: HonoMiddleware[];
|
|
52
36
|
loader?: (c: LoaderCtx) => TData | Promise<TData>;
|
|
53
37
|
seo?: SEOMeta | ((data: TData, params: Record<string, string>) => SEOMeta);
|
|
38
|
+
/** Override or disable the app-level layout for this route. */
|
|
54
39
|
layout?: LayoutDef | false;
|
|
55
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The Vue component to render for this route.
|
|
42
|
+
* Use () => import('./Page.vue') for automatic code splitting.
|
|
43
|
+
*/
|
|
44
|
+
component: Component | AsyncLoader;
|
|
56
45
|
}
|
|
57
46
|
interface GroupDef {
|
|
58
47
|
readonly __type: 'group';
|
|
@@ -63,7 +52,8 @@ interface GroupDef {
|
|
|
63
52
|
}
|
|
64
53
|
interface LayoutDef {
|
|
65
54
|
readonly __type: 'layout';
|
|
66
|
-
|
|
55
|
+
/** Vue layout component — must contain <slot /> for page content. */
|
|
56
|
+
component: Component;
|
|
67
57
|
}
|
|
68
58
|
interface ApiRouteDef {
|
|
69
59
|
readonly __type: 'api';
|
|
@@ -78,36 +68,63 @@ interface AppConfig {
|
|
|
78
68
|
routes: Route[];
|
|
79
69
|
notFound?: Component;
|
|
80
70
|
htmlAttrs?: Record<string, string>;
|
|
71
|
+
/** Extra HTML injected into <head> (e.g. font preloads). */
|
|
81
72
|
head?: string;
|
|
82
73
|
}
|
|
83
|
-
type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
|
|
84
|
-
declare function definePage<TData extends object = {}>(def: Omit<PageDef<TData>, '__type'>): PageDef<TData>;
|
|
85
|
-
declare function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef;
|
|
86
|
-
declare function defineLayout(Component: Component<LayoutProps>): LayoutDef;
|
|
87
|
-
declare function defineApiRoute(path: string, register: ApiRouteDef['register']): ApiRouteDef;
|
|
88
74
|
interface ResolvedRoute {
|
|
89
75
|
fullPath: string;
|
|
90
76
|
page: PageDef<any>;
|
|
91
77
|
layout: LayoutDef | false | undefined;
|
|
92
78
|
middleware: HonoMiddleware[];
|
|
93
79
|
}
|
|
94
|
-
declare function resolveRoutes(routes: Route[], options?: {
|
|
95
|
-
prefix?: string;
|
|
96
|
-
middleware?: HonoMiddleware[];
|
|
97
|
-
layout?: LayoutDef | false;
|
|
98
|
-
}): {
|
|
99
|
-
pages: ResolvedRoute[];
|
|
100
|
-
apis: ApiRouteDef[];
|
|
101
|
-
};
|
|
102
80
|
interface CompiledPath {
|
|
103
81
|
re: RegExp;
|
|
104
82
|
keys: string[];
|
|
105
83
|
}
|
|
106
|
-
|
|
107
|
-
|
|
84
|
+
type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
|
|
85
|
+
/** Custom request header that identifies an SPA navigation (JSON payload). */
|
|
108
86
|
declare const SPA_HEADER = "x-fnetro-spa";
|
|
87
|
+
/** window key for SSR-injected per-page loader data. */
|
|
109
88
|
declare const STATE_KEY = "__FNETRO_STATE__";
|
|
89
|
+
/** window key for SSR-injected URL params. */
|
|
110
90
|
declare const PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
91
|
+
/** window key for SSR-injected SEO meta. */
|
|
111
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
|
+
/**
|
|
100
|
+
* Returns true when `c` is an async factory function (i.e. `() => import(...)`)
|
|
101
|
+
* rather than a resolved Vue component object.
|
|
102
|
+
*
|
|
103
|
+
* Used by both server.ts (to resolve the import before SSR) and client.ts
|
|
104
|
+
* (to wrap with defineAsyncComponent for lazy hydration).
|
|
105
|
+
*/
|
|
106
|
+
declare function isAsyncLoader(c: unknown): c is AsyncLoader;
|
|
107
|
+
declare function definePage<TData extends object = Record<string, never>>(def: Omit<PageDef<TData>, '__type'>): PageDef<TData>;
|
|
108
|
+
declare function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef;
|
|
109
|
+
/** Wrap a Vue layout component (must render <slot />) as a FNetro layout. */
|
|
110
|
+
declare function defineLayout(component: Component): LayoutDef;
|
|
111
|
+
declare function defineApiRoute(path: string, register: ApiRouteDef['register']): ApiRouteDef;
|
|
112
|
+
declare function compilePath(path: string): CompiledPath;
|
|
113
|
+
declare function matchPath(cp: CompiledPath, pathname: string): Record<string, string> | null;
|
|
114
|
+
/**
|
|
115
|
+
* Convert FNetro `[param]` syntax to Vue Router `:param` syntax.
|
|
116
|
+
*
|
|
117
|
+
* `/posts/[slug]` → `/posts/:slug`
|
|
118
|
+
* `/files/[...path]` → `/files/:path(.*)*`
|
|
119
|
+
*/
|
|
120
|
+
declare function toVueRouterPath(fnetroPath: string): string;
|
|
121
|
+
declare function resolveRoutes(routes: Route[], options?: {
|
|
122
|
+
prefix?: string;
|
|
123
|
+
middleware?: HonoMiddleware[];
|
|
124
|
+
layout?: LayoutDef | false;
|
|
125
|
+
}): {
|
|
126
|
+
pages: ResolvedRoute[];
|
|
127
|
+
apis: ApiRouteDef[];
|
|
128
|
+
};
|
|
112
129
|
|
|
113
|
-
export { type ApiRouteDef, type AppConfig, type ClientMiddleware, type CompiledPath, type GroupDef, type HonoMiddleware, type LayoutDef, type
|
|
130
|
+
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, compilePath, defineApiRoute, defineGroup, defineLayout, definePage, isAsyncLoader, matchPath, resolveRoutes, toVueRouterPath };
|
package/dist/core.js
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
|
+
// types.ts
|
|
2
|
+
var SPA_HEADER = "x-fnetro-spa";
|
|
3
|
+
var STATE_KEY = "__FNETRO_STATE__";
|
|
4
|
+
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
5
|
+
var SEO_KEY = "__FNETRO_SEO__";
|
|
6
|
+
var DATA_KEY = /* @__PURE__ */ Symbol.for("fnetro:data");
|
|
7
|
+
|
|
1
8
|
// core.ts
|
|
9
|
+
var VUE_BRANDS = ["__name", "__file", "__vccOpts", "setup", "render", "data", "components"];
|
|
10
|
+
function isAsyncLoader(c) {
|
|
11
|
+
if (typeof c !== "function") return false;
|
|
12
|
+
const f = c;
|
|
13
|
+
for (const brand of VUE_BRANDS) {
|
|
14
|
+
if (brand in f) return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
2
18
|
function definePage(def) {
|
|
3
19
|
return { __type: "page", ...def };
|
|
4
20
|
}
|
|
5
21
|
function defineGroup(def) {
|
|
6
22
|
return { __type: "group", ...def };
|
|
7
23
|
}
|
|
8
|
-
function defineLayout(
|
|
9
|
-
return { __type: "layout",
|
|
24
|
+
function defineLayout(component) {
|
|
25
|
+
return { __type: "layout", component };
|
|
10
26
|
}
|
|
11
27
|
function defineApiRoute(path, register) {
|
|
12
28
|
return { __type: "api", path, register };
|
|
13
29
|
}
|
|
30
|
+
function compilePath(path) {
|
|
31
|
+
const keys = [];
|
|
32
|
+
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
33
|
+
keys.push(k);
|
|
34
|
+
return "(.*)";
|
|
35
|
+
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
36
|
+
keys.push(k);
|
|
37
|
+
return "([^/]+)";
|
|
38
|
+
}).replace(/\*/g, "(.*)");
|
|
39
|
+
return { re: new RegExp(`^${src}$`), keys };
|
|
40
|
+
}
|
|
41
|
+
function matchPath(cp, pathname) {
|
|
42
|
+
const m = pathname.match(cp.re);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
const params = {};
|
|
45
|
+
cp.keys.forEach((k, i) => {
|
|
46
|
+
params[k] = decodeURIComponent(m[i + 1] ?? "");
|
|
47
|
+
});
|
|
48
|
+
return params;
|
|
49
|
+
}
|
|
50
|
+
function toVueRouterPath(fnetroPath) {
|
|
51
|
+
return fnetroPath.replace(/\[\.\.\.([^\]]+)\]/g, ":$1(.*)*").replace(/\[([^\]]+)\]/g, ":$1");
|
|
52
|
+
}
|
|
14
53
|
function resolveRoutes(routes, options = {}) {
|
|
15
54
|
const pages = [];
|
|
16
55
|
const apis = [];
|
|
@@ -21,7 +60,11 @@ function resolveRoutes(routes, options = {}) {
|
|
|
21
60
|
const prefix = (options.prefix ?? "") + route.prefix;
|
|
22
61
|
const mw = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
23
62
|
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
24
|
-
const sub = resolveRoutes(route.routes, {
|
|
63
|
+
const sub = resolveRoutes(route.routes, {
|
|
64
|
+
prefix,
|
|
65
|
+
middleware: mw,
|
|
66
|
+
...layout !== void 0 && { layout }
|
|
67
|
+
});
|
|
25
68
|
pages.push(...sub.pages);
|
|
26
69
|
apis.push(...sub.apis);
|
|
27
70
|
} else {
|
|
@@ -35,31 +78,8 @@ function resolveRoutes(routes, options = {}) {
|
|
|
35
78
|
}
|
|
36
79
|
return { pages, apis };
|
|
37
80
|
}
|
|
38
|
-
function compilePath(path) {
|
|
39
|
-
const keys = [];
|
|
40
|
-
const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
|
|
41
|
-
keys.push(k);
|
|
42
|
-
return "(.*)";
|
|
43
|
-
}).replace(/\[([^\]]+)\]/g, (_, k) => {
|
|
44
|
-
keys.push(k);
|
|
45
|
-
return "([^/]+)";
|
|
46
|
-
}).replace(/\*/g, "(.*)");
|
|
47
|
-
return { re: new RegExp(`^${src}$`), keys };
|
|
48
|
-
}
|
|
49
|
-
function matchPath(compiled, pathname) {
|
|
50
|
-
const m = pathname.match(compiled.re);
|
|
51
|
-
if (!m) return null;
|
|
52
|
-
const params = {};
|
|
53
|
-
compiled.keys.forEach((k, i) => {
|
|
54
|
-
params[k] = decodeURIComponent(m[i + 1] ?? "");
|
|
55
|
-
});
|
|
56
|
-
return params;
|
|
57
|
-
}
|
|
58
|
-
var SPA_HEADER = "x-fnetro-spa";
|
|
59
|
-
var STATE_KEY = "__FNETRO_STATE__";
|
|
60
|
-
var PARAMS_KEY = "__FNETRO_PARAMS__";
|
|
61
|
-
var SEO_KEY = "__FNETRO_SEO__";
|
|
62
81
|
export {
|
|
82
|
+
DATA_KEY,
|
|
63
83
|
PARAMS_KEY,
|
|
64
84
|
SEO_KEY,
|
|
65
85
|
SPA_HEADER,
|
|
@@ -69,6 +89,8 @@ export {
|
|
|
69
89
|
defineGroup,
|
|
70
90
|
defineLayout,
|
|
71
91
|
definePage,
|
|
92
|
+
isAsyncLoader,
|
|
72
93
|
matchPath,
|
|
73
|
-
resolveRoutes
|
|
94
|
+
resolveRoutes,
|
|
95
|
+
toVueRouterPath
|
|
74
96
|
};
|