@surface/core
Version:
Provides core functionality of many @surfaces modules.
380 lines (379 loc) • 14.2 kB
JavaScript
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);
}