@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.
- package/README.md +1194 -179
- package/client.ts +307 -307
- package/core.ts +734 -734
- package/package.json +91 -91
- package/server.ts +415 -415
package/core.ts
CHANGED
|
@@ -1,734 +1,734 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// FNetro · core.ts
|
|
3
|
-
// Full Vue-like reactivity + route / layout / middleware definitions
|
|
4
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
import type { Context, MiddlewareHandler, Hono } from 'hono'
|
|
7
|
-
|
|
8
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
9
|
-
// § 1 Dependency tracking
|
|
10
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
11
|
-
|
|
12
|
-
export type EffectFn = () => void | (() => void)
|
|
13
|
-
|
|
14
|
-
const RAW = Symbol('raw')
|
|
15
|
-
const IS_REACTIVE = Symbol('isReactive')
|
|
16
|
-
const IS_READONLY = Symbol('isReadonly')
|
|
17
|
-
const IS_REF = Symbol('isRef')
|
|
18
|
-
const MARK_RAW = Symbol('markRaw')
|
|
19
|
-
|
|
20
|
-
// Per-target, per-key subscriber sets
|
|
21
|
-
const targetMap = new WeakMap<object, Map<PropertyKey, Set<ReactiveEffect>>>()
|
|
22
|
-
|
|
23
|
-
let activeEffect: ReactiveEffect | null = null
|
|
24
|
-
let shouldTrack = true
|
|
25
|
-
let trackStack: boolean[] = []
|
|
26
|
-
|
|
27
|
-
function pauseTracking() { trackStack.push(shouldTrack); shouldTrack = false }
|
|
28
|
-
function resetTracking() { shouldTrack = trackStack.pop() ?? true }
|
|
29
|
-
|
|
30
|
-
function track(target: object, key: PropertyKey) {
|
|
31
|
-
if (!shouldTrack || !activeEffect) return
|
|
32
|
-
let depsMap = targetMap.get(target)
|
|
33
|
-
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
|
|
34
|
-
let dep = depsMap.get(key)
|
|
35
|
-
if (!dep) depsMap.set(key, (dep = new Set()))
|
|
36
|
-
trackEffect(activeEffect, dep)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function trackEffect(effect: ReactiveEffect, dep: Set<ReactiveEffect>) {
|
|
40
|
-
if (!dep.has(effect)) {
|
|
41
|
-
dep.add(effect)
|
|
42
|
-
effect.deps.push(dep)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function trigger(target: object, key: PropertyKey, newVal?: unknown, oldVal?: unknown) {
|
|
47
|
-
const depsMap = targetMap.get(target)
|
|
48
|
-
if (!depsMap) return
|
|
49
|
-
const effects: ReactiveEffect[] = []
|
|
50
|
-
const computedEffects: ReactiveEffect[] = []
|
|
51
|
-
|
|
52
|
-
depsMap.get(key)?.forEach(e => {
|
|
53
|
-
if (e !== activeEffect) {
|
|
54
|
-
e.computed ? computedEffects.push(e) : effects.push(e)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
// Computed run first so dependents see fresh values
|
|
58
|
-
;[...computedEffects, ...effects].forEach(e => {
|
|
59
|
-
if (e.active) e.scheduler ? e.scheduler() : e.run()
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
64
|
-
// § 2 ReactiveEffect
|
|
65
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
66
|
-
|
|
67
|
-
export class ReactiveEffect {
|
|
68
|
-
deps: Set<ReactiveEffect>[] = []
|
|
69
|
-
active = true
|
|
70
|
-
cleanup?: () => void
|
|
71
|
-
computed = false
|
|
72
|
-
|
|
73
|
-
constructor(
|
|
74
|
-
public fn: () => any,
|
|
75
|
-
public scheduler?: () => void,
|
|
76
|
-
public scope?: EffectScope,
|
|
77
|
-
) {
|
|
78
|
-
scope?.effects.push(this)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
run(): any {
|
|
82
|
-
if (!this.active) return this.fn()
|
|
83
|
-
const prevEffect = activeEffect
|
|
84
|
-
const prevShouldTrack = shouldTrack
|
|
85
|
-
shouldTrack = true
|
|
86
|
-
activeEffect = this
|
|
87
|
-
this.cleanup?.()
|
|
88
|
-
this.cleanup = undefined
|
|
89
|
-
this.deps.length = 0
|
|
90
|
-
try {
|
|
91
|
-
const result = this.fn()
|
|
92
|
-
if (typeof result === 'function') this.cleanup = result
|
|
93
|
-
return result
|
|
94
|
-
} finally {
|
|
95
|
-
activeEffect = prevEffect
|
|
96
|
-
shouldTrack = prevShouldTrack
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
stop() {
|
|
101
|
-
if (this.active) {
|
|
102
|
-
cleanupEffect(this)
|
|
103
|
-
this.active = false
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function cleanupEffect(e: ReactiveEffect) {
|
|
109
|
-
e.deps.forEach(dep => dep.delete(e))
|
|
110
|
-
e.deps.length = 0
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
114
|
-
// § 3 EffectScope
|
|
115
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
116
|
-
|
|
117
|
-
let activeScope: EffectScope | undefined
|
|
118
|
-
|
|
119
|
-
export class EffectScope {
|
|
120
|
-
effects: ReactiveEffect[] = []
|
|
121
|
-
cleanups: (() => void)[] = []
|
|
122
|
-
active = true
|
|
123
|
-
|
|
124
|
-
run<T>(fn: () => T): T {
|
|
125
|
-
const prev = activeScope
|
|
126
|
-
activeScope = this
|
|
127
|
-
try { return fn() }
|
|
128
|
-
finally { activeScope = prev }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
stop() {
|
|
132
|
-
if (this.active) {
|
|
133
|
-
this.effects.forEach(e => e.stop())
|
|
134
|
-
this.cleanups.forEach(fn => fn())
|
|
135
|
-
this.active = false
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
onCleanup(fn: () => void) { this.cleanups.push(fn) }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function effectScope(): EffectScope {
|
|
143
|
-
return new EffectScope()
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function getCurrentScope(): EffectScope | undefined {
|
|
147
|
-
return activeScope
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function onScopeDispose(fn: () => void) {
|
|
151
|
-
activeScope?.onCleanup(fn)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
155
|
-
// § 4 effect / watchEffect
|
|
156
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
157
|
-
|
|
158
|
-
export function effect(fn: EffectFn): () => void {
|
|
159
|
-
const e = new ReactiveEffect(fn, undefined, activeScope)
|
|
160
|
-
e.run()
|
|
161
|
-
return () => e.stop()
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface WatchEffectOptions {
|
|
165
|
-
flush?: 'sync' | 'post'
|
|
166
|
-
onTrack?: (e: any) => void
|
|
167
|
-
onTrigger?: (e: any) => void
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function watchEffect(fn: EffectFn, opts?: WatchEffectOptions): () => void {
|
|
171
|
-
const e = new ReactiveEffect(fn, undefined, activeScope)
|
|
172
|
-
e.run()
|
|
173
|
-
return () => e.stop()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
177
|
-
// § 5 Ref
|
|
178
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
179
|
-
|
|
180
|
-
export interface Ref<T = unknown> {
|
|
181
|
-
value: T
|
|
182
|
-
readonly [IS_REF]: true
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const refTarget = Symbol('refTarget')
|
|
186
|
-
|
|
187
|
-
class RefImpl<T> implements Ref<T> {
|
|
188
|
-
readonly [IS_REF] = true as const
|
|
189
|
-
private _value: T
|
|
190
|
-
private _subscribers = new Set<() => void>()
|
|
191
|
-
|
|
192
|
-
constructor(value: T, private readonly shallow = false) {
|
|
193
|
-
this._value = shallow ? value : toReactive(value)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
get value(): T {
|
|
197
|
-
track(this as any, refTarget)
|
|
198
|
-
this._subscribers.forEach(fn => { /* for useSyncExternalStore */ })
|
|
199
|
-
return this._value
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
set value(next: T) {
|
|
203
|
-
const newVal = this.shallow ? next : toReactive(next)
|
|
204
|
-
if (!hasChanged(newVal, this._value)) return
|
|
205
|
-
this._value = newVal
|
|
206
|
-
trigger(this as any, refTarget, newVal, this._value)
|
|
207
|
-
this._subscribers.forEach(fn => fn())
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Subscribe for useSyncExternalStore */
|
|
211
|
-
subscribe(fn: () => void): () => void {
|
|
212
|
-
this._subscribers.add(fn)
|
|
213
|
-
return () => this._subscribers.delete(fn)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
peek(): T { return this._value }
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function ref<T>(value: T): Ref<T> {
|
|
220
|
-
return isRef(value) ? value as Ref<T> : new RefImpl(value)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function shallowRef<T>(value: T): Ref<T> {
|
|
224
|
-
return new RefImpl(value, true)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function triggerRef(r: Ref): void {
|
|
228
|
-
if (r instanceof RefImpl) {
|
|
229
|
-
trigger(r as any, refTarget)
|
|
230
|
-
;(r as any)._subscribers.forEach((fn: () => void) => fn())
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function isRef<T = unknown>(r: unknown): r is Ref<T> {
|
|
235
|
-
return !!r && typeof r === 'object' && (r as any)[IS_REF] === true
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function unref<T>(r: T | Ref<T>): T {
|
|
239
|
-
return isRef(r) ? r.value : r
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function toRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<T[K]> {
|
|
243
|
-
const r = new RefImpl<T[K]>(undefined as T[K], false)
|
|
244
|
-
Object.defineProperty(r, 'value', {
|
|
245
|
-
get() { track(r as any, refTarget); return obj[key] },
|
|
246
|
-
set(v: T[K]) { obj[key] = v; trigger(r as any, refTarget, v, obj[key]) }
|
|
247
|
-
})
|
|
248
|
-
return r
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function toRefs<T extends object>(obj: T): { [K in keyof T]: Ref<T[K]> } {
|
|
252
|
-
const result = {} as { [K in keyof T]: Ref<T[K]> }
|
|
253
|
-
for (const key in obj) result[key] = toRef(obj, key)
|
|
254
|
-
return result
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
258
|
-
// § 6 Computed
|
|
259
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
260
|
-
|
|
261
|
-
export interface WritableComputedRef<T> extends Ref<T> {
|
|
262
|
-
readonly effect: ReactiveEffect
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export interface ComputedRef<T> extends WritableComputedRef<T> {
|
|
266
|
-
readonly value: T
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
class ComputedRefImpl<T> implements Ref<T> {
|
|
270
|
-
readonly [IS_REF] = true as const
|
|
271
|
-
readonly effect: ReactiveEffect
|
|
272
|
-
private _value!: T
|
|
273
|
-
private _dirty = true
|
|
274
|
-
private _subscribers = new Set<() => void>()
|
|
275
|
-
|
|
276
|
-
constructor(getter: () => T, private setter?: (v: T) => void) {
|
|
277
|
-
this.effect = new ReactiveEffect(getter, () => {
|
|
278
|
-
if (!this._dirty) {
|
|
279
|
-
this._dirty = true
|
|
280
|
-
trigger(this as any, refTarget)
|
|
281
|
-
this._subscribers.forEach(fn => fn())
|
|
282
|
-
}
|
|
283
|
-
}, activeScope)
|
|
284
|
-
this.effect.computed = true
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
get value(): T {
|
|
288
|
-
track(this as any, refTarget)
|
|
289
|
-
if (this._dirty) {
|
|
290
|
-
this._dirty = false
|
|
291
|
-
this._value = this.effect.run()
|
|
292
|
-
}
|
|
293
|
-
return this._value
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
set value(v: T) {
|
|
297
|
-
this.setter?.(v)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
subscribe(fn: () => void): () => void {
|
|
301
|
-
this._subscribers.add(fn)
|
|
302
|
-
return () => this._subscribers.delete(fn)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
peek(): T { return this._value }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function computed<T>(getter: () => T): ComputedRef<T>
|
|
309
|
-
export function computed<T>(opts: { get: () => T; set: (v: T) => void }): WritableComputedRef<T>
|
|
310
|
-
export function computed<T>(arg: (() => T) | { get: () => T; set: (v: T) => void }): ComputedRef<T> {
|
|
311
|
-
if (typeof arg === 'function') {
|
|
312
|
-
return new ComputedRefImpl(arg) as ComputedRef<T>
|
|
313
|
-
}
|
|
314
|
-
return new ComputedRefImpl(arg.get, arg.set) as ComputedRef<T>
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
318
|
-
// § 7 Reactive proxy
|
|
319
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
320
|
-
|
|
321
|
-
const reactiveMap = new WeakMap<object, object>()
|
|
322
|
-
const readonlyMap = new WeakMap<object, object>()
|
|
323
|
-
const shallowReactiveMap = new WeakMap<object, object>()
|
|
324
|
-
|
|
325
|
-
function toReactive<T>(value: T): T {
|
|
326
|
-
return value !== null && typeof value === 'object' ? reactive(value as object) as T : value
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const arrayInstrumentations: Record<string, Function> = {}
|
|
330
|
-
;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
|
|
331
|
-
arrayInstrumentations[method] = function (this: unknown[], ...args: unknown[]) {
|
|
332
|
-
const arr = toRaw(this) as unknown[]
|
|
333
|
-
for (let i = 0; i < this.length; i++) track(arr, i)
|
|
334
|
-
let res = (arr as any)[method](...args)
|
|
335
|
-
if (res === -1 || res === false) res = (arr as any)[method](...args.map(toRaw))
|
|
336
|
-
return res
|
|
337
|
-
}
|
|
338
|
-
})
|
|
339
|
-
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
|
|
340
|
-
arrayInstrumentations[method] = function (this: unknown[], ...args: unknown[]) {
|
|
341
|
-
pauseTracking()
|
|
342
|
-
const res = (toRaw(this) as any)[method].apply(this, args)
|
|
343
|
-
resetTracking()
|
|
344
|
-
return res
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
function createHandler(shallow = false, readonly = false): ProxyHandler<object> {
|
|
349
|
-
return {
|
|
350
|
-
get(target, key, receiver) {
|
|
351
|
-
if (key === RAW) return target
|
|
352
|
-
if (key === IS_REACTIVE) return !readonly
|
|
353
|
-
if (key === IS_READONLY) return readonly
|
|
354
|
-
if (key === MARK_RAW) return (target as any)[MARK_RAW]
|
|
355
|
-
|
|
356
|
-
const isArray = Array.isArray(target)
|
|
357
|
-
if (!readonly && isArray && hasOwn(arrayInstrumentations, key as string)) {
|
|
358
|
-
return Reflect.get(arrayInstrumentations, key, receiver)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const res = Reflect.get(target, key, receiver)
|
|
362
|
-
if (typeof key === 'symbol' || key === '__proto__') return res
|
|
363
|
-
|
|
364
|
-
if (!readonly) track(target, key)
|
|
365
|
-
|
|
366
|
-
if (shallow) return res
|
|
367
|
-
if (isRef(res)) return isArray ? res : res.value
|
|
368
|
-
return res !== null && typeof res === 'object' && !res[MARK_RAW]
|
|
369
|
-
? readonly ? readonlyProxy(res) : reactive(res)
|
|
370
|
-
: res
|
|
371
|
-
},
|
|
372
|
-
set(target, key, value, receiver) {
|
|
373
|
-
if (readonly) {
|
|
374
|
-
console.warn(`[fnetro] Cannot set "${String(key)}" on readonly object`)
|
|
375
|
-
return true
|
|
376
|
-
}
|
|
377
|
-
const oldVal = (target as any)[key]
|
|
378
|
-
const result = Reflect.set(target, key, value, receiver)
|
|
379
|
-
if (hasChanged(value, oldVal)) trigger(target, key, value, oldVal)
|
|
380
|
-
return result
|
|
381
|
-
},
|
|
382
|
-
deleteProperty(target, key) {
|
|
383
|
-
if (readonly) return true
|
|
384
|
-
const hadKey = hasOwn(target, key as string)
|
|
385
|
-
const result = Reflect.deleteProperty(target, key)
|
|
386
|
-
if (hadKey && result) trigger(target, key)
|
|
387
|
-
return result
|
|
388
|
-
},
|
|
389
|
-
has(target, key) {
|
|
390
|
-
const res = Reflect.has(target, key)
|
|
391
|
-
track(target, key)
|
|
392
|
-
return res
|
|
393
|
-
},
|
|
394
|
-
ownKeys(target) {
|
|
395
|
-
track(target, Array.isArray(target) ? 'length' : '__iterate__')
|
|
396
|
-
return Reflect.ownKeys(target)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export function reactive<T extends object>(target: T): T {
|
|
402
|
-
if (isReadonly(target)) return target
|
|
403
|
-
if ((target as any)[MARK_RAW]) return target
|
|
404
|
-
if (reactiveMap.has(target)) return reactiveMap.get(target) as T
|
|
405
|
-
const proxy = new Proxy(target, createHandler()) as T
|
|
406
|
-
reactiveMap.set(target, proxy)
|
|
407
|
-
return proxy
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export function shallowReactive<T extends object>(target: T): T {
|
|
411
|
-
if (shallowReactiveMap.has(target)) return shallowReactiveMap.get(target) as T
|
|
412
|
-
const proxy = new Proxy(target, createHandler(true)) as T
|
|
413
|
-
shallowReactiveMap.set(target, proxy)
|
|
414
|
-
return proxy
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function readonlyProxy<T extends object>(target: T): T {
|
|
418
|
-
if (readonlyMap.has(target)) return readonlyMap.get(target) as T
|
|
419
|
-
const proxy = new Proxy(target, createHandler(false, true)) as T
|
|
420
|
-
readonlyMap.set(target, proxy)
|
|
421
|
-
return proxy
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function readonly<T extends object>(target: T): Readonly<T> {
|
|
425
|
-
return readonlyProxy(target)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function markRaw<T extends object>(value: T): T {
|
|
429
|
-
;(value as any)[MARK_RAW] = true
|
|
430
|
-
return value
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export function toRaw<T>(observed: T): T {
|
|
434
|
-
const raw = (observed as any)?.[RAW]
|
|
435
|
-
return raw ? toRaw(raw) : observed
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export function isReactive(value: unknown): boolean {
|
|
439
|
-
if (isReadonly(value)) return isReactive((value as any)[RAW])
|
|
440
|
-
return !!(value && (value as any)[IS_REACTIVE])
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function isReadonly(value: unknown): boolean {
|
|
444
|
-
return !!(value && (value as any)[IS_READONLY])
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
448
|
-
// § 8 watch
|
|
449
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
450
|
-
|
|
451
|
-
export type WatchSource<T = unknown> = Ref<T> | ComputedRef<T> | (() => T)
|
|
452
|
-
export type MultiSource = WatchSource[] | readonly WatchSource[]
|
|
453
|
-
type MapSources<T, Immediate = false> = {
|
|
454
|
-
[K in keyof T]: T[K] extends WatchSource<infer V>
|
|
455
|
-
? Immediate extends true ? V | undefined : V
|
|
456
|
-
: T[K] extends object ? T[K] : never
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
export interface WatchOptions<Immediate = boolean> {
|
|
460
|
-
immediate?: Immediate
|
|
461
|
-
deep?: boolean
|
|
462
|
-
once?: boolean
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
type StopHandle = () => void
|
|
466
|
-
type CleanupFn = (fn: () => void) => void
|
|
467
|
-
|
|
468
|
-
function traverse(value: unknown, seen = new Set()): unknown {
|
|
469
|
-
if (!value || typeof value !== 'object' || seen.has(value)) return value
|
|
470
|
-
seen.add(value)
|
|
471
|
-
if (isRef(value)) { traverse(value.value, seen); return value }
|
|
472
|
-
if (Array.isArray(value)) { value.forEach(v => traverse(v, seen)); return value }
|
|
473
|
-
for (const key in value as object) traverse((value as any)[key], seen)
|
|
474
|
-
return value
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function normalizeSource<T>(src: WatchSource<T> | MultiSource): () => any {
|
|
478
|
-
if (Array.isArray(src)) return () => src.map(s => isRef(s) ? s.value : s())
|
|
479
|
-
if (isRef(src)) return () => src.value
|
|
480
|
-
return src as () => T
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function watch<T>(
|
|
484
|
-
source: WatchSource<T>,
|
|
485
|
-
cb: (val: T, old: T | undefined, cleanup: CleanupFn) => void,
|
|
486
|
-
opts?: WatchOptions
|
|
487
|
-
): StopHandle
|
|
488
|
-
export function watch<T extends MultiSource>(
|
|
489
|
-
source: T,
|
|
490
|
-
cb: (val: MapSources<T>, old: MapSources<T, true>, cleanup: CleanupFn) => void,
|
|
491
|
-
opts?: WatchOptions
|
|
492
|
-
): StopHandle
|
|
493
|
-
export function watch(source: any, cb: any, opts: WatchOptions = {}): StopHandle {
|
|
494
|
-
const getter = opts.deep
|
|
495
|
-
? () => traverse(normalizeSource(source)())
|
|
496
|
-
: normalizeSource(source)
|
|
497
|
-
|
|
498
|
-
let oldVal: any = undefined
|
|
499
|
-
let cleanupFn: (() => void) | undefined
|
|
500
|
-
|
|
501
|
-
const cleanup: CleanupFn = (fn) => { cleanupFn = fn }
|
|
502
|
-
|
|
503
|
-
const job = () => {
|
|
504
|
-
if (!effect.active) return
|
|
505
|
-
cleanupFn?.(); cleanupFn = undefined
|
|
506
|
-
const newVal = effect.run()
|
|
507
|
-
if (opts.deep || hasChanged(newVal, oldVal)) {
|
|
508
|
-
cb(newVal, oldVal, cleanup)
|
|
509
|
-
oldVal = newVal
|
|
510
|
-
}
|
|
511
|
-
if (opts.once) effect.stop()
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const effect = new ReactiveEffect(getter, job, activeScope)
|
|
515
|
-
|
|
516
|
-
if (opts.immediate) {
|
|
517
|
-
cleanupFn?.(); cleanupFn = undefined
|
|
518
|
-
const val = effect.run()
|
|
519
|
-
cb(val, oldVal, cleanup)
|
|
520
|
-
oldVal = val
|
|
521
|
-
} else {
|
|
522
|
-
oldVal = effect.run()
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return () => effect.stop()
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
529
|
-
// § 9 Component hooks (JSX-aware)
|
|
530
|
-
// Server: returns plain values. Client: patched by client.ts
|
|
531
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
532
|
-
|
|
533
|
-
interface FNetroHooks {
|
|
534
|
-
useValue<T>(r: Ref<T> | (() => T)): T
|
|
535
|
-
useLocalRef<T>(init: T): Ref<T>
|
|
536
|
-
useLocalReactive<T extends object>(init: T): T
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// SSR fallbacks — no re-renders needed on server
|
|
540
|
-
export const __hooks: FNetroHooks = {
|
|
541
|
-
useValue: (r) => isRef(r) ? r.value : r(),
|
|
542
|
-
useLocalRef: (init) => ref(init),
|
|
543
|
-
useLocalReactive: (init) => reactive(init),
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Subscribe to a Ref or computed getter inside a JSX component.
|
|
548
|
-
* On the server, returns the current value (no reactivity needed).
|
|
549
|
-
* On the client, re-renders the component whenever the value changes.
|
|
550
|
-
*
|
|
551
|
-
* @example
|
|
552
|
-
* const count = ref(0)
|
|
553
|
-
* function Counter() {
|
|
554
|
-
* const n = use(count)
|
|
555
|
-
* return <button onClick={() => count.value++}>{n}</button>
|
|
556
|
-
* }
|
|
557
|
-
*/
|
|
558
|
-
export function use<T>(source: Ref<T> | (() => T)): T {
|
|
559
|
-
return __hooks.useValue(source)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Create a component-local reactive Ref.
|
|
564
|
-
* Unlike module-level `ref()`, this is scoped to the component lifecycle.
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* function Input() {
|
|
568
|
-
* const text = useLocalRef('')
|
|
569
|
-
* return <input value={use(text)} onInput={e => text.value = e.target.value} />
|
|
570
|
-
* }
|
|
571
|
-
*/
|
|
572
|
-
export function useLocalRef<T>(init: T): Ref<T> {
|
|
573
|
-
return __hooks.useLocalRef(init)
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Create a component-local reactive object.
|
|
578
|
-
* @example
|
|
579
|
-
* function Form() {
|
|
580
|
-
* const form = useLocalReactive({ name: '', email: '' })
|
|
581
|
-
* return <input value={form.name} onInput={e => form.name = e.target.value} />
|
|
582
|
-
* }
|
|
583
|
-
*/
|
|
584
|
-
export function useLocalReactive<T extends object>(init: T): T {
|
|
585
|
-
return __hooks.useLocalReactive(init)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
589
|
-
// § 10 Route / App definitions
|
|
590
|
-
// ══════════════════════════════════════════════════════════════════════════════
|
|
591
|
-
|
|
592
|
-
export type LoaderCtx = Context
|
|
593
|
-
export type FNetroMiddleware = MiddlewareHandler
|
|
594
|
-
export type AnyJSX = any
|
|
595
|
-
|
|
596
|
-
export interface PageDef<TData extends object = {}> {
|
|
597
|
-
readonly __type: 'page'
|
|
598
|
-
path: string
|
|
599
|
-
/** Middleware applied only to this route */
|
|
600
|
-
middleware?: FNetroMiddleware[]
|
|
601
|
-
/** Server-side data loader. Return value becomes Page props. */
|
|
602
|
-
loader?: (c: LoaderCtx) => TData | Promise<TData>
|
|
603
|
-
/** Override the group/app layout for this page. Pass `false` to use no layout. */
|
|
604
|
-
layout?: LayoutDef | false
|
|
605
|
-
/** The JSX page component */
|
|
606
|
-
Page: (props: TData & { url: string; params: Record<string, string> }) => AnyJSX
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export interface GroupDef {
|
|
610
|
-
readonly __type: 'group'
|
|
611
|
-
/** URL prefix — e.g. '/admin' */
|
|
612
|
-
prefix: string
|
|
613
|
-
/** Layout override for all pages in this group */
|
|
614
|
-
layout?: LayoutDef | false
|
|
615
|
-
/** Middleware applied to every route in the group */
|
|
616
|
-
middleware?: FNetroMiddleware[]
|
|
617
|
-
/** Pages and nested groups */
|
|
618
|
-
routes: (PageDef<any> | GroupDef | ApiRouteDef)[]
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
export interface LayoutDef {
|
|
622
|
-
readonly __type: 'layout'
|
|
623
|
-
Component: (props: { children: AnyJSX; url: string; params: Record<string, string> }) => AnyJSX
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
export interface ApiRouteDef {
|
|
627
|
-
readonly __type: 'api'
|
|
628
|
-
/** Mount path — e.g. '/api' or '/api/admin' */
|
|
629
|
-
path: string
|
|
630
|
-
/** Register raw Hono routes on the provided sub-app */
|
|
631
|
-
register: (app: Hono, middleware: FNetroMiddleware[]) => void
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
export interface MiddlewareDef {
|
|
635
|
-
readonly __type: 'middleware'
|
|
636
|
-
handler: FNetroMiddleware
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
export interface AppConfig {
|
|
640
|
-
/** Default layout for all pages */
|
|
641
|
-
layout?: LayoutDef
|
|
642
|
-
/** Global middleware applied before every route */
|
|
643
|
-
middleware?: FNetroMiddleware[]
|
|
644
|
-
/** Top-level routes, groups, and API routes */
|
|
645
|
-
routes: (PageDef<any> | GroupDef | ApiRouteDef)[]
|
|
646
|
-
/** 404 page */
|
|
647
|
-
notFound?: () => AnyJSX
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// ── Builder functions ─────────────────────────────────────────────────────────
|
|
651
|
-
|
|
652
|
-
export function definePage<TData extends object = {}>(
|
|
653
|
-
def: Omit<PageDef<TData>, '__type'>
|
|
654
|
-
): PageDef<TData> {
|
|
655
|
-
return { __type: 'page', ...def }
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
export function defineGroup(
|
|
659
|
-
def: Omit<GroupDef, '__type'>
|
|
660
|
-
): GroupDef {
|
|
661
|
-
return { __type: 'group', ...def }
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function defineLayout(
|
|
665
|
-
Component: LayoutDef['Component']
|
|
666
|
-
): LayoutDef {
|
|
667
|
-
return { __type: 'layout', Component }
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
export function defineMiddleware(handler: FNetroMiddleware): MiddlewareDef {
|
|
671
|
-
return { __type: 'middleware', handler }
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
export function defineApiRoute(
|
|
675
|
-
path: string,
|
|
676
|
-
register: ApiRouteDef['register']
|
|
677
|
-
): ApiRouteDef {
|
|
678
|
-
return { __type: 'api', path, register }
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ── Internal route resolution ─────────────────────────────────────────────────
|
|
682
|
-
|
|
683
|
-
export interface ResolvedRoute {
|
|
684
|
-
fullPath: string
|
|
685
|
-
page: PageDef<any>
|
|
686
|
-
layout: LayoutDef | false | undefined
|
|
687
|
-
middleware: FNetroMiddleware[]
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export function resolveRoutes(
|
|
691
|
-
routes: (PageDef<any> | GroupDef | ApiRouteDef)[],
|
|
692
|
-
options: {
|
|
693
|
-
prefix?: string
|
|
694
|
-
middleware?: FNetroMiddleware[]
|
|
695
|
-
layout?: LayoutDef | false
|
|
696
|
-
} = {}
|
|
697
|
-
): { pages: ResolvedRoute[]; apis: ApiRouteDef[] } {
|
|
698
|
-
const pages: ResolvedRoute[] = []
|
|
699
|
-
const apis: ApiRouteDef[] = []
|
|
700
|
-
|
|
701
|
-
for (const route of routes) {
|
|
702
|
-
if (route.__type === 'api') {
|
|
703
|
-
apis.push({ ...route, path: (options.prefix ?? '') + route.path })
|
|
704
|
-
} else if (route.__type === 'group') {
|
|
705
|
-
const prefix = (options.prefix ?? '') + route.prefix
|
|
706
|
-
const mw = [...(options.middleware ?? []), ...(route.middleware ?? [])]
|
|
707
|
-
const layout = route.layout !== undefined ? route.layout : options.layout
|
|
708
|
-
const sub = resolveRoutes(route.routes, { prefix, middleware: mw, layout })
|
|
709
|
-
pages.push(...sub.pages)
|
|
710
|
-
apis.push(...sub.apis)
|
|
711
|
-
} else {
|
|
712
|
-
const fullPath = (options.prefix ?? '') + route.path
|
|
713
|
-
const layout = route.layout !== undefined ? route.layout : options.layout
|
|
714
|
-
const middleware = [...(options.middleware ?? []), ...(route.middleware ?? [])]
|
|
715
|
-
pages.push({ fullPath, page: route, layout, middleware })
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return { pages, apis }
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// ── Shared constants ──────────────────────────────────────────────────────────
|
|
723
|
-
export const SPA_HEADER = 'x-fnetro-spa'
|
|
724
|
-
export const STATE_KEY = '__FNETRO_STATE__'
|
|
725
|
-
export const PARAMS_KEY = '__FNETRO_PARAMS__'
|
|
726
|
-
|
|
727
|
-
// ── Utilities ─────────────────────────────────────────────────────────────────
|
|
728
|
-
function hasChanged(a: unknown, b: unknown): boolean {
|
|
729
|
-
return !Object.is(a, b)
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function hasOwn(obj: object, key: string): boolean {
|
|
733
|
-
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
734
|
-
}
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// FNetro · core.ts
|
|
3
|
+
// Full Vue-like reactivity + route / layout / middleware definitions
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import type { Context, MiddlewareHandler, Hono } from 'hono'
|
|
7
|
+
|
|
8
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
// § 1 Dependency tracking
|
|
10
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
11
|
+
|
|
12
|
+
export type EffectFn = () => void | (() => void)
|
|
13
|
+
|
|
14
|
+
const RAW = Symbol('raw')
|
|
15
|
+
const IS_REACTIVE = Symbol('isReactive')
|
|
16
|
+
const IS_READONLY = Symbol('isReadonly')
|
|
17
|
+
const IS_REF = Symbol('isRef')
|
|
18
|
+
const MARK_RAW = Symbol('markRaw')
|
|
19
|
+
|
|
20
|
+
// Per-target, per-key subscriber sets
|
|
21
|
+
const targetMap = new WeakMap<object, Map<PropertyKey, Set<ReactiveEffect>>>()
|
|
22
|
+
|
|
23
|
+
let activeEffect: ReactiveEffect | null = null
|
|
24
|
+
let shouldTrack = true
|
|
25
|
+
let trackStack: boolean[] = []
|
|
26
|
+
|
|
27
|
+
function pauseTracking() { trackStack.push(shouldTrack); shouldTrack = false }
|
|
28
|
+
function resetTracking() { shouldTrack = trackStack.pop() ?? true }
|
|
29
|
+
|
|
30
|
+
function track(target: object, key: PropertyKey) {
|
|
31
|
+
if (!shouldTrack || !activeEffect) return
|
|
32
|
+
let depsMap = targetMap.get(target)
|
|
33
|
+
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
|
|
34
|
+
let dep = depsMap.get(key)
|
|
35
|
+
if (!dep) depsMap.set(key, (dep = new Set()))
|
|
36
|
+
trackEffect(activeEffect, dep)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function trackEffect(effect: ReactiveEffect, dep: Set<ReactiveEffect>) {
|
|
40
|
+
if (!dep.has(effect)) {
|
|
41
|
+
dep.add(effect)
|
|
42
|
+
effect.deps.push(dep)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function trigger(target: object, key: PropertyKey, newVal?: unknown, oldVal?: unknown) {
|
|
47
|
+
const depsMap = targetMap.get(target)
|
|
48
|
+
if (!depsMap) return
|
|
49
|
+
const effects: ReactiveEffect[] = []
|
|
50
|
+
const computedEffects: ReactiveEffect[] = []
|
|
51
|
+
|
|
52
|
+
depsMap.get(key)?.forEach(e => {
|
|
53
|
+
if (e !== activeEffect) {
|
|
54
|
+
e.computed ? computedEffects.push(e) : effects.push(e)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
// Computed run first so dependents see fresh values
|
|
58
|
+
;[...computedEffects, ...effects].forEach(e => {
|
|
59
|
+
if (e.active) e.scheduler ? e.scheduler() : e.run()
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
// § 2 ReactiveEffect
|
|
65
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
export class ReactiveEffect {
|
|
68
|
+
deps: Set<ReactiveEffect>[] = []
|
|
69
|
+
active = true
|
|
70
|
+
cleanup?: () => void
|
|
71
|
+
computed = false
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
public fn: () => any,
|
|
75
|
+
public scheduler?: () => void,
|
|
76
|
+
public scope?: EffectScope,
|
|
77
|
+
) {
|
|
78
|
+
scope?.effects.push(this)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
run(): any {
|
|
82
|
+
if (!this.active) return this.fn()
|
|
83
|
+
const prevEffect = activeEffect
|
|
84
|
+
const prevShouldTrack = shouldTrack
|
|
85
|
+
shouldTrack = true
|
|
86
|
+
activeEffect = this
|
|
87
|
+
this.cleanup?.()
|
|
88
|
+
this.cleanup = undefined
|
|
89
|
+
this.deps.length = 0
|
|
90
|
+
try {
|
|
91
|
+
const result = this.fn()
|
|
92
|
+
if (typeof result === 'function') this.cleanup = result
|
|
93
|
+
return result
|
|
94
|
+
} finally {
|
|
95
|
+
activeEffect = prevEffect
|
|
96
|
+
shouldTrack = prevShouldTrack
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stop() {
|
|
101
|
+
if (this.active) {
|
|
102
|
+
cleanupEffect(this)
|
|
103
|
+
this.active = false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cleanupEffect(e: ReactiveEffect) {
|
|
109
|
+
e.deps.forEach(dep => dep.delete(e))
|
|
110
|
+
e.deps.length = 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// § 3 EffectScope
|
|
115
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
let activeScope: EffectScope | undefined
|
|
118
|
+
|
|
119
|
+
export class EffectScope {
|
|
120
|
+
effects: ReactiveEffect[] = []
|
|
121
|
+
cleanups: (() => void)[] = []
|
|
122
|
+
active = true
|
|
123
|
+
|
|
124
|
+
run<T>(fn: () => T): T {
|
|
125
|
+
const prev = activeScope
|
|
126
|
+
activeScope = this
|
|
127
|
+
try { return fn() }
|
|
128
|
+
finally { activeScope = prev }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stop() {
|
|
132
|
+
if (this.active) {
|
|
133
|
+
this.effects.forEach(e => e.stop())
|
|
134
|
+
this.cleanups.forEach(fn => fn())
|
|
135
|
+
this.active = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onCleanup(fn: () => void) { this.cleanups.push(fn) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function effectScope(): EffectScope {
|
|
143
|
+
return new EffectScope()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getCurrentScope(): EffectScope | undefined {
|
|
147
|
+
return activeScope
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function onScopeDispose(fn: () => void) {
|
|
151
|
+
activeScope?.onCleanup(fn)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
155
|
+
// § 4 effect / watchEffect
|
|
156
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
|
|
158
|
+
export function effect(fn: EffectFn): () => void {
|
|
159
|
+
const e = new ReactiveEffect(fn, undefined, activeScope)
|
|
160
|
+
e.run()
|
|
161
|
+
return () => e.stop()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface WatchEffectOptions {
|
|
165
|
+
flush?: 'sync' | 'post'
|
|
166
|
+
onTrack?: (e: any) => void
|
|
167
|
+
onTrigger?: (e: any) => void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function watchEffect(fn: EffectFn, opts?: WatchEffectOptions): () => void {
|
|
171
|
+
const e = new ReactiveEffect(fn, undefined, activeScope)
|
|
172
|
+
e.run()
|
|
173
|
+
return () => e.stop()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// § 5 Ref
|
|
178
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
export interface Ref<T = unknown> {
|
|
181
|
+
value: T
|
|
182
|
+
readonly [IS_REF]: true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const refTarget = Symbol('refTarget')
|
|
186
|
+
|
|
187
|
+
class RefImpl<T> implements Ref<T> {
|
|
188
|
+
readonly [IS_REF] = true as const
|
|
189
|
+
private _value: T
|
|
190
|
+
private _subscribers = new Set<() => void>()
|
|
191
|
+
|
|
192
|
+
constructor(value: T, private readonly shallow = false) {
|
|
193
|
+
this._value = shallow ? value : toReactive(value)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get value(): T {
|
|
197
|
+
track(this as any, refTarget)
|
|
198
|
+
this._subscribers.forEach(fn => { /* for useSyncExternalStore */ })
|
|
199
|
+
return this._value
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
set value(next: T) {
|
|
203
|
+
const newVal = this.shallow ? next : toReactive(next)
|
|
204
|
+
if (!hasChanged(newVal, this._value)) return
|
|
205
|
+
this._value = newVal
|
|
206
|
+
trigger(this as any, refTarget, newVal, this._value)
|
|
207
|
+
this._subscribers.forEach(fn => fn())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Subscribe for useSyncExternalStore */
|
|
211
|
+
subscribe(fn: () => void): () => void {
|
|
212
|
+
this._subscribers.add(fn)
|
|
213
|
+
return () => this._subscribers.delete(fn)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
peek(): T { return this._value }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function ref<T>(value: T): Ref<T> {
|
|
220
|
+
return isRef(value) ? value as Ref<T> : new RefImpl(value)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function shallowRef<T>(value: T): Ref<T> {
|
|
224
|
+
return new RefImpl(value, true)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function triggerRef(r: Ref): void {
|
|
228
|
+
if (r instanceof RefImpl) {
|
|
229
|
+
trigger(r as any, refTarget)
|
|
230
|
+
;(r as any)._subscribers.forEach((fn: () => void) => fn())
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function isRef<T = unknown>(r: unknown): r is Ref<T> {
|
|
235
|
+
return !!r && typeof r === 'object' && (r as any)[IS_REF] === true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function unref<T>(r: T | Ref<T>): T {
|
|
239
|
+
return isRef(r) ? r.value : r
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function toRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<T[K]> {
|
|
243
|
+
const r = new RefImpl<T[K]>(undefined as T[K], false)
|
|
244
|
+
Object.defineProperty(r, 'value', {
|
|
245
|
+
get() { track(r as any, refTarget); return obj[key] },
|
|
246
|
+
set(v: T[K]) { obj[key] = v; trigger(r as any, refTarget, v, obj[key]) }
|
|
247
|
+
})
|
|
248
|
+
return r
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function toRefs<T extends object>(obj: T): { [K in keyof T]: Ref<T[K]> } {
|
|
252
|
+
const result = {} as { [K in keyof T]: Ref<T[K]> }
|
|
253
|
+
for (const key in obj) result[key] = toRef(obj, key)
|
|
254
|
+
return result
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
258
|
+
// § 6 Computed
|
|
259
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
260
|
+
|
|
261
|
+
export interface WritableComputedRef<T> extends Ref<T> {
|
|
262
|
+
readonly effect: ReactiveEffect
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface ComputedRef<T> extends WritableComputedRef<T> {
|
|
266
|
+
readonly value: T
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class ComputedRefImpl<T> implements Ref<T> {
|
|
270
|
+
readonly [IS_REF] = true as const
|
|
271
|
+
readonly effect: ReactiveEffect
|
|
272
|
+
private _value!: T
|
|
273
|
+
private _dirty = true
|
|
274
|
+
private _subscribers = new Set<() => void>()
|
|
275
|
+
|
|
276
|
+
constructor(getter: () => T, private setter?: (v: T) => void) {
|
|
277
|
+
this.effect = new ReactiveEffect(getter, () => {
|
|
278
|
+
if (!this._dirty) {
|
|
279
|
+
this._dirty = true
|
|
280
|
+
trigger(this as any, refTarget)
|
|
281
|
+
this._subscribers.forEach(fn => fn())
|
|
282
|
+
}
|
|
283
|
+
}, activeScope)
|
|
284
|
+
this.effect.computed = true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get value(): T {
|
|
288
|
+
track(this as any, refTarget)
|
|
289
|
+
if (this._dirty) {
|
|
290
|
+
this._dirty = false
|
|
291
|
+
this._value = this.effect.run()
|
|
292
|
+
}
|
|
293
|
+
return this._value
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
set value(v: T) {
|
|
297
|
+
this.setter?.(v)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
subscribe(fn: () => void): () => void {
|
|
301
|
+
this._subscribers.add(fn)
|
|
302
|
+
return () => this._subscribers.delete(fn)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
peek(): T { return this._value }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function computed<T>(getter: () => T): ComputedRef<T>
|
|
309
|
+
export function computed<T>(opts: { get: () => T; set: (v: T) => void }): WritableComputedRef<T>
|
|
310
|
+
export function computed<T>(arg: (() => T) | { get: () => T; set: (v: T) => void }): ComputedRef<T> {
|
|
311
|
+
if (typeof arg === 'function') {
|
|
312
|
+
return new ComputedRefImpl(arg) as ComputedRef<T>
|
|
313
|
+
}
|
|
314
|
+
return new ComputedRefImpl(arg.get, arg.set) as ComputedRef<T>
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
318
|
+
// § 7 Reactive proxy
|
|
319
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
320
|
+
|
|
321
|
+
const reactiveMap = new WeakMap<object, object>()
|
|
322
|
+
const readonlyMap = new WeakMap<object, object>()
|
|
323
|
+
const shallowReactiveMap = new WeakMap<object, object>()
|
|
324
|
+
|
|
325
|
+
function toReactive<T>(value: T): T {
|
|
326
|
+
return value !== null && typeof value === 'object' ? reactive(value as object) as T : value
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const arrayInstrumentations: Record<string, Function> = {}
|
|
330
|
+
;['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
|
|
331
|
+
arrayInstrumentations[method] = function (this: unknown[], ...args: unknown[]) {
|
|
332
|
+
const arr = toRaw(this) as unknown[]
|
|
333
|
+
for (let i = 0; i < this.length; i++) track(arr, i)
|
|
334
|
+
let res = (arr as any)[method](...args)
|
|
335
|
+
if (res === -1 || res === false) res = (arr as any)[method](...args.map(toRaw))
|
|
336
|
+
return res
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
|
|
340
|
+
arrayInstrumentations[method] = function (this: unknown[], ...args: unknown[]) {
|
|
341
|
+
pauseTracking()
|
|
342
|
+
const res = (toRaw(this) as any)[method].apply(this, args)
|
|
343
|
+
resetTracking()
|
|
344
|
+
return res
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
function createHandler(shallow = false, readonly = false): ProxyHandler<object> {
|
|
349
|
+
return {
|
|
350
|
+
get(target, key, receiver) {
|
|
351
|
+
if (key === RAW) return target
|
|
352
|
+
if (key === IS_REACTIVE) return !readonly
|
|
353
|
+
if (key === IS_READONLY) return readonly
|
|
354
|
+
if (key === MARK_RAW) return (target as any)[MARK_RAW]
|
|
355
|
+
|
|
356
|
+
const isArray = Array.isArray(target)
|
|
357
|
+
if (!readonly && isArray && hasOwn(arrayInstrumentations, key as string)) {
|
|
358
|
+
return Reflect.get(arrayInstrumentations, key, receiver)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const res = Reflect.get(target, key, receiver)
|
|
362
|
+
if (typeof key === 'symbol' || key === '__proto__') return res
|
|
363
|
+
|
|
364
|
+
if (!readonly) track(target, key)
|
|
365
|
+
|
|
366
|
+
if (shallow) return res
|
|
367
|
+
if (isRef(res)) return isArray ? res : res.value
|
|
368
|
+
return res !== null && typeof res === 'object' && !res[MARK_RAW]
|
|
369
|
+
? readonly ? readonlyProxy(res) : reactive(res)
|
|
370
|
+
: res
|
|
371
|
+
},
|
|
372
|
+
set(target, key, value, receiver) {
|
|
373
|
+
if (readonly) {
|
|
374
|
+
console.warn(`[fnetro] Cannot set "${String(key)}" on readonly object`)
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
const oldVal = (target as any)[key]
|
|
378
|
+
const result = Reflect.set(target, key, value, receiver)
|
|
379
|
+
if (hasChanged(value, oldVal)) trigger(target, key, value, oldVal)
|
|
380
|
+
return result
|
|
381
|
+
},
|
|
382
|
+
deleteProperty(target, key) {
|
|
383
|
+
if (readonly) return true
|
|
384
|
+
const hadKey = hasOwn(target, key as string)
|
|
385
|
+
const result = Reflect.deleteProperty(target, key)
|
|
386
|
+
if (hadKey && result) trigger(target, key)
|
|
387
|
+
return result
|
|
388
|
+
},
|
|
389
|
+
has(target, key) {
|
|
390
|
+
const res = Reflect.has(target, key)
|
|
391
|
+
track(target, key)
|
|
392
|
+
return res
|
|
393
|
+
},
|
|
394
|
+
ownKeys(target) {
|
|
395
|
+
track(target, Array.isArray(target) ? 'length' : '__iterate__')
|
|
396
|
+
return Reflect.ownKeys(target)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function reactive<T extends object>(target: T): T {
|
|
402
|
+
if (isReadonly(target)) return target
|
|
403
|
+
if ((target as any)[MARK_RAW]) return target
|
|
404
|
+
if (reactiveMap.has(target)) return reactiveMap.get(target) as T
|
|
405
|
+
const proxy = new Proxy(target, createHandler()) as T
|
|
406
|
+
reactiveMap.set(target, proxy)
|
|
407
|
+
return proxy
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function shallowReactive<T extends object>(target: T): T {
|
|
411
|
+
if (shallowReactiveMap.has(target)) return shallowReactiveMap.get(target) as T
|
|
412
|
+
const proxy = new Proxy(target, createHandler(true)) as T
|
|
413
|
+
shallowReactiveMap.set(target, proxy)
|
|
414
|
+
return proxy
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function readonlyProxy<T extends object>(target: T): T {
|
|
418
|
+
if (readonlyMap.has(target)) return readonlyMap.get(target) as T
|
|
419
|
+
const proxy = new Proxy(target, createHandler(false, true)) as T
|
|
420
|
+
readonlyMap.set(target, proxy)
|
|
421
|
+
return proxy
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function readonly<T extends object>(target: T): Readonly<T> {
|
|
425
|
+
return readonlyProxy(target)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function markRaw<T extends object>(value: T): T {
|
|
429
|
+
;(value as any)[MARK_RAW] = true
|
|
430
|
+
return value
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function toRaw<T>(observed: T): T {
|
|
434
|
+
const raw = (observed as any)?.[RAW]
|
|
435
|
+
return raw ? toRaw(raw) : observed
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function isReactive(value: unknown): boolean {
|
|
439
|
+
if (isReadonly(value)) return isReactive((value as any)[RAW])
|
|
440
|
+
return !!(value && (value as any)[IS_REACTIVE])
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function isReadonly(value: unknown): boolean {
|
|
444
|
+
return !!(value && (value as any)[IS_READONLY])
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
448
|
+
// § 8 watch
|
|
449
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
450
|
+
|
|
451
|
+
export type WatchSource<T = unknown> = Ref<T> | ComputedRef<T> | (() => T)
|
|
452
|
+
export type MultiSource = WatchSource[] | readonly WatchSource[]
|
|
453
|
+
type MapSources<T, Immediate = false> = {
|
|
454
|
+
[K in keyof T]: T[K] extends WatchSource<infer V>
|
|
455
|
+
? Immediate extends true ? V | undefined : V
|
|
456
|
+
: T[K] extends object ? T[K] : never
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface WatchOptions<Immediate = boolean> {
|
|
460
|
+
immediate?: Immediate
|
|
461
|
+
deep?: boolean
|
|
462
|
+
once?: boolean
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
type StopHandle = () => void
|
|
466
|
+
type CleanupFn = (fn: () => void) => void
|
|
467
|
+
|
|
468
|
+
function traverse(value: unknown, seen = new Set()): unknown {
|
|
469
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return value
|
|
470
|
+
seen.add(value)
|
|
471
|
+
if (isRef(value)) { traverse(value.value, seen); return value }
|
|
472
|
+
if (Array.isArray(value)) { value.forEach(v => traverse(v, seen)); return value }
|
|
473
|
+
for (const key in value as object) traverse((value as any)[key], seen)
|
|
474
|
+
return value
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeSource<T>(src: WatchSource<T> | MultiSource): () => any {
|
|
478
|
+
if (Array.isArray(src)) return () => src.map(s => isRef(s) ? s.value : s())
|
|
479
|
+
if (isRef(src)) return () => src.value
|
|
480
|
+
return src as () => T
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function watch<T>(
|
|
484
|
+
source: WatchSource<T>,
|
|
485
|
+
cb: (val: T, old: T | undefined, cleanup: CleanupFn) => void,
|
|
486
|
+
opts?: WatchOptions
|
|
487
|
+
): StopHandle
|
|
488
|
+
export function watch<T extends MultiSource>(
|
|
489
|
+
source: T,
|
|
490
|
+
cb: (val: MapSources<T>, old: MapSources<T, true>, cleanup: CleanupFn) => void,
|
|
491
|
+
opts?: WatchOptions
|
|
492
|
+
): StopHandle
|
|
493
|
+
export function watch(source: any, cb: any, opts: WatchOptions = {}): StopHandle {
|
|
494
|
+
const getter = opts.deep
|
|
495
|
+
? () => traverse(normalizeSource(source)())
|
|
496
|
+
: normalizeSource(source)
|
|
497
|
+
|
|
498
|
+
let oldVal: any = undefined
|
|
499
|
+
let cleanupFn: (() => void) | undefined
|
|
500
|
+
|
|
501
|
+
const cleanup: CleanupFn = (fn) => { cleanupFn = fn }
|
|
502
|
+
|
|
503
|
+
const job = () => {
|
|
504
|
+
if (!effect.active) return
|
|
505
|
+
cleanupFn?.(); cleanupFn = undefined
|
|
506
|
+
const newVal = effect.run()
|
|
507
|
+
if (opts.deep || hasChanged(newVal, oldVal)) {
|
|
508
|
+
cb(newVal, oldVal, cleanup)
|
|
509
|
+
oldVal = newVal
|
|
510
|
+
}
|
|
511
|
+
if (opts.once) effect.stop()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const effect = new ReactiveEffect(getter, job, activeScope)
|
|
515
|
+
|
|
516
|
+
if (opts.immediate) {
|
|
517
|
+
cleanupFn?.(); cleanupFn = undefined
|
|
518
|
+
const val = effect.run()
|
|
519
|
+
cb(val, oldVal, cleanup)
|
|
520
|
+
oldVal = val
|
|
521
|
+
} else {
|
|
522
|
+
oldVal = effect.run()
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return () => effect.stop()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
529
|
+
// § 9 Component hooks (JSX-aware)
|
|
530
|
+
// Server: returns plain values. Client: patched by client.ts
|
|
531
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
532
|
+
|
|
533
|
+
interface FNetroHooks {
|
|
534
|
+
useValue<T>(r: Ref<T> | (() => T)): T
|
|
535
|
+
useLocalRef<T>(init: T): Ref<T>
|
|
536
|
+
useLocalReactive<T extends object>(init: T): T
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// SSR fallbacks — no re-renders needed on server
|
|
540
|
+
export const __hooks: FNetroHooks = {
|
|
541
|
+
useValue: (r) => isRef(r) ? r.value : r(),
|
|
542
|
+
useLocalRef: (init) => ref(init),
|
|
543
|
+
useLocalReactive: (init) => reactive(init),
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Subscribe to a Ref or computed getter inside a JSX component.
|
|
548
|
+
* On the server, returns the current value (no reactivity needed).
|
|
549
|
+
* On the client, re-renders the component whenever the value changes.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* const count = ref(0)
|
|
553
|
+
* function Counter() {
|
|
554
|
+
* const n = use(count)
|
|
555
|
+
* return <button onClick={() => count.value++}>{n}</button>
|
|
556
|
+
* }
|
|
557
|
+
*/
|
|
558
|
+
export function use<T>(source: Ref<T> | (() => T)): T {
|
|
559
|
+
return __hooks.useValue(source)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Create a component-local reactive Ref.
|
|
564
|
+
* Unlike module-level `ref()`, this is scoped to the component lifecycle.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* function Input() {
|
|
568
|
+
* const text = useLocalRef('')
|
|
569
|
+
* return <input value={use(text)} onInput={e => text.value = e.target.value} />
|
|
570
|
+
* }
|
|
571
|
+
*/
|
|
572
|
+
export function useLocalRef<T>(init: T): Ref<T> {
|
|
573
|
+
return __hooks.useLocalRef(init)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Create a component-local reactive object.
|
|
578
|
+
* @example
|
|
579
|
+
* function Form() {
|
|
580
|
+
* const form = useLocalReactive({ name: '', email: '' })
|
|
581
|
+
* return <input value={form.name} onInput={e => form.name = e.target.value} />
|
|
582
|
+
* }
|
|
583
|
+
*/
|
|
584
|
+
export function useLocalReactive<T extends object>(init: T): T {
|
|
585
|
+
return __hooks.useLocalReactive(init)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
589
|
+
// § 10 Route / App definitions
|
|
590
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
591
|
+
|
|
592
|
+
export type LoaderCtx = Context
|
|
593
|
+
export type FNetroMiddleware = MiddlewareHandler
|
|
594
|
+
export type AnyJSX = any
|
|
595
|
+
|
|
596
|
+
export interface PageDef<TData extends object = {}> {
|
|
597
|
+
readonly __type: 'page'
|
|
598
|
+
path: string
|
|
599
|
+
/** Middleware applied only to this route */
|
|
600
|
+
middleware?: FNetroMiddleware[]
|
|
601
|
+
/** Server-side data loader. Return value becomes Page props. */
|
|
602
|
+
loader?: (c: LoaderCtx) => TData | Promise<TData>
|
|
603
|
+
/** Override the group/app layout for this page. Pass `false` to use no layout. */
|
|
604
|
+
layout?: LayoutDef | false
|
|
605
|
+
/** The JSX page component */
|
|
606
|
+
Page: (props: TData & { url: string; params: Record<string, string> }) => AnyJSX
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export interface GroupDef {
|
|
610
|
+
readonly __type: 'group'
|
|
611
|
+
/** URL prefix — e.g. '/admin' */
|
|
612
|
+
prefix: string
|
|
613
|
+
/** Layout override for all pages in this group */
|
|
614
|
+
layout?: LayoutDef | false
|
|
615
|
+
/** Middleware applied to every route in the group */
|
|
616
|
+
middleware?: FNetroMiddleware[]
|
|
617
|
+
/** Pages and nested groups */
|
|
618
|
+
routes: (PageDef<any> | GroupDef | ApiRouteDef)[]
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export interface LayoutDef {
|
|
622
|
+
readonly __type: 'layout'
|
|
623
|
+
Component: (props: { children: AnyJSX; url: string; params: Record<string, string> }) => AnyJSX
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export interface ApiRouteDef {
|
|
627
|
+
readonly __type: 'api'
|
|
628
|
+
/** Mount path — e.g. '/api' or '/api/admin' */
|
|
629
|
+
path: string
|
|
630
|
+
/** Register raw Hono routes on the provided sub-app */
|
|
631
|
+
register: (app: Hono, middleware: FNetroMiddleware[]) => void
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export interface MiddlewareDef {
|
|
635
|
+
readonly __type: 'middleware'
|
|
636
|
+
handler: FNetroMiddleware
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export interface AppConfig {
|
|
640
|
+
/** Default layout for all pages */
|
|
641
|
+
layout?: LayoutDef
|
|
642
|
+
/** Global middleware applied before every route */
|
|
643
|
+
middleware?: FNetroMiddleware[]
|
|
644
|
+
/** Top-level routes, groups, and API routes */
|
|
645
|
+
routes: (PageDef<any> | GroupDef | ApiRouteDef)[]
|
|
646
|
+
/** 404 page */
|
|
647
|
+
notFound?: () => AnyJSX
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── Builder functions ─────────────────────────────────────────────────────────
|
|
651
|
+
|
|
652
|
+
export function definePage<TData extends object = {}>(
|
|
653
|
+
def: Omit<PageDef<TData>, '__type'>
|
|
654
|
+
): PageDef<TData> {
|
|
655
|
+
return { __type: 'page', ...def }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function defineGroup(
|
|
659
|
+
def: Omit<GroupDef, '__type'>
|
|
660
|
+
): GroupDef {
|
|
661
|
+
return { __type: 'group', ...def }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function defineLayout(
|
|
665
|
+
Component: LayoutDef['Component']
|
|
666
|
+
): LayoutDef {
|
|
667
|
+
return { __type: 'layout', Component }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function defineMiddleware(handler: FNetroMiddleware): MiddlewareDef {
|
|
671
|
+
return { __type: 'middleware', handler }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function defineApiRoute(
|
|
675
|
+
path: string,
|
|
676
|
+
register: ApiRouteDef['register']
|
|
677
|
+
): ApiRouteDef {
|
|
678
|
+
return { __type: 'api', path, register }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ── Internal route resolution ─────────────────────────────────────────────────
|
|
682
|
+
|
|
683
|
+
export interface ResolvedRoute {
|
|
684
|
+
fullPath: string
|
|
685
|
+
page: PageDef<any>
|
|
686
|
+
layout: LayoutDef | false | undefined
|
|
687
|
+
middleware: FNetroMiddleware[]
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function resolveRoutes(
|
|
691
|
+
routes: (PageDef<any> | GroupDef | ApiRouteDef)[],
|
|
692
|
+
options: {
|
|
693
|
+
prefix?: string
|
|
694
|
+
middleware?: FNetroMiddleware[]
|
|
695
|
+
layout?: LayoutDef | false
|
|
696
|
+
} = {}
|
|
697
|
+
): { pages: ResolvedRoute[]; apis: ApiRouteDef[] } {
|
|
698
|
+
const pages: ResolvedRoute[] = []
|
|
699
|
+
const apis: ApiRouteDef[] = []
|
|
700
|
+
|
|
701
|
+
for (const route of routes) {
|
|
702
|
+
if (route.__type === 'api') {
|
|
703
|
+
apis.push({ ...route, path: (options.prefix ?? '') + route.path })
|
|
704
|
+
} else if (route.__type === 'group') {
|
|
705
|
+
const prefix = (options.prefix ?? '') + route.prefix
|
|
706
|
+
const mw = [...(options.middleware ?? []), ...(route.middleware ?? [])]
|
|
707
|
+
const layout = route.layout !== undefined ? route.layout : options.layout
|
|
708
|
+
const sub = resolveRoutes(route.routes, { prefix, middleware: mw, layout })
|
|
709
|
+
pages.push(...sub.pages)
|
|
710
|
+
apis.push(...sub.apis)
|
|
711
|
+
} else {
|
|
712
|
+
const fullPath = (options.prefix ?? '') + route.path
|
|
713
|
+
const layout = route.layout !== undefined ? route.layout : options.layout
|
|
714
|
+
const middleware = [...(options.middleware ?? []), ...(route.middleware ?? [])]
|
|
715
|
+
pages.push({ fullPath, page: route, layout, middleware })
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return { pages, apis }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── Shared constants ──────────────────────────────────────────────────────────
|
|
723
|
+
export const SPA_HEADER = 'x-fnetro-spa'
|
|
724
|
+
export const STATE_KEY = '__FNETRO_STATE__'
|
|
725
|
+
export const PARAMS_KEY = '__FNETRO_PARAMS__'
|
|
726
|
+
|
|
727
|
+
// ── Utilities ─────────────────────────────────────────────────────────────────
|
|
728
|
+
function hasChanged(a: unknown, b: unknown): boolean {
|
|
729
|
+
return !Object.is(a, b)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function hasOwn(obj: object, key: string): boolean {
|
|
733
|
+
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
734
|
+
}
|