Skip to content

Commit

Permalink
refactor: Optimized script generation
Browse files Browse the repository at this point in the history
  • Loading branch information
erayhanoglu committed Nov 21, 2024
1 parent 31fee04 commit b0eeb4f
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 132 deletions.
276 changes: 148 additions & 128 deletions src/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export namespace merge {

/**
*/
keepArrays?: boolean | NodeCallback;
moveArrays?: boolean | NodeCallback;

/**
* Do not overwrite existing properties if set true
Expand Down Expand Up @@ -91,7 +91,7 @@ export function getMergeFunction(options?: merge.Options): Function {
const cacheKey =
f(options?.deep) +
',' +
f(options?.keepArrays) +
f(options?.moveArrays) +
',' +
f(options?.keepExisting) +
',' +
Expand Down Expand Up @@ -123,180 +123,185 @@ function buildMerge(options?: merge.Options): Function {
'isObject',
'arrayClone',
];
let script = `
const _merge = (_trgVal, _srcVal, _curPath) =>
mergeFunction(_trgVal, _srcVal, _curPath, options, mergeFunction, isPlainObject, isObject, arrayClone);
const keys = Object.getOwnPropertyNames(source);
keys.push(...Object.getOwnPropertySymbols(source));
let key;
let descriptor;
let srcVal;
let trgVal;
`;

if (typeof options?.deep === 'function') {
script += `
const deepCallback = options.deep;
let subPath;
`;
const scriptL0: any[] = [
`
const _merge = (_trgVal, _srcVal, _curPath) =>
mergeFunction(_trgVal, _srcVal, _curPath, options, mergeFunction, isPlainObject, isObject, arrayClone);
const keys = Object.getOwnPropertyNames(source);
keys.push(...Object.getOwnPropertySymbols(source));
let key;
let descriptor;
let srcVal;
let trgVal;
`,
];
if (options?.deep) {
scriptL0.push(`let subPath;`, `let _isPlain;`, `let _isArray;`);
if (typeof options?.deep === 'function') {
scriptL0.push(`const deepCallback = options.deep;`);
}
}

if (typeof options?.ignore === 'function') {
script += ' const ignoreCallback = options.ignore;\n';
scriptL0.push('const ignoreCallback = options.ignore;');
}

if (typeof options?.filter === 'function') {
script += ' const filterCallback = options.filter;\n';
scriptL0.push('const filterCallback = options.filter;');
}

if (typeof options?.copyDescriptors === 'function') {
script += `
const copyDescriptorsCallback = options.copyDescriptors;
let copyDescriptors;
`;
scriptL0.push(`const copyDescriptorsCallback = options.copyDescriptors;`);
}

if (typeof options?.keepArrays === 'function') {
script += ' const keepArraysCallback = options.keepArrays;\n';
if (typeof options?.moveArrays === 'function') {
scriptL0.push(`const moveArraysCallback = options.moveArrays;`);
}

script += `
let i = 0;
const len = keys.length;
for (i = 0; i < len; i++) {
key = keys[i];
/** Should not overwrite __proto__ and constructor properties */
if (key === '__proto__' || key === 'constructor') continue;
`;
scriptL0.push(`
let i = 0;
const len = keys.length;
for (i = 0; i < len; i++) {
key = keys[i];
/** Should not overwrite __proto__ and constructor properties */
if (key === '__proto__' || key === 'constructor') continue;
`);
const scriptL1For: any[] = [];
scriptL0.push(scriptL1For);
scriptL0.push('}');

/** ************* filter *****************/
if (options?.filter) {
script += `
if (!filterCallback(key, curPath, target, source)) {
delete target[key];
continue;
}
`;
scriptL1For.push(`
if (!filterCallback(key, curPath, target, source)) {
delete target[key];
continue;
}`);
}

/** ************* ignore *****************/
if (typeof options?.ignore === 'function') {
script += `
if (Object.prototype.hasOwnProperty.call(target, key) &&
ignoreCallback(key, curPath, target, source)
) continue;
`;
scriptL1For.push(`
if (
Object.prototype.hasOwnProperty.call(target, key) &&
ignoreCallback(key, curPath, target, source)
) continue;
`);
}

script += `
descriptor = { ...Object.getOwnPropertyDescriptor(source, key) };
`;
// scriptL1For.push(
// `descriptor = { ...Object.getOwnPropertyDescriptor(source, key) };`,
// );

/** ************* copyDescriptors *****************/
if (options?.copyDescriptors === true) {
script += `
if ((descriptor.get || descriptor.set)) {
Object.defineProperty(target, key, descriptor);
continue;
}
srcVal = source[key];
`;
} else if (typeof options?.copyDescriptors === 'function') {
script += `
copyDescriptors = copyDescriptorsCallback(key, curPath, target, source)
if (copyDescriptors) {
if ((descriptor.get || descriptor.set)) {
Object.defineProperty(target, key, descriptor);
continue;
}
srcVal = source[key];
} else {
srcVal = source[key];
descriptor.value = srcVal;
delete descriptor.get;
delete descriptor.set;
descriptor.enumerable = true;
descriptor.configurable = true;
descriptor.writable = true;
if (options?.copyDescriptors) {
let scriptL2Descriptors = scriptL1For;

if (typeof options?.copyDescriptors === 'function') {
scriptL1For.push(
'if (copyDescriptorsCallback(key, curPath, target, source)) {',
);
scriptL2Descriptors = [];
scriptL1For.push(scriptL2Descriptors);
scriptL1For.push(`} else`);
scriptL1For.push(
` descriptor = {enumerable: true, configurable: true, writable: true}`,
);
}
`;
scriptL2Descriptors.push(`
descriptor = { ...Object.getOwnPropertyDescriptor(source, key) }
if ((descriptor.get || descriptor.set)) {
Object.defineProperty(target, key, descriptor);
continue;
}
srcVal = source[key];`);
} else {
script += `
srcVal = source[key];
descriptor.value = srcVal;
delete descriptor.get;
delete descriptor.set;
descriptor.enumerable = true;
descriptor.configurable = true;
descriptor.writable = true;
`;
scriptL1For.push(
`descriptor = {enumerable: true, configurable: true, writable: true}`,
`srcVal = source[key];`,
);
}

/** ************* keepExisting *****************/
if (options?.keepExisting) {
script += `
if (hasOwnProperty.call(target, key)) continue;
`;
scriptL1For.push(`if (hasOwnProperty.call(target, key)) continue;`);
}

/** ************* ignoreUndefined *****************/
if (options?.ignoreUndefined ?? true) {
script += `
if (srcVal === undefined) continue;
`;
scriptL1For.push(`if (srcVal === undefined) continue;`);
}

/** ************* ignoreNulls *****************/
if (options?.ignoreNulls) {
script += `
if (srcVal === null) continue;
`;
scriptL1For.push(`if (srcVal === null) continue;`);
}

/** ************* deep *****************/
if (options?.deep) {
script += ` subPath = curPath + (curPath ? '.' : '') + key;\n`;
/** ************* keepArrays *****************/
if (!options?.keepArrays) {
script += ` if (Array.isArray(srcVal)) {\n`;
if (typeof options?.keepArrays === 'function') {
script += `
if (!keepArraysCallback(key, subPath, target, source)) {
descriptor.value = arrayClone(srcVal, _merge, subPath);
Object.defineProperty(target, key, descriptor);
continue;
}\n`;
} else {
script += `
descriptor.value = arrayClone(srcVal, _merge, subPath);
Object.defineProperty(target, key, descriptor);
continue;
}\n`;
}
}
scriptL1For.push(`
_isPlain = isPlainObject(srcVal);
_isArray = Array.isArray(srcVal);
if (_isPlain || _isArray) {`);
const scriptL2Deep: any[] = [];
scriptL1For.push(scriptL2Deep);
scriptL1For.push('}');

let scriptL3Deep = scriptL2Deep;

/** ************* isPlainObject *****************/
if (typeof options?.deep === 'function') {
script += ` if (isPlainObject(srcVal) && deepCallback(key, subPath, target, source)) {`;
} else script += ` if (isPlainObject(srcVal)) {`;
script += `
trgVal = target[key];
if (!isObject(trgVal)) {
descriptor.value = trgVal = {};
Object.defineProperty(target, key, descriptor);
scriptL2Deep.push(`
subPath = curPath + (curPath ? '.' : '') + key;
if (deepCallback(key, subPath, target, source)) {`);
scriptL3Deep = [];
scriptL2Deep.push(scriptL3Deep);
scriptL2Deep.push('}');
}

/** ************* _isPlain *****************/
scriptL3Deep.push(`
if (_isPlain) {
trgVal = target[key];
if (!isObject(trgVal)) {
descriptor.value = trgVal = {};
Object.defineProperty(target, key, descriptor);
}
_merge(trgVal, srcVal, subPath, options);
continue;
}`);

/** ************* moveArrays *****************/
if (!options?.moveArrays || typeof options?.moveArrays === 'function') {
scriptL3Deep.push(`if (_isArray) {`);
const scriptL4IsArray: any[] = [];
scriptL3Deep.push(scriptL4IsArray);
scriptL3Deep.push('}');

let scriptL5CloneArrays = scriptL4IsArray;

if (typeof options?.moveArrays === 'function') {
scriptL4IsArray.push(
`if (!moveArraysCallback(key, subPath, target, source)) {`,
);
scriptL5CloneArrays = [];
scriptL4IsArray.push(scriptL5CloneArrays);
scriptL4IsArray.push('}');
}
_merge(trgVal, srcVal, subPath, options);
continue;
}\n`;
scriptL5CloneArrays.push(`
descriptor.value = arrayClone(srcVal, _merge, subPath);
Object.defineProperty(target, key, descriptor);
continue;
`);
}
}

/** ************* finalize *****************/
script += `
descriptor.value = srcVal;
Object.defineProperty(target, key, descriptor);
}
return target;
`;
scriptL1For.push(`
descriptor.value = srcVal;
Object.defineProperty(target, key, descriptor);`);
scriptL0.push('return target;');

const script = _flattenText(scriptL0);
// eslint-disable-next-line @typescript-eslint/no-implied-eval,prefer-const
return Function(...args, script);
}
Expand All @@ -308,3 +313,18 @@ function arrayClone(arr: any[], _merge: Function, curPath: string): any[] {
return x;
});
}

function _flattenText(arr: any[], level = 0): string {
const indent = ' '.repeat(level);
return arr
.map(v => {
if (Array.isArray(v)) return _flattenText(v, level + 1);
return (
indent +
String(v)
.trim()
.replace(/\n/g, '\n' + indent)
);
})
.join('\n');
}
8 changes: 4 additions & 4 deletions test/merge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,10 @@ describe('merge', () => {
expect(o.foo).not.toEqual(b.foo);
});

it('should not clone array values if keepArrays set true', () => {
it('should not clone array values if moveArrays set true', () => {
const a: any = { foo: [1, 2] };
const b: any = { foo: [2, 3, { a: 1 }] };
const o: any = merge(a, b, { deep: true, keepArrays: true });
const o: any = merge(a, b, { deep: true, moveArrays: true });
expect(o).toStrictEqual(b);
expect(o.foo).toStrictEqual(b.foo);
b.foo[2].a = 2;
Expand All @@ -348,12 +348,12 @@ describe('merge', () => {
expect(o.foo).toEqual(b.foo);
});

it('should not clone array values if keepArrays set a callback', () => {
it('should not clone array values if moveArrays callback return true', () => {
const a: any = { foo: [1, 2] };
const b: any = { foo: [2, 3, { a: 1 }] };
const o: any = merge(a, b, {
deep: true,
keepArrays: k => k === 'foo',
moveArrays: k => k === 'foo',
});
expect(o).toStrictEqual(b);
expect(o.foo).toStrictEqual(b.foo);
Expand Down

0 comments on commit b0eeb4f

Please sign in to comment.