ivue 1.6.1 → 2.0.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/Reactive.ts CHANGED
@@ -1,100 +1,128 @@
1
- import { toRaw, isRef, type Ref } from 'vue';
1
+ import {
2
+ effectScope,
3
+ isRef,
4
+ toRaw,
5
+ watch,
6
+ watchEffect,
7
+ type ExtractPropTypes,
8
+ type Ref,
9
+ } from 'vue';
2
10
 
3
11
  /**
4
12
  * Constants & Helpers
5
13
  */
6
- const prototypeHasOwnProperty = Object.prototype.hasOwnProperty.call.bind(
7
- Object.prototype.hasOwnProperty
8
- );
14
+ const hasOwn = Object.hasOwn;
9
15
  const getPrototypeOf = Object.getPrototypeOf;
10
16
  const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
11
- const defineProperty = Object.defineProperty;
12
17
  const getOwnPropertyNames = Object.getOwnPropertyNames;
18
+ const defineProperty = Object.defineProperty;
13
19
  const objectPrototype = Object.prototype;
14
- const getOwnPropertySymbols = Object.getOwnPropertySymbols;
15
20
 
16
- const $stopEffects = '$stopEffects';
17
- const fn = 'function';
18
- const RAW = Symbol('ivue_raw');
19
- const SK_MAP = '__ivue_sk'; // Store for cache keys
20
- const PROCESSED = Symbol('ivue_processed'); // Flag to mark prototype as "Reactified"
21
+ // Identity symbols are global so every bundled copy agrees with objects
22
+ // stamped by another copy of the engine.
23
+ const RAW = Symbol.for('ivue.raw'); // Per-instance back-pointer to the raw object
24
+ const SCOPE = Symbol.for('ivue.scope'); // Lazily-created per-instance effect scope
25
+ // Marks a prototype level as "Reactified"; its VALUE is the list of
26
+ // engine-created instance-cache symbols for that level, so teardown can
27
+ // remove exactly the engine's cells and nothing else.
28
+ const PROCESSED = Symbol.for('ivue.processed');
21
29
 
22
30
  /**
23
- * Get unique symbol key for super method/property.
31
+ * Resolve the TRUE raw instance from whatever `this` the engine was entered
32
+ * with — the raw object itself, a Vue `reactive()` proxy, or a foreign proxy
33
+ * chain such as Vue's component expose proxy.
34
+ *
35
+ * Neither primitive is sufficient alone:
36
+ *
37
+ * - `toRaw(this)` cannot unwrap a foreign (non-Vue-reactive) proxy — e.g.
38
+ * Vue's component expose proxy — so it can return the proxy unchanged.
39
+ * - The RAW back-pointer read through a Vue reactive proxy comes back
40
+ * DEEP-WRAPPED: a reactive proxy wraps symbol-keyed object reads in
41
+ * `reactive(raw)`. Binding a method (or computed closure) to that wrapped
42
+ * value poisons the per-instance cache with ref-unwrapping `this`
43
+ * semantics — `this.x.value` then crashes, because `this.x` auto-unwraps
44
+ * to the plain value.
45
+ *
46
+ * So: try `toRaw()` first (one step for the common reactive-proxy path —
47
+ * consulting the pointer first costs a wrap+unwrap round-trip per access),
48
+ * and fall back to the pointer, normalized with `toRaw()`, for everything
49
+ * `toRaw()` cannot see through.
24
50
  */
