@netrojs/fnetro 0.1.5 → 0.2.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 +500 -897
- package/client.ts +220 -200
- package/core.ts +146 -644
- package/dist/client.d.ts +99 -155
- package/dist/client.js +177 -570
- package/dist/core.d.ts +69 -156
- package/dist/core.js +31 -452
- package/dist/server.d.ts +120 -179
- package/dist/server.js +278 -553
- package/package.json +17 -8
- package/server.ts +455 -247
package/client.ts
CHANGED
|
@@ -1,227 +1,203 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
2
|
// FNetro · client.ts
|
|
3
|
-
//
|
|
3
|
+
// SolidJS hydration · SPA routing · client middleware · SEO sync · prefetch
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createSignal, createMemo, createComponent } from 'solid-js'
|
|
7
|
+
import { hydrate } from 'solid-js/web'
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
__hooks, ref, reactive, computed, watchEffect, isRef,
|
|
14
|
-
SPA_HEADER, STATE_KEY, PARAMS_KEY,
|
|
15
|
-
type Ref, type AppConfig, type ResolvedRoute,
|
|
16
|
-
type LayoutDef,
|
|
9
|
+
resolveRoutes, compilePath, matchPath,
|
|
10
|
+
SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY,
|
|
11
|
+
type AppConfig, type ResolvedRoute, type CompiledPath,
|
|
12
|
+
type LayoutDef, type SEOMeta, type ClientMiddleware,
|
|
17
13
|
} from './core'
|
|
18
|
-
import { resolveRoutes } from './core'
|
|
19
14
|
|
|
20
15
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
21
|
-
// § 1
|
|
16
|
+
// § 1 Compiled route cache (module-level, populated on boot)
|
|
22
17
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
* Connect a Ref (or computed getter) to the current JSX component.
|
|
26
|
-
* Re-renders whenever the source changes.
|
|
27
|
-
*/
|
|
28
|
-
function clientUseValue<T>(source: Ref<T> | (() => T)): T {
|
|
29
|
-
if (isRef(source)) {
|
|
30
|
-
// Fast path: useSyncExternalStore is ideal for refs
|
|
31
|
-
return useSyncExternalStore(
|
|
32
|
-
(notify) => (source as any).subscribe(notify),
|
|
33
|
-
() => (source as any).peek?.() ?? source.value,
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
// Getter: wrap in a computed ref, then subscribe
|
|
37
|
-
const c = useMemo(() => computed(source as () => T), [source])
|
|
38
|
-
return useSyncExternalStore(
|
|
39
|
-
(notify) => (c as any).subscribe(notify),
|
|
40
|
-
() => (c as any).peek?.() ?? c.value,
|
|
41
|
-
)
|
|
42
|
-
}
|
|
19
|
+
interface CRoute { route: ResolvedRoute; cp: CompiledPath }
|
|
43
20
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
useSyncExternalStore(
|
|
54
|
-
(notify) => (r as any).subscribe(notify),
|
|
55
|
-
() => (r as any).peek?.() ?? r.value,
|
|
56
|
-
)
|
|
57
|
-
return r
|
|
21
|
+
let _routes: CRoute[] = []
|
|
22
|
+
let _appLayout: LayoutDef | undefined
|
|
23
|
+
|
|
24
|
+
function findRoute(pathname: string) {
|
|
25
|
+
for (const { route, cp } of _routes) {
|
|
26
|
+
const params = matchPath(cp, pathname)
|
|
27
|
+
if (params !== null) return { route, params }
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
58
30
|
}
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function clientUseLocalReactive<T extends object>(init: T): T {
|
|
64
|
-
const stableRef = useHonoRef<T | null>(null)
|
|
65
|
-
if (stableRef.current === null) stableRef.current = reactive(init)
|
|
66
|
-
const proxy = stableRef.current!
|
|
67
|
-
|
|
68
|
-
// watchEffect to re-render whenever any tracked key changes
|
|
69
|
-
const [tick, setTick] = useState(0)
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
return watchEffect(() => {
|
|
72
|
-
// Touch all keys to establish tracking
|
|
73
|
-
JSON.stringify(proxy)
|
|
74
|
-
// Schedule re-render (not on first run)
|
|
75
|
-
setTick(t => t + 1)
|
|
76
|
-
})
|
|
77
|
-
}, [])
|
|
32
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// § 2 Navigation state signal
|
|
34
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
78
35
|
|
|
79
|
-
|
|
36
|
+
interface NavState {
|
|
37
|
+
path: string
|
|
38
|
+
data: Record<string, unknown>
|
|
39
|
+
params: Record<string, string>
|
|
80
40
|
}
|
|
81
41
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
useValue: clientUseValue,
|
|
85
|
-
useLocalRef: clientUseLocalRef,
|
|
86
|
-
useLocalReactive: clientUseLocalReactive,
|
|
87
|
-
})
|
|
42
|
+
// Populated by createAppRoot(); exposed so navigate() can update it.
|
|
43
|
+
let _setNav: ((s: NavState) => void) | null = null
|
|
88
44
|
|
|
89
45
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
90
|
-
// §
|
|
46
|
+
// § 3 Client middleware
|
|
91
47
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
92
48
|
|
|
93
|
-
|
|
94
|
-
route: ResolvedRoute
|
|
95
|
-
re: RegExp
|
|
96
|
-
keys: string[]
|
|
97
|
-
}
|
|
49
|
+
const _mw: ClientMiddleware[] = []
|
|
98
50
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Register a client-side navigation middleware.
|
|
53
|
+
* Must be called **before** `boot()`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* useClientMiddleware(async (url, next) => {
|
|
57
|
+
* if (!isLoggedIn() && url.startsWith('/dashboard')) {
|
|
58
|
+
* await navigate('/login')
|
|
59
|
+
* return // cancel original navigation
|
|
60
|
+
* }
|
|
61
|
+
* await next()
|
|
62
|
+
* })
|
|
63
|
+
*/
|
|
64
|
+
export function useClientMiddleware(mw: ClientMiddleware): void {
|
|
65
|
+
_mw.push(mw)
|
|
106
66
|
}
|
|
107
67
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return { route: c.route, params }
|
|
115
|
-
}
|
|
68
|
+
async function runMiddleware(url: string, done: () => Promise<void>): Promise<void> {
|
|
69
|
+
const chain = [..._mw, async (_u: string, next: () => Promise<void>) => { await done(); await next() }]
|
|
70
|
+
let i = 0
|
|
71
|
+
const run = async (): Promise<void> => {
|
|
72
|
+
const fn = chain[i++]
|
|
73
|
+
if (fn) await fn(url, run)
|
|
116
74
|
}
|
|
117
|
-
|
|
75
|
+
await run()
|
|
118
76
|
}
|
|
119
77
|
|
|
120
78
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
121
|
-
// §
|
|
79
|
+
// § 4 SEO — client-side <head> sync
|
|
122
80
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
123
81
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
82
|
+
function setMeta(selector: string, attr: string, val: string | undefined): void {
|
|
83
|
+
if (!val) { document.querySelector(selector)?.remove(); return }
|
|
84
|
+
let el = document.querySelector<HTMLMetaElement>(selector)
|
|
85
|
+
if (!el) {
|
|
86
|
+
el = document.createElement('meta')
|
|
87
|
+
const m = /\[(\w+[:-]?\w*)="([^"]+)"\]/.exec(selector)
|
|
88
|
+
if (m) el.setAttribute(m[1], m[2])
|
|
89
|
+
document.head.appendChild(el)
|
|
90
|
+
}
|
|
91
|
+
el.setAttribute(attr, val)
|
|
132
92
|
}
|
|
133
93
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
94
|
+
function syncSEO(seo: SEOMeta): void {
|
|
95
|
+
if (seo.title) document.title = seo.title
|
|
96
|
+
|
|
97
|
+
setMeta('[name="description"]', 'content', seo.description)
|
|
98
|
+
setMeta('[name="keywords"]', 'content', seo.keywords)
|
|
99
|
+
setMeta('[name="robots"]', 'content', seo.robots)
|
|
100
|
+
setMeta('[name="theme-color"]', 'content', seo.themeColor)
|
|
101
|
+
setMeta('[property="og:title"]', 'content', seo.ogTitle)
|
|
102
|
+
setMeta('[property="og:description"]', 'content', seo.ogDescription)
|
|
103
|
+
setMeta('[property="og:image"]', 'content', seo.ogImage)
|
|
104
|
+
setMeta('[property="og:url"]', 'content', seo.ogUrl)
|
|
105
|
+
setMeta('[property="og:type"]', 'content', seo.ogType)
|
|
106
|
+
setMeta('[name="twitter:card"]', 'content', seo.twitterCard)
|
|
107
|
+
setMeta('[name="twitter:title"]', 'content', seo.twitterTitle)
|
|
108
|
+
setMeta('[name="twitter:description"]','content', seo.twitterDescription)
|
|
109
|
+
setMeta('[name="twitter:image"]', 'content', seo.twitterImage)
|
|
110
|
+
|
|
111
|
+
// Canonical link
|
|
112
|
+
const canon = seo.canonical
|
|
113
|
+
let linkEl = document.querySelector<HTMLLinkElement>('link[rel="canonical"]')
|
|
114
|
+
if (canon) {
|
|
115
|
+
if (!linkEl) {
|
|
116
|
+
linkEl = document.createElement('link')
|
|
117
|
+
linkEl.rel = 'canonical'
|
|
118
|
+
document.head.appendChild(linkEl)
|
|
119
|
+
}
|
|
120
|
+
linkEl.href = canon
|
|
121
|
+
} else {
|
|
122
|
+
linkEl?.remove()
|
|
123
|
+
}
|
|
138
124
|
}
|
|
139
125
|
|
|
140
126
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
141
|
-
// §
|
|
127
|
+
// § 5 Prefetch cache
|
|
142
128
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
130
|
+
interface NavPayload {
|
|
131
|
+
state: Record<string, unknown>
|
|
132
|
+
params: Record<string, string>
|
|
133
|
+
seo: SEOMeta
|
|
134
|
+
url: string
|
|
135
|
+
}
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
const _cache = new Map<string, Promise<NavPayload>>()
|
|
138
|
+
|
|
139
|
+
function fetchPayload(href: string): Promise<NavPayload> {
|
|
140
|
+
if (!_cache.has(href)) {
|
|
141
|
+
_cache.set(
|
|
142
|
+
href,
|
|
143
|
+
fetch(href, { headers: { [SPA_HEADER]: '1' } })
|
|
144
|
+
.then(r => {
|
|
145
|
+
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`)
|
|
146
|
+
return r.json() as Promise<NavPayload>
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
154
149
|
}
|
|
155
|
-
return
|
|
150
|
+
return _cache.get(href)!
|
|
156
151
|
}
|
|
157
152
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
url: string,
|
|
162
|
-
params: Record<string, string>
|
|
163
|
-
) {
|
|
164
|
-
const container = document.getElementById('fnetro-app')!
|
|
165
|
-
const pageNode = (jsx as any)(route.page.Page, { ...data, url, params })
|
|
166
|
-
const layout = route.layout !== undefined ? route.layout : currentLayout
|
|
167
|
-
const tree = layout
|
|
168
|
-
? (jsx as any)(layout.Component, { url, params, children: pageNode })
|
|
169
|
-
: pageNode
|
|
170
|
-
render(tree, container)
|
|
171
|
-
}
|
|
153
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// § 6 navigate / prefetch
|
|
155
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
172
156
|
|
|
173
157
|
export interface NavigateOptions {
|
|
174
158
|
replace?: boolean
|
|
175
|
-
scroll?:
|
|
159
|
+
scroll?: boolean
|
|
176
160
|
}
|
|
177
161
|
|
|
178
|
-
export async function navigate(
|
|
179
|
-
to: string,
|
|
180
|
-
opts: NavigateOptions = {}
|
|
181
|
-
): Promise<void> {
|
|
162
|
+
export async function navigate(to: string, opts: NavigateOptions = {}): Promise<void> {
|
|
182
163
|
const u = new URL(to, location.origin)
|
|
183
164
|
if (u.origin !== location.origin) { location.href = to; return }
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...(window as any)[STATE_KEY],
|
|
200
|
-
[u.pathname]: payload.state ?? {}
|
|
165
|
+
if (!findRoute(u.pathname)) { location.href = to; return }
|
|
166
|
+
|
|
167
|
+
await runMiddleware(u.pathname, async () => {
|
|
168
|
+
try {
|
|
169
|
+
const payload = await fetchPayload(u.toString())
|
|
170
|
+
history[opts.replace ? 'replaceState' : 'pushState'](
|
|
171
|
+
{ url: u.pathname }, '', u.pathname,
|
|
172
|
+
)
|
|
173
|
+
if (opts.scroll !== false) window.scrollTo(0, 0)
|
|
174
|
+
|
|
175
|
+
_setNav?.({ path: u.pathname, data: payload.state ?? {}, params: payload.params ?? {} })
|
|
176
|
+
syncSEO(payload.seo ?? {})
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('[fnetro] Navigation error:', err)
|
|
179
|
+
location.href = to
|
|
201
180
|
}
|
|
202
|
-
|
|
203
|
-
} catch (e) {
|
|
204
|
-
console.error('[fnetro] Navigation failed:', e)
|
|
205
|
-
location.href = to
|
|
206
|
-
}
|
|
181
|
+
})
|
|
207
182
|
}
|
|
208
183
|
|
|
209
|
-
/** Warm the prefetch cache for a URL
|
|
184
|
+
/** Warm the prefetch cache for a URL on hover/focus/etc. */
|
|
210
185
|
export function prefetch(url: string): void {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
186
|
+
try {
|
|
187
|
+
const u = new URL(url, location.origin)
|
|
188
|
+
if (u.origin !== location.origin || !findRoute(u.pathname)) return
|
|
189
|
+
fetchPayload(u.toString())
|
|
190
|
+
} catch { /* ignore invalid URLs */ }
|
|
215
191
|
}
|
|
216
192
|
|
|
217
193
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
218
|
-
// §
|
|
194
|
+
// § 7 DOM event intercepts
|
|
219
195
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
220
196
|
|
|
221
|
-
function
|
|
197
|
+
function onLinkClick(e: MouseEvent): void {
|
|
222
198
|
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
223
199
|
const a = e.composedPath().find(
|
|
224
|
-
(el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
|
|
200
|
+
(el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement,
|
|
225
201
|
)
|
|
226
202
|
if (!a?.href) return
|
|
227
203
|
if (a.target && a.target !== '_self') return
|
|
@@ -232,76 +208,120 @@ function interceptClicks(e: MouseEvent) {
|
|
|
232
208
|
navigate(a.href)
|
|
233
209
|
}
|
|
234
210
|
|
|
235
|
-
function
|
|
211
|
+
function onLinkHover(e: MouseEvent): void {
|
|
236
212
|
const a = e.composedPath().find(
|
|
237
|
-
(el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
|
|
213
|
+
(el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement,
|
|
238
214
|
)
|
|
239
215
|
if (a?.href) prefetch(a.href)
|
|
240
216
|
}
|
|
241
217
|
|
|
242
|
-
function onPopState() {
|
|
218
|
+
function onPopState(): void {
|
|
243
219
|
navigate(location.href, { replace: true, scroll: false })
|
|
244
220
|
}
|
|
245
221
|
|
|
246
222
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
247
|
-
// §
|
|
223
|
+
// § 8 App root component (created inside hydrate's reactive owner)
|
|
224
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
function AppRoot(props: { initial: NavState; appLayout: LayoutDef | undefined }): any {
|
|
227
|
+
const [nav, setNav] = createSignal<NavState>(props.initial)
|
|
228
|
+
// Expose setter so navigate() can trigger re-renders
|
|
229
|
+
_setNav = setNav
|
|
230
|
+
|
|
231
|
+
const view = createMemo(() => {
|
|
232
|
+
const { path, data, params } = nav()
|
|
233
|
+
const m = findRoute(path)
|
|
234
|
+
|
|
235
|
+
if (!m) {
|
|
236
|
+
// No match client-side — shouldn't happen but handle gracefully
|
|
237
|
+
return null as any
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const layout = m.route.layout !== undefined ? m.route.layout : props.appLayout
|
|
241
|
+
const pageEl = createComponent(m.route.page.Page as any, { ...data, url: path, params })
|
|
242
|
+
|
|
243
|
+
if (!layout) return pageEl
|
|
244
|
+
|
|
245
|
+
return createComponent(layout.Component as any, {
|
|
246
|
+
url: path,
|
|
247
|
+
params,
|
|
248
|
+
get children() { return pageEl },
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return view
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
256
|
+
// § 9 boot()
|
|
248
257
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
249
258
|
|
|
250
259
|
export interface BootOptions extends AppConfig {
|
|
251
|
-
/**
|
|
252
|
-
* Enable hover-based prefetching (default: true).
|
|
253
|
-
* Fires a SPA fetch when the user hovers any <a> that matches a route.
|
|
254
|
-
*/
|
|
260
|
+
/** Enable hover-based prefetching. @default true */
|
|
255
261
|
prefetchOnHover?: boolean
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
export async function boot(options: BootOptions): Promise<void> {
|
|
259
265
|
const { pages } = resolveRoutes(options.routes, {
|
|
260
|
-
layout:
|
|
266
|
+
layout: options.layout,
|
|
261
267
|
middleware: [],
|
|
262
268
|
})
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
currentLayout = options.layout
|
|
270
|
+
_routes = pages.map(r => ({ route: r, cp: compilePath(r.fullPath) }))
|
|
271
|
+
_appLayout = options.layout
|
|
267
272
|
|
|
268
273
|
const pathname = location.pathname
|
|
269
|
-
|
|
274
|
+
if (!findRoute(pathname)) {
|
|
275
|
+
console.warn(`[fnetro] No route matched "${pathname}" — skipping hydration`)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Server-injected initial state (no refetch needed on first load)
|
|
280
|
+
const stateMap = (window as any)[STATE_KEY] as Record<string, Record<string, unknown>> ?? {}
|
|
281
|
+
const paramsMap = (window as any)[PARAMS_KEY] as Record<string, string> ?? {}
|
|
282
|
+
const seoData = (window as any)[SEO_KEY] as SEOMeta ?? {}
|
|
283
|
+
|
|
284
|
+
const initial: NavState = {
|
|
285
|
+
path: pathname,
|
|
286
|
+
data: stateMap[pathname] ?? {},
|
|
287
|
+
params: paramsMap,
|
|
288
|
+
}
|
|
270
289
|
|
|
271
|
-
|
|
272
|
-
|
|
290
|
+
const container = document.getElementById('fnetro-app')
|
|
291
|
+
if (!container) {
|
|
292
|
+
console.error('[fnetro] #fnetro-app not found — aborting hydration')
|
|
273
293
|
return
|
|
274
294
|
}
|
|
275
295
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
const paramsMap: Record<string, string> = (window as any)[PARAMS_KEY] ?? {}
|
|
279
|
-
const data = stateMap[pathname] ?? {}
|
|
296
|
+
// Sync initial SEO (document.title etc.)
|
|
297
|
+
syncSEO(seoData)
|
|
280
298
|
|
|
281
|
-
|
|
299
|
+
// Hydrate the server-rendered HTML with SolidJS
|
|
300
|
+
hydrate(
|
|
301
|
+
() => createComponent(AppRoot as any, { initial, appLayout: _appLayout }) as any,
|
|
302
|
+
container,
|
|
303
|
+
)
|
|
282
304
|
|
|
283
|
-
// Wire up navigation
|
|
284
|
-
document.addEventListener('click',
|
|
305
|
+
// Wire up SPA navigation
|
|
306
|
+
document.addEventListener('click', onLinkClick)
|
|
285
307
|
if (options.prefetchOnHover !== false) {
|
|
286
|
-
document.addEventListener('mouseover',
|
|
308
|
+
document.addEventListener('mouseover', onLinkHover)
|
|
287
309
|
}
|
|
288
310
|
window.addEventListener('popstate', onPopState)
|
|
289
|
-
|
|
290
|
-
for (const fn of afterNavListeners) await fn(pathname)
|
|
291
311
|
}
|
|
292
312
|
|
|
293
313
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
294
|
-
// §
|
|
314
|
+
// § 10 Re-exports
|
|
295
315
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
316
|
+
|
|
296
317
|
export {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
triggerRef, use, useLocalRef, useLocalReactive,
|
|
301
|
-
definePage, defineGroup, defineLayout, defineMiddleware, defineApiRoute,
|
|
318
|
+
definePage, defineGroup, defineLayout, defineApiRoute,
|
|
319
|
+
resolveRoutes, compilePath, matchPath,
|
|
320
|
+
SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY,
|
|
302
321
|
} from './core'
|
|
322
|
+
|
|
303
323
|
export type {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
324
|
+
AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef, Route,
|
|
325
|
+
PageProps, LayoutProps, SEOMeta, HonoMiddleware, LoaderCtx,
|
|
326
|
+
ResolvedRoute, CompiledPath, ClientMiddleware,
|
|
307
327
|
} from './core'
|