@pyreon/vue-compat 0.2.1 → 0.3.1

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 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
- * Allows you to write familiar Vue 3 Composition API code while getting Pyreon's
7
- * fine-grained reactivity and superior performance.
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 run ONCE (setup phase), not on every render.
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
- * 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).
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(newValue: T) {
71
- s.set(newValue)
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
- * Backed by Pyreon's `computed()`, wrapped in a `.value` accessor.
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
- * Difference from Vue: uses Pyreon's fine-grained per-property signals.
167
- * Direct mutation triggers only affected signals.
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
- * Difference from Vue: uses a simple Proxy with a set trap that throws,
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] = toRef(obj, 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
- * Difference from Vue: `deep` option is ignored Pyreon tracks dependencies automatically.
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
- const newValue = getter()
297
- if (initialized) {
298
- // Only call cb if value actually changed (or on first tracked run)
299
- cb(newValue, oldValue)
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
- * Difference from Vue: identical to Pyreon's `effect()`.
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 e = effect(fn)
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
- onMount(() => {
330
- fn()
331
- return undefined
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 before the component is unmounted.
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
- onUnmount(fn)
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
- onUpdate(fn)
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 `onMount()`.
587
+ * In Pyreon there is no pre-mount phase — maps to `onMounted()`.
357
588
  */
358
589
  export function onBeforeMount(fn: () => void): void {
359
- onMount(() => {
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 `onUnmount()`.
595
+ * In Pyreon there is no pre-unmount phase — maps to `onUnmounted()`.
368
596
  */
369
597
  export function onBeforeUnmount(fn: () => void): void {
370
- onUnmount(fn)
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
- * Difference from Vue: backed by Pyreon's context stack (pushContext/popContext).
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 = getOrCreateContext<T>(key)
405
- pushContext(new Map([[ctx.id, value]]))
406
- onUnmount(() => popContext())
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 {