25
- function getSuperKey(proto: any, key: string): symbol {
26
- // Ensure the map exists on this specific prototype (shadowing parents)
27
- if (!prototypeHasOwnProperty(proto, SK_MAP)) {
28
- defineProperty(proto, SK_MAP, {
29
- value: {},
30
- configurable: true,
31
- enumerable: false,
32
- writable: true,
33
- });
51
+ function resolveRaw(self: any) {
52
+ const unwrapped = toRaw(self);
53
+ if (unwrapped !== self) {
54
+ // Genuine Vue reactive proxy. Stamp the back-pointer (once, directly on
55
+ // the raw — no proxy set traps) so foreign proxy chains can still
56
+ // resolve the true raw through it.
57
+ return unwrapped[RAW] ?? (unwrapped[RAW] = unwrapped);
58
+ }
59
+ // `self` is the raw object itself, or a foreign proxy over it.
60
+ const viaPointer = self[RAW];
61
+ if (viaPointer) {
62
+ // A pointer read through a proxy chain may come back deep-wrapped —
63
+ // normalize; on the raw object it is already the raw itself.
64
+ return viaPointer === self ? viaPointer : toRaw(viaPointer);
34
65
  }
35
- const map = proto[SK_MAP];
36
- return map[key] || (map[key] = Symbol(key));
66
+ // First-ever engine access on a plain raw instance (direct `new Class()`).
67
+ return (self[RAW] = self);
37
68
  }
38
69
 
39
70
  /**
40
- * Convert method to a lazy-bound prototype method.
71
+ * Convert a method to a lazy-bound prototype method.
72
+ *
73
+ * The bound function is created once, on first access, and cached on the raw
74
+ * object under a unique per-(prototype,key) symbol — giving referentially
75
+ * stable, correctly-bound methods with zero per-instance construction cost.
41
76
  */
42
- function convertToLazyBoundMethod(proto: any, key: string, superKey: symbol) {
43
- const desc = getOwnPropertyDescriptor(proto, key);
44
- if (!desc || typeof desc.value !== fn) return;
45
-
46
- const originalFn = desc.value;
47
-
77
+ function convertToLazyBoundMethod(
78
+ proto: any,
79
+ key: string,
80
+ superKey: symbol,
81
+ originalFn: (...args: any[]) => any,
82
+ ) {
48
83
  defineProperty(proto, key, {
49
84
  configurable: true,
50
85
  enumerable: false,
51
86
  get(this: any) {
52
- const raw = this[RAW] ?? (this[RAW] = toRaw(this));
87
+ const raw = resolveRaw(this);
53
88
  return raw[superKey] ?? (raw[superKey] = originalFn.bind(raw));
54
89
  },
55
90
  set(this: any, newFn: any) {
56
- toRaw(this)[superKey] = newFn;
91
+ resolveRaw(this)[superKey] = newFn;
57
92
  },
58
93
  });
59
94
  }
60
95
 
61
96
  /**
62
- * Convert a property to a lazy-computed property.
97
+ * Convert a getter to a lazily-cached Ref cell.
98
+ *
99
+ * Only ever called when the descriptor has a getter, so `originalGetter` is
100
+ * always defined here. A getter that returns any Ref — ref(), shallowRef(),
101
+ * computed() (a ComputedRef IS a Ref) — is cached under the instance symbol
102
+ * (stable reactive identity); a getter that returns a plain value
103
+ * de-optimizes back to a native getter on the prototype, removing all
104
+ * overhead for future instances.
63
105
  */
64
- function convertToLazyComputed<T extends object>(
65
- proto: T,
106
+ function convertToLazyRef(
107
+ proto: any,
66
108
  key: string,
67
- superKey: symbol
109
+ superKey: symbol,
110
+ originalGetter: (this: any) => any,
111
+ originalSetter: ((this: any, v: any) => any) | undefined,
68
112
  ) {
69
- const desc = getOwnPropertyDescriptor(proto, key);
70
- const originalGetter = desc?.get;
71
- const originalSetter = desc?.set;
72
-
73
- // Optimization: Properties starting with $ are assumed singletons
113
+ // Optimization: Properties starting with $ are assumed singletons.
74
114
  const cacheWhole = key[0] === '$';
75
115
 
76
- // DEV WARNING: Setter/Getter mismatch
77
- if (import.meta.env.DEV && originalGetter && originalSetter) {
78
- try {
79
- const testVal = originalGetter.call({});
80
- if (isRef(testVal)) {
81
- console.warn(
82
- `[ivue] API conflict on "${key}": Getter returns Ref but Setter is standard.`
83
- );
84
- }
85
- } catch (e) {}
86
- }
87
-
88
116
  const newGetter = function (this: any) {
89
- const raw = toRaw(this);
117
+ const raw = resolveRaw(this);
90
118
 
91
- // 1. Check Cache
119
+ // 1. Check cache
92
120
  if (superKey in raw) return raw[superKey];
93
121
 
94
- // 2. Execute Original
95
- const result = originalGetter!.call(raw);
122
+ // 2. Execute original
123
+ const result = originalGetter.call(raw);
96
124
 
97
- // 3. Handle Result
125
+ // 3. Handle result
98
126
  if (cacheWhole) {
99
127
  // Cache result forever (Singleton pattern)
100
128
  raw[superKey] = result;
@@ -105,19 +133,17 @@ function convertToLazyComputed<T extends object>(
105
133
  // Cache Ref instance (Reactivity pattern)
106
134
  raw[superKey] = result;
107
135
  } else {
108
- // DE-OPTIMIZATION: It's just a value. Restore original getter on prototype.
109
- // This removes overhead for all future calls on this prototype.
136
+ // DE-OPTIMIZATION: It's just a value. Restore a native getter on the
137
+ // prototype, removing the wrapper overhead for all future instances.
110
138
  defineProperty(proto, key, {
111
139
  configurable: true,
112
140
  enumerable: false,
113
- get: originalGetter
114
- ? function (this: any) {
115
- return originalGetter.call(toRaw(this));
116
- }
117
- : undefined,
141
+ get(this: any) {
142
+ return originalGetter.call(resolveRaw(this));
143
+ },
118
144
  set: originalSetter
119
145
  ? function (this: any, v: any) {
120
- return originalSetter.call(toRaw(this), v);
146
+ return originalSetter.call(resolveRaw(this), v);
121
147
  }
122
148
  : undefined,
123
149
  });
@@ -129,10 +155,10 @@ function convertToLazyComputed<T extends object>(
129
155
  defineProperty(proto, key, {
130
156
  configurable: true,
131
157
  enumerable: false,
132
- get: originalGetter ? newGetter : undefined,
158
+ get: newGetter,
133
159
  set: originalSetter
134
160
  ? function (this: any, v: any) {
135
- return originalSetter.call(toRaw(this), v);
161
+ return originalSetter.call(resolveRaw(this), v);
136
162
  }
137
163
  : undefined,
138
164
  });
@@ -141,10 +167,10 @@ function convertToLazyComputed<T extends object>(
141
167
  /**
142
168
  * Create a reactive class.
143
169
  * @param targetClass The class to make reactive.
144
- * @returns A reactive version of the class.
170
+ * @returns A reactive version of the class (the same class, transformed in place).
145
171
  */
146
172
  export function Reactive<C extends new (...args: any) => any>(
147
- targetClass: C
173
+ targetClass: C,
148
174
  ): ReactiveClass<C> & { Instance: ReactiveInstance<InstanceType<C>> } {
149
175
  const chain: any[] = [];
150
176
 
@@ -158,53 +184,109 @@ export function Reactive<C extends new (...args: any) => any>(
158
184
  chain.reverse();
159
185
 
160
186
  for (const prototype of chain) {
161
- // OPTIMIZATION: Skip if this prototype layer is already "Reactified"
187
+ // OPTIMIZATION: Skip if this prototype layer is already "Reactified".
162
188
  // This handles diamond inheritance and multiple Reactive children safely.
163
- if (prototypeHasOwnProperty(prototype, PROCESSED)) continue;
189
+ if (hasOwn(prototype, PROCESSED)) continue;
164
190
 
165
191
  const names = getOwnPropertyNames(prototype);
192
+ const cacheKeys: symbol[] = [];
166
193
 
167
194
  for (const key of names) {
168
195
  if (key === 'constructor') continue;
169
- const desc = getOwnPropertyDescriptor(prototype, key);
170
- if (!desc) continue;
171
-
172
- const superKey = getSuperKey(prototype, key);
173
-
174
- if (typeof desc.value === fn) {
175
- convertToLazyBoundMethod(prototype, key, superKey);
196
+ const desc = getOwnPropertyDescriptor(prototype, key)!;
197
+
198
+ if (typeof desc.value === 'function') {
199
+ // A fresh symbol per (prototype,key). Because each prototype level gets
200
+ // its own symbol, a child override and its `super` counterpart cache
201
+ // under different keys and never collide.
202
+ const superKey = Symbol(key);
203
+ cacheKeys.push(superKey);
204
+ convertToLazyBoundMethod(prototype, key, superKey, desc.value);
176
205
  } else if (desc.get) {
177
- convertToLazyComputed(prototype, key, superKey);
206
+ const superKey = Symbol(key);
207
+ cacheKeys.push(superKey);
208
+ convertToLazyRef(prototype, key, superKey, desc.get, desc.set);
178
209
  }
179
210
  }
180
211
 
181
- // Mark this specific prototype level as processed
212
+ // Mark this prototype level as processed; the marker carries the
213
+ // level's engine cache keys (see PROCESSED above).
182
214
  defineProperty(prototype, PROCESSED, {
183
- configurable: false,
184
- enumerable: false,
185
- value: true,
215
+ value: cacheKeys,
186
216
  });
187
217
  }
188
218
 
189
- // Inject cleanup
190
- if (!prototypeHasOwnProperty(targetClass.prototype, $stopEffects)) {
191
- defineProperty(targetClass.prototype, $stopEffects, {
219
+ // Inject the $watch/$watchEffect/$stopEffects helpers. The guard is an
220
+ // IDEMPOTENCY SENTINEL only — one key stands for the whole trio, so a
221
+ // repeated Reactive() call (diamond imports, duplicate bundled engine
222
+ // copies) skips re-injection. It is NOT override protection: the $-helper
223
+ // names are reserved engine API (richer cleanup is an ordinary method
224
+ // that calls $stopEffects() itself — ivue never auto-calls user code).
225
+ if (!hasOwn(targetClass.prototype, '$stopEffects')) {
226
+ /**
227
+ * Register a watcher in this instance's lazily-created effect scope.
228
+ * The scope is allocated only on first use, so pure-data classes that
229
+ * never watch pay nothing. Has the same signature as Vue's `watch`.
230
+ */
231
+ defineProperty(targetClass.prototype, '$watch', {
232
+ enumerable: false,
233
+ configurable: true,
234
+ writable: true,
235
+ value: function (this: any, ...args: any[]) {
236
+ const raw = resolveRaw(this);
237
+ const scope =
238
+ raw[SCOPE] ?? (raw[SCOPE] = effectScope(true /* detached */));
239
+ return scope.run(() => (watch as any)(...args));
240
+ },
241
+ });
242
+
243
+ /**
244
+ * Register a watchEffect in the same lazy per-instance scope.
245
+ */
246
+ defineProperty(targetClass.prototype, '$watchEffect', {
247
+ enumerable: false,
248
+ configurable: true,
249
+ writable: true,
250
+ value: function (this: any, ...args: any[]) {
251
+ const raw = resolveRaw(this);
252
+ const scope =
253
+ raw[SCOPE] ?? (raw[SCOPE] = effectScope(true /* detached */));
254
+ return scope.run(() => (watchEffect as any)(...args));
255
+ },
256
+ });
257
+
258
+ /**
259
+ * Tear down the instance: stop its effect scope (any watchers created via
260
+ * $watch) and drop all cached cells so refs/computeds become collectable.
261
+ * No hooks — ivue never calls user code; compose richer cleanup as an
262
+ * ordinary method that does its own work and then calls $stopEffects().
263
+ */
264
+ defineProperty(targetClass.prototype, '$stopEffects', {
192
265
  enumerable: false,
193
266
  configurable: true,
194
267
  writable: true,
195
268
  value: function (this: any) {
196
- const raw = toRaw(this);
197
- if (typeof raw.stopEffects === fn) raw.stopEffects();
198
-
199
- const symbols = getOwnPropertySymbols(raw);
200
- for (const symbol of symbols) {
201
- if (symbol === RAW) continue;
202
- const cached = raw[symbol];
203
- // Stop computed/effect if present
204
- if (cached && cached.effect && typeof cached.effect.stop === fn) {
205
- cached.effect.stop();
269
+ const raw = resolveRaw(this);
270
+ try {
271
+ const scope = raw[SCOPE];
272
+ if (scope) scope.stop();
273
+ } finally {
274
+ // SCOPE is ivue-owned but is not a method/getter cache key.
275
+ delete raw[SCOPE];
276
+
277
+ // Each processed prototype's PROCESSED marker carries the
278
+ // symbols it may cache on an instance. Walk Child -> Base and
279
+ // remove only those known keys.
280
+ let prototype = getPrototypeOf(raw);
281
+ while (prototype && prototype !== objectPrototype) {
282
+ const cacheKeys = prototype[PROCESSED] as
283
+ | readonly symbol[]
284
+ | undefined;
285
+ if (cacheKeys) {
286
+ for (const cacheKey of cacheKeys) delete raw[cacheKey];
287
+ }
288
+ prototype = getPrototypeOf(prototype);
206
289
  }
207
- delete raw[symbol];
208
290
  }
209
291
  },
210
292
  });
@@ -273,34 +355,47 @@ export const isClass = (val: any): boolean => {
273
355
  * but leave primitive properties and functions intact so that
274
356
  * the final object is fully defineComponent() style compatible.
275
357
  *
358
+ * The default cloner is the native `structuredClone` (zero-dependency, handles
359
+ * plain data, Map/Set/Date/typed arrays, circular refs). For defaults that
360
+ * contain class instances or functions — which `structuredClone` cannot clone —
361
+ * pass a `customCloner` such as lodash `cloneDeep`.
362
+ *
276
363
  * @param defaults Regular object of default key -> values
277
364
  * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default
365
+ * @param customCloner Optional cloner used for object/array defaults (defaults to structuredClone)
278
366
  * @returns Props declared in defineComponent() style with all properties having default property declared.
279
367
  */
280
368
  export const propsWithDefaults = <T extends VuePropsObject>(
281
369
  defaults: Record<string, any>,
282
370
  typedProps: T,
283
371
  // Optional: Allows user to pass a custom cloner if structuredClone isn't enough
284
- customCloner?: (val: any) => any
372
+ customCloner?: (val: any) => any,
285
373
  ): VuePropsWithDefaults<T> => {
374
+ // NON-MUTATING: descriptor objects are routinely SHARED between props
375
+ // maps (`{ ...baseParamsTypes, extra }` — the spread copies the outer
376
+ // object but every inner `{ type }` descriptor stays the same reference).
377
+ // Writing `.default` in place would silently rewrite the base component's
378
+ // defaults; each descriptor is copied instead.
379
+ const result: Record<string, any> = {};
286
380
  for (const prop in typedProps) {
287
381
  const def = defaults?.[prop];
288
382
  const typed = typedProps[prop];
383
+ result[prop] = { ...typed };
289
384
 
290
385
  if (typed.required || def === undefined) continue;
291
386
 
292
387
  if (typeof def === 'object' && def !== null) {
293
- typedProps[prop].default = () =>
388
+ result[prop].default = () =>
294
389
  customCloner ? customCloner(def) : structuredClone(def);
295
390
  } else {
296
391
  if (isClass(def)) {
297
- typedProps[prop].default = () => def;
392
+ result[prop].default = () => def;
298
393
  } else {
299
- typedProps[prop].default = def;
394
+ result[prop].default = def;
300
395
  }
301
396
  }
302
397
  }
303
- return typedProps as VuePropsWithDefaults<T>;
398
+ return result as VuePropsWithDefaults<T>;
304
399
  };
305
400
 
306
401
  /**
@@ -310,29 +405,105 @@ type GetterKeys<T> = {
310
405
  [K in keyof T]: T[K] extends (...args: any[]) => any
311
406
  ? never
312
407
  : T[K] extends undefined
313
- ? never
314
- : K;
408
+ ? never
409
+ : K;
315
410
  }[keyof T];
316
411
 
317
412
  type GetterReturn<T, K extends keyof T> = T[K] extends (...args: any[]) => any
318
413
  ? never
319
414
  : T[K];
320
415
  type WritableComputedLike = Ref<any> & { set: (...args: any[]) => any };
321
- type IsWritableGetter<R> = R extends Ref<any>
322
- ? true
323
- : R extends WritableComputedLike
324
- ? true
325
- : false;
416
+ type IsWritableGetter<R> =
417
+ R extends Ref<any> ? true : R extends WritableComputedLike ? true : false;
326
418
 
327
419
  type WritableGetters<T> = {
328
- [K in GetterKeys<T> as IsWritableGetter<GetterReturn<T, K>> extends true
329
- ? K
330
- : never]-?: T[K];
420
+ [
421
+ K in GetterKeys<T> as IsWritableGetter<GetterReturn<T, K>> extends true
422
+ ? K
423
+ : never
424
+ ]-?: T[K];
331
425
  };
332
426
 
333
427
  export type ReactiveInstance<T> = T &
334
- WritableGetters<T> & { $stopEffects: () => void };
428
+ WritableGetters<T> & {
429
+ /** Register a watcher in the instance's lazy effect scope (same signature as Vue `watch`). */
430
+ $watch: typeof watch;
431
+ /** Register a watchEffect in the instance's lazy effect scope (same signature as Vue `watchEffect`). */
432
+ $watchEffect: typeof watchEffect;
433
+ /** Stop the instance's effect scope and drop cached cells. */
434
+ $stopEffects: () => void;
435
+ };
335
436
 
336
437
  export type ReactiveClass<C extends new (...args: any) => any> = new (
337
438
  ...args: ConstructorParameters<C>
338
439
  ) => ReactiveInstance<InstanceType<C>>;
440
+
441
+ /**
442
+ * Component-authoring type utilities (types only — erased at build time).
443
+ * These complement `propsWithDefaults` for the params/defaults component
444
+ * architecture: object-declared emits, extensible slots, and precise
445
+ * handler-parameter extraction.
446
+ */
447
+
448
+ /** Any JavaScript function of any type. */
449
+ export type AnyFn = (...args: any[]) => any;
450
+
451
+ /** Convert Record to Union Type. */
452
+ export type RecordToUnion<T extends Record<string, any>> = T[keyof T];
453
+
454
+ /** Gets object T property by key K. */
455
+ export type ValueOf<T extends Record<any, any>, K extends keyof T> = T[K];
456
+
457
+ /** Convert Union Type to Intersection Type. */
458
+ export type UnionToIntersection<U> = (
459
+ U extends any ? (k: U) => void : never
460
+ ) extends (k: infer I) => void
461
+ ? I
462
+ : never;
463
+
464
+ /** Prefix keys of an interface T with a prefix P. */
465
+ export type PrefixKeys<T, P extends string | undefined = undefined> = {
466
+ [K in Extract<keyof T, string> as P extends string ? `${P}${K}` : K]: T[K];
467
+ };
468
+
469
+ /** Extracts object-declared emit validators into the emit-function interface. */
470
+ export type ExtractEmitTypes<T extends Record<string, any>> =
471
+ UnionToIntersection<
472
+ RecordToUnion<{
473
+ [K in keyof T]: (evt: K, ...args: Parameters<T[K]>) => void;
474
+ }>
475
+ >;
476
+
477
+ /**
478
+ * Extract properties as all-assigned (non-optional) because every one of
479
+ * them carries a default.
480
+ */
481
+ export type ExtractPropDefaultTypes<O> = {
482
+ [K in keyof O]: K extends keyof ExtractPropTypes<O>
483
+ ? ExtractPropTypes<O>[K]
484
+ : never;
485
+ };
486
+
487
+ /**
488
+ * Extend a slots interface T with prefixed 'before--' & 'after--' slots to
489
+ * create fully extensible wrapped components.
490
+ */
491
+ export type ExtendSlots<T> = PrefixKeys<T, 'before--'> &
492
+ T &
493
+ PrefixKeys<T, 'after--'>;
494
+
495
+ /** Get function arguments Parameters<F> parameter by index K. */
496
+ export type FnParameter<F extends AnyFn, K extends number> = Parameters<F>[K];
497
+
498
+ /** Get interface T property K's function arguments as Parameters. */
499
+ export type IFnParameters<
500
+ T extends Record<any, any>,
501
+ K extends string,
502
+ > = Parameters<Required<Pick<T, K>>[K]>;
503
+
504
+ /** Get interface T property P's function parameter by index K. */
505
+ export type IFnParameter<
506
+ T extends Record<any, any>,
507
+ P extends keyof T,
508
+ K extends number,
509
+ > = FnParameter<NonNullable<T[P]> extends AnyFn ? NonNullable<T[P]> : never, K>;