@netrojs/fnetro 0.1.2 → 0.1.4

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.
Files changed (5) hide show
  1. package/README.md +1194 -179
  2. package/client.ts +307 -307
  3. package/core.ts +734 -734
  4. package/package.json +91 -91
  5. package/server.ts +415 -415
package/client.ts CHANGED
@@ -1,307 +1,307 @@
1
- // ─────────────────────────────────────────────────────────────────────────────
2
- // FNetro · client.ts
3
- // SPA runtime · hook patching · navigation · prefetch · lifecycle
4
- // ─────────────────────────────────────────────────────────────────────────────
5
-
6
- import { render } from 'hono/jsx/dom'
7
- import { jsx } from 'hono/jsx'
8
- import {
9
- useState, useEffect, useMemo, useRef as useHonoRef,
10
- useSyncExternalStore,
11
- } from 'hono/jsx'
12
- import {
13
- __hooks, ref, reactive, computed, watchEffect, isRef,
14
- SPA_HEADER, STATE_KEY, PARAMS_KEY,
15
- type Ref, type AppConfig, type ResolvedRoute,
16
- type LayoutDef,
17
- } from './core'
18
- import { resolveRoutes } from './core'
19
-
20
- // ══════════════════════════════════════════════════════════════════════════════
21
- // § 1 Patch reactivity hooks for hono/jsx/dom
22
- // ══════════════════════════════════════════════════════════════════════════════
23
-
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
- }
43
-
44
- /**
45
- * Component-local Ref — stable across re-renders, lost on unmount.
46
- */
47
- function clientUseLocalRef<T>(init: T): Ref<T> {
48
- // Create the ref once (stable ref object via hono's useRef)
49
- const stableRef = useHonoRef<Ref<T> | null>(null)
50
- if (stableRef.current === null) stableRef.current = ref(init)
51
- const r = stableRef.current!
52
- // Subscribe so mutations trigger re-render
53
- useSyncExternalStore(
54
- (notify) => (r as any).subscribe(notify),
55
- () => (r as any).peek?.() ?? r.value,
56
- )
57
- return r
58
- }
59
-
60
- /**
61
- * Component-local reactive object — deep proxy, re-renders on any mutation.
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
- }, [])
78
-
79
- return proxy
80
- }
81
-
82
- // Patch the module-level hook table
83
- Object.assign(__hooks, {
84
- useValue: clientUseValue,
85
- useLocalRef: clientUseLocalRef,
86
- useLocalReactive: clientUseLocalReactive,
87
- })
88
-
89
- // ══════════════════════════════════════════════════════════════════════════════
90
- // § 2 Path matching (mirrors server)
91
- // ══════════════════════════════════════════════════════════════════════════════
92
-
93
- interface CompiledRoute {
94
- route: ResolvedRoute
95
- re: RegExp
96
- keys: string[]
97
- }
98
-
99
- function compileRoute(r: ResolvedRoute): CompiledRoute {
100
- const keys: string[] = []
101
- const src = r.fullPath
102
- .replace(/\[\.\.\.([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '(.*)' })
103
- .replace(/\[([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '([^/]+)' })
104
- .replace(/\*/g, '(.*)')
105
- return { route: r, re: new RegExp(`^${src}$`), keys }
106
- }
107
-
108
- function matchRoute(compiled: CompiledRoute[], pathname: string) {
109
- for (const c of compiled) {
110
- const m = pathname.match(c.re)
111
- if (m) {
112
- const params: Record<string, string> = {}
113
- c.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1]) })
114
- return { route: c.route, params }
115
- }
116
- }
117
- return null
118
- }
119
-
120
- // ══════════════════════════════════════════════════════════════════════════════
121
- // § 3 Navigation lifecycle hooks
122
- // ══════════════════════════════════════════════════════════════════════════════
123
-
124
- type NavListener = (url: string) => void | Promise<void>
125
- const beforeNavListeners: NavListener[] = []
126
- const afterNavListeners: NavListener[] = []
127
-
128
- /** Called before each SPA navigation. Returning false cancels. */
129
- export function onBeforeNavigate(fn: NavListener): () => void {
130
- beforeNavListeners.push(fn)
131
- return () => beforeNavListeners.splice(beforeNavListeners.indexOf(fn), 1)
132
- }
133
-
134
- /** Called after each SPA navigation (including initial boot). */
135
- export function onAfterNavigate(fn: NavListener): () => void {
136
- afterNavListeners.push(fn)
137
- return () => afterNavListeners.splice(afterNavListeners.indexOf(fn), 1)
138
- }
139
-
140
- // ══════════════════════════════════════════════════════════════════════════════
141
- // § 4 SPA navigation
142
- // ══════════════════════════════════════════════════════════════════════════════
143
-
144
- let compiled: CompiledRoute[] = []
145
- let currentConfig: AppConfig
146
- let currentLayout: LayoutDef | undefined
147
- const prefetchCache = new Map<string, Promise<any>>()
148
-
149
- function fetchPage(url: string): Promise<any> {
150
- if (!prefetchCache.has(url)) {
151
- prefetchCache.set(url, fetch(url, {
152
- headers: { [SPA_HEADER]: '1' }
153
- }).then(r => r.json()))
154
- }
155
- return prefetchCache.get(url)!
156
- }
157
-
158
- async function renderPage(
159
- route: ResolvedRoute,
160
- data: object,
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
- }
172
-
173
- export interface NavigateOptions {
174
- replace?: boolean
175
- scroll?: boolean
176
- }
177
-
178
- export async function navigate(
179
- to: string,
180
- opts: NavigateOptions = {}
181
- ): Promise<void> {
182
- const u = new URL(to, location.origin)
183
- if (u.origin !== location.origin) { location.href = to; return }
184
-
185
- // Run before-nav hooks
186
- for (const fn of beforeNavListeners) await fn(u.pathname)
187
-
188
- const match = matchRoute(compiled, u.pathname)
189
- if (!match) { location.href = to; return }
190
-
191
- try {
192
- const payload = await fetchPage(u.toString())
193
- const method = opts.replace ? 'replaceState' : 'pushState'
194
- history[method]({ url: u.pathname }, '', u.pathname)
195
- if (opts.scroll !== false) window.scrollTo(0, 0)
196
- await renderPage(match.route, payload.state ?? {}, u.pathname, payload.params ?? {})
197
- // Cache state for popstate
198
- ;(window as any)[STATE_KEY] = {
199
- ...(window as any)[STATE_KEY],
200
- [u.pathname]: payload.state ?? {}
201
- }
202
- for (const fn of afterNavListeners) await fn(u.pathname)
203
- } catch (e) {
204
- console.error('[fnetro] Navigation failed:', e)
205
- location.href = to
206
- }
207
- }
208
-
209
- /** Warm the prefetch cache for a URL (call on hover / mousedown). */
210
- export function prefetch(url: string): void {
211
- const u = new URL(url, location.origin)
212
- if (u.origin !== location.origin) return
213
- if (!matchRoute(compiled, u.pathname)) return
214
- fetchPage(u.toString())
215
- }
216
-
217
- // ══════════════════════════════════════════════════════════════════════════════
218
- // § 5 Click interceptor + popstate
219
- // ══════════════════════════════════════════════════════════════════════════════
220
-
221
- function interceptClicks(e: MouseEvent) {
222
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
223
- const a = e.composedPath().find(
224
- (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
225
- )
226
- if (!a?.href) return
227
- if (a.target && a.target !== '_self') return
228
- if (a.hasAttribute('data-no-spa') || a.rel?.includes('external')) return
229
- const u = new URL(a.href)
230
- if (u.origin !== location.origin) return
231
- e.preventDefault()
232
- navigate(a.href)
233
- }
234
-
235
- function interceptHover(e: MouseEvent) {
236
- const a = e.composedPath().find(
237
- (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
238
- )
239
- if (a?.href) prefetch(a.href)
240
- }
241
-
242
- function onPopState() {
243
- navigate(location.href, { replace: true, scroll: false })
244
- }
245
-
246
- // ══════════════════════════════════════════════════════════════════════════════
247
- // § 6 boot()
248
- // ══════════════════════════════════════════════════════════════════════════════
249
-
250
- 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
- */
255
- prefetchOnHover?: boolean
256
- }
257
-
258
- export async function boot(options: BootOptions): Promise<void> {
259
- const { pages } = resolveRoutes(options.routes, {
260
- layout: options.layout,
261
- middleware: [],
262
- })
263
-
264
- compiled = pages.map(compileRoute)
265
- currentConfig = options
266
- currentLayout = options.layout
267
-
268
- const pathname = location.pathname
269
- const match = matchRoute(compiled, pathname)
270
-
271
- if (!match) {
272
- console.warn(`[fnetro] No route matched "${pathname}" — not hydrating`)
273
- return
274
- }
275
-
276
- // Read server-injected state (no refetch!)
277
- const stateMap: Record<string, object> = (window as any)[STATE_KEY] ?? {}
278
- const paramsMap: Record<string, string> = (window as any)[PARAMS_KEY] ?? {}
279
- const data = stateMap[pathname] ?? {}
280
-
281
- await renderPage(match.route, data, pathname, paramsMap)
282
-
283
- // Wire up navigation
284
- document.addEventListener('click', interceptClicks)
285
- if (options.prefetchOnHover !== false) {
286
- document.addEventListener('mouseover', interceptHover)
287
- }
288
- window.addEventListener('popstate', onPopState)
289
-
290
- for (const fn of afterNavListeners) await fn(pathname)
291
- }
292
-
293
- // ══════════════════════════════════════════════════════════════════════════════
294
- // § 7 Re-export core for client code that imports only client.ts
295
- // ══════════════════════════════════════════════════════════════════════════════
296
- export {
297
- ref, shallowRef, reactive, shallowReactive, readonly,
298
- computed, effect, watch, watchEffect, effectScope,
299
- toRef, toRefs, unref, isRef, isReactive, isReadonly, markRaw, toRaw,
300
- triggerRef, use, useLocalRef, useLocalReactive,
301
- definePage, defineGroup, defineLayout, defineMiddleware, defineApiRoute,
302
- } from './core'
303
- export type {
304
- Ref, ComputedRef, WritableComputedRef,
305
- AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef,
306
- WatchSource, WatchOptions,
307
- } from './core'
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // FNetro · client.ts
3
+ // SPA runtime · hook patching · navigation · prefetch · lifecycle
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+
6
+ import { render } from 'hono/jsx/dom'
7
+ import { jsx } from 'hono/jsx'
8
+ import {
9
+ useState, useEffect, useMemo, useRef as useHonoRef,
10
+ useSyncExternalStore,
11
+ } from 'hono/jsx'
12
+ import {
13
+ __hooks, ref, reactive, computed, watchEffect, isRef,
14
+ SPA_HEADER, STATE_KEY, PARAMS_KEY,
15
+ type Ref, type AppConfig, type ResolvedRoute,
16
+ type LayoutDef,
17
+ } from './core'
18
+ import { resolveRoutes } from './core'
19
+
20
+ // ══════════════════════════════════════════════════════════════════════════════
21
+ // § 1 Patch reactivity hooks for hono/jsx/dom
22
+ // ══════════════════════════════════════════════════════════════════════════════
23
+
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
+ }
43
+
44
+ /**
45
+ * Component-local Ref — stable across re-renders, lost on unmount.
46
+ */
47
+ function clientUseLocalRef<T>(init: T): Ref<T> {
48
+ // Create the ref once (stable ref object via hono's useRef)
49
+ const stableRef = useHonoRef<Ref<T> | null>(null)
50
+ if (stableRef.current === null) stableRef.current = ref(init)
51
+ const r = stableRef.current!
52
+ // Subscribe so mutations trigger re-render
53
+ useSyncExternalStore(
54
+ (notify) => (r as any).subscribe(notify),
55
+ () => (r as any).peek?.() ?? r.value,
56
+ )
57
+ return r
58
+ }
59
+
60
+ /**
61
+ * Component-local reactive object — deep proxy, re-renders on any mutation.
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
+ }, [])
78
+
79
+ return proxy
80
+ }
81
+
82
+ // Patch the module-level hook table
83
+ Object.assign(__hooks, {
84
+ useValue: clientUseValue,
85
+ useLocalRef: clientUseLocalRef,
86
+ useLocalReactive: clientUseLocalReactive,
87
+ })
88
+
89
+ // ══════════════════════════════════════════════════════════════════════════════
90
+ // § 2 Path matching (mirrors server)
91
+ // ══════════════════════════════════════════════════════════════════════════════
92
+
93
+ interface CompiledRoute {
94
+ route: ResolvedRoute
95
+ re: RegExp
96
+ keys: string[]
97
+ }
98
+
99
+ function compileRoute(r: ResolvedRoute): CompiledRoute {
100
+ const keys: string[] = []
101
+ const src = r.fullPath
102
+ .replace(/\[\.\.\.([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '(.*)' })
103
+ .replace(/\[([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '([^/]+)' })
104
+ .replace(/\*/g, '(.*)')
105
+ return { route: r, re: new RegExp(`^${src}$`), keys }
106
+ }
107
+
108
+ function matchRoute(compiled: CompiledRoute[], pathname: string) {
109
+ for (const c of compiled) {
110
+ const m = pathname.match(c.re)
111
+ if (m) {
112
+ const params: Record<string, string> = {}
113
+ c.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1]) })
114
+ return { route: c.route, params }
115
+ }
116
+ }
117
+ return null
118
+ }
119
+
120
+ // ══════════════════════════════════════════════════════════════════════════════
121
+ // § 3 Navigation lifecycle hooks
122
+ // ══════════════════════════════════════════════════════════════════════════════
123
+
124
+ type NavListener = (url: string) => void | Promise<void>
125
+ const beforeNavListeners: NavListener[] = []
126
+ const afterNavListeners: NavListener[] = []
127
+
128
+ /** Called before each SPA navigation. Returning false cancels. */
129
+ export function onBeforeNavigate(fn: NavListener): () => void {
130
+ beforeNavListeners.push(fn)
131
+ return () => beforeNavListeners.splice(beforeNavListeners.indexOf(fn), 1)
132
+ }
133
+
134
+ /** Called after each SPA navigation (including initial boot). */
135
+ export function onAfterNavigate(fn: NavListener): () => void {
136
+ afterNavListeners.push(fn)
137
+ return () => afterNavListeners.splice(afterNavListeners.indexOf(fn), 1)
138
+ }
139
+
140
+ // ══════════════════════════════════════════════════════════════════════════════
141
+ // § 4 SPA navigation
142
+ // ══════════════════════════════════════════════════════════════════════════════
143
+
144
+ let compiled: CompiledRoute[] = []
145
+ let currentConfig: AppConfig
146
+ let currentLayout: LayoutDef | undefined
147
+ const prefetchCache = new Map<string, Promise<any>>()
148
+
149
+ function fetchPage(url: string): Promise<any> {
150
+ if (!prefetchCache.has(url)) {
151
+ prefetchCache.set(url, fetch(url, {
152
+ headers: { [SPA_HEADER]: '1' }
153
+ }).then(r => r.json()))
154
+ }
155
+ return prefetchCache.get(url)!
156
+ }
157
+
158
+ async function renderPage(
159
+ route: ResolvedRoute,
160
+ data: object,
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
+ }
172
+
173
+ export interface NavigateOptions {
174
+ replace?: boolean
175
+ scroll?: boolean
176
+ }
177
+
178
+ export async function navigate(
179
+ to: string,
180
+ opts: NavigateOptions = {}
181
+ ): Promise<void> {
182
+ const u = new URL(to, location.origin)
183
+ if (u.origin !== location.origin) { location.href = to; return }
184
+
185
+ // Run before-nav hooks
186
+ for (const fn of beforeNavListeners) await fn(u.pathname)
187
+
188
+ const match = matchRoute(compiled, u.pathname)
189
+ if (!match) { location.href = to; return }
190
+
191
+ try {
192
+ const payload = await fetchPage(u.toString())
193
+ const method = opts.replace ? 'replaceState' : 'pushState'
194
+ history[method]({ url: u.pathname }, '', u.pathname)
195
+ if (opts.scroll !== false) window.scrollTo(0, 0)
196
+ await renderPage(match.route, payload.state ?? {}, u.pathname, payload.params ?? {})
197
+ // Cache state for popstate
198
+ ;(window as any)[STATE_KEY] = {
199
+ ...(window as any)[STATE_KEY],
200
+ [u.pathname]: payload.state ?? {}
201
+ }
202
+ for (const fn of afterNavListeners) await fn(u.pathname)
203
+ } catch (e) {
204
+ console.error('[fnetro] Navigation failed:', e)
205
+ location.href = to
206
+ }
207
+ }
208
+
209
+ /** Warm the prefetch cache for a URL (call on hover / mousedown). */
210
+ export function prefetch(url: string): void {
211
+ const u = new URL(url, location.origin)
212
+ if (u.origin !== location.origin) return
213
+ if (!matchRoute(compiled, u.pathname)) return
214
+ fetchPage(u.toString())
215
+ }
216
+
217
+ // ══════════════════════════════════════════════════════════════════════════════
218
+ // § 5 Click interceptor + popstate
219
+ // ══════════════════════════════════════════════════════════════════════════════
220
+
221
+ function interceptClicks(e: MouseEvent) {
222
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
223
+ const a = e.composedPath().find(
224
+ (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
225
+ )
226
+ if (!a?.href) return
227
+ if (a.target && a.target !== '_self') return
228
+ if (a.hasAttribute('data-no-spa') || a.rel?.includes('external')) return
229
+ const u = new URL(a.href)
230
+ if (u.origin !== location.origin) return
231
+ e.preventDefault()
232
+ navigate(a.href)
233
+ }
234
+
235
+ function interceptHover(e: MouseEvent) {
236
+ const a = e.composedPath().find(
237
+ (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement
238
+ )
239
+ if (a?.href) prefetch(a.href)
240
+ }
241
+
242
+ function onPopState() {
243
+ navigate(location.href, { replace: true, scroll: false })
244
+ }
245
+
246
+ // ══════════════════════════════════════════════════════════════════════════════
247
+ // § 6 boot()
248
+ // ══════════════════════════════════════════════════════════════════════════════
249
+
250
+ 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
+ */
255
+ prefetchOnHover?: boolean
256
+ }
257
+
258
+ export async function boot(options: BootOptions): Promise<void> {
259
+ const { pages } = resolveRoutes(options.routes, {
260
+ layout: options.layout,
261
+ middleware: [],
262
+ })
263
+
264
+ compiled = pages.map(compileRoute)
265
+ currentConfig = options
266
+ currentLayout = options.layout
267
+
268
+ const pathname = location.pathname
269
+ const match = matchRoute(compiled, pathname)
270
+
271
+ if (!match) {
272
+ console.warn(`[fnetro] No route matched "${pathname}" — not hydrating`)
273
+ return
274
+ }
275
+
276
+ // Read server-injected state (no refetch!)
277
+ const stateMap: Record<string, object> = (window as any)[STATE_KEY] ?? {}
278
+ const paramsMap: Record<string, string> = (window as any)[PARAMS_KEY] ?? {}
279
+ const data = stateMap[pathname] ?? {}
280
+
281
+ await renderPage(match.route, data, pathname, paramsMap)
282
+
283
+ // Wire up navigation
284
+ document.addEventListener('click', interceptClicks)
285
+ if (options.prefetchOnHover !== false) {
286
+ document.addEventListener('mouseover', interceptHover)
287
+ }
288
+ window.addEventListener('popstate', onPopState)
289
+
290
+ for (const fn of afterNavListeners) await fn(pathname)
291
+ }
292
+
293
+ // ══════════════════════════════════════════════════════════════════════════════
294
+ // § 7 Re-export core for client code that imports only client.ts
295
+ // ══════════════════════════════════════════════════════════════════════════════
296
+ export {
297
+ ref, shallowRef, reactive, shallowReactive, readonly,
298
+ computed, effect, watch, watchEffect, effectScope,
299
+ toRef, toRefs, unref, isRef, isReactive, isReadonly, markRaw, toRaw,
300
+ triggerRef, use, useLocalRef, useLocalReactive,
301
+ definePage, defineGroup, defineLayout, defineMiddleware, defineApiRoute,
302
+ } from './core'
303
+ export type {
304
+ Ref, ComputedRef, WritableComputedRef,
305
+ AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef,
306
+ WatchSource, WatchOptions,
307
+ } from './core'