@pyreon/vue-compat 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +261 -65
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +265 -64
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +22 -49
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +14 -4
- package/src/index.ts +307 -81
- package/src/jsx-runtime.ts +178 -0
- package/src/tests/vue-compat.test.ts +348 -119
package/src/index.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pyreon/vue-compat
|
|
3
3
|
*
|
|
4
|
-
* Vue 3-compatible Composition API that runs on Pyreon's reactive engine
|
|
4
|
+
* Vue 3-compatible Composition API that runs on Pyreon's reactive engine,
|
|
5
|
+
* using a hook-indexed re-render model.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* All stateful APIs (ref, computed, reactive, watch, lifecycle hooks, etc.)
|
|
8
|
+
* use hook-indexing to persist state across re-renders. The component body
|
|
9
|
+
* re-executes when state changes (driven by a version signal in the JSX
|
|
10
|
+
* runtime), and hook-indexed calls return the same objects across renders.
|
|
8
11
|
*
|
|
9
12
|
* DIFFERENCES FROM VUE 3:
|
|
10
13
|
* - `deep` option in watch() is ignored — Pyreon tracks dependencies automatically.
|
|
11
14
|
* - `shallowReactive` uses per-property signals (still shallow, but Pyreon-flavored).
|
|
12
15
|
* - `readonly` returns a Proxy that throws on set (not Vue's readonly proxy).
|
|
13
16
|
* - `defineComponent` only supports Composition API (setup function), not Options API.
|
|
14
|
-
* - Components
|
|
17
|
+
* - Components re-execute their body on state change (hook-indexed re-render model).
|
|
15
18
|
*
|
|
16
19
|
* USAGE:
|
|
17
20
|
* Replace `import { ref, computed, watch } from "vue"` with
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
signal,
|
|
40
43
|
} from "@pyreon/reactivity"
|
|
41
44
|
import { mount as pyreonMount } from "@pyreon/runtime-dom"
|
|
45
|
+
import { getCurrentCtx, getHookIndex } from "./jsx-runtime"
|
|
42
46
|
|
|
43
47
|
// ─── Internal symbols ─────────────────────────────────────────────────────────
|
|
44
48
|
|
|
@@ -57,18 +61,43 @@ export interface Ref<T = unknown> {
|
|
|
57
61
|
* Creates a reactive ref wrapping the given value.
|
|
58
62
|
* Access via `.value` — reads track, writes trigger.
|
|
59
63
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
64
|
+
* Inside a component: hook-indexed. The setter also calls `scheduleRerender()`.
|
|
65
|
+
* Outside a component: creates a standalone reactive ref.
|
|
62
66
|
*/
|
|
63
67
|
export function ref<T>(value: T): Ref<T> {
|
|
68
|
+
const ctx = getCurrentCtx()
|
|
69
|
+
if (ctx) {
|
|
70
|
+
const idx = getHookIndex()
|
|
71
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as Ref<T>
|
|
72
|
+
|
|
73
|
+
const s = signal(value)
|
|
74
|
+
const { scheduleRerender } = ctx
|
|
75
|
+
const r = {
|
|
76
|
+
[V_IS_REF]: true as const,
|
|
77
|
+
get value(): T {
|
|
78
|
+
return s()
|
|
79
|
+
},
|
|
80
|
+
set value(v: T) {
|
|
81
|
+
s.set(v)
|
|
82
|
+
scheduleRerender()
|
|
83
|
+
},
|
|
84
|
+
/** @internal — access underlying signal for triggerRef */
|
|
85
|
+
_signal: s,
|
|
86
|
+
_scheduleRerender: scheduleRerender,
|
|
87
|
+
}
|
|
88
|
+
ctx.hooks[idx] = r
|
|
89
|
+
return r as Ref<T>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Outside component
|
|
64
93
|
const s = signal(value)
|
|
65
94
|
const r = {
|
|
66
95
|
[V_IS_REF]: true as const,
|
|
67
96
|
get value(): T {
|
|
68
97
|
return s()
|
|
69
98
|
},
|
|
70
|
-
set value(
|
|
71
|
-
s.set(
|
|
99
|
+
set value(v: T) {
|
|
100
|
+
s.set(v)
|
|
72
101
|
},
|
|
73
102
|
/** @internal — access underlying signal for triggerRef */
|
|
74
103
|
_signal: s,
|
|
@@ -78,8 +107,6 @@ export function ref<T>(value: T): Ref<T> {
|
|
|
78
107
|
|
|
79
108
|
/**
|
|
80
109
|
* Creates a shallow ref — same as `ref()` in Pyreon since signals are inherently shallow.
|
|
81
|
-
*
|
|
82
|
-
* Difference from Vue: identical to `ref()` — Pyreon signals don't perform deep conversion.
|
|
83
110
|
*/
|
|
84
111
|
export function shallowRef<T>(value: T): Ref<T> {
|
|
85
112
|
return ref(value)
|
|
@@ -89,13 +116,16 @@ export function shallowRef<T>(value: T): Ref<T> {
|
|
|
89
116
|
* Force trigger a ref's subscribers, even if the value hasn't changed.
|
|
90
117
|
*/
|
|
91
118
|
export function triggerRef<T>(r: Ref<T>): void {
|
|
92
|
-
const internal = r as Ref<T> & { _signal: Signal<T
|
|
119
|
+
const internal = r as Ref<T> & { _signal: Signal<T>; _scheduleRerender?: () => void }
|
|
93
120
|
if (internal._signal) {
|
|
94
121
|
// Force notify by setting the same value with Object.is bypass
|
|
95
122
|
const current = internal._signal.peek()
|
|
96
123
|
internal._signal.set(undefined as T)
|
|
97
124
|
internal._signal.set(current)
|
|
98
125
|
}
|
|
126
|
+
if (internal._scheduleRerender) {
|
|
127
|
+
internal._scheduleRerender()
|
|
128
|
+
}
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
/**
|
|
@@ -125,11 +155,9 @@ export interface WritableComputedRef<T = unknown> extends Ref<T> {
|
|
|
125
155
|
}
|
|
126
156
|
|
|
127
157
|
/**
|
|
128
|
-
* Creates a computed ref. Supports both readonly and writable forms
|
|
129
|
-
* - `computed(() => value)` — readonly ComputedRef
|
|
130
|
-
* - `computed({ get, set })` — writable WritableComputedRef
|
|
158
|
+
* Creates a computed ref. Supports both readonly and writable forms.
|
|
131
159
|
*
|
|
132
|
-
*
|
|
160
|
+
* Inside a component: hook-indexed.
|
|
133
161
|
*/
|
|
134
162
|
export function computed<T>(getter: () => T): ComputedRef<T>
|
|
135
163
|
export function computed<T>(options: {
|
|
@@ -139,6 +167,33 @@ export function computed<T>(options: {
|
|
|
139
167
|
export function computed<T>(
|
|
140
168
|
fnOrOptions: (() => T) | { get: () => T; set: (value: T) => void },
|
|
141
169
|
): ComputedRef<T> | WritableComputedRef<T> {
|
|
170
|
+
const ctx = getCurrentCtx()
|
|
171
|
+
if (ctx) {
|
|
172
|
+
const idx = getHookIndex()
|
|
173
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as ComputedRef<T>
|
|
174
|
+
|
|
175
|
+
const getter = typeof fnOrOptions === "function" ? fnOrOptions : fnOrOptions.get
|
|
176
|
+
const setter = typeof fnOrOptions === "object" ? fnOrOptions.set : undefined
|
|
177
|
+
const c = pyreonComputed(getter)
|
|
178
|
+
const { scheduleRerender } = ctx
|
|
179
|
+
const r = {
|
|
180
|
+
[V_IS_REF]: true as const,
|
|
181
|
+
get value(): T {
|
|
182
|
+
return c()
|
|
183
|
+
},
|
|
184
|
+
set value(v: T) {
|
|
185
|
+
if (!setter) {
|
|
186
|
+
throw new Error("Cannot set value of a computed ref — computed refs are readonly")
|
|
187
|
+
}
|
|
188
|
+
setter(v)
|
|
189
|
+
scheduleRerender()
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
ctx.hooks[idx] = r
|
|
193
|
+
return r as ComputedRef<T>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Outside component
|
|
142
197
|
const getter = typeof fnOrOptions === "function" ? fnOrOptions : fnOrOptions.get
|
|
143
198
|
const setter = typeof fnOrOptions === "object" ? fnOrOptions.set : undefined
|
|
144
199
|
const c = pyreonComputed(getter)
|
|
@@ -159,41 +214,75 @@ export function computed<T>(
|
|
|
159
214
|
|
|
160
215
|
// ─── Reactive / Readonly ──────────────────────────────────────────────────────
|
|
161
216
|
|
|
217
|
+
// WeakMap to track raw objects behind reactive proxies
|
|
218
|
+
const rawMap = new WeakMap<object, object>()
|
|
219
|
+
|
|
162
220
|
/**
|
|
163
221
|
* Creates a deeply reactive proxy from a plain object.
|
|
164
222
|
* Backed by Pyreon's `createStore()`.
|
|
165
223
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
224
|
+
* Inside a component: hook-indexed. Proxy wrapper intercepts sets to
|
|
225
|
+
* call `scheduleRerender()`.
|
|
168
226
|
*/
|
|
169
227
|
export function reactive<T extends object>(obj: T): T {
|
|
228
|
+
const ctx = getCurrentCtx()
|
|
229
|
+
if (ctx) {
|
|
230
|
+
const idx = getHookIndex()
|
|
231
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as T
|
|
232
|
+
|
|
233
|
+
const proxy = createStore(obj)
|
|
234
|
+
rawMap.set(proxy as object, obj)
|
|
235
|
+
const { scheduleRerender } = ctx
|
|
236
|
+
const wrapped = new Proxy(proxy, {
|
|
237
|
+
set(target, key, value, receiver) {
|
|
238
|
+
const result = Reflect.set(target, key, value, receiver)
|
|
239
|
+
scheduleRerender()
|
|
240
|
+
return result
|
|
241
|
+
},
|
|
242
|
+
deleteProperty(target, key) {
|
|
243
|
+
const result = Reflect.deleteProperty(target, key)
|
|
244
|
+
scheduleRerender()
|
|
245
|
+
return result
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
rawMap.set(wrapped as object, obj)
|
|
249
|
+
ctx.hooks[idx] = wrapped
|
|
250
|
+
return wrapped as T
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Outside component
|
|
170
254
|
const proxy = createStore(obj)
|
|
171
|
-
// Store raw reference for toRaw()
|
|
172
255
|
rawMap.set(proxy as object, obj)
|
|
173
256
|
return proxy
|
|
174
257
|
}
|
|
175
258
|
|
|
176
259
|
/**
|
|
177
|
-
* Creates a shallow reactive proxy.
|
|
178
|
-
* In Pyreon, `createStore` is already per-property (not deeply recursive for primitives),
|
|
179
|
-
* but nested objects will be wrapped. For truly shallow behavior, use individual refs.
|
|
180
|
-
*
|
|
181
|
-
* Difference from Vue: backed by `createStore()` — same as `reactive()` in practice.
|
|
260
|
+
* Creates a shallow reactive proxy — same as `reactive()` in Pyreon.
|
|
182
261
|
*/
|
|
183
262
|
export function shallowReactive<T extends object>(obj: T): T {
|
|
184
263
|
return reactive(obj)
|
|
185
264
|
}
|
|
186
265
|
|
|
187
|
-
// WeakMap to track raw objects behind reactive proxies
|
|
188
|
-
const rawMap = new WeakMap<object, object>()
|
|
189
|
-
|
|
190
266
|
/**
|
|
191
267
|
* Returns a readonly proxy that throws on mutation attempts.
|
|
192
268
|
*
|
|
193
|
-
*
|
|
194
|
-
* rather than Vue's full readonly reactive system.
|
|
269
|
+
* Inside a component: hook-indexed.
|
|
195
270
|
*/
|
|
196
271
|
export function readonly<T extends object>(obj: T): Readonly<T> {
|
|
272
|
+
const ctx = getCurrentCtx()
|
|
273
|
+
if (ctx) {
|
|
274
|
+
const idx = getHookIndex()
|
|
275
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as Readonly<T>
|
|
276
|
+
|
|
277
|
+
const proxy = _createReadonlyProxy(obj)
|
|
278
|
+
ctx.hooks[idx] = proxy
|
|
279
|
+
return proxy
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return _createReadonlyProxy(obj)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _createReadonlyProxy<T extends object>(obj: T): Readonly<T> {
|
|
197
286
|
const proxy = new Proxy(obj, {
|
|
198
287
|
get(target, key) {
|
|
199
288
|
if (key === V_IS_READONLY) return true
|
|
@@ -214,8 +303,6 @@ export function readonly<T extends object>(obj: T): Readonly<T> {
|
|
|
214
303
|
|
|
215
304
|
/**
|
|
216
305
|
* Returns the raw (unwrapped) object behind a reactive or readonly proxy.
|
|
217
|
-
*
|
|
218
|
-
* Difference from Vue: only works for objects created via `reactive()` or `readonly()`.
|
|
219
306
|
*/
|
|
220
307
|
export function toRaw<T extends object>(proxy: T): T {
|
|
221
308
|
// Check readonly first
|
|
@@ -231,8 +318,24 @@ export function toRaw<T extends object>(proxy: T): T {
|
|
|
231
318
|
/**
|
|
232
319
|
* Creates a ref linked to a property of a reactive object.
|
|
233
320
|
* Reading/writing the ref's `.value` reads/writes the original property.
|
|
321
|
+
*
|
|
322
|
+
* Inside a component: hook-indexed.
|
|
234
323
|
*/
|
|
235
324
|
export function toRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<T[K]> {
|
|
325
|
+
const ctx = getCurrentCtx()
|
|
326
|
+
if (ctx) {
|
|
327
|
+
const idx = getHookIndex()
|
|
328
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as Ref<T[K]>
|
|
329
|
+
|
|
330
|
+
const r = _createToRef(obj, key)
|
|
331
|
+
ctx.hooks[idx] = r
|
|
332
|
+
return r
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return _createToRef(obj, key)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function _createToRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<T[K]> {
|
|
236
339
|
const r = {
|
|
237
340
|
[V_IS_REF]: true as const,
|
|
238
341
|
get value(): T[K] {
|
|
@@ -248,11 +351,27 @@ export function toRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<
|
|
|
248
351
|
/**
|
|
249
352
|
* Converts all properties of a reactive object into individual refs.
|
|
250
353
|
* Each ref is linked to the original property (not a copy).
|
|
354
|
+
*
|
|
355
|
+
* Inside a component: hook-indexed (the entire result, not individual refs).
|
|
251
356
|
*/
|
|
252
357
|
export function toRefs<T extends object>(obj: T): { [K in keyof T]: Ref<T[K]> } {
|
|
358
|
+
const ctx = getCurrentCtx()
|
|
359
|
+
if (ctx) {
|
|
360
|
+
const idx = getHookIndex()
|
|
361
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as { [K in keyof T]: Ref<T[K]> }
|
|
362
|
+
|
|
363
|
+
const result = {} as { [K in keyof T]: Ref<T[K]> }
|
|
364
|
+
for (const key of Object.keys(obj) as (keyof T)[]) {
|
|
365
|
+
// Create refs directly (not via exported toRef) to avoid extra hook index consumption
|
|
366
|
+
result[key] = _createToRef(obj, key)
|
|
367
|
+
}
|
|
368
|
+
ctx.hooks[idx] = result
|
|
369
|
+
return result
|
|
370
|
+
}
|
|
371
|
+
|
|
253
372
|
const result = {} as { [K in keyof T]: Ref<T[K]> }
|
|
254
373
|
for (const key of Object.keys(obj) as (keyof T)[]) {
|
|
255
|
-
result[key] =
|
|
374
|
+
result[key] = _createToRef(obj, key)
|
|
256
375
|
}
|
|
257
376
|
return result
|
|
258
377
|
}
|
|
@@ -270,16 +389,54 @@ type WatchSource<T> = Ref<T> | (() => T)
|
|
|
270
389
|
|
|
271
390
|
/**
|
|
272
391
|
* Watches a reactive source and calls `cb` when it changes.
|
|
273
|
-
* Tracks old and new values.
|
|
274
392
|
*
|
|
275
|
-
*
|
|
276
|
-
* Returns a stop function to dispose the watcher.
|
|
393
|
+
* Inside a component: hook-indexed, created once. Disposed on unmount.
|
|
277
394
|
*/
|
|
278
395
|
export function watch<T>(
|
|
279
396
|
source: WatchSource<T>,
|
|
280
397
|
cb: (newValue: T, oldValue: T | undefined) => void,
|
|
281
398
|
options?: WatchOptions,
|
|
282
399
|
): () => void {
|
|
400
|
+
const ctx = getCurrentCtx()
|
|
401
|
+
if (ctx) {
|
|
402
|
+
const idx = getHookIndex()
|
|
403
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as () => void
|
|
404
|
+
|
|
405
|
+
const getter = isRef(source) ? () => source.value : (source as () => T)
|
|
406
|
+
let oldValue: T | undefined
|
|
407
|
+
let initialized = false
|
|
408
|
+
|
|
409
|
+
if (options?.immediate) {
|
|
410
|
+
oldValue = undefined
|
|
411
|
+
const current = getter()
|
|
412
|
+
cb(current, oldValue)
|
|
413
|
+
oldValue = current
|
|
414
|
+
initialized = true
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let running = false
|
|
418
|
+
const e = effect(() => {
|
|
419
|
+
if (running) return
|
|
420
|
+
running = true
|
|
421
|
+
try {
|
|
422
|
+
const newValue = getter()
|
|
423
|
+
if (initialized) {
|
|
424
|
+
cb(newValue, oldValue)
|
|
425
|
+
}
|
|
426
|
+
oldValue = newValue
|
|
427
|
+
initialized = true
|
|
428
|
+
} finally {
|
|
429
|
+
running = false
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const stop = () => e.dispose()
|
|
434
|
+
ctx.hooks[idx] = stop
|
|
435
|
+
ctx.unmountCallbacks.push(stop)
|
|
436
|
+
return stop
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Outside component
|
|
283
440
|
const getter = isRef(source) ? () => source.value : (source as () => T)
|
|
284
441
|
let oldValue: T | undefined
|
|
285
442
|
let initialized = false
|
|
@@ -292,14 +449,20 @@ export function watch<T>(
|
|
|
292
449
|
initialized = true
|
|
293
450
|
}
|
|
294
451
|
|
|
452
|
+
let running = false
|
|
295
453
|
const e = effect(() => {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
454
|
+
if (running) return
|
|
455
|
+
running = true
|
|
456
|
+
try {
|
|
457
|
+
const newValue = getter()
|
|
458
|
+
if (initialized) {
|
|
459
|
+
cb(newValue, oldValue)
|
|
460
|
+
}
|
|
461
|
+
oldValue = newValue
|
|
462
|
+
initialized = true
|
|
463
|
+
} finally {
|
|
464
|
+
running = false
|
|
300
465
|
}
|
|
301
|
-
oldValue = newValue
|
|
302
|
-
initialized = true
|
|
303
466
|
})
|
|
304
467
|
|
|
305
468
|
return () => e.dispose()
|
|
@@ -309,11 +472,40 @@ export function watch<T>(
|
|
|
309
472
|
* Runs the given function reactively — re-executes whenever its tracked
|
|
310
473
|
* dependencies change.
|
|
311
474
|
*
|
|
312
|
-
*
|
|
313
|
-
* Returns a stop function.
|
|
475
|
+
* Inside a component: hook-indexed, created once. Disposed on unmount.
|
|
314
476
|
*/
|
|
315
477
|
export function watchEffect(fn: () => void): () => void {
|
|
316
|
-
const
|
|
478
|
+
const ctx = getCurrentCtx()
|
|
479
|
+
if (ctx) {
|
|
480
|
+
const idx = getHookIndex()
|
|
481
|
+
if (idx < ctx.hooks.length) return ctx.hooks[idx] as () => void
|
|
482
|
+
|
|
483
|
+
let running = false
|
|
484
|
+
const e = effect(() => {
|
|
485
|
+
if (running) return
|
|
486
|
+
running = true
|
|
487
|
+
try {
|
|
488
|
+
fn()
|
|
489
|
+
} finally {
|
|
490
|
+
running = false
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
const stop = () => e.dispose()
|
|
494
|
+
ctx.hooks[idx] = stop
|
|
495
|
+
ctx.unmountCallbacks.push(stop)
|
|
496
|
+
return stop
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let running = false
|
|
500
|
+
const e = effect(() => {
|
|
501
|
+
if (running) return
|
|
502
|
+
running = true
|
|
503
|
+
try {
|
|
504
|
+
fn()
|
|
505
|
+
} finally {
|
|
506
|
+
running = false
|
|
507
|
+
}
|
|
508
|
+
})
|
|
317
509
|
return () => e.dispose()
|
|
318
510
|
}
|
|
319
511
|
|
|
@@ -321,61 +513,95 @@ export function watchEffect(fn: () => void): () => void {
|
|
|
321
513
|
|
|
322
514
|
/**
|
|
323
515
|
* Registers a callback to run after the component is mounted.
|
|
324
|
-
*
|
|
325
|
-
* Difference from Vue: maps directly to Pyreon's `onMount()`.
|
|
326
|
-
* In Pyreon there is no distinction between beforeMount and mounted.
|
|
516
|
+
* Hook-indexed: only registered on first render.
|
|
327
517
|
*/
|
|
328
518
|
export function onMounted(fn: () => void): void {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
519
|
+
const ctx = getCurrentCtx()
|
|
520
|
+
if (!ctx) {
|
|
521
|
+
// Fallback: use Pyreon's lifecycle directly (e.g., inside defineComponent without jsx-runtime)
|
|
522
|
+
onMount(() => {
|
|
523
|
+
fn()
|
|
524
|
+
return undefined
|
|
525
|
+
})
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
const idx = getHookIndex()
|
|
529
|
+
if (idx < ctx.hooks.length) return // Already registered
|
|
530
|
+
ctx.hooks[idx] = true
|
|
531
|
+
// Schedule to run after render via microtask
|
|
532
|
+
ctx.pendingEffects.push({
|
|
533
|
+
fn: () => {
|
|
534
|
+
fn()
|
|
535
|
+
return undefined
|
|
536
|
+
},
|
|
537
|
+
deps: [],
|
|
538
|
+
cleanup: undefined,
|
|
332
539
|
})
|
|
333
540
|
}
|
|
334
541
|
|
|
335
542
|
/**
|
|
336
|
-
* Registers a callback to run
|
|
337
|
-
*
|
|
338
|
-
* Difference from Vue: maps to Pyreon's `onUnmount()`.
|
|
339
|
-
* In Pyreon there is no distinction between beforeUnmount and unmounted.
|
|
543
|
+
* Registers a callback to run when the component is unmounted.
|
|
544
|
+
* Hook-indexed: only registered on first render.
|
|
340
545
|
*/
|
|
341
546
|
export function onUnmounted(fn: () => void): void {
|
|
342
|
-
|
|
547
|
+
const ctx = getCurrentCtx()
|
|
548
|
+
if (!ctx) {
|
|
549
|
+
onUnmount(fn)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
const idx = getHookIndex()
|
|
553
|
+
if (idx < ctx.hooks.length) return // Already registered
|
|
554
|
+
ctx.hooks[idx] = true
|
|
555
|
+
ctx.unmountCallbacks.push(fn)
|
|
343
556
|
}
|
|
344
557
|
|
|
345
558
|
/**
|
|
346
|
-
* Registers a callback to run after a reactive update.
|
|
347
|
-
*
|
|
348
|
-
* Difference from Vue: maps to Pyreon's `onUpdate()`.
|
|
559
|
+
* Registers a callback to run after a reactive update (not on initial mount).
|
|
560
|
+
* Hook-indexed: registered once, fires on each re-render.
|
|
349
561
|
*/
|
|
350
562
|
export function onUpdated(fn: () => void): void {
|
|
351
|
-
|
|
563
|
+
const ctx = getCurrentCtx()
|
|
564
|
+
if (!ctx) {
|
|
565
|
+
onUpdate(fn)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
const idx = getHookIndex()
|
|
569
|
+
if (idx >= ctx.hooks.length) {
|
|
570
|
+
// First render — just mark as registered, don't fire
|
|
571
|
+
ctx.hooks[idx] = true
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
// Re-render — schedule the callback
|
|
575
|
+
ctx.pendingEffects.push({
|
|
576
|
+
fn: () => {
|
|
577
|
+
fn()
|
|
578
|
+
return undefined
|
|
579
|
+
},
|
|
580
|
+
deps: undefined,
|
|
581
|
+
cleanup: undefined,
|
|
582
|
+
})
|
|
352
583
|
}
|
|
353
584
|
|
|
354
585
|
/**
|
|
355
586
|
* Registers a callback to run before mount.
|
|
356
|
-
* In Pyreon there is no pre-mount phase — maps to `
|
|
587
|
+
* In Pyreon there is no pre-mount phase — maps to `onMounted()`.
|
|
357
588
|
*/
|
|
358
589
|
export function onBeforeMount(fn: () => void): void {
|
|
359
|
-
|
|
360
|
-
fn()
|
|
361
|
-
return undefined
|
|
362
|
-
})
|
|
590
|
+
onMounted(fn)
|
|
363
591
|
}
|
|
364
592
|
|
|
365
593
|
/**
|
|
366
594
|
* Registers a callback to run before unmount.
|
|
367
|
-
* In Pyreon there is no pre-unmount phase — maps to `
|
|
595
|
+
* In Pyreon there is no pre-unmount phase — maps to `onUnmounted()`.
|
|
368
596
|
*/
|
|
369
597
|
export function onBeforeUnmount(fn: () => void): void {
|
|
370
|
-
|
|
598
|
+
onUnmounted(fn)
|
|
371
599
|
}
|
|
372
600
|
|
|
373
601
|
// ─── nextTick ─────────────────────────────────────────────────────────────────
|
|
374
602
|
|
|
375
603
|
/**
|
|
376
604
|
* Returns a Promise that resolves after all pending reactive updates have flushed.
|
|
377
|
-
*
|
|
378
|
-
* Difference from Vue: identical to Pyreon's `nextTick()`.
|
|
379
605
|
*/
|
|
380
606
|
export function nextTick(): Promise<void> {
|
|
381
607
|
return pyreonNextTick()
|
|
@@ -396,20 +622,26 @@ function getOrCreateContext<T>(key: string | symbol, defaultValue?: T) {
|
|
|
396
622
|
/**
|
|
397
623
|
* Provides a value to all descendant components.
|
|
398
624
|
*
|
|
399
|
-
*
|
|
400
|
-
* Must be called during component setup. The value is scoped to the component
|
|
401
|
-
* tree — not globally shared.
|
|
625
|
+
* Inside a component: hook-indexed, pushed once. Popped on unmount.
|
|
402
626
|
*/
|
|
403
627
|
export function provide<T>(key: string | symbol, value: T): void {
|
|
404
|
-
const ctx =
|
|
405
|
-
|
|
406
|
-
|
|
628
|
+
const ctx = getCurrentCtx()
|
|
629
|
+
if (ctx) {
|
|
630
|
+
const idx = getHookIndex()
|
|
631
|
+
if (idx < ctx.hooks.length) return // Already provided
|
|
632
|
+
ctx.hooks[idx] = true
|
|
633
|
+
const vueCtx = getOrCreateContext<T>(key)
|
|
634
|
+
pushContext(new Map([[vueCtx.id, value]]))
|
|
635
|
+
ctx.unmountCallbacks.push(() => popContext())
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
// Outside component — use Pyreon's provide directly
|
|
639
|
+
const vueCtx = getOrCreateContext<T>(key)
|
|
640
|
+
pushContext(new Map([[vueCtx.id, value]]))
|
|
407
641
|
}
|
|
408
642
|
|
|
409
643
|
/**
|
|
410
644
|
* Injects a value provided by an ancestor component.
|
|
411
|
-
*
|
|
412
|
-
* Difference from Vue: backed by Pyreon's context system (useContext).
|
|
413
645
|
*/
|
|
414
646
|
export function inject<T>(key: string | symbol, defaultValue?: T): T | undefined {
|
|
415
647
|
const ctx = getOrCreateContext<T>(key)
|
|
@@ -429,9 +661,6 @@ interface ComponentOptions<P extends Props = Props> {
|
|
|
429
661
|
/**
|
|
430
662
|
* Defines a component using Vue 3 Composition API style.
|
|
431
663
|
* Only supports the `setup()` function — Options API is not supported.
|
|
432
|
-
*
|
|
433
|
-
* Difference from Vue: returns a Pyreon `ComponentFn`. No template/render option —
|
|
434
|
-
* the setup function should return a render function or VNode directly.
|
|
435
664
|
*/
|
|
436
665
|
export function defineComponent<P extends Props = Props>(
|
|
437
666
|
options: ComponentOptions<P> | ((props: P) => VNodeChild),
|
|
@@ -468,9 +697,6 @@ interface App {
|
|
|
468
697
|
|
|
469
698
|
/**
|
|
470
699
|
* Creates a Pyreon application instance — Vue 3 `createApp()` compatible.
|
|
471
|
-
*
|
|
472
|
-
* Difference from Vue: does not support plugins, directives, or global config.
|
|
473
|
-
* The component receives `props` if provided.
|
|
474
700
|
*/
|
|
475
701
|
export function createApp(component: ComponentFn, props?: Props): App {
|
|
476
702
|
return {
|