-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathindex.ts
More file actions
117 lines (103 loc) · 2.68 KB
/
index.ts
File metadata and controls
117 lines (103 loc) · 2.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
export interface DifferenceCreate {
type: "CREATE";
path: (string | number)[];
value: any;
}
export interface DifferenceRemove {
type: "REMOVE";
path: (string | number)[];
oldValue: any;
}
export interface DifferenceChange {
type: "CHANGE";
path: (string | number)[];
value: any;
oldValue: any;
}
export type Difference = DifferenceCreate | DifferenceRemove | DifferenceChange;
interface Options {
cyclesFix: boolean;
}
const richTypes = ['Date', 'RegExp', 'String', 'Number'];
const temporalTypes = Object.getOwnPropertyNames(globalThis.Temporal||{});
export default function diff(
obj: Record<string, any> | any[],
newObj: Record<string, any> | any[],
options: Partial<Options> = { cyclesFix: true },
_stack: Record<string, any>[] = [],
): Difference[] {
let diffs: Difference[] = [];
const isObjArray = Array.isArray(obj);
for (const key in obj) {
const value = obj[key];
const path = isObjArray ? +key : key;
if (!(key in newObj)) {
diffs.push({
type: "REMOVE",
path: [path],
oldValue: value,
});
continue;
}
const newValue = newObj[key];
const areCompatibleObjects =
typeof value === "object" &&
typeof newValue === "object" &&
Array.isArray(value) === Array.isArray(newValue);
// Only compute for non-null objects — primitives and null skip this
// entirely since Object.getPrototypeOf is expensive to call on every key
const objConstructor =
areCompatibleObjects && value
? Object.getPrototypeOf(value)?.constructor?.name
: undefined;
if (
value &&
newValue &&
areCompatibleObjects &&
!richTypes.includes(objConstructor) &&
!temporalTypes.includes(objConstructor) &&
(!options.cyclesFix || !_stack.includes(value))
) {
// Recurse into objects and arrays
if (options.cyclesFix) {
_stack.push(value);
}
const subDiffs = diff(value, newValue, options, _stack);
if (options.cyclesFix) {
_stack.pop();
}
for (const subDiff of subDiffs) {
subDiff.path.unshift(path);
diffs.push(subDiff);
}
} else if (
!(
Object.is(value, newValue) /* treat nulls as equivalent */ ||
(areCompatibleObjects &&
temporalTypes.includes(objConstructor) &&
String(value) === String(newValue)) ||
(areCompatibleObjects &&
richTypes.includes(objConstructor) &&
(isNaN(value) ? value + "" === newValue + "" : +value === +newValue))
)
) {
diffs.push({
path: [path],
type: "CHANGE",
value: newValue,
oldValue: value,
});
}
}
const isNewObjArray = Array.isArray(newObj);
for (const key in newObj) {
if (!(key in obj)) {
diffs.push({
type: "CREATE",
path: [isNewObjArray ? +key : key],
value: newObj[key],
});
}
}
return diffs;
}