@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 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`.