source: node_modules/ts-mixer/dist/esm/index.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 17.5 KB
Line 
1/**
2 * Utility function that works like `Object.apply`, but copies getters and setters properly as well. Additionally gives
3 * the option to exclude properties by name.
4 */
5const copyProps = (dest, src, exclude = []) => {
6 const props = Object.getOwnPropertyDescriptors(src);
7 for (let prop of exclude)
8 delete props[prop];
9 Object.defineProperties(dest, props);
10};
11/**
12 * Returns the full chain of prototypes up until Object.prototype given a starting object. The order of prototypes will
13 * be closest to farthest in the chain.
14 */
15const protoChain = (obj, currentChain = [obj]) => {
16 const proto = Object.getPrototypeOf(obj);
17 if (proto === null)
18 return currentChain;
19 return protoChain(proto, [...currentChain, proto]);
20};
21/**
22 * Identifies the nearest ancestor common to all the given objects in their prototype chains. For most unrelated
23 * objects, this function should return Object.prototype.
24 */
25const nearestCommonProto = (...objs) => {
26 if (objs.length === 0)
27 return undefined;
28 let commonProto = undefined;
29 const protoChains = objs.map(obj => protoChain(obj));
30 while (protoChains.every(protoChain => protoChain.length > 0)) {
31 const protos = protoChains.map(protoChain => protoChain.pop());
32 const potentialCommonProto = protos[0];
33 if (protos.every(proto => proto === potentialCommonProto))
34 commonProto = potentialCommonProto;
35 else
36 break;
37 }
38 return commonProto;
39};
40/**
41 * Creates a new prototype object that is a mixture of the given prototypes. The mixing is achieved by first
42 * identifying the nearest common ancestor and using it as the prototype for a new object. Then all properties/methods
43 * downstream of this prototype (ONLY downstream) are copied into the new object.
44 *
45 * The resulting prototype is more performant than softMixProtos(...), as well as ES5 compatible. However, it's not as
46 * flexible as updates to the source prototypes aren't captured by the mixed result. See softMixProtos for why you may
47 * want to use that instead.
48 */
49const hardMixProtos = (ingredients, constructor, exclude = []) => {
50 var _a;
51 const base = (_a = nearestCommonProto(...ingredients)) !== null && _a !== void 0 ? _a : Object.prototype;
52 const mixedProto = Object.create(base);
53 // Keeps track of prototypes we've already visited to avoid copying the same properties multiple times. We init the
54 // list with the proto chain below the nearest common ancestor because we don't want any of those methods mixed in
55 // when they will already be accessible via prototype access.
56 const visitedProtos = protoChain(base);
57 for (let prototype of ingredients) {
58 let protos = protoChain(prototype);
59 // Apply the prototype chain in reverse order so that old methods don't override newer ones.
60 for (let i = protos.length - 1; i >= 0; i--) {
61 let newProto = protos[i];
62 if (visitedProtos.indexOf(newProto) === -1) {
63 copyProps(mixedProto, newProto, ['constructor', ...exclude]);
64 visitedProtos.push(newProto);
65 }
66 }
67 }
68 mixedProto.constructor = constructor;
69 return mixedProto;
70};
71const unique = (arr) => arr.filter((e, i) => arr.indexOf(e) == i);
72
73/**
74 * Finds the ingredient with the given prop, searching in reverse order and breadth-first if searching ingredient
75 * prototypes is required.
76 */
77const getIngredientWithProp = (prop, ingredients) => {
78 const protoChains = ingredients.map(ingredient => protoChain(ingredient));
79 // since we search breadth-first, we need to keep track of our depth in the prototype chains
80 let protoDepth = 0;
81 // not all prototype chains are the same depth, so this remains true as long as at least one of the ingredients'
82 // prototype chains has an object at this depth
83 let protosAreLeftToSearch = true;
84 while (protosAreLeftToSearch) {
85 // with the start of each horizontal slice, we assume this is the one that's deeper than any of the proto chains
86 protosAreLeftToSearch = false;
87 // scan through the ingredients right to left
88 for (let i = ingredients.length - 1; i >= 0; i--) {
89 const searchTarget = protoChains[i][protoDepth];
90 if (searchTarget !== undefined && searchTarget !== null) {
91 // if we find something, this is proof that this horizontal slice potentially more objects to search
92 protosAreLeftToSearch = true;
93 // eureka, we found it
94 if (Object.getOwnPropertyDescriptor(searchTarget, prop) != undefined) {
95 return protoChains[i][0];
96 }
97 }
98 }
99 protoDepth++;
100 }
101 return undefined;
102};
103/**
104 * "Mixes" ingredients by wrapping them in a Proxy. The optional prototype argument allows the mixed object to sit
105 * downstream of an existing prototype chain. Note that "properties" cannot be added, deleted, or modified.
106 */
107const proxyMix = (ingredients, prototype = Object.prototype) => new Proxy({}, {
108 getPrototypeOf() {
109 return prototype;
110 },
111 setPrototypeOf() {
112 throw Error('Cannot set prototype of Proxies created by ts-mixer');
113 },
114 getOwnPropertyDescriptor(_, prop) {
115 return Object.getOwnPropertyDescriptor(getIngredientWithProp(prop, ingredients) || {}, prop);
116 },
117 defineProperty() {
118 throw new Error('Cannot define new properties on Proxies created by ts-mixer');
119 },
120 has(_, prop) {
121 return getIngredientWithProp(prop, ingredients) !== undefined || prototype[prop] !== undefined;
122 },
123 get(_, prop) {
124 return (getIngredientWithProp(prop, ingredients) || prototype)[prop];
125 },
126 set(_, prop, val) {
127 const ingredientWithProp = getIngredientWithProp(prop, ingredients);
128 if (ingredientWithProp === undefined)
129 throw new Error('Cannot set new properties on Proxies created by ts-mixer');
130 ingredientWithProp[prop] = val;
131 return true;
132 },
133 deleteProperty() {
134 throw new Error('Cannot delete properties on Proxies created by ts-mixer');
135 },
136 ownKeys() {
137 return ingredients
138 .map(Object.getOwnPropertyNames)
139 .reduce((prev, curr) => curr.concat(prev.filter(key => curr.indexOf(key) < 0)));
140 },
141});
142/**
143 * Creates a new proxy-prototype object that is a "soft" mixture of the given prototypes. The mixing is achieved by
144 * proxying all property access to the ingredients. This is not ES5 compatible and less performant. However, any
145 * changes made to the source prototypes will be reflected in the proxy-prototype, which may be desirable.
146 */
147const softMixProtos = (ingredients, constructor) => proxyMix([...ingredients, { constructor }]);
148
149const settings = {
150 initFunction: null,
151 staticsStrategy: 'copy',
152 prototypeStrategy: 'copy',
153 decoratorInheritance: 'deep',
154};
155
156// Keeps track of constituent classes for every mixin class created by ts-mixer.
157const mixins = new Map();
158const getMixinsForClass = (clazz) => mixins.get(clazz);
159const registerMixins = (mixedClass, constituents) => mixins.set(mixedClass, constituents);
160const hasMixin = (instance, mixin) => {
161 if (instance instanceof mixin)
162 return true;
163 const constructor = instance.constructor;
164 const visited = new Set();
165 let frontier = new Set();
166 frontier.add(constructor);
167 while (frontier.size > 0) {
168 // check if the frontier has the mixin we're looking for. if not, we can say we visited every item in the frontier
169 if (frontier.has(mixin))
170 return true;
171 frontier.forEach(item => visited.add(item));
172 // build a new frontier based on the associated mixin classes and prototype chains of each frontier item
173 const newFrontier = new Set();
174 frontier.forEach(item => {
175 var _a;
176 const itemConstituents = (_a = mixins.get(item)) !== null && _a !== void 0 ? _a : protoChain(item.prototype).map(proto => proto.constructor).filter(item => item !== null);
177 if (itemConstituents)
178 itemConstituents.forEach(constituent => {
179 if (!visited.has(constituent) && !frontier.has(constituent))
180 newFrontier.add(constituent);
181 });
182 });
183 // we have a new frontier, now search again
184 frontier = newFrontier;
185 }
186 // if we get here, we couldn't find the mixin anywhere in the prototype chain or associated mixin classes
187 return false;
188};
189
190const mergeObjectsOfDecorators = (o1, o2) => {
191 var _a, _b;
192 const allKeys = unique([...Object.getOwnPropertyNames(o1), ...Object.getOwnPropertyNames(o2)]);
193 const mergedObject = {};
194 for (let key of allKeys)
195 mergedObject[key] = unique([...((_a = o1 === null || o1 === void 0 ? void 0 : o1[key]) !== null && _a !== void 0 ? _a : []), ...((_b = o2 === null || o2 === void 0 ? void 0 : o2[key]) !== null && _b !== void 0 ? _b : [])]);
196 return mergedObject;
197};
198const mergePropertyAndMethodDecorators = (d1, d2) => {
199 var _a, _b, _c, _d;
200 return ({
201 property: mergeObjectsOfDecorators((_a = d1 === null || d1 === void 0 ? void 0 : d1.property) !== null && _a !== void 0 ? _a : {}, (_b = d2 === null || d2 === void 0 ? void 0 : d2.property) !== null && _b !== void 0 ? _b : {}),
202 method: mergeObjectsOfDecorators((_c = d1 === null || d1 === void 0 ? void 0 : d1.method) !== null && _c !== void 0 ? _c : {}, (_d = d2 === null || d2 === void 0 ? void 0 : d2.method) !== null && _d !== void 0 ? _d : {}),
203 });
204};
205const mergeDecorators = (d1, d2) => {
206 var _a, _b, _c, _d, _e, _f;
207 return ({
208 class: unique([...(_a = d1 === null || d1 === void 0 ? void 0 : d1.class) !== null && _a !== void 0 ? _a : [], ...(_b = d2 === null || d2 === void 0 ? void 0 : d2.class) !== null && _b !== void 0 ? _b : []]),
209 static: mergePropertyAndMethodDecorators((_c = d1 === null || d1 === void 0 ? void 0 : d1.static) !== null && _c !== void 0 ? _c : {}, (_d = d2 === null || d2 === void 0 ? void 0 : d2.static) !== null && _d !== void 0 ? _d : {}),
210 instance: mergePropertyAndMethodDecorators((_e = d1 === null || d1 === void 0 ? void 0 : d1.instance) !== null && _e !== void 0 ? _e : {}, (_f = d2 === null || d2 === void 0 ? void 0 : d2.instance) !== null && _f !== void 0 ? _f : {}),
211 });
212};
213const decorators = new Map();
214const findAllConstituentClasses = (...classes) => {
215 var _a;
216 const allClasses = new Set();
217 const frontier = new Set([...classes]);
218 while (frontier.size > 0) {
219 for (let clazz of frontier) {
220 const protoChainClasses = protoChain(clazz.prototype).map(proto => proto.constructor);
221 const mixinClasses = (_a = getMixinsForClass(clazz)) !== null && _a !== void 0 ? _a : [];
222 const potentiallyNewClasses = [...protoChainClasses, ...mixinClasses];
223 const newClasses = potentiallyNewClasses.filter(c => !allClasses.has(c));
224 for (let newClass of newClasses)
225 frontier.add(newClass);
226 allClasses.add(clazz);
227 frontier.delete(clazz);
228 }
229 }
230 return [...allClasses];
231};
232const deepDecoratorSearch = (...classes) => {
233 const decoratorsForClassChain = findAllConstituentClasses(...classes)
234 .map(clazz => decorators.get(clazz))
235 .filter(decorators => !!decorators);
236 if (decoratorsForClassChain.length == 0)
237 return {};
238 if (decoratorsForClassChain.length == 1)
239 return decoratorsForClassChain[0];
240 return decoratorsForClassChain.reduce((d1, d2) => mergeDecorators(d1, d2));
241};
242const directDecoratorSearch = (...classes) => {
243 const classDecorators = classes.map(clazz => getDecoratorsForClass(clazz));
244 if (classDecorators.length === 0)
245 return {};
246 if (classDecorators.length === 1)
247 return classDecorators[0];
248 return classDecorators.reduce((d1, d2) => mergeDecorators(d1, d2));
249};
250const getDecoratorsForClass = (clazz) => {
251 let decoratorsForClass = decorators.get(clazz);
252 if (!decoratorsForClass) {
253 decoratorsForClass = {};
254 decorators.set(clazz, decoratorsForClass);
255 }
256 return decoratorsForClass;
257};
258const decorateClass = (decorator) => ((clazz) => {
259 const decoratorsForClass = getDecoratorsForClass(clazz);
260 let classDecorators = decoratorsForClass.class;
261 if (!classDecorators) {
262 classDecorators = [];
263 decoratorsForClass.class = classDecorators;
264 }
265 classDecorators.push(decorator);
266 return decorator(clazz);
267});
268const decorateMember = (decorator) => ((object, key, ...otherArgs) => {
269 var _a, _b, _c;
270 const decoratorTargetType = typeof object === 'function' ? 'static' : 'instance';
271 const decoratorType = typeof object[key] === 'function' ? 'method' : 'property';
272 const clazz = decoratorTargetType === 'static' ? object : object.constructor;
273 const decoratorsForClass = getDecoratorsForClass(clazz);
274 const decoratorsForTargetType = (_a = decoratorsForClass === null || decoratorsForClass === void 0 ? void 0 : decoratorsForClass[decoratorTargetType]) !== null && _a !== void 0 ? _a : {};
275 decoratorsForClass[decoratorTargetType] = decoratorsForTargetType;
276 let decoratorsForType = (_b = decoratorsForTargetType === null || decoratorsForTargetType === void 0 ? void 0 : decoratorsForTargetType[decoratorType]) !== null && _b !== void 0 ? _b : {};
277 decoratorsForTargetType[decoratorType] = decoratorsForType;
278 let decoratorsForKey = (_c = decoratorsForType === null || decoratorsForType === void 0 ? void 0 : decoratorsForType[key]) !== null && _c !== void 0 ? _c : [];
279 decoratorsForType[key] = decoratorsForKey;
280 // @ts-ignore: array is type `A[] | B[]` and item is type `A | B`, so technically a type error, but it's fine
281 decoratorsForKey.push(decorator);
282 // @ts-ignore
283 return decorator(object, key, ...otherArgs);
284});
285const decorate = (decorator) => ((...args) => {
286 if (args.length === 1)
287 return decorateClass(decorator)(args[0]);
288 return decorateMember(decorator)(...args);
289});
290
291function Mixin(...constructors) {
292 var _a, _b, _c;
293 const prototypes = constructors.map(constructor => constructor.prototype);
294 // Here we gather up the init functions of the ingredient prototypes, combine them into one init function, and
295 // attach it to the mixed class prototype. The reason we do this is because we want the init functions to mix
296 // similarly to constructors -- not methods, which simply override each other.
297 const initFunctionName = settings.initFunction;
298 if (initFunctionName !== null) {
299 const initFunctions = prototypes
300 .map(proto => proto[initFunctionName])
301 .filter(func => typeof func === 'function');
302 const combinedInitFunction = function (...args) {
303 for (let initFunction of initFunctions)
304 initFunction.apply(this, args);
305 };
306 const extraProto = { [initFunctionName]: combinedInitFunction };
307 prototypes.push(extraProto);
308 }
309 function MixedClass(...args) {
310 for (const constructor of constructors)
311 // @ts-ignore: potentially abstract class
312 copyProps(this, new constructor(...args));
313 if (initFunctionName !== null && typeof this[initFunctionName] === 'function')
314 this[initFunctionName].apply(this, args);
315 }
316 MixedClass.prototype = settings.prototypeStrategy === 'copy'
317 ? hardMixProtos(prototypes, MixedClass)
318 : softMixProtos(prototypes, MixedClass);
319 Object.setPrototypeOf(MixedClass, settings.staticsStrategy === 'copy'
320 ? hardMixProtos(constructors, null, ['prototype'])
321 : proxyMix(constructors, Function.prototype));
322 let DecoratedMixedClass = MixedClass;
323 if (settings.decoratorInheritance !== 'none') {
324 const classDecorators = settings.decoratorInheritance === 'deep'
325 ? deepDecoratorSearch(...constructors)
326 : directDecoratorSearch(...constructors);
327 for (let decorator of (_a = classDecorators === null || classDecorators === void 0 ? void 0 : classDecorators.class) !== null && _a !== void 0 ? _a : []) {
328 const result = decorator(DecoratedMixedClass);
329 if (result) {
330 DecoratedMixedClass = result;
331 }
332 }
333 applyPropAndMethodDecorators((_b = classDecorators === null || classDecorators === void 0 ? void 0 : classDecorators.static) !== null && _b !== void 0 ? _b : {}, DecoratedMixedClass);
334 applyPropAndMethodDecorators((_c = classDecorators === null || classDecorators === void 0 ? void 0 : classDecorators.instance) !== null && _c !== void 0 ? _c : {}, DecoratedMixedClass.prototype);
335 }
336 registerMixins(DecoratedMixedClass, constructors);
337 return DecoratedMixedClass;
338}
339const applyPropAndMethodDecorators = (propAndMethodDecorators, target) => {
340 const propDecorators = propAndMethodDecorators.property;
341 const methodDecorators = propAndMethodDecorators.method;
342 if (propDecorators)
343 for (let key in propDecorators)
344 for (let decorator of propDecorators[key])
345 decorator(target, key);
346 if (methodDecorators)
347 for (let key in methodDecorators)
348 for (let decorator of methodDecorators[key])
349 decorator(target, key, Object.getOwnPropertyDescriptor(target, key));
350};
351/**
352 * A decorator version of the `Mixin` function. You'll want to use this instead of `Mixin` for mixing generic classes.
353 */
354const mix = (...ingredients) => decoratedClass => {
355 // @ts-ignore
356 const mixedClass = Mixin(...ingredients.concat([decoratedClass]));
357 Object.defineProperty(mixedClass, 'name', {
358 value: decoratedClass.name,
359 writable: false,
360 });
361 return mixedClass;
362};
363
364export { Mixin, decorate, hasMixin, mix, settings };
Note: See TracBrowser for help on using the repository browser.