Skip to content

Commit 8314f02

Browse files
bteasindresorhus
andauthored
Add ignore option (#21)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 74b6172 commit 8314f02

File tree

4 files changed

+402
-16
lines changed

4 files changed

+402
-16
lines changed

index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
2+
export type IgnoreContext = {
3+
/** Key of the current object or array item. */
4+
key: string | number | undefined;
5+
/** Value of the current object or array item. */
6+
value: unknown;
7+
/** Path to the current object or array item. */
8+
path: Array<string | number>;
9+
/** Current depth in the object or array. */
10+
depth: number;
11+
};
12+
113
export type Options = {
214
/**
315
Recursively sort keys, including keys of objects inside arrays.
@@ -10,6 +22,11 @@ export type Options = {
1022
[Compare function.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
1123
*/
1224
readonly compare?: (left: string, right: string) => number;
25+
26+
/**
27+
Decide whether to skip sorting of certain options based on the result returned by ignore function. This only applies when `deep` is set to `true`.
28+
*/
29+
readonly ignore?: (context: IgnoreContext) => boolean;
1330
};
1431

1532
/**

index.js

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ export default function sortKeys(object, options = {}) {
55
throw new TypeError('Expected a plain object or array');
66
}
77

8-
const {deep, compare} = options;
8+
const {deep, compare, ignore} = options;
99
const cache = new WeakMap();
1010

11-
const deepSortArray = array => {
11+
const deepSortArray = (array, path = []) => {
1212
const resultFromCache = cache.get(array);
1313
if (resultFromCache !== undefined) {
1414
return resultFromCache;
@@ -17,44 +17,94 @@ export default function sortKeys(object, options = {}) {
1717
const result = [];
1818
cache.set(array, result);
1919

20-
result.push(...array.map(item => {
21-
if (Array.isArray(item)) {
22-
return deepSortArray(item);
20+
result.push(...array.map((item, index) => {
21+
path.push(index);
22+
try {
23+
if (deep && ignore && ignore(
24+
{
25+
key: index,
26+
value: item,
27+
path: [...path],
28+
depth: path.length,
29+
})) {
30+
return item;
31+
}
32+
33+
if (Array.isArray(item)) {
34+
return deepSortArray(item, path);
35+
}
36+
37+
if (isPlainObject(item)) {
38+
return _sortKeys(item, path);
39+
}
40+
41+
return item;
42+
} finally {
43+
path.pop();
2344
}
24-
25-
if (isPlainObject(item)) {
26-
return _sortKeys(item);
27-
}
28-
29-
return item;
3045
}));
3146

3247
return result;
3348
};
3449

35-
const _sortKeys = object => {
50+
const _sortKeys = (object, path = []) => {
3651
const resultFromCache = cache.get(object);
3752
if (resultFromCache !== undefined) {
3853
return resultFromCache;
3954
}
4055

4156
const result = {};
42-
const keys = Object.keys(object).sort(compare);
57+
const originalKeys = Object.keys(object);
58+
const keys = deep && ignore && ignore({
59+
key: undefined,
60+
value: object,
61+
path: [...path],
62+
depth: path.length + 1,
63+
}) ? originalKeys : originalKeys.sort(compare);
4364

4465
cache.set(object, result);
4566

4667
for (const key of keys) {
4768
const value = object[key];
4869
let newValue;
70+
path.push(key);
71+
72+
if (deep && ignore && ignore(
73+
{
74+
key,
75+
value,
76+
path: [...path],
77+
depth: path.length,
78+
})) {
79+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
80+
if (descriptor && ('get' in descriptor || 'set' in descriptor)) {
81+
Object.defineProperty(result, key, descriptor);
82+
} else {
83+
Object.defineProperty(result, key, {
84+
...descriptor,
85+
value,
86+
});
87+
}
88+
89+
path.pop();
90+
continue;
91+
}
4992

5093
if (deep && Array.isArray(value)) {
51-
newValue = deepSortArray(value);
94+
newValue = deepSortArray(value, path);
5295
} else {
53-
newValue = deep && isPlainObject(value) ? _sortKeys(value) : value;
96+
newValue = deep && isPlainObject(value) ? _sortKeys(value, path) : value;
97+
}
98+
99+
path.pop();
100+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
101+
if (descriptor && ('get' in descriptor || 'set' in descriptor)) {
102+
Object.defineProperty(result, key, descriptor);
103+
continue;
54104
}
55105

56106
Object.defineProperty(result, key, {
57-
...Object.getOwnPropertyDescriptor(object, key),
107+
...descriptor,
58108
value: newValue,
59109
});
60110
}

readme.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,54 @@ Recursively sort keys, including keys of objects inside arrays.
5959
Type: `Function`
6060

6161
[Compare function.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)
62+
63+
##### ignore(context)
64+
65+
Type: `Function`
66+
67+
Decide whether to skip sorting of certain keys or branches based on the result returned by ignore function. This only applies when `deep` is set to `true`.
68+
69+
###### context
70+
71+
Type: `object`
72+
73+
_key_
74+
75+
Type: `string | number | undefined`
76+
77+
Key of the current object or array item. When `undefined`, the callback decides whether to sort the current object’s own keys (object-level decision).
78+
79+
Examples:
80+
```ts
81+
// Preserve top-level object key order, but still deep-sort children
82+
sortKeys(input, {
83+
deep: true,
84+
ignore: ({key, path}) => key === undefined && path.length === 0
85+
});
86+
87+
// Skip sorting at depth 3 only (object-level)
88+
sortKeys(input, {
89+
deep: true,
90+
ignore: ({key, depth}) => key === undefined && depth === 3
91+
});
92+
```
93+
94+
_value_
95+
96+
Type: `unknown`
97+
98+
Value of the current object or array item.
99+
100+
_path_
101+
102+
Type: `Array<string | number>`
103+
104+
Path to the current object or array item.
105+
106+
_depth_
107+
108+
Type: `number`
109+
110+
Current depth in the object or array. For per-key callbacks, `depth === path.length` (after pushing the key/index). For object-level decisions (when `key` is `undefined`), `depth === path.length + 1`.
111+
112+
**Note:** If the object-level callback returns `true`, the current object's own key order is preserved, but children are still deep-sorted unless those are also ignored.

0 commit comments

Comments
 (0)