@pyreon/vue-compat 0.13.0 → 0.14.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/lib/analysis/index.js.html +1 -1
- package/lib/analysis/jsx-runtime.js.html +1 -1
- package/lib/index.js +408 -28
- package/lib/index.js.map +1 -1
- package/lib/jsx-runtime.js +9 -0
- package/lib/jsx-runtime.js.map +1 -1
- package/lib/types/index.d.ts +168 -8
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/jsx-runtime.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +622 -21
- package/src/jsx-runtime.ts +15 -0
- package/src/tests/jsx-runtime-wrapper.test.ts +87 -0
- package/src/tests/new-apis.test.ts +1303 -0
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
onUnmount,
|
|
30
30
|
onUpdate,
|
|
31
31
|
popContext,
|
|
32
|
+
Portal,
|
|
32
33
|
pushContext,
|
|
33
34
|
h as pyreonH,
|
|
34
35
|
useContext,
|
|
@@ -46,8 +47,9 @@ import { getCurrentCtx, getHookIndex } from './jsx-runtime'
|
|
|
46
47
|
|
|
47
48
|
// ─── Internal symbols ─────────────────────────────────────────────────────────
|
|
48
49
|
|
|
49
|
-
const V_IS_REF = Symbol('__v_isRef')
|
|
50
|
+
const V_IS_REF = Symbol.for('__v_isRef')
|
|
50
51
|
const V_IS_READONLY = Symbol('__v_isReadonly')
|
|
52
|
+
const V_SKIP = Symbol('__v_skip')
|
|
51
53
|
const V_RAW = Symbol('__v_raw')
|
|
52
54
|
|
|
53
55
|
// ─── Ref ──────────────────────────────────────────────────────────────────────
|
|
@@ -144,6 +146,16 @@ export function unref<T>(r: T | Ref<T>): T {
|
|
|
144
146
|
return isRef(r) ? r.value : r
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Unwraps a ref, calls a getter, or returns the value as-is.
|
|
151
|
+
* Vue 3.3+ API for normalizing ref/getter/value inputs.
|
|
152
|
+
*/
|
|
153
|
+
export function toValue<T>(source: Ref<T> | (() => T) | T): T {
|
|
154
|
+
if (isRef(source)) return source.value
|
|
155
|
+
if (typeof source === 'function') return (source as () => T)()
|
|
156
|
+
return source
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
// ─── Computed ─────────────────────────────────────────────────────────────────
|
|
148
160
|
|
|
149
161
|
export interface ComputedRef<T = unknown> extends Ref<T> {
|
|
@@ -225,6 +237,8 @@ const rawMap = new WeakMap<object, object>()
|
|
|
225
237
|
* call `scheduleRerender()`.
|
|
226
238
|
*/
|
|
227
239
|
export function reactive<T extends object>(obj: T): T {
|
|
240
|
+
if ((obj as Record<symbol, boolean>)[V_SKIP]) return obj
|
|
241
|
+
|
|
228
242
|
const ctx = getCurrentCtx()
|
|
229
243
|
if (ctx) {
|
|
230
244
|
const idx = getHookIndex()
|
|
@@ -282,12 +296,54 @@ export function readonly<T extends object>(obj: T): Readonly<T> {
|
|
|
282
296
|
return _createReadonlyProxy(obj)
|
|
283
297
|
}
|
|
284
298
|
|
|
285
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Returns a shallow readonly proxy — only top-level properties throw on set.
|
|
301
|
+
* Nested objects are NOT wrapped in readonly (unlike `readonly()`).
|
|
302
|
+
*/
|
|
303
|
+
export function shallowReadonly<T extends object>(obj: T): Readonly<T> {
|
|
304
|
+
const ctx = getCurrentCtx()
|
|
305
|
+
if (ctx) {
|
|
306
|
+
const idx = getHookIndex()
|
|
307
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as Readonly<T>
|
|
308
|
+
|
|
309
|
+
const proxy = _createShallowReadonlyProxy(obj)
|
|
310
|
+
ctx.hooks[idx] = proxy
|
|
311
|
+
return proxy
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return _createShallowReadonlyProxy(obj)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function _createShallowReadonlyProxy<T extends object>(obj: T): Readonly<T> {
|
|
286
318
|
const proxy = new Proxy(obj, {
|
|
287
319
|
get(target, key) {
|
|
288
320
|
if (key === V_IS_READONLY) return true
|
|
289
321
|
if (key === V_RAW) return target
|
|
290
322
|
return Reflect.get(target, key)
|
|
323
|
+
// NO recursive wrapping — shallow
|
|
324
|
+
},
|
|
325
|
+
set(_target, key) {
|
|
326
|
+
if (key === V_IS_READONLY || key === V_RAW) return true
|
|
327
|
+
throw new Error(`Cannot set property "${String(key)}" on a readonly object`)
|
|
328
|
+
},
|
|
329
|
+
deleteProperty(_target, key) {
|
|
330
|
+
throw new Error(`Cannot delete property "${String(key)}" from a readonly object`)
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
return proxy as Readonly<T>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _createReadonlyProxy<T extends object>(obj: T): Readonly<T> {
|
|
337
|
+
const proxy = new Proxy(obj, {
|
|
338
|
+
get(target, key) {
|
|
339
|
+
if (key === V_IS_READONLY) return true
|
|
340
|
+
if (key === V_RAW) return target
|
|
341
|
+
const value = Reflect.get(target, key)
|
|
342
|
+
// Recursively wrap nested objects in readonly
|
|
343
|
+
if (value !== null && typeof value === 'object' && !isRef(value)) {
|
|
344
|
+
return _createReadonlyProxy(value as object)
|
|
345
|
+
}
|
|
346
|
+
return value
|
|
291
347
|
},
|
|
292
348
|
set(_target, key) {
|
|
293
349
|
// Internal symbols used for identification are allowed
|
|
@@ -383,20 +439,169 @@ export interface WatchOptions {
|
|
|
383
439
|
immediate?: boolean
|
|
384
440
|
/** Ignored in Pyreon — dependencies are tracked automatically. */
|
|
385
441
|
deep?: boolean
|
|
442
|
+
/** Accepted for compatibility but not meaningfully differentiated in Pyreon. */
|
|
443
|
+
flush?: 'pre' | 'post' | 'sync'
|
|
386
444
|
}
|
|
387
445
|
|
|
388
446
|
type WatchSource<T> = Ref<T> | (() => T)
|
|
389
447
|
|
|
390
448
|
/**
|
|
391
|
-
* Watches a reactive source and calls `cb` when it changes.
|
|
449
|
+
* Watches a reactive source (or array of sources) and calls `cb` when it changes.
|
|
392
450
|
*
|
|
393
451
|
* Inside a component: hook-indexed, created once. Disposed on unmount.
|
|
394
452
|
*/
|
|
453
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
454
|
+
export function watch<T>(
|
|
455
|
+
source: WatchSource<T>,
|
|
456
|
+
cb: (newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void,
|
|
457
|
+
options?: WatchOptions,
|
|
458
|
+
): () => void
|
|
459
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
460
|
+
export function watch<T extends readonly WatchSource<any>[]>(
|
|
461
|
+
sources: [...T],
|
|
462
|
+
cb: (
|
|
463
|
+
newValues: { [K in keyof T]: T[K] extends WatchSource<infer V> ? V : never },
|
|
464
|
+
oldValues: { [K in keyof T]: T[K] extends WatchSource<infer V> ? V | undefined : never },
|
|
465
|
+
onCleanup: (fn: () => void) => void,
|
|
466
|
+
) => void,
|
|
467
|
+
options?: WatchOptions,
|
|
468
|
+
): () => void
|
|
469
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
395
470
|
export function watch<T>(
|
|
471
|
+
source: WatchSource<T> | WatchSource<T>[],
|
|
472
|
+
cb: (newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void,
|
|
473
|
+
options?: WatchOptions,
|
|
474
|
+
): () => void {
|
|
475
|
+
// Array of sources — multi-watch
|
|
476
|
+
if (Array.isArray(source)) {
|
|
477
|
+
return _watchArray(
|
|
478
|
+
source as WatchSource<unknown>[],
|
|
479
|
+
cb as (newValue: unknown, oldValue: unknown) => void,
|
|
480
|
+
options,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
return _watchSingle(source as WatchSource<T>, cb, options)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function _watchArray(
|
|
487
|
+
sources: WatchSource<unknown>[],
|
|
488
|
+
cb: (newValues: unknown, oldValues: unknown, onCleanup: (fn: () => void) => void) => void,
|
|
489
|
+
options?: WatchOptions,
|
|
490
|
+
): () => void {
|
|
491
|
+
const getters = sources.map((s) => (isRef(s) ? () => (s as Ref).value : (s as () => unknown)))
|
|
492
|
+
|
|
493
|
+
let cleanupFn: (() => void) | undefined
|
|
494
|
+
const onCleanup = (fn: () => void) => {
|
|
495
|
+
cleanupFn = fn
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const runCleanup = () => {
|
|
499
|
+
if (cleanupFn) {
|
|
500
|
+
cleanupFn()
|
|
501
|
+
cleanupFn = undefined
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const ctx = getCurrentCtx()
|
|
506
|
+
if (ctx) {
|
|
507
|
+
const idx = getHookIndex()
|
|
508
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as () => void
|
|
509
|
+
|
|
510
|
+
let oldValues: unknown[] | undefined
|
|
511
|
+
let initialized = false
|
|
512
|
+
|
|
513
|
+
if (options?.immediate) {
|
|
514
|
+
const current = getters.map((g) => g())
|
|
515
|
+
cb(current, getters.map(() => undefined), onCleanup)
|
|
516
|
+
oldValues = current
|
|
517
|
+
initialized = true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let running = false
|
|
521
|
+
const combined = pyreonComputed(() => getters.map((g) => g()))
|
|
522
|
+
const e = effect(() => {
|
|
523
|
+
if (running) return
|
|
524
|
+
running = true
|
|
525
|
+
try {
|
|
526
|
+
const newValues = combined()
|
|
527
|
+
if (initialized) {
|
|
528
|
+
runCleanup()
|
|
529
|
+
cb([...newValues], oldValues ? [...oldValues] : getters.map(() => undefined), onCleanup)
|
|
530
|
+
}
|
|
531
|
+
oldValues = [...newValues]
|
|
532
|
+
initialized = true
|
|
533
|
+
} finally {
|
|
534
|
+
running = false
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
const stop = () => {
|
|
539
|
+
runCleanup()
|
|
540
|
+
e.dispose()
|
|
541
|
+
}
|
|
542
|
+
ctx.hooks[idx] = stop
|
|
543
|
+
ctx.unmountCallbacks.push(stop)
|
|
544
|
+
return stop
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Outside component
|
|
548
|
+
let oldValues: unknown[] | undefined
|
|
549
|
+
let initialized = false
|
|
550
|
+
|
|
551
|
+
if (options?.immediate) {
|
|
552
|
+
const current = getters.map((g) => g())
|
|
553
|
+
cb(current, getters.map(() => undefined), onCleanup)
|
|
554
|
+
oldValues = current
|
|
555
|
+
initialized = true
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let running = false
|
|
559
|
+
const combined = pyreonComputed(() => getters.map((g) => g()))
|
|
560
|
+
const e = effect(() => {
|
|
561
|
+
if (running) return
|
|
562
|
+
running = true
|
|
563
|
+
try {
|
|
564
|
+
const newValues = combined()
|
|
565
|
+
if (initialized) {
|
|
566
|
+
runCleanup()
|
|
567
|
+
cb([...newValues], oldValues ? [...oldValues] : getters.map(() => undefined), onCleanup)
|
|
568
|
+
}
|
|
569
|
+
oldValues = [...newValues]
|
|
570
|
+
initialized = true
|
|
571
|
+
} finally {
|
|
572
|
+
running = false
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
const stop = () => {
|
|
577
|
+
runCleanup()
|
|
578
|
+
e.dispose()
|
|
579
|
+
}
|
|
580
|
+
if (_currentEffectScope) {
|
|
581
|
+
;(
|
|
582
|
+
_currentEffectScope as EffectScopeCompat & { _cleanups: (() => void)[] }
|
|
583
|
+
)._cleanups.push(stop)
|
|
584
|
+
}
|
|
585
|
+
return stop
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function _watchSingle<T>(
|
|
396
589
|
source: WatchSource<T>,
|
|
397
|
-
cb: (newValue: T, oldValue: T | undefined) => void,
|
|
590
|
+
cb: (newValue: T, oldValue: T | undefined, onCleanup: (fn: () => void) => void) => void,
|
|
398
591
|
options?: WatchOptions,
|
|
399
592
|
): () => void {
|
|
593
|
+
let cleanupFn: (() => void) | undefined
|
|
594
|
+
const onCleanup = (fn: () => void) => {
|
|
595
|
+
cleanupFn = fn
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const runCleanup = () => {
|
|
599
|
+
if (cleanupFn) {
|
|
600
|
+
cleanupFn()
|
|
601
|
+
cleanupFn = undefined
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
400
605
|
const ctx = getCurrentCtx()
|
|
401
606
|
if (ctx) {
|
|
402
607
|
const idx = getHookIndex()
|
|
@@ -409,7 +614,7 @@ export function watch<T>(
|
|
|
409
614
|
if (options?.immediate) {
|
|
410
615
|
oldValue = undefined
|
|
411
616
|
const current = getter()
|
|
412
|
-
cb(current, oldValue)
|
|
617
|
+
cb(current, oldValue, onCleanup)
|
|
413
618
|
oldValue = current
|
|
414
619
|
initialized = true
|
|
415
620
|
}
|
|
@@ -421,7 +626,8 @@ export function watch<T>(
|
|
|
421
626
|
try {
|
|
422
627
|
const newValue = getter()
|
|
423
628
|
if (initialized) {
|
|
424
|
-
|
|
629
|
+
runCleanup()
|
|
630
|
+
cb(newValue, oldValue, onCleanup)
|
|
425
631
|
}
|
|
426
632
|
oldValue = newValue
|
|
427
633
|
initialized = true
|
|
@@ -430,7 +636,10 @@ export function watch<T>(
|
|
|
430
636
|
}
|
|
431
637
|
})
|
|
432
638
|
|
|
433
|
-
const stop = () =>
|
|
639
|
+
const stop = () => {
|
|
640
|
+
runCleanup()
|
|
641
|
+
e.dispose()
|
|
642
|
+
}
|
|
434
643
|
ctx.hooks[idx] = stop
|
|
435
644
|
ctx.unmountCallbacks.push(stop)
|
|
436
645
|
return stop
|
|
@@ -444,7 +653,7 @@ export function watch<T>(
|
|
|
444
653
|
if (options?.immediate) {
|
|
445
654
|
oldValue = undefined
|
|
446
655
|
const current = getter()
|
|
447
|
-
cb(current, oldValue)
|
|
656
|
+
cb(current, oldValue, onCleanup)
|
|
448
657
|
oldValue = current
|
|
449
658
|
initialized = true
|
|
450
659
|
}
|
|
@@ -456,7 +665,8 @@ export function watch<T>(
|
|
|
456
665
|
try {
|
|
457
666
|
const newValue = getter()
|
|
458
667
|
if (initialized) {
|
|
459
|
-
|
|
668
|
+
runCleanup()
|
|
669
|
+
cb(newValue, oldValue, onCleanup)
|
|
460
670
|
}
|
|
461
671
|
oldValue = newValue
|
|
462
672
|
initialized = true
|
|
@@ -465,17 +675,43 @@ export function watch<T>(
|
|
|
465
675
|
}
|
|
466
676
|
})
|
|
467
677
|
|
|
468
|
-
|
|
678
|
+
const stop = () => {
|
|
679
|
+
runCleanup()
|
|
680
|
+
e.dispose()
|
|
681
|
+
}
|
|
682
|
+
if (_currentEffectScope) {
|
|
683
|
+
;(
|
|
684
|
+
_currentEffectScope as EffectScopeCompat & { _cleanups: (() => void)[] }
|
|
685
|
+
)._cleanups.push(stop)
|
|
686
|
+
}
|
|
687
|
+
return stop
|
|
469
688
|
}
|
|
470
689
|
|
|
471
690
|
/**
|
|
472
691
|
* Runs the given function reactively — re-executes whenever its tracked
|
|
473
|
-
* dependencies change.
|
|
692
|
+
* dependencies change. Passes an `onCleanup` registration function to the
|
|
693
|
+
* callback, matching Vue 3's `watchEffect((onCleanup) => { ... })` API.
|
|
474
694
|
*
|
|
475
695
|
* Inside a component: hook-indexed, created once. Disposed on unmount.
|
|
476
696
|
*/
|
|
477
|
-
export function watchEffect(
|
|
697
|
+
export function watchEffect(
|
|
698
|
+
fn: (onCleanup: (fn: () => void) => void) => void,
|
|
699
|
+
): () => void {
|
|
478
700
|
const ctx = getCurrentCtx()
|
|
701
|
+
|
|
702
|
+
let cleanupFn: (() => void) | undefined
|
|
703
|
+
const onCleanup = (cleanup: () => void) => {
|
|
704
|
+
cleanupFn = cleanup
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const runEffect = () => {
|
|
708
|
+
if (cleanupFn) {
|
|
709
|
+
cleanupFn()
|
|
710
|
+
cleanupFn = undefined
|
|
711
|
+
}
|
|
712
|
+
fn(onCleanup)
|
|
713
|
+
}
|
|
714
|
+
|
|
479
715
|
if (ctx) {
|
|
480
716
|
const idx = getHookIndex()
|
|
481
717
|
if (idx < ctx.hooks.length) return ctx.hooks[idx] as () => void
|
|
@@ -485,12 +721,15 @@ export function watchEffect(fn: () => void): () => void {
|
|
|
485
721
|
if (running) return
|
|
486
722
|
running = true
|
|
487
723
|
try {
|
|
488
|
-
|
|
724
|
+
runEffect()
|
|
489
725
|
} finally {
|
|
490
726
|
running = false
|
|
491
727
|
}
|
|
492
728
|
})
|
|
493
|
-
const stop = () =>
|
|
729
|
+
const stop = () => {
|
|
730
|
+
if (cleanupFn) cleanupFn()
|
|
731
|
+
e.dispose()
|
|
732
|
+
}
|
|
494
733
|
ctx.hooks[idx] = stop
|
|
495
734
|
ctx.unmountCallbacks.push(stop)
|
|
496
735
|
return stop
|
|
@@ -501,12 +740,22 @@ export function watchEffect(fn: () => void): () => void {
|
|
|
501
740
|
if (running) return
|
|
502
741
|
running = true
|
|
503
742
|
try {
|
|
504
|
-
|
|
743
|
+
runEffect()
|
|
505
744
|
} finally {
|
|
506
745
|
running = false
|
|
507
746
|
}
|
|
508
747
|
})
|
|
509
|
-
|
|
748
|
+
const stop = () => {
|
|
749
|
+
if (cleanupFn) cleanupFn()
|
|
750
|
+
e.dispose()
|
|
751
|
+
}
|
|
752
|
+
// Register with current effect scope if one is active
|
|
753
|
+
if (_currentEffectScope) {
|
|
754
|
+
;(
|
|
755
|
+
_currentEffectScope as EffectScopeCompat & { _cleanups: (() => void)[] }
|
|
756
|
+
)._cleanups.push(stop)
|
|
757
|
+
}
|
|
758
|
+
return stop
|
|
510
759
|
}
|
|
511
760
|
|
|
512
761
|
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
@@ -641,20 +890,32 @@ export function provide<T>(key: string | symbol, value: T): void {
|
|
|
641
890
|
|
|
642
891
|
/**
|
|
643
892
|
* Injects a value provided by an ancestor component.
|
|
893
|
+
* Supports Vue 3's factory default pattern: `inject(key, () => expensiveDefault, true)`.
|
|
644
894
|
*/
|
|
645
|
-
export function inject<T>(
|
|
895
|
+
export function inject<T>(
|
|
896
|
+
key: string | symbol,
|
|
897
|
+
defaultValue?: T | (() => T),
|
|
898
|
+
treatDefaultAsFactory?: boolean,
|
|
899
|
+
): T | undefined {
|
|
646
900
|
const ctx = getOrCreateContext<T>(key)
|
|
647
901
|
const value = useContext(ctx)
|
|
648
|
-
|
|
902
|
+
if (value !== undefined) return value
|
|
903
|
+
if (defaultValue === undefined) return undefined
|
|
904
|
+
if (treatDefaultAsFactory && typeof defaultValue === 'function') {
|
|
905
|
+
return (defaultValue as () => T)()
|
|
906
|
+
}
|
|
907
|
+
return defaultValue as T
|
|
649
908
|
}
|
|
650
909
|
|
|
651
910
|
// ─── defineComponent ──────────────────────────────────────────────────────────
|
|
652
911
|
|
|
653
912
|
interface ComponentOptions<P extends Props = Props> {
|
|
654
913
|
/** The setup function — called once during component initialization. */
|
|
655
|
-
setup: (props: P) => (() => VNodeChild) | VNodeChild
|
|
914
|
+
setup: (props: P, ctx?: SetupContext) => (() => VNodeChild) | VNodeChild
|
|
656
915
|
/** Optional name for debugging. */
|
|
657
916
|
name?: string
|
|
917
|
+
/** Prop definitions (not validated at runtime, used for type documentation). */
|
|
918
|
+
props?: Record<string, unknown>
|
|
658
919
|
}
|
|
659
920
|
|
|
660
921
|
/**
|
|
@@ -668,7 +929,21 @@ export function defineComponent<P extends Props = Props>(
|
|
|
668
929
|
return options as ComponentFn<P>
|
|
669
930
|
}
|
|
670
931
|
const comp = (props: P) => {
|
|
671
|
-
|
|
932
|
+
// Extract children from props for slots
|
|
933
|
+
const children = (props as Record<string, unknown>).children as VNodeChild | undefined
|
|
934
|
+
// Create a minimal SetupContext
|
|
935
|
+
const setupCtx: SetupContext = {
|
|
936
|
+
emit: (event: string, ...args: unknown[]) => {
|
|
937
|
+
const handlerKey = `on${event.charAt(0).toUpperCase()}${event.slice(1)}`
|
|
938
|
+
const handler = (props as Record<string, unknown>)[handlerKey]
|
|
939
|
+
if (typeof handler === 'function') (handler as (...a: unknown[]) => void)(...args)
|
|
940
|
+
},
|
|
941
|
+
slots: {
|
|
942
|
+
default: children !== undefined ? (() => children) : undefined,
|
|
943
|
+
} as Record<string, (() => VNodeChild) | undefined>,
|
|
944
|
+
attrs: props as Record<string, unknown>,
|
|
945
|
+
}
|
|
946
|
+
const result = options.setup(props, setupCtx)
|
|
672
947
|
if (typeof result === 'function') {
|
|
673
948
|
return (result as () => VNodeChild)()
|
|
674
949
|
}
|
|
@@ -680,6 +955,58 @@ export function defineComponent<P extends Props = Props>(
|
|
|
680
955
|
return comp as ComponentFn<P>
|
|
681
956
|
}
|
|
682
957
|
|
|
958
|
+
// ─── defineAsyncComponent ───────────────────────────────────────────────────
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Defines an async component that lazily loads on first use.
|
|
962
|
+
* Supports both a bare loader function and an options object with
|
|
963
|
+
* loadingComponent, errorComponent, delay, and timeout.
|
|
964
|
+
*
|
|
965
|
+
* Returns a ComponentFn with a `__loading` property for Suspense integration.
|
|
966
|
+
*/
|
|
967
|
+
export function defineAsyncComponent<P extends Props = Props>(
|
|
968
|
+
loader:
|
|
969
|
+
| (() => Promise<{ default: ComponentFn<P> }>)
|
|
970
|
+
| {
|
|
971
|
+
loader: () => Promise<{ default: ComponentFn<P> }>
|
|
972
|
+
loadingComponent?: ComponentFn
|
|
973
|
+
errorComponent?: ComponentFn
|
|
974
|
+
delay?: number
|
|
975
|
+
timeout?: number
|
|
976
|
+
},
|
|
977
|
+
): ComponentFn<P> & { __loading: () => boolean } {
|
|
978
|
+
const load = typeof loader === 'function' ? loader : loader.loader
|
|
979
|
+
|
|
980
|
+
const loaded = signal<ComponentFn<P> | null>(null)
|
|
981
|
+
const error = signal<Error | null>(null)
|
|
982
|
+
let promise: Promise<unknown> | null = null
|
|
983
|
+
|
|
984
|
+
const startLoad = () => {
|
|
985
|
+
if (promise) return
|
|
986
|
+
promise = load().then(
|
|
987
|
+
(mod) => loaded.set(mod.default),
|
|
988
|
+
(err) => error.set(err instanceof Error ? err : new Error(String(err))),
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const AsyncComp = ((props: P) => {
|
|
993
|
+
startLoad()
|
|
994
|
+
const err = error()
|
|
995
|
+
if (err) throw err
|
|
996
|
+
const comp = loaded()
|
|
997
|
+
if (!comp) return null
|
|
998
|
+
return comp(props)
|
|
999
|
+
}) as ComponentFn<P> & { __loading: () => boolean }
|
|
1000
|
+
|
|
1001
|
+
AsyncComp.__loading = () => {
|
|
1002
|
+
const isLoading = loaded() === null && error() === null
|
|
1003
|
+
if (isLoading) startLoad()
|
|
1004
|
+
return isLoading
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return AsyncComp
|
|
1008
|
+
}
|
|
1009
|
+
|
|
683
1010
|
// ─── h ────────────────────────────────────────────────────────────────────────
|
|
684
1011
|
|
|
685
1012
|
/**
|
|
@@ -692,24 +1019,298 @@ export { Fragment, pyreonH as h }
|
|
|
692
1019
|
interface App {
|
|
693
1020
|
/** Mount the application into a DOM element. Returns an unmount function. */
|
|
694
1021
|
mount(el: string | Element): () => void
|
|
1022
|
+
/** Install a plugin. */
|
|
1023
|
+
use(plugin: { install: (app: App) => void }): App
|
|
1024
|
+
/** Provide a value to the entire app tree. */
|
|
1025
|
+
provide<T>(key: string | symbol, value: T): App
|
|
695
1026
|
}
|
|
696
1027
|
|
|
697
1028
|
/**
|
|
698
1029
|
* Creates a Pyreon application instance — Vue 3 `createApp()` compatible.
|
|
699
1030
|
*/
|
|
700
1031
|
export function createApp(component: ComponentFn, props?: Props): App {
|
|
701
|
-
|
|
1032
|
+
const provisions: Array<{ key: string | symbol; value: unknown }> = []
|
|
1033
|
+
|
|
1034
|
+
const app: App = {
|
|
702
1035
|
mount(el: string | Element): () => void {
|
|
703
1036
|
const container = typeof el === 'string' ? document.querySelector(el) : el
|
|
704
1037
|
if (!container) {
|
|
705
1038
|
throw new Error(`Cannot find mount target: ${el}`)
|
|
706
1039
|
}
|
|
1040
|
+
// Push app-level provisions before mounting
|
|
1041
|
+
for (const { key, value } of provisions) {
|
|
1042
|
+
const ctx = getOrCreateContext(key)
|
|
1043
|
+
pushContext(new Map([[ctx.id, value]]))
|
|
1044
|
+
}
|
|
707
1045
|
const vnode = pyreonH(component, props ?? null)
|
|
708
1046
|
return pyreonMount(vnode, container)
|
|
709
1047
|
},
|
|
1048
|
+
use(plugin: { install: (app: App) => void }): App {
|
|
1049
|
+
plugin.install(app)
|
|
1050
|
+
return app
|
|
1051
|
+
},
|
|
1052
|
+
provide<T>(key: string | symbol, value: T): App {
|
|
1053
|
+
provisions.push({ key, value })
|
|
1054
|
+
return app
|
|
1055
|
+
},
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return app
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// ─── isReactive / isReadonly / isProxy / markRaw ─────────────────────────────
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Returns `true` if the value was created by `reactive()`.
|
|
1065
|
+
*/
|
|
1066
|
+
export function isReactive(value: unknown): boolean {
|
|
1067
|
+
return value !== null && typeof value === 'object' && rawMap.has(value as object)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Returns `true` if the value was created by `readonly()`.
|
|
1072
|
+
*/
|
|
1073
|
+
export function isReadonly(value: unknown): boolean {
|
|
1074
|
+
return (
|
|
1075
|
+
value !== null &&
|
|
1076
|
+
typeof value === 'object' &&
|
|
1077
|
+
(value as Record<symbol, unknown>)[V_IS_READONLY] === true
|
|
1078
|
+
)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Returns `true` if the value is either reactive or readonly.
|
|
1083
|
+
*/
|
|
1084
|
+
export function isProxy(value: unknown): boolean {
|
|
1085
|
+
return isReactive(value) || isReadonly(value)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Marks an object so that `reactive()` will return it as-is (not wrapped).
|
|
1090
|
+
*/
|
|
1091
|
+
export function markRaw<T extends object>(obj: T): T {
|
|
1092
|
+
;(obj as Record<symbol, boolean>)[V_SKIP] = true
|
|
1093
|
+
return obj
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ─── effectScope / getCurrentScope / onScopeDispose ──────────────────────────
|
|
1097
|
+
|
|
1098
|
+
export interface EffectScopeCompat {
|
|
1099
|
+
/** Run a function within this scope. Returns undefined if scope is stopped. */
|
|
1100
|
+
run<T>(fn: () => T): T | undefined
|
|
1101
|
+
/** Stop the scope and dispose all collected effects/cleanups. */
|
|
1102
|
+
stop(): void
|
|
1103
|
+
/** Whether the scope is still active. */
|
|
1104
|
+
active: boolean
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
let _currentEffectScope: EffectScopeCompat | null = null
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Creates an effect scope that collects reactive effects for grouped disposal.
|
|
1111
|
+
*
|
|
1112
|
+
* @param detached - If true, the scope is not collected by a parent scope.
|
|
1113
|
+
*/
|
|
1114
|
+
export function effectScope(detached?: boolean): EffectScopeCompat {
|
|
1115
|
+
const cleanups: (() => void)[] = []
|
|
1116
|
+
let active = true
|
|
1117
|
+
|
|
1118
|
+
const scope: EffectScopeCompat = {
|
|
1119
|
+
get active() {
|
|
1120
|
+
return active
|
|
1121
|
+
},
|
|
1122
|
+
run<T>(fn: () => T): T | undefined {
|
|
1123
|
+
if (!active) return undefined
|
|
1124
|
+
const prev = _currentEffectScope
|
|
1125
|
+
_currentEffectScope = scope
|
|
1126
|
+
try {
|
|
1127
|
+
return fn()
|
|
1128
|
+
} finally {
|
|
1129
|
+
_currentEffectScope = prev
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
stop() {
|
|
1133
|
+
if (!active) return
|
|
1134
|
+
active = false
|
|
1135
|
+
for (const fn of cleanups) fn()
|
|
1136
|
+
cleanups.length = 0
|
|
1137
|
+
},
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Auto-collect in parent scope unless detached
|
|
1141
|
+
if (!detached && _currentEffectScope) {
|
|
1142
|
+
const parentCleanups = (_currentEffectScope as EffectScopeCompat & { _cleanups?: (() => void)[] })
|
|
1143
|
+
._cleanups
|
|
1144
|
+
if (parentCleanups) parentCleanups.push(() => scope.stop())
|
|
1145
|
+
}
|
|
1146
|
+
;(scope as EffectScopeCompat & { _cleanups: (() => void)[] })._cleanups = cleanups
|
|
1147
|
+
|
|
1148
|
+
return scope
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Returns the current active effect scope, or undefined if none.
|
|
1153
|
+
*/
|
|
1154
|
+
export function getCurrentScope(): EffectScopeCompat | undefined {
|
|
1155
|
+
return _currentEffectScope ?? undefined
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Registers a cleanup function on the current effect scope.
|
|
1160
|
+
*/
|
|
1161
|
+
export function onScopeDispose(fn: () => void): void {
|
|
1162
|
+
if (_currentEffectScope) {
|
|
1163
|
+
;(
|
|
1164
|
+
_currentEffectScope as EffectScopeCompat & { _cleanups: (() => void)[] }
|
|
1165
|
+
)._cleanups.push(fn)
|
|
710
1166
|
}
|
|
711
1167
|
}
|
|
712
1168
|
|
|
1169
|
+
// ─── onErrorCaptured / onRenderTracked / onRenderTriggered ───────────────────
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Registers an error capture handler.
|
|
1173
|
+
* No direct equivalent in Pyreon — stored but not actively used.
|
|
1174
|
+
*/
|
|
1175
|
+
export function onErrorCaptured(fn: (err: Error) => boolean | void): void {
|
|
1176
|
+
const ctx = getCurrentCtx()
|
|
1177
|
+
if (ctx) {
|
|
1178
|
+
const idx = getHookIndex()
|
|
1179
|
+
if (idx < ctx.hooks.length) return
|
|
1180
|
+
ctx.hooks[idx] = fn
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Dev-only lifecycle hook — no-op in Pyreon.
|
|
1186
|
+
*/
|
|
1187
|
+
export function onRenderTracked(
|
|
1188
|
+
_fn: (event: { key: string; type: string }) => void,
|
|
1189
|
+
): void {
|
|
1190
|
+
// Dev-only hook — no equivalent in Pyreon
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Dev-only lifecycle hook — no-op in Pyreon.
|
|
1195
|
+
*/
|
|
1196
|
+
export function onRenderTriggered(
|
|
1197
|
+
_fn: (event: { key: string; type: string }) => void,
|
|
1198
|
+
): void {
|
|
1199
|
+
// Dev-only hook — no equivalent in Pyreon
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ─── Teleport / KeepAlive ────────────────────────────────────────────────────
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Teleport — renders children into a different DOM element.
|
|
1206
|
+
* Maps to Pyreon's Portal.
|
|
1207
|
+
*/
|
|
1208
|
+
export function Teleport(props: {
|
|
1209
|
+
to: string | Element
|
|
1210
|
+
children?: VNodeChild
|
|
1211
|
+
}): VNodeChild {
|
|
1212
|
+
const target =
|
|
1213
|
+
typeof props.to === 'string' ? document.querySelector(props.to) : props.to
|
|
1214
|
+
if (!target) return props.children ?? null
|
|
1215
|
+
return Portal({ target, children: props.children ?? null })
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* KeepAlive — not supported in Pyreon. Renders children as-is.
|
|
1220
|
+
*/
|
|
1221
|
+
export function KeepAlive(props: { children?: VNodeChild }): VNodeChild {
|
|
1222
|
+
return props.children ?? null
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ─── watchPostEffect / watchSyncEffect ───────────────────────────────────────
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Runs a watchEffect that flushes after DOM updates.
|
|
1229
|
+
* In Pyreon, same as `watchEffect()`.
|
|
1230
|
+
*/
|
|
1231
|
+
export function watchPostEffect(
|
|
1232
|
+
fn: (onCleanup: (fn: () => void) => void) => void,
|
|
1233
|
+
): () => void {
|
|
1234
|
+
return watchEffect(fn)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Runs a watchEffect that flushes synchronously.
|
|
1239
|
+
* In Pyreon, same as `watchEffect()`.
|
|
1240
|
+
*/
|
|
1241
|
+
export function watchSyncEffect(
|
|
1242
|
+
fn: (onCleanup: (fn: () => void) => void) => void,
|
|
1243
|
+
): () => void {
|
|
1244
|
+
return watchEffect(fn)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ─── customRef ───────────────────────────────────────────────────────────────
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Creates a customized ref with explicit control over dependency tracking
|
|
1251
|
+
* and update triggering.
|
|
1252
|
+
*/
|
|
1253
|
+
export function customRef<T>(
|
|
1254
|
+
factory: (
|
|
1255
|
+
track: () => void,
|
|
1256
|
+
trigger: () => void,
|
|
1257
|
+
) => { get: () => T; set: (v: T) => void },
|
|
1258
|
+
): Ref<T> {
|
|
1259
|
+
const s = signal(0)
|
|
1260
|
+
const { get, set } = factory(
|
|
1261
|
+
() => { s(); return undefined as never }, // track — reading the signal subscribes
|
|
1262
|
+
() => s.set(s.peek() + 1), // trigger — bump version to re-notify
|
|
1263
|
+
)
|
|
1264
|
+
return {
|
|
1265
|
+
[V_IS_REF]: true as const,
|
|
1266
|
+
get value(): T {
|
|
1267
|
+
return get()
|
|
1268
|
+
},
|
|
1269
|
+
set value(v: T) {
|
|
1270
|
+
set(v)
|
|
1271
|
+
},
|
|
1272
|
+
} as Ref<T>
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// ─── version ─────────────────────────────────────────────────────────────────
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Compatibility version string — indicates Vue 3 API compatibility.
|
|
1279
|
+
*/
|
|
1280
|
+
export const version = '3.5.0-pyreon'
|
|
1281
|
+
|
|
1282
|
+
// ─── Type exports ────────────────────────────────────────────────────────────
|
|
1283
|
+
|
|
1284
|
+
export type { ComponentFn as Component } from '@pyreon/core'
|
|
1285
|
+
export type { VNodeChild as VNode } from '@pyreon/core'
|
|
1286
|
+
|
|
1287
|
+
/** Vue-compatible PropType — a callable that returns T. */
|
|
1288
|
+
export type PropType<T> = { (): T }
|
|
1289
|
+
|
|
1290
|
+
/** Extract prop types from a component's props definition. */
|
|
1291
|
+
export type ExtractPropTypes<T> = {
|
|
1292
|
+
[K in keyof T]: T[K] extends PropType<infer V> ? V : T[K]
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Vue-compatible emits options type. */
|
|
1296
|
+
export type EmitsOptions = Record<string, (...args: unknown[]) => void>
|
|
1297
|
+
|
|
1298
|
+
/** Vue-compatible setup context. */
|
|
1299
|
+
export type SetupContext = {
|
|
1300
|
+
emit: (event: string, ...args: unknown[]) => void
|
|
1301
|
+
slots: Record<string, (() => VNodeChild) | undefined>
|
|
1302
|
+
attrs: Record<string, unknown>
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** Vue-compatible plugin interface. */
|
|
1306
|
+
export type Plugin = { install: (app: App) => void }
|
|
1307
|
+
|
|
1308
|
+
/** Vue-compatible directive type (stub). */
|
|
1309
|
+
export type Directive = Record<string, unknown>
|
|
1310
|
+
|
|
1311
|
+
/** Vue-compatible injection key with type branding. */
|
|
1312
|
+
export type InjectionKey<T> = symbol & { __type: T }
|
|
1313
|
+
|
|
713
1314
|
// ─── Additional re-exports ────────────────────────────────────────────────────
|
|
714
1315
|
|
|
715
1316
|
export { batch } from '@pyreon/reactivity'
|