@sandboxed/diff 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +127 -0
- package/docs/config.md +165 -0
- package/docs/utils.md +76 -0
- package/lib/index.cjs +687 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +87 -0
- package/lib/index.js +660 -0
- package/lib/index.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2025 Víctor Cruz
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @sandboxed/diff
|
|
2
|
+
|
|
3
|
+
A **zero dependency, high-performance, security-conscious** JavaScript diffing library for comparing complex data structures with ease.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ⚡️ **Zero dependencies** – lightweight and no external libraries required
|
|
8
|
+
- 📝 Detects **additions, deletions, and modifications**
|
|
9
|
+
- 💡 Supports **Primitives, Objects, Arrays, Maps, and Sets**
|
|
10
|
+
- 🔄 **Handles circular references** safely
|
|
11
|
+
- 🛠️ **Highly configurable** to fit different use cases
|
|
12
|
+
- 🚨 **Built with security in mind** to prevent prototype pollution and other risks
|
|
13
|
+
- 💻 Works in both **Node.js and browser environments**
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm install @sandboxed/diff
|
|
19
|
+
|
|
20
|
+
yarn add @sandboxed/diff
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Supports `esm` and `cjs`
|
|
24
|
+
|
|
25
|
+
Works with both ESM (`import`) and CJS (`require`). Use the syntax that matches your environment:
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// ESM
|
|
29
|
+
import diff, { ChangeType } from '@sandboxed/diff';
|
|
30
|
+
|
|
31
|
+
// CJS option 1
|
|
32
|
+
const diff = require('@sandboxed/diff').default;
|
|
33
|
+
const { ChangeType } = require('@sandboxed/diff');
|
|
34
|
+
|
|
35
|
+
// CJS option 2
|
|
36
|
+
const { default: diff, ChangeType } = require('@sandboxed/diff');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
#### `diff(lhs: any, rhs: any, config?: DiffConfig): Diff`
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import diff, { ChangeType } from '@sandboxed/diff';
|
|
45
|
+
|
|
46
|
+
const a = { name: "Alice", age: 25 };
|
|
47
|
+
const b = { name: "Alice", age: 26, city: "New York" };
|
|
48
|
+
|
|
49
|
+
const result = diff(a, b);
|
|
50
|
+
|
|
51
|
+
console.log(result);
|
|
52
|
+
console.log(result.toDiffString());
|
|
53
|
+
console.log(result.equal); // false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Output**:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
[
|
|
60
|
+
{ type: 'noop', str: '{', depth: 0, path: [] },
|
|
61
|
+
{
|
|
62
|
+
type: 'noop',
|
|
63
|
+
str: '"name": "Alice",',
|
|
64
|
+
depth: 1,
|
|
65
|
+
path: [ 'name', { deleted: false, value: 'Alice' } ]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'remove',
|
|
69
|
+
str: '"age": 25,',
|
|
70
|
+
depth: 1,
|
|
71
|
+
path: [ 'age', { deleted: true, value: 25 } ]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'update',
|
|
75
|
+
str: '"age": 26,',
|
|
76
|
+
depth: 1,
|
|
77
|
+
path: [ 'age', { deleted: false, value: 26 } ]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'add',
|
|
81
|
+
str: '"city": "New York",',
|
|
82
|
+
depth: 1,
|
|
83
|
+
path: [ 'city', { deleted: false, value: 'New York' } ]
|
|
84
|
+
},
|
|
85
|
+
{ type: 'noop', str: '}', depth: 0, path: [] }
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
// ---
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
"name": "Alice",
|
|
92
|
+
- "age": 25,
|
|
93
|
+
! "age": 26,
|
|
94
|
+
+ "city": "New York",
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Config
|
|
99
|
+
|
|
100
|
+
| option | Description |
|
|
101
|
+
|-|-|
|
|
102
|
+
|[config.include](/docs/config.md#include-changetype--changetype)| Include only these change types from the diff result. Can be combined with `exclude`. |
|
|
103
|
+
|[config.exclude](/docs/config.md#exclude-changetype--changetype)| Excludes the change types from the diff result. Can be combined with `include`. |
|
|
104
|
+
|[config.strict](/docs/config.md#strict-boolean)| Performs loose type check if disabled. |
|
|
105
|
+
|[config.showUpdatedOnly](/docs/config.md#showupdatedonly-boolean)| `@sandboxed/diff` creates a `ChangeType.REMOVE` entry for every `ChangeType.UPDATE`. This flags prevents this behavior. |
|
|
106
|
+
|[config.pathHints](/docs/config.md#pathhints-pathints)| Hashmap of `map` and `set` path hints. These strings will be used in the `path` array to provide a hit about the object's type. |
|
|
107
|
+
|[config.redactKeys](/docs/config.md#redactkeys-arraystring)| List of keys that should be redacted from the output. Works with `string` based keys and serialized `Symbol`. |
|
|
108
|
+
|[config.maxDepth](/docs/config.md#maxdepth-number)| Max depth that the diffing function can traverse. |
|
|
109
|
+
|[config.maxKeys](/docs/config.md#maxkeys-number)| Max keys the diffing function can traverse. |
|
|
110
|
+
|[config.timeout](/docs/config.md#timeout-number)| Milliseconds before throwing a timeout error. |
|
|
111
|
+
|
|
112
|
+
## Utils
|
|
113
|
+
|
|
114
|
+
| util | Description |
|
|
115
|
+
|-|-|
|
|
116
|
+
|[toDiffString](/docs/utils.md#diff-string-output)| Generates the diff string representation of the diff result. |
|
|
117
|
+
|[equal](/docs/utils.md#equality-detection)| Determines whether the inputs are structurally equal based on the diff result. |
|
|
118
|
+
|
|
119
|
+
## Motivation
|
|
120
|
+
|
|
121
|
+
Many diffing libraries are optimized for either structured output or human-readable text, but rarely both. `@sandboxed/diff` is designed to provide a structured diff result along with a utility to generate a string representation, making it easy to use in both programmatic logic and UI rendering.
|
|
122
|
+
|
|
123
|
+
Trade-off: It may be **slower than other libraries**, but if you prioritize structured diffs with a built-in string representation, `@sandboxed/diff` is a great fit.
|
|
124
|
+
|
|
125
|
+
## LICENSE
|
|
126
|
+
|
|
127
|
+
[MIT](LICENSE)
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
|
|
2
|
+
## Config
|
|
3
|
+
|
|
4
|
+
#### `.include: ChangeType | ChangeType[]`
|
|
5
|
+
|
|
6
|
+
|||
|
|
7
|
+
|-|-|
|
|
8
|
+
| **Description** | Include only these change types from the diff result. Can be combined with `exclude`. |
|
|
9
|
+
| **Default** | `[ChangeType.NOOP, ChangeType.ADD, ChangeType.UPDATE, ChangeType.REMOVE]` |
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
diff(a, b, { include: [ChangeType.ADD] }); // only additions
|
|
13
|
+
|
|
14
|
+
diff(a, b, {
|
|
15
|
+
include: [ChangeType.ADD, ChangeType.NOOP],
|
|
16
|
+
}); // only additions + unchanged data
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
#### `.exclude: ChangeType | ChangeType[]`
|
|
22
|
+
|||
|
|
23
|
+
|-|-|
|
|
24
|
+
| **Description** | Excludes the change types from the diff result. Can be combined with `include`. |
|
|
25
|
+
| **Default** | `[]` |
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
diff(a, b, { exclude: ChangeType.NOOP });
|
|
29
|
+
|
|
30
|
+
diff(a, b, { exclude: [ChangeType.ADD, ChangeType.NOOP] });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
#### `.strict: boolean`
|
|
36
|
+
|
|
37
|
+
|||
|
|
38
|
+
|-|-|
|
|
39
|
+
| **Description** | Performs loose type check if disabled. |
|
|
40
|
+
| **Default** | `true` |
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const a = { foo: 1 };
|
|
44
|
+
const b = { foo: '1' };
|
|
45
|
+
|
|
46
|
+
console.log(diff(a, b).equal); // false
|
|
47
|
+
|
|
48
|
+
console.log(diff(a, b, { strict: false }).equal); // true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
#### `.showUpdatedOnly: boolean`
|
|
54
|
+
|
|
55
|
+
|||
|
|
56
|
+
|-|-|
|
|
57
|
+
| **Description** | `@sandboxed/diff` creates a `ChangeType.REMOVE` entry for every `ChangeType.UPDATE`. This flags prevents this behavior. |
|
|
58
|
+
| **Default** | `false` |
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
const a = { foo: 'baz' };
|
|
62
|
+
const b = { foo: 'bar' };
|
|
63
|
+
|
|
64
|
+
console.log(diff(a, b, { showUpdatedOnly: true }));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Output**:
|
|
68
|
+
```javascript
|
|
69
|
+
[
|
|
70
|
+
{ type: 'noop', str: '{', depth: 0, path: [] },
|
|
71
|
+
{
|
|
72
|
+
type: 'update',
|
|
73
|
+
str: '"foo": "bar",',
|
|
74
|
+
depth: 1,
|
|
75
|
+
path: [ 'foo', { deleted: false, value: 'bar' } ]
|
|
76
|
+
},
|
|
77
|
+
{ type: 'noop', str: '}', depth: 0, path: [] }
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
#### `.pathHints: PatHints`
|
|
84
|
+
|
|
85
|
+
|||
|
|
86
|
+
|-|-|
|
|
87
|
+
| **Description** | Hashmap of `map` and `set` path hints. These strings will be used in the `path` array to provide a hit about the object's type. |
|
|
88
|
+
| **Default** | `{ map: '__MAP__', set: '__SET__' }` |
|
|
89
|
+
|
|
90
|
+
⚠️ Warning: **Complex keys are not recursively diffed**, they are treated as references only.
|
|
91
|
+
**Assume that any string entry in the path array comes from plain objects, and numeric entries come from arrays**. Without these hints, tracking back to the origin can be difficult, though can be disabled if not needed.
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const a = new Map([['foo', 'baz']]);
|
|
95
|
+
const b = new Map([['foo', 'bar']]);
|
|
96
|
+
|
|
97
|
+
const result = diff(a, b, { showUpdatedOnly: true });
|
|
98
|
+
|
|
99
|
+
// "foo: bar" update
|
|
100
|
+
console.log(result[1].path); // ['__MAP__', 'foo', { deleted: false, value: 'bar' }]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
#### `.redactKeys: Array<string>`
|
|
106
|
+
|
|
107
|
+
|||
|
|
108
|
+
|-|-|
|
|
109
|
+
| **Description** | List of keys that should be redacted from the output. Works with `string` based keys and serialized `Symbol`.|
|
|
110
|
+
|**Default** | `[ 'password', 'secret', 'token', 'Symbol(password)', 'Symbol (secret)', 'Symbol(token)' ]` |
|
|
111
|
+
|
|
112
|
+
⚠️ Warning: Only the result `str` is redacted, the `path` array still contains the reference to the actual values. Be careful when using this for logging.
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const a = { password: 'pwd' };
|
|
116
|
+
const b = { password: 'secret' };
|
|
117
|
+
|
|
118
|
+
console.log(diff(a, b, { showUpdatedOnly: true }));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Output**:
|
|
122
|
+
```javascript
|
|
123
|
+
[
|
|
124
|
+
{ type: 'noop', str: '{', depth: 0, path: [] },
|
|
125
|
+
{
|
|
126
|
+
type: 'update',
|
|
127
|
+
str: '"password": "*****",',
|
|
128
|
+
depth: 1,
|
|
129
|
+
path: [ 'password', { deleted: false, value: 'secret' } ]
|
|
130
|
+
},
|
|
131
|
+
{ type: 'noop', str: '}', depth: 0, path: [] }
|
|
132
|
+
]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
#### `.maxDepth: number`
|
|
138
|
+
|
|
139
|
+
|||
|
|
140
|
+
|-|-|
|
|
141
|
+
| **Description** | Max depth that the diffing function can traverse. Throws when reaching the max. |
|
|
142
|
+
| **Default** | `50` |
|
|
143
|
+
| **Throws** | `Max depth exceeded!` |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
#### `.maxKeys: number`
|
|
148
|
+
|
|
149
|
+
|||
|
|
150
|
+
|-|-|
|
|
151
|
+
| **Description** | Max keys the diffing function can traverse. Throws when reaching the max. |
|
|
152
|
+
|**Default** | `50` |
|
|
153
|
+
|**Throws** | `Object is too big to continue! Aborting.` |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
#### `.timeout: number`
|
|
158
|
+
|
|
159
|
+
|||
|
|
160
|
+
|-|-|
|
|
161
|
+
| **Description** | Milliseconds before throwing a timeout error. |
|
|
162
|
+
|**Default** | `1000` |
|
|
163
|
+
|**Throws** | `Diff took too much time! Aborting.` |
|
|
164
|
+
|
|
165
|
+
⚠️ Warning: The diffing function does not check for object size in memory. The process can still hang if the system is unable to handle the object in memory.
|
package/docs/utils.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
## Utils
|
|
2
|
+
|
|
3
|
+
### Diff string output
|
|
4
|
+
|
|
5
|
+
**`.toDiffString(config?: DiffStringConfig): string`**
|
|
6
|
+
|
|
7
|
+
Highly configurable util that generates the diff string representation of the diff result:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import diff from '@sandboxed/diff';
|
|
11
|
+
|
|
12
|
+
const a = { name: "Alice", age: 25 };
|
|
13
|
+
const b = { name: "Alice", age: 26, city: "New York" };
|
|
14
|
+
|
|
15
|
+
console.log(diff(a, b).toDiffString());
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Output**:
|
|
19
|
+
```
|
|
20
|
+
{
|
|
21
|
+
"name": "Alice",
|
|
22
|
+
- "age": 25,
|
|
23
|
+
! "age": 26,
|
|
24
|
+
+ "city": "New York",
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Config options
|
|
29
|
+
|
|
30
|
+
| config | default | Description |
|
|
31
|
+
|------------|----------|-------------|
|
|
32
|
+
| withColors | `true` | Formats the string using AnsiColors. |
|
|
33
|
+
| colors | `object` | Hashmap for coloring each line based on type: `[ChangeType]: (string) => string`. Should be compatible with `chalk`. |
|
|
34
|
+
| symbols | `object` | Hashmap for prefixing each line based on type: `[ChangeType]: string`. |
|
|
35
|
+
| wrapper | `[]` | Array with `string` entries. Wraps the result between the first two strings. |
|
|
36
|
+
| indentSize | `2` | Whitespace after the `config.symbols`. Indentation is done using `space`. |
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Equality detection
|
|
40
|
+
|
|
41
|
+
**`.equal: boolen`**
|
|
42
|
+
|
|
43
|
+
Determines whether the inputs are structurally equal based on the diff result. It ignores any `ChangeType.NOOP` items.
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import diff from '@sandboxed/diff';
|
|
47
|
+
|
|
48
|
+
const a = { name: "Alice", age: 25 };
|
|
49
|
+
const b = { name: "Alice", age: 26, city: "New York" };
|
|
50
|
+
|
|
51
|
+
console.log(diff(a, b).equal); // Output: false
|
|
52
|
+
|
|
53
|
+
// --
|
|
54
|
+
|
|
55
|
+
const c = { name: 'Alice', foo: new Set([1, 2, 'test']) };
|
|
56
|
+
const d = { name: 'Alice', foo: new Set(['test', 2, 1]) };
|
|
57
|
+
|
|
58
|
+
console.log(diff(c, d).equal); // Output: true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### ⚠️ Warning
|
|
62
|
+
|
|
63
|
+
Be aware that `.equal` is affected by the diff result. Should be used with caution when `cofig.include` or `config.exclude` are provided.
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
import diff, { ChangeType } from '@sandboxed/diff';
|
|
67
|
+
|
|
68
|
+
const a = { name: 'Alice', foo: new Set([1, 2, 'test']) };
|
|
69
|
+
const b = { name: 'Alice', bar: new Set(['test', 2, 1]) };
|
|
70
|
+
|
|
71
|
+
console.log(
|
|
72
|
+
diff(a, b, { exclude: [ChangeType.ADD, ChangeType.REMOVE] }).equal
|
|
73
|
+
); // Output: true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Given that the diff result will not detect the changes in **`foo`**(`ChangeType.REMOVE`) or **`bar`** (`ChangeType.ADD`), the diff result will contain only `ChangeType.NOOP`, causing `.equal` to be `true`.
|