refactor: replace custom cloneDeep with structuredClone - #10933
refactor: replace custom cloneDeep with structuredClone#10933VishalRaut2106 wants to merge 2 commits into
Conversation
### Description Replaced the custom recursive cloneDeep implementation in src/utils.ts with Node.js native structuredClone(), since the CLI minimum Node.js requirement is now v18+. This removes technical debt and improves cloning performance. Includes a fallback to handle edge-cases where objects have functions (which are not serializable). ### Scenarios Tested Ran Mocha tests against utilities utilizing cloneDeep (e.g. src/requireConfig.spec.ts) to ensure object references are properly broken and clones act independently without triggering DataCloneError. ### Sample Commands N/A
There was a problem hiding this comment.
Code Review
This pull request simplifies the cloneDeep utility in src/utils.ts by replacing the custom deep cloning logic with the native structuredClone API. However, the new implementation introduces a correctness regression where non-serializable objects (such as those containing functions) silently return the original reference upon failure, leading to unexpected mutations. Additionally, removing the fast-path checks for primitives and functions causes a performance regression. It is recommended to restore the fast-path checks and fall back to _.cloneDeep if structuredClone fails.
| export function cloneDeep<T>(obj: T): T { | ||
| if (typeof obj !== "object" || !obj) { | ||
| if (obj === undefined) { | ||
| return obj; | ||
| } | ||
| if (obj instanceof RegExp) { | ||
| return RegExp(obj, obj.flags) as typeof obj; | ||
| } | ||
| if (obj instanceof Date) { | ||
| return new Date(obj) as typeof obj; | ||
| } | ||
| if (Array.isArray(obj)) { | ||
| return cloneArray(obj) as typeof obj; | ||
| } | ||
| if (obj instanceof Map) { | ||
| return new Map(obj.entries()) as typeof obj; | ||
| try { | ||
| return structuredClone(obj); | ||
| } catch (e) { | ||
| return obj; | ||
| } | ||
| return cloneObject(obj as Record<string, unknown>) as typeof obj; | ||
| } |
There was a problem hiding this comment.
The new implementation introduces two significant issues:
- Correctness Regression (High Severity): If
structuredClonethrows an error (e.g., when the object contains a function, symbol, or other non-serializable property), thecatchblock silently returns the original object referenceobj. This is a regression from the originalcloneDeepwhich recursively cloned the object structure and copied functions by reference, ensuring the outer object reference was broken. Returning the original reference means any subsequent mutations to the "cloned" object will unexpectedly mutate the original object, leading to silent and hard-to-debug bugs. - Performance Regression (Medium Severity): The fast-path check for primitives,
null, and functions has been removed. Only checkingobj === undefinedmeans all other primitives (like strings, numbers, booleans,null) and functions will go through thetry/catchblock and callstructuredClone(), which is much slower than a simple type check.
We can resolve both issues by restoring the fast-path check and falling back to _.cloneDeep (which is already imported in this file from lodash) when structuredClone fails.
| export function cloneDeep<T>(obj: T): T { | |
| if (typeof obj !== "object" || !obj) { | |
| if (obj === undefined) { | |
| return obj; | |
| } | |
| if (obj instanceof RegExp) { | |
| return RegExp(obj, obj.flags) as typeof obj; | |
| } | |
| if (obj instanceof Date) { | |
| return new Date(obj) as typeof obj; | |
| } | |
| if (Array.isArray(obj)) { | |
| return cloneArray(obj) as typeof obj; | |
| } | |
| if (obj instanceof Map) { | |
| return new Map(obj.entries()) as typeof obj; | |
| try { | |
| return structuredClone(obj); | |
| } catch (e) { | |
| return obj; | |
| } | |
| return cloneObject(obj as Record<string, unknown>) as typeof obj; | |
| } | |
| export function cloneDeep<T>(obj: T): T { | |
| if (typeof obj !== "object" || !obj) { | |
| return obj; | |
| } | |
| try { | |
| return structuredClone(obj); | |
| } catch (e) { | |
| return _.cloneDeep(obj); | |
| } | |
| } |
References
- Reduce nesting as much as possible: Code should avoid unnecessarily deep nesting or long periods of nesting. Use early returns, continue, and break statements in functions and loops to handle edge cases early and keep main logic flat. (link)
refactor: replace custom cloneDeep with structuredClone
Description
Replaced the custom recursive
cloneDeepimplementation insrc/utils.tswith Node.js' nativestructuredClone(), since the CLI's minimum Node.js requirement is now v18+. This removes technical debt and improves cloning performance. Includes a fallback to handle edge-cases where objects have functions (which aren't serializable).Scenarios Tested
Ran Mocha tests against utilities utilizing
cloneDeep(e.g.src/requireConfig.spec.ts) to ensure object references are properly broken and clones act independently without triggeringDataCloneError.Sample Commands
N/A