@pyreon/vue-compat 0.1.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/src/index.ts ADDED
@@ -0,0 +1,481 @@
1
+ /**
2
+ * @pyreon/vue-compat
3
+ *
4
+ * Vue 3-compatible Composition API that runs on Pyreon's reactive engine.
5
+ *
6
+ * Allows you to write familiar Vue 3 Composition API code while getting Pyreon's
7
+ * fine-grained reactivity and superior performance.
8
+ *
9
+ * DIFFERENCES FROM VUE 3:
10
+ * - `deep` option in watch() is ignored — Pyreon tracks dependencies automatically.
11
+ * - `shallowReactive` uses per-property signals (still shallow, but Pyreon-flavored).
12
+ * - `readonly` returns a Proxy that throws on set (not Vue's readonly proxy).
13
+ * - `defineComponent` only supports Composition API (setup function), not Options API.
14
+ * - Components run ONCE (setup phase), not on every render.
15
+ *
16
+ * USAGE:
17
+ * Replace `import { ref, computed, watch } from "vue"` with
18
+ * `import { ref, computed, watch } from "@pyreon/vue-compat"`
19
+ */
20
+
21
+ import type { ComponentFn, Props, VNodeChild } from "@pyreon/core"
22
+ import {
23
+ createContext,
24
+ Fragment,
25
+ onMount,
26
+ onUnmount,
27
+ onUpdate,
28
+ popContext,
29
+ pushContext,
30
+ h as pyreonH,
31
+ useContext,
32
+ } from "@pyreon/core"
33
+ import {
34
+ createStore,
35
+ effect,
36
+ computed as pyreonComputed,
37
+ nextTick as pyreonNextTick,
38
+ type Signal,
39
+ signal,
40
+ } from "@pyreon/reactivity"
41
+ import { mount as pyreonMount } from "@pyreon/runtime-dom"
42
+
43
+ // ─── Internal symbols ─────────────────────────────────────────────────────────
44
+
45
+ const V_IS_REF = Symbol("__v_isRef")
46
+ const V_IS_READONLY = Symbol("__v_isReadonly")
47
+ const V_RAW = Symbol("__v_raw")
48
+
49
+ // ─── Ref ──────────────────────────────────────────────────────────────────────
50
+
51
+ export interface Ref<T = unknown> {
52
+ value: T
53
+ readonly [V_IS_REF]: true
54
+ }
55
+
56
+ /**
57
+ * Creates a reactive ref wrapping the given value.
58
+ * Access via `.value` — reads track, writes trigger.
59
+ *
60
+ * Difference from Vue: backed by a Pyreon signal. No `__v_isShallow` distinction
61
+ * at runtime since Pyreon signals are always shallow (deep reactivity is via stores).
62
+ */
63
+ export function ref<T>(value: T): Ref<T> {
64
+ const s = signal(value)
65
+ const r = {
66
+ [V_IS_REF]: true as const,
67
+ get value(): T {
68
+ return s()
69
+ },
70
+ set value(newValue: T) {
71
+ s.set(newValue)
72
+ },
73
+ /** @internal — access underlying signal for triggerRef */
74
+ _signal: s,
75
+ }
76
+ return r as Ref<T>
77
+ }
78
+
79
+ /**
80
+ * 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
+ */
84
+ export function shallowRef<T>(value: T): Ref<T> {
85
+ return ref(value)
86
+ }
87
+
88
+ /**
89
+ * Force trigger a ref's subscribers, even if the value hasn't changed.
90
+ */
91
+ export function triggerRef<T>(r: Ref<T>): void {
92
+ const internal = r as Ref<T> & { _signal: Signal<T> }
93
+ if (internal._signal) {
94
+ // Force notify by setting the same value with Object.is bypass
95
+ const current = internal._signal.peek()
96
+ internal._signal.set(undefined as T)
97
+ internal._signal.set(current)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Returns `true` if the value is a ref (created by `ref()` or `computed()`).
103
+ */
104
+ export function isRef(val: unknown): val is Ref {
105
+ return (
106
+ val !== null && typeof val === "object" && (val as Record<symbol, unknown>)[V_IS_REF] === true
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Unwraps a ref: if it has `.value`, return `.value`; otherwise return as-is.
112
+ */
113
+ export function unref<T>(r: T | Ref<T>): T {
114
+ return isRef(r) ? r.value : r
115
+ }
116
+
117
+ // ─── Computed ─────────────────────────────────────────────────────────────────
118
+
119
+ export interface ComputedRef<T = unknown> extends Ref<T> {
120
+ readonly value: T
121
+ }
122
+
123
+ /**
124
+ * Creates a computed ref. Supports both readonly and writable forms:
125
+ * - `computed(() => value)` — readonly
126
+ * - `computed({ get: () => value, set: (v) => ... })` — writable
127
+ *
128
+ * Backed by Pyreon's `computed()`, wrapped in a `.value` accessor.
129
+ */
130
+ export function computed<T>(
131
+ fnOrOptions: (() => T) | { get: () => T; set: (value: T) => void },
132
+ ): ComputedRef<T> {
133
+ const getter = typeof fnOrOptions === "function" ? fnOrOptions : fnOrOptions.get
134
+ const setter = typeof fnOrOptions === "object" ? fnOrOptions.set : undefined
135
+ const c = pyreonComputed(getter)
136
+ const r = {
137
+ [V_IS_REF]: true as const,
138
+ get value(): T {
139
+ return c()
140
+ },
141
+ set value(v: T) {
142
+ if (!setter) {
143
+ throw new Error("Cannot set value of a computed ref — computed refs are readonly")
144
+ }
145
+ setter(v)
146
+ },
147
+ }
148
+ return r as ComputedRef<T>
149
+ }
150
+
151
+ // ─── Reactive / Readonly ──────────────────────────────────────────────────────
152
+
153
+ /**
154
+ * Creates a deeply reactive proxy from a plain object.
155
+ * Backed by Pyreon's `createStore()`.
156
+ *
157
+ * Difference from Vue: uses Pyreon's fine-grained per-property signals.
158
+ * Direct mutation triggers only affected signals.
159
+ */
160
+ export function reactive<T extends object>(obj: T): T {
161
+ const proxy = createStore(obj)
162
+ // Store raw reference for toRaw()
163
+ rawMap.set(proxy as object, obj)
164
+ return proxy
165
+ }
166
+
167
+ /**
168
+ * Creates a shallow reactive proxy.
169
+ * In Pyreon, `createStore` is already per-property (not deeply recursive for primitives),
170
+ * but nested objects will be wrapped. For truly shallow behavior, use individual refs.
171
+ *
172
+ * Difference from Vue: backed by `createStore()` — same as `reactive()` in practice.
173
+ */
174
+ export function shallowReactive<T extends object>(obj: T): T {
175
+ return reactive(obj)
176
+ }
177
+
178
+ // WeakMap to track raw objects behind reactive proxies
179
+ const rawMap = new WeakMap<object, object>()
180
+
181
+ /**
182
+ * Returns a readonly proxy that throws on mutation attempts.
183
+ *
184
+ * Difference from Vue: uses a simple Proxy with a set trap that throws,
185
+ * rather than Vue's full readonly reactive system.
186
+ */
187
+ export function readonly<T extends object>(obj: T): Readonly<T> {
188
+ const proxy = new Proxy(obj, {
189
+ get(target, key) {
190
+ if (key === V_IS_READONLY) return true
191
+ if (key === V_RAW) return target
192
+ return Reflect.get(target, key)
193
+ },
194
+ set(_target, key) {
195
+ // Internal symbols used for identification are allowed
196
+ if (key === V_IS_READONLY || key === V_RAW) return true
197
+ throw new Error(`Cannot set property "${String(key)}" on a readonly object`)
198
+ },
199
+ deleteProperty(_target, key) {
200
+ throw new Error(`Cannot delete property "${String(key)}" from a readonly object`)
201
+ },
202
+ })
203
+ return proxy as Readonly<T>
204
+ }
205
+
206
+ /**
207
+ * Returns the raw (unwrapped) object behind a reactive or readonly proxy.
208
+ *
209
+ * Difference from Vue: only works for objects created via `reactive()` or `readonly()`.
210
+ */
211
+ export function toRaw<T extends object>(proxy: T): T {
212
+ // Check readonly first
213
+ const readonlyRaw = (proxy as Record<symbol, unknown>)[V_RAW]
214
+ if (readonlyRaw) return readonlyRaw as T
215
+ // Check reactive
216
+ const raw = rawMap.get(proxy as object)
217
+ return (raw as T) ?? proxy
218
+ }
219
+
220
+ // ─── toRef / toRefs ───────────────────────────────────────────────────────────
221
+
222
+ /**
223
+ * Creates a ref linked to a property of a reactive object.
224
+ * Reading/writing the ref's `.value` reads/writes the original property.
225
+ */
226
+ export function toRef<T extends object, K extends keyof T>(obj: T, key: K): Ref<T[K]> {
227
+ const r = {
228
+ [V_IS_REF]: true as const,
229
+ get value(): T[K] {
230
+ return obj[key]
231
+ },
232
+ set value(newValue: T[K]) {
233
+ obj[key] = newValue
234
+ },
235
+ }
236
+ return r as Ref<T[K]>
237
+ }
238
+
239
+ /**
240
+ * Converts all properties of a reactive object into individual refs.
241
+ * Each ref is linked to the original property (not a copy).
242
+ */
243
+ export function toRefs<T extends object>(obj: T): { [K in keyof T]: Ref<T[K]> } {
244
+ const result = {} as { [K in keyof T]: Ref<T[K]> }
245
+ for (const key of Object.keys(obj) as (keyof T)[]) {
246
+ result[key] = toRef(obj, key)
247
+ }
248
+ return result
249
+ }
250
+
251
+ // ─── Watch ────────────────────────────────────────────────────────────────────
252
+
253
+ export interface WatchOptions {
254
+ /** Call the callback immediately with current value. Default: false */
255
+ immediate?: boolean
256
+ /** Ignored in Pyreon — dependencies are tracked automatically. */
257
+ deep?: boolean
258
+ }
259
+
260
+ type WatchSource<T> = Ref<T> | (() => T)
261
+
262
+ /**
263
+ * Watches a reactive source and calls `cb` when it changes.
264
+ * Tracks old and new values.
265
+ *
266
+ * Difference from Vue: `deep` option is ignored — Pyreon tracks dependencies automatically.
267
+ * Returns a stop function to dispose the watcher.
268
+ */
269
+ export function watch<T>(
270
+ source: WatchSource<T>,
271
+ cb: (newValue: T, oldValue: T | undefined) => void,
272
+ options?: WatchOptions,
273
+ ): () => void {
274
+ const getter = isRef(source) ? () => source.value : (source as () => T)
275
+ let oldValue: T | undefined
276
+ let initialized = false
277
+
278
+ if (options?.immediate) {
279
+ oldValue = undefined
280
+ const current = getter()
281
+ cb(current, oldValue)
282
+ oldValue = current
283
+ initialized = true
284
+ }
285
+
286
+ const e = effect(() => {
287
+ const newValue = getter()
288
+ if (initialized) {
289
+ // Only call cb if value actually changed (or on first tracked run)
290
+ cb(newValue, oldValue)
291
+ }
292
+ oldValue = newValue
293
+ initialized = true
294
+ })
295
+
296
+ return () => e.dispose()
297
+ }
298
+
299
+ /**
300
+ * Runs the given function reactively — re-executes whenever its tracked
301
+ * dependencies change.
302
+ *
303
+ * Difference from Vue: identical to Pyreon's `effect()`.
304
+ * Returns a stop function.
305
+ */
306
+ export function watchEffect(fn: () => void): () => void {
307
+ const e = effect(fn)
308
+ return () => e.dispose()
309
+ }
310
+
311
+ // ─── Lifecycle ────────────────────────────────────────────────────────────────
312
+
313
+ /**
314
+ * Registers a callback to run after the component is mounted.
315
+ *
316
+ * Difference from Vue: maps directly to Pyreon's `onMount()`.
317
+ * In Pyreon there is no distinction between beforeMount and mounted.
318
+ */
319
+ export function onMounted(fn: () => void): void {
320
+ onMount(() => {
321
+ fn()
322
+ return undefined
323
+ })
324
+ }
325
+
326
+ /**
327
+ * Registers a callback to run before the component is unmounted.
328
+ *
329
+ * Difference from Vue: maps to Pyreon's `onUnmount()`.
330
+ * In Pyreon there is no distinction between beforeUnmount and unmounted.
331
+ */
332
+ export function onUnmounted(fn: () => void): void {
333
+ onUnmount(fn)
334
+ }
335
+
336
+ /**
337
+ * Registers a callback to run after a reactive update.
338
+ *
339
+ * Difference from Vue: maps to Pyreon's `onUpdate()`.
340
+ */
341
+ export function onUpdated(fn: () => void): void {
342
+ onUpdate(fn)
343
+ }
344
+
345
+ /**
346
+ * Registers a callback to run before mount.
347
+ * In Pyreon there is no pre-mount phase — maps to `onMount()`.
348
+ */
349
+ export function onBeforeMount(fn: () => void): void {
350
+ onMount(() => {
351
+ fn()
352
+ return undefined
353
+ })
354
+ }
355
+
356
+ /**
357
+ * Registers a callback to run before unmount.
358
+ * In Pyreon there is no pre-unmount phase — maps to `onUnmount()`.
359
+ */
360
+ export function onBeforeUnmount(fn: () => void): void {
361
+ onUnmount(fn)
362
+ }
363
+
364
+ // ─── nextTick ─────────────────────────────────────────────────────────────────
365
+
366
+ /**
367
+ * Returns a Promise that resolves after all pending reactive updates have flushed.
368
+ *
369
+ * Difference from Vue: identical to Pyreon's `nextTick()`.
370
+ */
371
+ export function nextTick(): Promise<void> {
372
+ return pyreonNextTick()
373
+ }
374
+
375
+ // ─── Provide / Inject ─────────────────────────────────────────────────────────
376
+
377
+ // Registry of string/symbol keys to Pyreon context objects (created lazily)
378
+ const _contextRegistry = new Map<string | symbol, ReturnType<typeof createContext>>()
379
+
380
+ function getOrCreateContext<T>(key: string | symbol, defaultValue?: T) {
381
+ if (!_contextRegistry.has(key)) {
382
+ _contextRegistry.set(key, createContext<T>(defaultValue as T))
383
+ }
384
+ return _contextRegistry.get(key) as ReturnType<typeof createContext<T>>
385
+ }
386
+
387
+ /**
388
+ * Provides a value to all descendant components.
389
+ *
390
+ * Difference from Vue: backed by Pyreon's context stack (pushContext/popContext).
391
+ * Must be called during component setup. The value is scoped to the component
392
+ * tree — not globally shared.
393
+ */
394
+ export function provide<T>(key: string | symbol, value: T): void {
395
+ const ctx = getOrCreateContext<T>(key)
396
+ pushContext(new Map([[ctx.id, value]]))
397
+ onUnmount(() => popContext())
398
+ }
399
+
400
+ /**
401
+ * Injects a value provided by an ancestor component.
402
+ *
403
+ * Difference from Vue: backed by Pyreon's context system (useContext).
404
+ */
405
+ export function inject<T>(key: string | symbol, defaultValue?: T): T | undefined {
406
+ const ctx = getOrCreateContext<T>(key)
407
+ const value = useContext(ctx)
408
+ return value !== undefined ? value : defaultValue
409
+ }
410
+
411
+ // ─── defineComponent ──────────────────────────────────────────────────────────
412
+
413
+ interface ComponentOptions<P extends Props = Props> {
414
+ /** The setup function — called once during component initialization. */
415
+ setup: (props: P) => (() => VNodeChild) | VNodeChild
416
+ /** Optional name for debugging. */
417
+ name?: string
418
+ }
419
+
420
+ /**
421
+ * Defines a component using Vue 3 Composition API style.
422
+ * Only supports the `setup()` function — Options API is not supported.
423
+ *
424
+ * Difference from Vue: returns a Pyreon `ComponentFn`. No template/render option —
425
+ * the setup function should return a render function or VNode directly.
426
+ */
427
+ export function defineComponent<P extends Props = Props>(
428
+ options: ComponentOptions<P> | ((props: P) => VNodeChild),
429
+ ): ComponentFn<P> {
430
+ if (typeof options === "function") {
431
+ return options as ComponentFn<P>
432
+ }
433
+ const comp = (props: P) => {
434
+ const result = options.setup(props)
435
+ if (typeof result === "function") {
436
+ return (result as () => VNodeChild)()
437
+ }
438
+ return result
439
+ }
440
+ if (options.name) {
441
+ Object.defineProperty(comp, "name", { value: options.name })
442
+ }
443
+ return comp as ComponentFn<P>
444
+ }
445
+
446
+ // ─── h ────────────────────────────────────────────────────────────────────────
447
+
448
+ /**
449
+ * Re-export of Pyreon's `h()` function for creating VNodes.
450
+ */
451
+ export { pyreonH as h, Fragment }
452
+
453
+ // ─── createApp ────────────────────────────────────────────────────────────────
454
+
455
+ interface App {
456
+ /** Mount the application into a DOM element. Returns an unmount function. */
457
+ mount(el: string | Element): () => void
458
+ }
459
+
460
+ /**
461
+ * Creates a Pyreon application instance — Vue 3 `createApp()` compatible.
462
+ *
463
+ * Difference from Vue: does not support plugins, directives, or global config.
464
+ * The component receives `props` if provided.
465
+ */
466
+ export function createApp(component: ComponentFn, props?: Props): App {
467
+ return {
468
+ mount(el: string | Element): () => void {
469
+ const container = typeof el === "string" ? document.querySelector(el) : el
470
+ if (!container) {
471
+ throw new Error(`Cannot find mount target: ${el}`)
472
+ }
473
+ const vnode = pyreonH(component, props ?? null)
474
+ return pyreonMount(vnode, container)
475
+ },
476
+ }
477
+ }
478
+
479
+ // ─── Additional re-exports ────────────────────────────────────────────────────
480
+
481
+ export { batch } from "@pyreon/reactivity"
@@ -0,0 +1,3 @@
1
+ import { GlobalRegistrator } from "@happy-dom/global-registrator"
2
+
3
+ GlobalRegistrator.register()