ivue 1.5.8 → 1.6.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/README.md +2 -3
- package/dist/env.d.ts +1 -0
- package/dist/index.es.js +212 -85
- package/dist/index.umd.js +1 -1
- package/dist/ivue.d.ts +127 -51
- package/lib/Reactive.ts +338 -0
- package/lib/__tests__/ivue.vitest.spec.ts +1403 -150
- package/lib/env.d.ts +1 -0
- package/lib/index.ts +1 -0
- package/lib/ivue.ts +661 -241
- package/lib/kernel.ts +18 -0
- package/package.json +16 -7
package/lib/Reactive.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { toRaw, isRef, type Ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Constants & Helpers
|
|
5
|
+
*/
|
|
6
|
+
const prototypeHasOwnProperty = Object.prototype.hasOwnProperty.call.bind(
|
|
7
|
+
Object.prototype.hasOwnProperty
|
|
8
|
+
);
|
|
9
|
+
const getPrototypeOf = Object.getPrototypeOf;
|
|
10
|
+
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
11
|
+
const defineProperty = Object.defineProperty;
|
|
12
|
+
const getOwnPropertyNames = Object.getOwnPropertyNames;
|
|
13
|
+
const objectPrototype = Object.prototype;
|
|
14
|
+
const getOwnPropertySymbols = Object.getOwnPropertySymbols;
|
|
15
|
+
|
|
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
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get unique symbol key for super method/property.
|
|
24
|
+
*/
|
|
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
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const map = proto[SK_MAP];
|
|
36
|
+
return map[key] || (map[key] = Symbol(key));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert method to a lazy-bound prototype method.
|
|
41
|
+
*/
|
|
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
|
+
|
|
48
|
+
defineProperty(proto, key, {
|
|
49
|
+
configurable: true,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
get(this: any) {
|
|
52
|
+
const raw = this[RAW] ?? (this[RAW] = toRaw(this));
|
|
53
|
+
return raw[superKey] ?? (raw[superKey] = originalFn.bind(raw));
|
|
54
|
+
},
|
|
55
|
+
set(this: any, newFn: any) {
|
|
56
|
+
toRaw(this)[superKey] = newFn;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert a property to a lazy-computed property.
|
|
63
|
+
*/
|
|
64
|
+
function convertToLazyComputed<T extends object>(
|
|
65
|
+
proto: T,
|
|
66
|
+
key: string,
|
|
67
|
+
superKey: symbol
|
|
68
|
+
) {
|
|
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
|
|
74
|
+
const cacheWhole = key[0] === '$';
|
|
75
|
+
|
|
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
|
+
const newGetter = function (this: any) {
|
|
89
|
+
const raw = toRaw(this);
|
|
90
|
+
|
|
91
|
+
// 1. Check Cache
|
|
92
|
+
if (superKey in raw) return raw[superKey];
|
|
93
|
+
|
|
94
|
+
// 2. Execute Original
|
|
95
|
+
const result = originalGetter!.call(raw);
|
|
96
|
+
|
|
97
|
+
// 3. Handle Result
|
|
98
|
+
if (cacheWhole) {
|
|
99
|
+
// Cache result forever (Singleton pattern)
|
|
100
|
+
raw[superKey] = result;
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isRef(result)) {
|
|
105
|
+
// Cache Ref instance (Reactivity pattern)
|
|
106
|
+
raw[superKey] = result;
|
|
107
|
+
} 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.
|
|
110
|
+
defineProperty(proto, key, {
|
|
111
|
+
configurable: true,
|
|
112
|
+
enumerable: false,
|
|
113
|
+
get: originalGetter
|
|
114
|
+
? function (this: any) {
|
|
115
|
+
return originalGetter.call(toRaw(this));
|
|
116
|
+
}
|
|
117
|
+
: undefined,
|
|
118
|
+
set: originalSetter
|
|
119
|
+
? function (this: any, v: any) {
|
|
120
|
+
return originalSetter.call(toRaw(this), v);
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
defineProperty(proto, key, {
|
|
130
|
+
configurable: true,
|
|
131
|
+
enumerable: false,
|
|
132
|
+
get: originalGetter ? newGetter : undefined,
|
|
133
|
+
set: originalSetter
|
|
134
|
+
? function (this: any, v: any) {
|
|
135
|
+
return originalSetter.call(toRaw(this), v);
|
|
136
|
+
}
|
|
137
|
+
: undefined,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a reactive class.
|
|
143
|
+
* @param targetClass The class to make reactive.
|
|
144
|
+
* @returns A reactive version of the class.
|
|
145
|
+
*/
|
|
146
|
+
export function Reactive<C extends new (...args: any) => any>(
|
|
147
|
+
targetClass: C
|
|
148
|
+
): ReactiveClass<C> & { Instance: ReactiveInstance<InstanceType<C>> } {
|
|
149
|
+
const chain: any[] = [];
|
|
150
|
+
|
|
151
|
+
let targetPrototype = targetClass.prototype;
|
|
152
|
+
while (targetPrototype && targetPrototype !== objectPrototype) {
|
|
153
|
+
chain.push(targetPrototype);
|
|
154
|
+
targetPrototype = getPrototypeOf(targetPrototype);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Process Base -> Child
|
|
158
|
+
chain.reverse();
|
|
159
|
+
|
|
160
|
+
for (const prototype of chain) {
|
|
161
|
+
// OPTIMIZATION: Skip if this prototype layer is already "Reactified"
|
|
162
|
+
// This handles diamond inheritance and multiple Reactive children safely.
|
|
163
|
+
if (prototypeHasOwnProperty(prototype, PROCESSED)) continue;
|
|
164
|
+
|
|
165
|
+
const names = getOwnPropertyNames(prototype);
|
|
166
|
+
|
|
167
|
+
for (const key of names) {
|
|
168
|
+
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);
|
|
176
|
+
} else if (desc.get) {
|
|
177
|
+
convertToLazyComputed(prototype, key, superKey);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mark this specific prototype level as processed
|
|
182
|
+
defineProperty(prototype, PROCESSED, {
|
|
183
|
+
configurable: false,
|
|
184
|
+
enumerable: false,
|
|
185
|
+
value: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Inject cleanup
|
|
190
|
+
if (!prototypeHasOwnProperty(targetClass.prototype, $stopEffects)) {
|
|
191
|
+
defineProperty(targetClass.prototype, $stopEffects, {
|
|
192
|
+
enumerable: false,
|
|
193
|
+
configurable: true,
|
|
194
|
+
writable: true,
|
|
195
|
+
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();
|
|
206
|
+
}
|
|
207
|
+
delete raw[symbol];
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return targetClass as any;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Vue props interface in defineComponent() style.
|
|
218
|
+
*/
|
|
219
|
+
export type VuePropsObject = Record<
|
|
220
|
+
string,
|
|
221
|
+
{ type: any; default?: any; required?: boolean }
|
|
222
|
+
>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Vue Props with default properties declared as existing and having values.
|
|
226
|
+
*/
|
|
227
|
+
export type VuePropsWithDefaults<T extends VuePropsObject> = {
|
|
228
|
+
[K in keyof T]: {
|
|
229
|
+
type: T[K]['type'];
|
|
230
|
+
default: T[K]['default'];
|
|
231
|
+
required?: boolean;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Determines if the value is a JavaScript Class.
|
|
237
|
+
* Note that class is a class function in JavaScript.
|
|
238
|
+
*
|
|
239
|
+
* @param val Any value
|
|
240
|
+
* @returns boolean If it's a JavaScript Class returns true
|
|
241
|
+
*/
|
|
242
|
+
export const isClass = (val: any): boolean => {
|
|
243
|
+
if (typeof val !== 'function') return false; // Not a function, so not a class function either
|
|
244
|
+
|
|
245
|
+
if (!val.prototype) return false; // Arrow function, so not a class
|
|
246
|
+
|
|
247
|
+
// Finally -> distinguish between a normal function and a class function
|
|
248
|
+
if (getOwnPropertyDescriptor(val, 'prototype')?.writable) {
|
|
249
|
+
// Has writable prototype
|
|
250
|
+
return false; // Normal function
|
|
251
|
+
} else {
|
|
252
|
+
return true; // Class -> Not a function
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Creates props with defaults in defineComponent() style.
|
|
257
|
+
*
|
|
258
|
+
* Merge defaults regular object with Vue types object
|
|
259
|
+
* declared in defineComponent() style.
|
|
260
|
+
*
|
|
261
|
+
* This is made so that the defaults can be declared "as they are"
|
|
262
|
+
* without requiring objects to be function callbacks returning an object.
|
|
263
|
+
*
|
|
264
|
+
* // You don't need to wrap objects in () => ({ nest: { nest :{} } })
|
|
265
|
+
* // You can just delcare them normally.
|
|
266
|
+
* const defaults = {
|
|
267
|
+
* nest: {
|
|
268
|
+
* nest
|
|
269
|
+
* }
|
|
270
|
+
* }
|
|
271
|
+
*
|
|
272
|
+
* This function will create the Vue expected callbacks for Objects, Arrays & Classes
|
|
273
|
+
* but leave primitive properties and functions intact so that
|
|
274
|
+
* the final object is fully defineComponent() style compatible.
|
|
275
|
+
*
|
|
276
|
+
* @param defaults Regular object of default key -> values
|
|
277
|
+
* @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default
|
|
278
|
+
* @returns Props declared in defineComponent() style with all properties having default property declared.
|
|
279
|
+
*/
|
|
280
|
+
export const propsWithDefaults = <T extends VuePropsObject>(
|
|
281
|
+
defaults: Record<string, any>,
|
|
282
|
+
typedProps: T,
|
|
283
|
+
// Optional: Allows user to pass a custom cloner if structuredClone isn't enough
|
|
284
|
+
customCloner?: (val: any) => any
|
|
285
|
+
): VuePropsWithDefaults<T> => {
|
|
286
|
+
for (const prop in typedProps) {
|
|
287
|
+
const def = defaults?.[prop];
|
|
288
|
+
const typed = typedProps[prop];
|
|
289
|
+
|
|
290
|
+
if (typed.required || def === undefined) continue;
|
|
291
|
+
|
|
292
|
+
if (typeof def === 'object' && def !== null) {
|
|
293
|
+
typedProps[prop].default = () =>
|
|
294
|
+
customCloner ? customCloner(def) : structuredClone(def);
|
|
295
|
+
} else {
|
|
296
|
+
if (isClass(def)) {
|
|
297
|
+
typedProps[prop].default = () => def;
|
|
298
|
+
} else {
|
|
299
|
+
typedProps[prop].default = def;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return typedProps as VuePropsWithDefaults<T>;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Type Utilities
|
|
308
|
+
*/
|
|
309
|
+
type GetterKeys<T> = {
|
|
310
|
+
[K in keyof T]: T[K] extends (...args: any[]) => any
|
|
311
|
+
? never
|
|
312
|
+
: T[K] extends undefined
|
|
313
|
+
? never
|
|
314
|
+
: K;
|
|
315
|
+
}[keyof T];
|
|
316
|
+
|
|
317
|
+
type GetterReturn<T, K extends keyof T> = T[K] extends (...args: any[]) => any
|
|
318
|
+
? never
|
|
319
|
+
: T[K];
|
|
320
|
+
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;
|
|
326
|
+
|
|
327
|
+
type WritableGetters<T> = {
|
|
328
|
+
[K in GetterKeys<T> as IsWritableGetter<GetterReturn<T, K>> extends true
|
|
329
|
+
? K
|
|
330
|
+
: never]-?: T[K];
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export type ReactiveInstance<T> = T &
|
|
334
|
+
WritableGetters<T> & { $stopEffects: () => void };
|
|
335
|
+
|
|
336
|
+
export type ReactiveClass<C extends new (...args: any) => any> = new (
|
|
337
|
+
...args: ConstructorParameters<C>
|
|
338
|
+
) => ReactiveInstance<InstanceType<C>>;
|