ivue 1.5.8 → 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.
@@ -0,0 +1,509 @@
1
+ import {
2
+ effectScope,
3
+ isRef,
4
+ toRaw,
5
+ watch,
6
+ watchEffect,
7
+ type ExtractPropTypes,
8
+ type Ref,
9
+ } from 'vue';
10
+
11
+ /**
12
+ * Constants & Helpers
13
+ */
14
+ const hasOwn = Object.hasOwn;
15
+ const getPrototypeOf = Object.getPrototypeOf;
16
+ const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
17
+ const getOwnPropertyNames = Object.getOwnPropertyNames;
18
+ const defineProperty = Object.defineProperty;
19
+ const objectPrototype = Object.prototype;
20
+
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');
29
+
30
+ /**
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.
50
+ */
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);
65
+ }
66
+ // First-ever engine access on a plain raw instance (direct `new Class()`).
67
+ return (self[RAW] = self);
68
+ }
69
+
70
+ /**
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.
76
+ */
77
+ function convertToLazyBoundMethod(
78
+ proto: any,
79
+ key: string,
80
+ superKey: symbol,
81
+ originalFn: (...args: any[]) => any,
82
+ ) {
83
+ defineProperty(proto, key, {
84
+ configurable: true,
85
+ enumerable: false,
86
+ get(this: any) {
87
+ const raw = resolveRaw(this);
88
+ return raw[superKey] ?? (raw[superKey] = originalFn.bind(raw));
89
+ },
90
+ set(this: any, newFn: any) {
91
+ resolveRaw(this)[superKey] = newFn;
92
+ },
93
+ });
94
+ }
95
+
96
+ /**
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.
105
+ */
106
+ function convertToLazyRef(
107
+ proto: any,
108
+ key: string,
109
+ superKey: symbol,
110
+ originalGetter: (this: any) => any,
111
+ originalSetter: ((this: any, v: any) => any) | undefined,
112
+ ) {
113
+ // Optimization: Properties starting with $ are assumed singletons.
114
+ const cacheWhole = key[0] === '$';
115
+
116
+ const newGetter = function (this: any) {
117
+ const raw = resolveRaw(this);
118
+
119
+ // 1. Check cache
120
+ if (superKey in raw) return raw[superKey];
121
+
122
+ // 2. Execute original
123
+ const result = originalGetter.call(raw);
124
+
125
+ // 3. Handle result
126
+ if (cacheWhole) {
127
+ // Cache result forever (Singleton pattern)
128
+ raw[superKey] = result;
129
+ return result;
130
+ }
131
+
132
+ if (isRef(result)) {
133
+ // Cache Ref instance (Reactivity pattern)
134
+ raw[superKey] = result;
135
+ } else {
136
+ // DE-OPTIMIZATION: It's just a value. Restore a native getter on the
137
+ // prototype, removing the wrapper overhead for all future instances.
138
+ defineProperty(proto, key, {
139
+ configurable: true,
140
+ enumerable: false,
141
+ get(this: any) {
142
+ return originalGetter.call(resolveRaw(this));
143
+ },
144
+ set: originalSetter
145
+ ? function (this: any, v: any) {
146
+ return originalSetter.call(resolveRaw(this), v);
147
+ }
148
+ : undefined,
149
+ });
150
+ }
151
+
152
+ return result;
153
+ };
154
+
155
+ defineProperty(proto, key, {
156
+ configurable: true,
157
+ enumerable: false,
158
+ get: newGetter,
159
+ set: originalSetter
160
+ ? function (this: any, v: any) {
161
+ return originalSetter.call(resolveRaw(this), v);
162
+ }
163
+ : undefined,
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Create a reactive class.
169
+ * @param targetClass The class to make reactive.
170
+ * @returns A reactive version of the class (the same class, transformed in place).
171
+ */
172
+ export function Reactive<C extends new (...args: any) => any>(
173
+ targetClass: C,
174
+ ): ReactiveClass<C> & { Instance: ReactiveInstance<InstanceType<C>> } {
175
+ const chain: any[] = [];
176
+
177
+ let targetPrototype = targetClass.prototype;
178
+ while (targetPrototype && targetPrototype !== objectPrototype) {
179
+ chain.push(targetPrototype);
180
+ targetPrototype = getPrototypeOf(targetPrototype);
181
+ }
182
+
183
+ // Process Base -> Child
184
+ chain.reverse();
185
+
186
+ for (const prototype of chain) {
187
+ // OPTIMIZATION: Skip if this prototype layer is already "Reactified".
188
+ // This handles diamond inheritance and multiple Reactive children safely.
189
+ if (hasOwn(prototype, PROCESSED)) continue;
190
+
191
+ const names = getOwnPropertyNames(prototype);
192
+ const cacheKeys: symbol[] = [];
193
+
194
+ for (const key of names) {
195
+ if (key === 'constructor') continue;
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);
205
+ } else if (desc.get) {
206
+ const superKey = Symbol(key);
207
+ cacheKeys.push(superKey);
208
+ convertToLazyRef(prototype, key, superKey, desc.get, desc.set);
209
+ }
210
+ }
211
+
212
+ // Mark this prototype level as processed; the marker carries the
213
+ // level's engine cache keys (see PROCESSED above).
214
+ defineProperty(prototype, PROCESSED, {
215
+ value: cacheKeys,
216
+ });
217
+ }
218
+
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', {
265
+ enumerable: false,
266
+ configurable: true,
267
+ writable: true,
268
+ value: function (this: any) {
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);
289
+ }
290
+ }
291
+ },
292
+ });
293
+ }
294
+
295
+ return targetClass as any;
296
+ }
297
+
298
+ /**
299
+ * Vue props interface in defineComponent() style.
300
+ */
301
+ export type VuePropsObject = Record<
302
+ string,
303
+ { type: any; default?: any; required?: boolean }
304
+ >;
305
+
306
+ /**
307
+ * Vue Props with default properties declared as existing and having values.
308
+ */
309
+ export type VuePropsWithDefaults<T extends VuePropsObject> = {
310
+ [K in keyof T]: {
311
+ type: T[K]['type'];
312
+ default: T[K]['default'];
313
+ required?: boolean;
314
+ };
315
+ };
316
+
317
+ /**
318
+ * Determines if the value is a JavaScript Class.
319
+ * Note that class is a class function in JavaScript.
320
+ *
321
+ * @param val Any value
322
+ * @returns boolean If it's a JavaScript Class returns true
323
+ */
324
+ export const isClass = (val: any): boolean => {
325
+ if (typeof val !== 'function') return false; // Not a function, so not a class function either
326
+
327
+ if (!val.prototype) return false; // Arrow function, so not a class
328
+
329
+ // Finally -> distinguish between a normal function and a class function
330
+ if (getOwnPropertyDescriptor(val, 'prototype')?.writable) {
331
+ // Has writable prototype
332
+ return false; // Normal function
333
+ } else {
334
+ return true; // Class -> Not a function
335
+ }
336
+ };
337
+ /**
338
+ * Creates props with defaults in defineComponent() style.
339
+ *
340
+ * Merge defaults regular object with Vue types object
341
+ * declared in defineComponent() style.
342
+ *
343
+ * This is made so that the defaults can be declared "as they are"
344
+ * without requiring objects to be function callbacks returning an object.
345
+ *
346
+ * // You don't need to wrap objects in () => ({ nest: { nest :{} } })
347
+ * // You can just delcare them normally.
348
+ * const defaults = {
349
+ * nest: {
350
+ * nest
351
+ * }
352
+ * }
353
+ *
354
+ * This function will create the Vue expected callbacks for Objects, Arrays & Classes
355
+ * but leave primitive properties and functions intact so that
356
+ * the final object is fully defineComponent() style compatible.
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
+ *
363
+ * @param defaults Regular object of default key -> values
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)
366
+ * @returns Props declared in defineComponent() style with all properties having default property declared.
367
+ */
368
+ export const propsWithDefaults = <T extends VuePropsObject>(
369
+ defaults: Record<string, any>,
370
+ typedProps: T,
371
+ // Optional: Allows user to pass a custom cloner if structuredClone isn't enough
372
+ customCloner?: (val: any) => any,
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> = {};
380
+ for (const prop in typedProps) {
381
+ const def = defaults?.[prop];
382
+ const typed = typedProps[prop];
383
+ result[prop] = { ...typed };
384
+
385
+ if (typed.required || def === undefined) continue;
386
+
387
+ if (typeof def === 'object' && def !== null) {
388
+ result[prop].default = () =>
389
+ customCloner ? customCloner(def) : structuredClone(def);
390
+ } else {
391
+ if (isClass(def)) {
392
+ result[prop].default = () => def;
393
+ } else {
394
+ result[prop].default = def;
395
+ }
396
+ }
397
+ }
398
+ return result as VuePropsWithDefaults<T>;
399
+ };
400
+
401
+ /**
402
+ * Type Utilities
403
+ */
404
+ type GetterKeys<T> = {
405
+ [K in keyof T]: T[K] extends (...args: any[]) => any
406
+ ? never
407
+ : T[K] extends undefined
408
+ ? never
409
+ : K;
410
+ }[keyof T];
411
+
412
+ type GetterReturn<T, K extends keyof T> = T[K] extends (...args: any[]) => any
413
+ ? never
414
+ : T[K];
415
+ type WritableComputedLike = Ref<any> & { set: (...args: any[]) => any };
416
+ type IsWritableGetter<R> =
417
+ R extends Ref<any> ? true : R extends WritableComputedLike ? true : false;
418
+
419
+ type WritableGetters<T> = {
420
+ [
421
+ K in GetterKeys<T> as IsWritableGetter<GetterReturn<T, K>> extends true
422
+ ? K
423
+ : never
424
+ ]-?: T[K];
425
+ };
426
+
427
+ export type ReactiveInstance<T> = T &
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
+ };
436
+
437
+ export type ReactiveClass<C extends new (...args: any) => any> = new (
438
+ ...args: ConstructorParameters<C>
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>;