@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/client.js
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
1
|
// client.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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");
|
|
5
23
|
|
|
6
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
|
+
}
|
|
7
34
|
function definePage(def) {
|
|
8
35
|
return { __type: "page", ...def };
|
|
9
36
|
}
|
|
10
37
|
function defineGroup(def) {
|
|
11
38
|
return { __type: "group", ...def };
|
|
12
39
|
}
|
|
13
|
-
function defineLayout(
|
|
14
|
-
return { __type: "layout",
|
|
40
|
+
function defineLayout(component) {
|
|
41
|
+
return { __type: "layout", component };
|
|
15
42
|
}
|
|
16
43
|
function defineApiRoute(path, register) {
|
|
17
44
|
return { __type: "api", path, register };
|
|
18
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
|
+
}
|
|
19
69
|
function resolveRoutes(routes, options = {}) {
|
|
20
70
|
const pages = [];
|
|
21
71
|
const apis = [];
|
|
@@ -26,7 +76,11 @@ function resolveRoutes(routes, options = {}) {
|
|
|
26
76
|
const prefix = (options.prefix ?? "") + route.prefix;
|
|
27
77
|
const mw = [...options.middleware ?? [], ...route.middleware ?? []];
|
|
28
78
|
const layout = route.layout !== void 0 ? route.layout : options.layout;
|
|
29
|
-
const sub = resolveRoutes(route.routes, {
|
|
79
|
+
const sub = resolveRoutes(route.routes, {
|
|
80
|
+
prefix,
|
|
81
|
+
middleware: mw,
|
|
82
|
+
...layout !== void 0 && { layout }
|
|
83
|
+
});
|
|
30
84
|
pages.push(...sub.pages);
|
|
31
85
|
apis.push(...sub.apis);
|
|
32
86
|
} else {
|
|
@@ -40,58 +94,14 @@ function resolveRoutes(routes, options = {}) {
|
|
|
40
94
|
}
|
|
41
95
|
return { pages, apis };
|
|
42
96
|
}
|
|
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
97
|
|
|
68
98
|
// client.ts
|
|
69
|
-
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (params !== null) return { route, params };
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
var _mw = [];
|
|
80
|
-
function useClientMiddleware(mw) {
|
|
81
|
-
_mw.push(mw);
|
|
82
|
-
}
|
|
83
|
-
async function runMiddleware(url, done) {
|
|
84
|
-
const chain = [..._mw, async (_u, next) => {
|
|
85
|
-
await done();
|
|
86
|
-
await next();
|
|
87
|
-
}];
|
|
88
|
-
let i = 0;
|
|
89
|
-
const run = async () => {
|
|
90
|
-
const fn = chain[i++];
|
|
91
|
-
if (fn) await fn(url, run);
|
|
92
|
-
};
|
|
93
|
-
await run();
|
|
94
|
-
}
|
|
99
|
+
import {
|
|
100
|
+
useRoute,
|
|
101
|
+
useRouter,
|
|
102
|
+
RouterLink,
|
|
103
|
+
RouterView as RouterView2
|
|
104
|
+
} from "vue-router";
|
|
95
105
|
function setMeta(selector, attr, val) {
|
|
96
106
|
if (!val) {
|
|
97
107
|
document.querySelector(selector)?.remove();
|
|
@@ -100,8 +110,8 @@ function setMeta(selector, attr, val) {
|
|
|
100
110
|
let el = document.querySelector(selector);
|
|
101
111
|
if (!el) {
|
|
102
112
|
el = document.createElement("meta");
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
113
|
+
const [, attrName = "", attrVal = ""] = /\[([^=]+)="([^"]+)"\]/.exec(selector) ?? [];
|
|
114
|
+
if (attrName) el.setAttribute(attrName, attrVal);
|
|
105
115
|
document.head.appendChild(el);
|
|
106
116
|
}
|
|
107
117
|
el.setAttribute(attr, val);
|
|
@@ -121,156 +131,140 @@ function syncSEO(seo) {
|
|
|
121
131
|
setMeta('[name="twitter:title"]', "content", seo.twitterTitle);
|
|
122
132
|
setMeta('[name="twitter:description"]', "content", seo.twitterDescription);
|
|
123
133
|
setMeta('[name="twitter:image"]', "content", seo.twitterImage);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
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);
|
|
131
140
|
}
|
|
132
|
-
|
|
141
|
+
link.href = seo.canonical;
|
|
133
142
|
} else {
|
|
134
|
-
|
|
143
|
+
link?.remove();
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
|
-
var
|
|
138
|
-
function
|
|
139
|
-
if (!
|
|
140
|
-
|
|
146
|
+
var _fetchCache = /* @__PURE__ */ new Map();
|
|
147
|
+
function fetchSPA(href) {
|
|
148
|
+
if (!_fetchCache.has(href)) {
|
|
149
|
+
_fetchCache.set(
|
|
141
150
|
href,
|
|
142
151
|
fetch(href, { headers: { [SPA_HEADER]: "1" } }).then((r) => {
|
|
143
|
-
if (!r.ok) throw new Error(
|
|
152
|
+
if (!r.ok) throw new Error(`[fnetro] ${r.status} ${r.statusText} \u2014 ${href}`);
|
|
144
153
|
return r.json();
|
|
145
154
|
})
|
|
146
155
|
);
|
|
147
156
|
}
|
|
148
|
-
return
|
|
157
|
+
return _fetchCache.get(href);
|
|
149
158
|
}
|
|
150
159
|
function prefetch(url) {
|
|
151
160
|
try {
|
|
152
161
|
const u = new URL(url, location.origin);
|
|
153
|
-
if (u.origin
|
|
154
|
-
fetchPayload(u.toString());
|
|
162
|
+
if (u.origin === location.origin) fetchSPA(u.toString());
|
|
155
163
|
} catch {
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
setData(payload.state ?? {});
|
|
169
|
-
setParams(payload.params ?? {});
|
|
170
|
-
syncSEO(payload.seo ?? {});
|
|
171
|
-
}).catch((err) => {
|
|
172
|
-
console.error("[fnetro] Failed to load route data:", err);
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
syncSEO(initialSeo);
|
|
166
|
+
var _mw = [];
|
|
167
|
+
function useClientMiddleware(mw) {
|
|
168
|
+
_mw.push(mw);
|
|
169
|
+
}
|
|
170
|
+
async function runMw(url, done) {
|
|
171
|
+
const chain = [
|
|
172
|
+
..._mw,
|
|
173
|
+
async (_, next) => {
|
|
174
|
+
await done();
|
|
175
|
+
await next();
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
if (!layout) return pageEl();
|
|
184
|
-
return createComponent(layout.Component, {
|
|
185
|
-
url: pathname,
|
|
186
|
-
params: params(),
|
|
187
|
-
get children() {
|
|
188
|
-
return pageEl();
|
|
189
|
-
}
|
|
190
|
-
});
|
|
177
|
+
];
|
|
178
|
+
let i = 0;
|
|
179
|
+
const run = async () => {
|
|
180
|
+
const fn = chain[i++];
|
|
181
|
+
if (fn) await fn(url, run);
|
|
191
182
|
};
|
|
183
|
+
await run();
|
|
192
184
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return;
|
|
185
|
+
var _pageData = reactive({});
|
|
186
|
+
function updatePageData(newData) {
|
|
187
|
+
for (const k of Object.keys(_pageData)) {
|
|
188
|
+
if (!(k in newData)) delete _pageData[k];
|
|
198
189
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
190
|
+
Object.assign(_pageData, newData);
|
|
191
|
+
}
|
|
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().");
|
|
202
196
|
}
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const payload = await fetchPayload(u.toString());
|
|
206
|
-
history[opts.replace ? "replaceState" : "pushState"](
|
|
207
|
-
{ url: u.pathname },
|
|
208
|
-
"",
|
|
209
|
-
u.pathname
|
|
210
|
-
);
|
|
211
|
-
if (opts.scroll !== false) window.scrollTo(0, 0);
|
|
212
|
-
syncSEO(payload.seo ?? {});
|
|
213
|
-
window.dispatchEvent(new PopStateEvent("popstate", { state: history.state }));
|
|
214
|
-
} catch (err) {
|
|
215
|
-
console.error("[fnetro] Navigation error:", err);
|
|
216
|
-
location.href = to;
|
|
217
|
-
}
|
|
218
|
-
});
|
|
197
|
+
return data;
|
|
219
198
|
}
|
|
220
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
|
+
}
|
|
221
205
|
const { pages } = resolveRoutes(options.routes, {
|
|
222
|
-
layout: options.layout,
|
|
206
|
+
...options.layout !== void 0 && { layout: options.layout },
|
|
223
207
|
middleware: []
|
|
224
208
|
});
|
|
225
|
-
_routes = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
|
|
226
|
-
_appLayout = options.layout;
|
|
227
|
-
const pathname = location.pathname;
|
|
228
|
-
if (!findRoute(pathname)) {
|
|
229
|
-
console.warn(`[fnetro] No route matched "${pathname}" \u2014 skipping hydration`);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
209
|
const stateMap = window[STATE_KEY] ?? {};
|
|
233
|
-
const paramsMap = window[PARAMS_KEY] ?? {};
|
|
234
210
|
const seoData = window[SEO_KEY] ?? {};
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
211
|
+
const pathname = location.pathname;
|
|
212
|
+
updatePageData(stateMap[pathname] ?? {});
|
|
213
|
+
syncSEO(seoData);
|
|
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();
|
|
239
229
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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);
|
|
256
|
+
if (options.prefetchOnHover !== false) {
|
|
263
257
|
document.addEventListener("mouseover", (e) => {
|
|
264
|
-
const a = e.composedPath().find(
|
|
265
|
-
(el) => el instanceof HTMLAnchorElement
|
|
266
|
-
);
|
|
258
|
+
const a = e.composedPath().find((el) => el instanceof HTMLAnchorElement);
|
|
267
259
|
if (a?.href) prefetch(a.href);
|
|
268
260
|
});
|
|
269
261
|
}
|
|
270
262
|
}
|
|
271
263
|
export {
|
|
272
|
-
|
|
264
|
+
DATA_KEY,
|
|
273
265
|
PARAMS_KEY,
|
|
266
|
+
RouterLink,
|
|
267
|
+
RouterView2 as RouterView,
|
|
274
268
|
SEO_KEY,
|
|
275
269
|
SPA_HEADER,
|
|
276
270
|
STATE_KEY,
|
|
@@ -280,15 +274,14 @@ export {
|
|
|
280
274
|
defineGroup,
|
|
281
275
|
defineLayout,
|
|
282
276
|
definePage,
|
|
283
|
-
|
|
277
|
+
isAsyncLoader,
|
|
284
278
|
matchPath,
|
|
285
|
-
navigate,
|
|
286
279
|
prefetch,
|
|
287
280
|
resolveRoutes,
|
|
288
281
|
syncSEO,
|
|
282
|
+
toVueRouterPath,
|
|
289
283
|
useClientMiddleware,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
useSearchParams
|
|
284
|
+
usePageData,
|
|
285
|
+
useRoute,
|
|
286
|
+
useRouter
|
|
294
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 };
|