UNPKG

@surface/core

Version:

Provides core functionality of many @surfaces modules.

380 lines (379 loc) 14.2 kB
import { assert, isIterable, typeGuard } from "./generic.js"; const PRIVATES = Symbol("core:privates"); const PRIMITIVE_TYPES = new Set([ "string", "number", "bigint", "boolean", "undefined", "symbol", ]); function applyRule(left, right, rule) { if (rule) { if (rule == "protected") { return left; } else if (typeof rule == "function") { return rule(left, right); } else if (left instanceof Object) { if (Array.isArray(left) && Array.isArray(right)) { return mergeArray(left, right, rule); } else if (rule == "merge" && right instanceof Object) { return { ...left, ...right }; } else if (rule instanceof Object && right instanceof Object) { return merge([left, right], rule); } } } return right; } function match(left, right) { return left instanceof RegExp && right instanceof RegExp ? left.source == right.source : Object.is(left, right); } function mergeArray(left, right, rule) { if (Array.isArray(rule)) { const result = [...left]; const matches = Object.entries(rule) .filter(([_, value]) => value == "match") .every(([key]) => match(left[key], right[key])); if (matches) { for (const index of Object.keys(right)) { const itemRule = rule[index]; const leftItem = left[index]; const rightItem = right[index]; result[index] = applyRule(leftItem, rightItem, itemRule); } } return result; } else if (rule == "...merge" || rule instanceof Object) { const result = [...left]; for (const index of Object.keys(right)) { const leftItem = left[index]; const rightItem = right[index]; result[index] = rule instanceof Object && leftItem instanceof Object && rightItem instanceof Object ? merge([leftItem, rightItem], rule) : rule == "...merge" && leftItem instanceof Object && rightItem instanceof Object ? { ...leftItem, ...rightItem } : rightItem; } return result; } switch (rule) { case "prepend": return right.concat(left); case "append": return left.concat(right); case "merge": default: return Object.values({ ...left, ...right }); } } export var DeepMergeFlags; (function (DeepMergeFlags) { DeepMergeFlags[DeepMergeFlags["IgnoreUndefined"] = 1] = "IgnoreUndefined"; DeepMergeFlags[DeepMergeFlags["ConcatArrays"] = 2] = "ConcatArrays"; DeepMergeFlags[DeepMergeFlags["MergeArrays"] = 4] = "MergeArrays"; })(DeepMergeFlags || (DeepMergeFlags = {})); export function clone(source) { if (Array.isArray(source)) { const values = []; for (const value of source) { if (value instanceof Object) { values.push(clone(value)); } else { values.push(value); } } return values; } const prototype = Object.create(Reflect.getPrototypeOf(source)); for (const key of Reflect.ownKeys(source)) { const value = source[key]; if (value instanceof Object) { prototype[key] = clone(value); } else { prototype[key] = value; } } return prototype; } export function deepEqual(left, right, compare) { if (Object.is(left, right) || compare?.(left, right)) { return true; } else if (left instanceof Function && right instanceof Function) { return left == right; } else if (left instanceof Date && right instanceof Date) { return left.getTime() == right.getTime(); } else if (Array.isArray(left) && Array.isArray(right) && left.length != right.length) { return false; } else if (left instanceof Set && right instanceof Set && left.size != right.size) { return false; } else if (left instanceof Map && right instanceof Map) { if (left.size == right.size) { for (const key of left.keys()) { if (!right.has(key) || !deepEqual(left.get(key), right.get(key), compare)) { return false; } } return true; } } else if (typeGuard(left, left instanceof Object) && typeGuard(right, right instanceof left.constructor)) { if (isIterable(left) && isIterable(right)) { const leftIterator = left[Symbol.iterator](); const rightIterator = right[Symbol.iterator](); let nextLeft = leftIterator.next(); let nextRight = rightIterator.next(); while (!nextLeft.done && !nextRight.done) { if (!deepEqual(nextLeft.value, nextRight?.value, compare)) { return false; } nextLeft = leftIterator.next(); nextRight = rightIterator.next(); } return true; } const leftKeys = Array.from(enumerateKeys(left)); const rightKeys = Array.from(enumerateKeys(right)); if (leftKeys.length == rightKeys.length) { for (const key of leftKeys) { if (!(key in right) || !deepEqual(left[key], right[key], compare)) { return false; } } } return true; } return false; } /** * Deeply merges two or more objects. * @param sources Objects to merge. */ export function deepMerge(sources, flags = 0) { const result = {}; for (const current of sources) { for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(current))) { const leftValue = result[key]; const rightValue = current[key]; let resultDescriptor = descriptor; if (leftValue instanceof Object) { if (Array.isArray(leftValue) && Array.isArray(rightValue)) { if (hasFlags(flags, DeepMergeFlags.ConcatArrays)) { resultDescriptor = { ...descriptor, value: [...leftValue, ...rightValue] }; } else if (hasFlags(flags, DeepMergeFlags.MergeArrays)) { const elements = [...leftValue]; for (const [index, rightItem] of Object.entries(rightValue)) { const leftItem = elements[index]; elements[index] = typeof leftItem == "object" && typeof rightValue == "object" ? deepMerge([leftItem, rightItem], flags) : rightItem; } resultDescriptor = { ...descriptor, value: elements }; } } else if (rightValue instanceof Object) { const value = deepMerge([leftValue, rightValue], flags); resultDescriptor = { ...descriptor, value }; } } else if (hasFlags(flags, DeepMergeFlags.IgnoreUndefined) && typeof rightValue == "object" && !Array.isArray(rightValue)) { const value = deepMerge([rightValue], flags); resultDescriptor = { ...descriptor, value }; } if (!(hasFlags(flags, DeepMergeFlags.IgnoreUndefined) && Object.is(rightValue, undefined))) { Reflect.defineProperty(result, key, resultDescriptor); } } } return result; } export function freeze(target) { for (const value of Object.values(target)) { if (value instanceof Object) { freeze(value); } } return Object.freeze(target); } export function isEsm(module) { return typeof module == "object" && module !== null && (!!Reflect.get(module, "__esModule") || Reflect.get(module, Symbol.toStringTag) == "Module"); } export function isPrimitive(value) { return value === null || PRIMITIVE_TYPES.has(typeof value); } export function isReadonly(...args) { const descriptor = args.length == 1 ? args[0] : getPropertyDescriptor(args[0], args[1]); return descriptor?.get ? !descriptor.set : !(descriptor?.writable ?? false); } export function getPropertyDescriptor(target, key) { let prototype = target; do { const descriptor = Object.getOwnPropertyDescriptor(prototype, key); if (descriptor) { return descriptor; } } while (prototype = Reflect.getPrototypeOf(prototype)); return null; } export function getValue(root, ...path) { const [key, ...keys] = path; if (keys.length > 0) { if (key in root) { return getValue(root[key], ...keys); } const typeName = root instanceof Function ? root.name : root.constructor.name; throw new Error(`Property "${key}" does not exists on type ${typeName}`); } return root[key]; } /** * Merges two or more objects using optional rules. * @param sources objects to merge * @param rules rules used to control merge * */ export function hasFlags(value, flags) { return (value & flags) == flags; } export function merge(sources, rules = {}) { const [first, ...remaining] = sources; const result = { ...first }; const matchKeys = Object.entries(rules).filter(x => x[1] == "match").map(x => x[0]); for (const current of remaining) { const hasMatch = matchKeys .every(key => match(result[key], current[key])); if (hasMatch) { for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(current))) { const leftValue = result[key]; const rightValue = current[key]; const rule = rules[key]; if (rule != "protected") { Reflect.defineProperty(result, key, { ...descriptor, value: applyRule(leftValue, rightValue, rule) }); } } } } return result; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function mix(constructor, mixins) { assert(mixins.length > 0, "Mixer requires at least one mixin"); const mixin = mixins.pop(); const $class = mixin(constructor); if (mixins.length > 0) { return mix($class, mixins); } return $class; } /** * Create an object using the provided keys. * @param keys Object keys * @param target If provided, all keys will inserted on target object */ export function objectFactory(keys, target = {}) { for (const entries of keys) { const [key, value] = entries; if (key.includes(".")) { const [name, ...rest] = key.split("."); target[name] = objectFactory([[rest.join("."), value]], target[name]); } else { target[key] = value; } } return target; } export function makePath(source, options) { const { keySeparator = ".", keyTransform = (x) => x, valueSeparator = ": " } = options ?? {}; const result = []; for (const [key, value] of Object.entries(source)) { if (value instanceof Object) { result.push(...makePath(value, options).map(x => keyTransform(key) + (keySeparator ?? ".") + x)); } else { result.push(`${keyTransform(key)}${valueSeparator ?? ": "}${value}`); } } return result; } export function privatesFrom(target) { if (!target[PRIVATES]) { Object.defineProperty(target, PRIVATES, { configurable: true, enumerable: false, value: {}, writable: false }); } return target[PRIVATES]; } export function proxyFrom(...instances) { const handler = { get(_, key) { const instance = instances.find(x => key in x); if (instance) { return instance[key]; } return undefined; }, getOwnPropertyDescriptor(_, key) { for (const instance of instances) { const descriptor = Reflect.getOwnPropertyDescriptor(instance, key); if (descriptor) { descriptor.configurable = true, descriptor.enumerable = true; return descriptor; } } return undefined; }, has: (_, key) => instances.some(x => key in x), ownKeys: () => Array.from(new Set(instances.map(x => Object.getOwnPropertyNames(x)).flatMap(x => x))), set(_, key, value) { const instance = instances.find(x => key in x); if (instance) { instance[key] = value; } else { instances[0][key] = value; } return true; }, }; return new Proxy(instances[0], handler); } export function resolveError(error) { return error instanceof Error ? error : new Error(String(error)); } export function setValue(value, root, ...path) { const key = path[path.length - 1]; if (path.length - 1 > 0) { const property = getValue(root, ...path.slice(0, path.length - 1)); property[key] = value; } else { root[key] = value; } } export function* enumerateKeys(target) { const set = new Set(); let prototype = target; do { for (const key of Object.keys(prototype)) { if (!set.has(key)) { set.add(key); yield key; } } } while ((prototype = Reflect.getPrototypeOf(prototype)) && prototype.constructor != Object); }