yeoman-generator
Version:
Rails-inspired generator system that provides scaffolding for your apps
149 lines (148 loc) • 5.04 kB
JavaScript
/**
* Object helpers of the storage, with the behavior of the lodash functions of the same name for the JSON-like data a
* storage holds: plain objects, arrays and primitives.
*/
const isObjectLike = (value) => typeof value === 'object' && value !== null;
const isPlainObject = (value) => {
if (!isObjectLike(value) || Object.prototype.toString.call(value) !== '[object Object]') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
};
const isIndex = (key) => typeof key === 'number' || (typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key));
// The keys a path can not go through, as lodash refuses them.
const isUnsafeKey = (key) => key === '__proto__' || key === 'constructor' || key === 'prototype';
const plainPropertyPattern = /^\w*$/;
const deepPropertyPattern = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
const propertyNamePattern = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
const escapeCharacterPattern = /\\(\\)?/g;
/**
* The keys of a path: `a.b[0]["c.d"]` is `['a', 'b', '0', 'c.d']`. A path that is a key of the object, or has no `.`
* nor `[`, is the key itself.
*/
const toPath = (path, object) => {
if (Array.isArray(path)) {
return [...path];
}
if (typeof path !== 'string') {
return [path];
}
if (plainPropertyPattern.test(path) || !deepPropertyPattern.test(path) || (isObjectLike(object) && path in object)) {
return [path];
}
const keys = [];
if (path.startsWith('.')) {
keys.push('');
}
path.replaceAll(propertyNamePattern, (match, number, quote, subString) => {
keys.push(quote ? subString.replaceAll(escapeCharacterPattern, '$1') : (number ?? match));
return match;
});
return keys;
};
/**
* The value at a path of an object, like lodash `get`.
*/
export const get = (object, path) => {
const keys = toPath(path, object);
let value = object;
for (const key of keys) {
if (value === null || value === undefined) {
return undefined;
}
value = value[key];
}
return keys.length > 0 ? value : undefined;
};
/**
* Set the value at a path of an object, creating the missing objects (arrays for an index), like lodash `set`.
*/
export const set = (object, path, value) => {
if (!isObjectLike(object)) {
return object;
}
const keys = toPath(path, object);
let nested = object;
for (const [index, key] of keys.entries()) {
if (isUnsafeKey(key)) {
return object;
}
if (index === keys.length - 1) {
nested[key] = value;
}
else {
const current = nested[key];
if (!isObjectLike(current)) {
nested[key] = isIndex(keys[index + 1]) ? [] : {};
}
nested = nested[key];
}
}
return object;
};
/**
* Assign to the target the properties of the sources it has as undefined, the first source first, like lodash
* `defaults`.
*/
export const defaults = (target, ...sources) => {
for (const source of sources) {
if (!source)
continue;
for (const key in source) {
if (target[key] === undefined) {
target[key] = source[key];
}
}
}
return target;
};
/**
* A deep copy of the arrays and plain objects of a value, the other values kept, like lodash `cloneDeep` for JSON-like
* data.
*/
export const cloneDeep = (value) => {
if (Array.isArray(value)) {
return value.map(item => cloneDeep(item));
}
if (isPlainObject(value)) {
const copy = Object.getPrototypeOf(value) === null ? Object.create(null) : {};
for (const key of Object.keys(value)) {
copy[key] = cloneDeep(value[key]);
}
return copy;
}
return value;
};
const mergeInto = (target, source) => {
for (const key in source) {
if (isUnsafeKey(key))
continue;
const sourceValue = source[key];
const targetValue = target[key];
let value;
if (Array.isArray(sourceValue)) {
value = Array.isArray(targetValue) ? targetValue : [];
mergeInto(value, sourceValue);
}
else if (isPlainObject(sourceValue)) {
value = isObjectLike(targetValue) && typeof targetValue !== 'function' ? targetValue : {};
mergeInto(value, sourceValue);
}
else {
value = sourceValue;
}
// An undefined source value only adds a missing key.
if ((value !== undefined && !Object.is(targetValue, value)) || (value === undefined && !(key in target))) {
target[key] = value;
}
}
};
export function merge(target, ...sources) {
for (const source of sources) {
if (isObjectLike(source)) {
mergeInto(target, source);
}
}
return target;
}