Remove lodash and async dependencies to reduce bundle size#84
Conversation
Replace lodash and async with native JavaScript equivalents: - Add utility helpers: get, defaults, omitNil, isEmpty, isPlainObject, deepMerge - Replace _.get(), _.defaults(), _.omitBy(), _.isEmpty(), _.isString(), _.isPlainObject(), _.isNil(), _.merge(), _.delay() with native alternatives - Replace asyncjs.forever() with recursive setTimeout loop - Remove eslint-plugin-you-dont-need-lodash-underscore This reduces Lambda bundle size by ~33KB (gzipped). Closes #83 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request removes the lodash and async dependencies to reduce bundle size, replacing them with native JavaScript implementations. While the intent is sound, there are critical issues with the implementation that need to be addressed before merging.
Key Changes:
- Added 6 utility helper functions (
get,defaults,omitNil,isEmpty,isPlainObject,deepMerge) to replace lodash functionality - Replaced all lodash method calls with native equivalents or the new utility helpers
- Replaced
async.forever()with a recursivesetTimeoutloop pattern - Removed dependencies from
package.jsonandpackage-lock.json - Removed
eslint-plugin-you-dont-need-lodash-underscoreand related ESLint configuration
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| index.js | Added utility helpers and replaced all lodash/async usage with native equivalents |
| test/sender.js | Replaced _.delay() with setTimeout() |
| package.json | Removed async, lodash, and eslint plugin dependencies; updated ESLint configuration |
| package-lock.json | Removed dependency entries and transitive dependencies for async, lodash, and eslint plugin |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const deepMerge = (target, source) => { | ||
| const result = { ...target }; | ||
| if (source && typeof source === "object" && !Array.isArray(source)) { | ||
| for (const key of Object.keys(source)) { | ||
| if (isPlainObject(source[key]) && isPlainObject(result[key])) { | ||
| result[key] = deepMerge(result[key], source[key]); | ||
| } else { | ||
| result[key] = source[key]; | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
|
|
There was a problem hiding this comment.
The deepMerge utility function is defined but never used in the codebase. This appears to be dead code that should either be used to replace the Object.assign() calls where _.merge() was previously used, or removed if it's not needed.
| const deepMerge = (target, source) => { | |
| const result = { ...target }; | |
| if (source && typeof source === "object" && !Array.isArray(source)) { | |
| for (const key of Object.keys(source)) { | |
| if (isPlainObject(source[key]) && isPlainObject(result[key])) { | |
| result[key] = deepMerge(result[key], source[key]); | |
| } else { | |
| result[key] = source[key]; | |
| } | |
| } | |
| } | |
| return result; | |
| }; |
| }; | ||
|
|
||
| _.merge(variables.input, turbotData); | ||
| Object.assign(variables.input, turbotData); |
There was a problem hiding this comment.
Replacing _.merge() with Object.assign() changes the merging behavior from deep merge to shallow merge. This is a breaking change that could cause issues if turbotData contains nested objects that should be deeply merged with variables.input. Consider using the deepMerge() utility function defined at the top of the file instead, or verify that shallow merging is the intended behavior for this specific use case.
| Object.assign(variables.input, turbotData); | |
| deepMerge(variables.input, turbotData); |
| }; | ||
|
|
||
| _.merge(variables.input, turbotData); | ||
| Object.assign(variables.input, turbotData); |
There was a problem hiding this comment.
Replacing _.merge() with Object.assign() changes the merging behavior from deep merge to shallow merge. This is a breaking change that could cause issues if turbotData contains nested objects that should be deeply merged with variables.input. Consider using the deepMerge() utility function defined at the top of the file instead, or verify that shallow merging is the intended behavior for this specific use case.
| Object.assign(variables.input, turbotData); | |
| if (turbotData) { | |
| variables.input = deepMerge(variables.input, turbotData); | |
| } |
| }; | ||
|
|
||
| _.merge(variables.input, turbotData); | ||
| Object.assign(variables.input, turbotData); |
There was a problem hiding this comment.
Replacing _.merge() with Object.assign() changes the merging behavior from deep merge to shallow merge. This is a breaking change that could cause issues if turbotData contains nested objects that should be deeply merged with variables.input. Consider using the deepMerge() utility function defined at the top of the file instead, or verify that shallow merging is the intended behavior for this specific use case.
| Object.assign(variables.input, turbotData); | |
| deepMerge(variables.input, turbotData); |
| }; | ||
|
|
||
| _.merge(variables.input, turbotData); | ||
| Object.assign(variables.input, turbotData); |
There was a problem hiding this comment.
Replacing _.merge() with Object.assign() changes the merging behavior from deep merge to shallow merge. This is a breaking change that could cause issues if turbotData contains nested objects that should be deeply merged with variables.input. Consider using the deepMerge() utility function defined at the top of the file instead, or verify that shallow merging is the intended behavior for this specific use case.
| Object.assign(variables.input, turbotData); | |
| deepMerge(variables.input, turbotData); |
| const { v4: uuidv4 } = require("uuid"); | ||
|
|
||
| // Utility helpers (replacing lodash) | ||
| const isPlainObject = (val) => typeof val === "object" && val !== null && val.constructor === Object; |
There was a problem hiding this comment.
The isPlainObject implementation may not handle edge cases correctly. Objects created with Object.create(null) don't have a constructor property and would throw an error when accessing val.constructor. Consider adding a check: val.constructor === Object || Object.getPrototypeOf(val) === null.
| const isPlainObject = (val) => typeof val === "object" && val !== null && val.constructor === Object; | |
| const isPlainObject = (val) => | |
| typeof val === "object" && | |
| val !== null && | |
| (val.constructor === Object || Object.getPrototypeOf(val) === null); |
|
I've asked Claude to check const _ = require("lodash");
// Helper function to check if value is plain object
const isPlainObject = (obj) => {
return Object.prototype.toString.call(obj) === "[object Object]";
};
// Custom deepMerge implementation (from the user)
const deepMerge = (target, source) => {
const result = { ...target };
if (source && typeof source === "object" && !Array.isArray(source)) {
for (const key of Object.keys(source)) {
if (isPlainObject(source[key]) && isPlainObject(result[key])) {
result[key] = deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
};
// Test cases
console.log("=" .repeat(80));
console.log("MERGE COMPARISON TESTS");
console.log("=" .repeat(80));
// Test 1: Simple nested objects
console.log("\n1. NESTED OBJECTS TEST");
console.log("-".repeat(80));
const target1 = {
a: 1,
b: {
c: 2,
d: 3,
},
};
const source1 = {
b: {
d: 4,
e: 5,
},
f: 6,
};
console.log("Target:", JSON.stringify(target1, null, 2));
console.log("Source:", JSON.stringify(source1, null, 2));
const assignResult1 = Object.assign({}, target1, source1);
const lodashResult1 = _.merge({}, target1, source1);
const deepMergeResult1 = deepMerge(target1, source1);
console.log("\nObject.assign result:", JSON.stringify(assignResult1, null, 2));
console.log("Expected: b.c is LOST (shallow copy replaces entire b object)");
console.log("\nlodash.merge result:", JSON.stringify(lodashResult1, null, 2));
console.log("Expected: b.c is PRESERVED (deep merge)");
console.log("\nCustom deepMerge result:", JSON.stringify(deepMergeResult1, null, 2));
console.log("Expected: b.c is PRESERVED (deep merge)");
// Test 2: Array handling
console.log("\n\n2. ARRAY HANDLING TEST");
console.log("-".repeat(80));
const target2 = {
items: [1, 2, 3],
nested: {
arr: ["a", "b"],
},
};
const source2 = {
items: [4, 5],
nested: {
arr: ["c"],
},
};
console.log("Target:", JSON.stringify(target2, null, 2));
console.log("Source:", JSON.stringify(source2, null, 2));
const assignResult2 = Object.assign({}, target2, source2);
const lodashResult2 = _.merge({}, target2, source2);
const deepMergeResult2 = deepMerge(target2, source2);
console.log("\nObject.assign result:", JSON.stringify(assignResult2, null, 2));
console.log("Expected: items = [4, 5] (array replaced)");
console.log("\nlodash.merge result:", JSON.stringify(lodashResult2, null, 2));
console.log("Expected: items = [4, 5, 3] (arrays merged by index!)");
console.log("\nCustom deepMerge result:", JSON.stringify(deepMergeResult2, null, 2));
console.log("Expected: items = [4, 5] (array replaced)");
// Test 3: Multiple levels deep
console.log("\n\n3. DEEP NESTING TEST");
console.log("-".repeat(80));
const target3 = {
level1: {
level2: {
level3: {
value: "original",
keep: "this",
},
},
},
};
const source3 = {
level1: {
level2: {
level3: {
value: "updated",
},
},
},
};
console.log("Target:", JSON.stringify(target3, null, 2));
console.log("Source:", JSON.stringify(source3, null, 2));
const assignResult3 = Object.assign({}, target3, source3);
const lodashResult3 = _.merge({}, target3, source3);
const deepMergeResult3 = deepMerge(target3, source3);
console.log("\nObject.assign result:", JSON.stringify(assignResult3, null, 2));
console.log("Expected: level1.level2.level3.keep is LOST");
console.log("\nlodash.merge result:", JSON.stringify(lodashResult3, null, 2));
console.log("Expected: level1.level2.level3.keep is PRESERVED");
console.log("\nCustom deepMerge result:", JSON.stringify(deepMergeResult3, null, 2));
console.log("Expected: level1.level2.level3.keep is PRESERVED");
// Test 4: Mutation test
console.log("\n\n4. MUTATION TEST (Does it modify original objects?)");
console.log("-".repeat(80));
const target4 = { a: 1, b: { c: 2 } };
const source4 = { b: { d: 3 } };
console.log("Before merge:");
console.log("Target:", JSON.stringify(target4, null, 2));
console.log("Source:", JSON.stringify(source4, null, 2));
// Test Object.assign
const target4a = JSON.parse(JSON.stringify(target4)); // Deep clone for test
const source4a = JSON.parse(JSON.stringify(source4));
Object.assign({}, target4a, source4a);
console.log("\nAfter Object.assign:");
console.log("Target modified?", JSON.stringify(target4a) !== JSON.stringify(target4));
console.log("Source modified?", JSON.stringify(source4a) !== JSON.stringify(source4));
// Test lodash.merge (NOTE: _.merge MUTATES the first argument!)
const target4b = JSON.parse(JSON.stringify(target4));
const source4b = JSON.parse(JSON.stringify(source4));
_.merge({}, target4b, source4b); // Using {} as first arg prevents mutation
console.log("\nAfter lodash.merge (with {} as first arg):");
console.log("Target modified?", JSON.stringify(target4b) !== JSON.stringify(target4));
console.log("Source modified?", JSON.stringify(source4b) !== JSON.stringify(source4));
// Test lodash.merge WITHOUT empty object first
const target4c = JSON.parse(JSON.stringify(target4));
const source4c = JSON.parse(JSON.stringify(source4));
_.merge(target4c, source4c); // MUTATES target4c
console.log("\nAfter lodash.merge (WITHOUT {} as first arg):");
console.log("Target modified?", JSON.stringify(target4c) !== JSON.stringify(target4), "- TARGET WAS MUTATED!");
// Test custom deepMerge
const target4d = JSON.parse(JSON.stringify(target4));
const source4d = JSON.parse(JSON.stringify(source4));
deepMerge(target4d, source4d);
console.log("\nAfter custom deepMerge:");
console.log("Target modified?", JSON.stringify(target4d) !== JSON.stringify(target4));
console.log("Source modified?", JSON.stringify(source4d) !== JSON.stringify(source4));
// Test 5: Undefined and null handling
console.log("\n\n5. UNDEFINED AND NULL HANDLING");
console.log("-".repeat(80));
const target5 = { a: 1, b: 2, c: 3 };
const source5 = { b: undefined, c: null, d: 4 };
console.log("Target:", JSON.stringify(target5, null, 2));
console.log("Source:", JSON.stringify(source5, null, 2));
const assignResult5 = Object.assign({}, target5, source5);
const lodashResult5 = _.merge({}, target5, source5);
const deepMergeResult5 = deepMerge(target5, source5);
console.log("\nObject.assign result:", JSON.stringify(assignResult5, null, 2));
console.log("\nlodash.merge result:", JSON.stringify(lodashResult5, null, 2));
console.log("Note: lodash.merge SKIPS undefined values!");
console.log("\nCustom deepMerge result:", JSON.stringify(deepMergeResult5, null, 2));
// Test 6: Performance comparison
console.log("\n\n6. PERFORMANCE TEST");
console.log("-".repeat(80));
const iterations = 100000;
// Create test objects
const perfTarget = {
a: 1,
b: { c: 2, d: { e: 3, f: 4 } },
g: [1, 2, 3],
h: "string",
};
const perfSource = {
b: { d: { f: 5, g: 6 } },
g: [4, 5],
i: "new",
};
// Test Object.assign
console.time("Object.assign");
for (let i = 0; i < iterations; i++) {
Object.assign({}, perfTarget, perfSource);
}
console.timeEnd("Object.assign");
// Test lodash.merge
console.time("lodash.merge");
for (let i = 0; i < iterations; i++) {
_.merge({}, perfTarget, perfSource);
}
console.timeEnd("lodash.merge");
// Test custom deepMerge
console.time("custom deepMerge");
for (let i = 0; i < iterations; i++) {
deepMerge(perfTarget, perfSource);
}
console.timeEnd("custom deepMerge");Summary: |
|
I think given that Object.assign is shallow copy, custom deepMerge function is better. However from the above test, there are 2 risks:
The Array handling could be a problem. This is the result of the Array handling test between the two: |
Summary
lodash(~25KB gzipped) andasync(~8KB gzipped) dependencieseslint-plugin-you-dont-need-lodash-underscoredev dependencyChanges
index.js:get,defaults,omitNil,isEmpty,isPlainObject,deepMergeasyncjs.forever()with recursivesetTimeoutlooptest/sender.jsto usesetTimeoutinstead of_.delay()Test plan
Closes #83
🤖 Generated with Claude Code