|
| 1 | +/** |
| 2 | +Context information passed to filter functions. |
| 3 | +*/ |
| 4 | +export type SortContext = { |
| 5 | + /** |
| 6 | + The current key being processed. |
| 7 | + */ |
| 8 | + readonly key: string; |
| 9 | + |
| 10 | + /** |
| 11 | + The value associated with the current key |
| 12 | + */ |
| 13 | + readonly value: unknown; |
| 14 | + |
| 15 | + /** |
| 16 | + The full path to this key as an array of path elements (for example, `['user', 'profile', 'name']`). |
| 17 | +
|
| 18 | + Array indices are stringified (for example, `['items', '0', 'title']`). |
1 | 19 |
|
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; |
| 20 | + Examples of generated paths (with depths): |
| 21 | + - ['user'] (depth: 0) |
| 22 | + - ['user', 'profile'] (depth: 1) |
| 23 | + - ['user', 'profile', 'name'] (depth: 2) |
| 24 | + - ['items'] (depth: 0) |
| 25 | + - ['items', '0'] (depth: 1) |
| 26 | + - ['items', '0', 'title'] (depth: 2) |
| 27 | + - ['items', '1'] (depth: 1) |
| 28 | + - ['items', '1', 'title'] (depth: 2) |
| 29 | + */ |
| 30 | + readonly path: readonly string[]; |
| 31 | + |
| 32 | + /** |
| 33 | + The current nesting depth (0 for root level). |
| 34 | + */ |
| 35 | + readonly depth: number; |
11 | 36 | };
|
12 | 37 |
|
13 | 38 | export type Options = {
|
14 | 39 | /**
|
15 |
| - Recursively sort keys, including keys of objects inside arrays. |
| 40 | + Compare function for sorting keys. |
16 | 41 |
|
17 |
| - @default false |
| 42 | + @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort |
| 43 | +
|
| 44 | + If omitted, remaining keys are sorted using the platform's default string sort. |
18 | 45 | */
|
19 |
| - readonly deep?: boolean; |
| 46 | + readonly compare?: (left: string, right: string) => number; |
20 | 47 |
|
21 | 48 | /**
|
22 |
| - [Compare function.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) |
| 49 | + Recursively sort keys, including keys of objects inside arrays. |
| 50 | +
|
| 51 | + @default false |
| 52 | +
|
| 53 | + Only plain objects are sorted; other object types are left as-is. For arrays, deep processing applies to their elements. |
| 54 | +
|
| 55 | + When a boolean: |
| 56 | + - `true`: Deep process all nested objects and arrays. |
| 57 | + - `false`: Only sort keys at the current level. |
| 58 | +
|
| 59 | + When a function, it receives a context object and should return `true` to enable deep processing for that specific key-value pair. |
| 60 | + The context is `SortContext` with `{ key, value, path, depth }`. |
| 61 | +
|
| 62 | + @example |
| 63 | + ``` |
| 64 | + // Fine-grained deep control with context |
| 65 | + sortKeys(data, { |
| 66 | + deep: ({key, value, path, depth}) => { |
| 67 | + // Only deep process up to 2 levels |
| 68 | + if (depth >= 2) { |
| 69 | + return false; |
| 70 | + } |
| 71 | +
|
| 72 | + // Skip deep processing of large arrays for performance |
| 73 | + if (Array.isArray(value) && (value as any[]).length > 100) { |
| 74 | + return false; |
| 75 | + } |
| 76 | +
|
| 77 | + // Skip config objects entirely |
| 78 | + if (path.includes('config')) { |
| 79 | + return false; |
| 80 | + } |
| 81 | +
|
| 82 | + return true; |
| 83 | + } |
| 84 | + }); |
| 85 | + ``` |
23 | 86 | */
|
24 |
| - readonly compare?: (left: string, right: string) => number; |
| 87 | + readonly deep?: boolean | ((context: SortContext) => boolean); |
25 | 88 |
|
26 | 89 | /**
|
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`. |
| 90 | + Keys to ignore during sorting. Ignored keys appear first in their original order, followed by the sorted keys. Remaining keys are sorted by `compare`, or by default string sort if `compare` is not provided. |
| 91 | +
|
| 92 | + @default [] |
| 93 | +
|
| 94 | + Only affects the ordering of object keys; it does not control deep processing, and array indices are not sorted or filtered. |
| 95 | +
|
| 96 | + Can be an array of key names, or a function that receives context and returns true to ignore the key. |
| 97 | +
|
| 98 | + @example |
| 99 | + ``` |
| 100 | + // Ignore by name; ignored keys keep original order and appear first |
| 101 | + sortKeys({c: 0, _private: 1, a: 0, b: 0}, {ignoreKeys: ['_private']}); |
| 102 | + //=> {_private: 1, a: 0, b: 0, c: 0} |
| 103 | +
|
| 104 | + // Ignore by function with multiple conditions |
| 105 | + sortKeys(data, { |
| 106 | + ignoreKeys: ({key, value, path, depth}) => { |
| 107 | + // Ignore private keys at root level |
| 108 | + if (key.startsWith('_') && depth === 0) { |
| 109 | + return true; |
| 110 | + } |
| 111 | +
|
| 112 | + // Ignore metadata keys in user objects |
| 113 | + if (path[0] === 'user' && key === 'metadata') { |
| 114 | + return true; |
| 115 | + } |
| 116 | +
|
| 117 | + // Ignore empty objects |
| 118 | + if (typeof value === 'object' && Object.keys(value as any).length === 0) { |
| 119 | + return true; |
| 120 | + } |
| 121 | +
|
| 122 | + return false; |
| 123 | + } |
| 124 | + }); |
| 125 | + ``` |
28 | 126 | */
|
29 |
| - readonly ignore?: (context: IgnoreContext) => boolean; |
| 127 | + readonly ignoreKeys?: readonly string[] | ((context: SortContext) => boolean); |
30 | 128 | };
|
31 | 129 |
|
32 | 130 | /**
|
33 | 131 | Sort the keys of an object.
|
34 | 132 |
|
| 133 | +@param object - The object or array to sort. |
35 | 134 | @returns A new object with sorted keys.
|
36 | 135 |
|
| 136 | +Property descriptors are preserved, including accessors (get/set); getters are not invoked or deep-processed. Circular references are supported and preserved. |
| 137 | +
|
| 138 | +When it's an object: |
| 139 | +- Only plain objects are deeply processed. |
| 140 | +- Only enumerable own string keys are considered; symbol and non-enumerable properties are ignored. |
| 141 | +
|
| 142 | +When it's an array: |
| 143 | +- Array order is unchanged; holes in sparse arrays are preserved. |
| 144 | +- Elements may be deep-processed if `deep` enables it. |
| 145 | +- Extra enumerable properties on arrays are ignored. |
| 146 | +
|
37 | 147 | @example
|
38 |
| -``` |
39 | 148 | import sortKeys from 'sort-keys';
|
40 | 149 |
|
| 150 | +// Basic usage |
41 | 151 | sortKeys({c: 0, a: 0, b: 0});
|
42 | 152 | //=> {a: 0, b: 0, c: 0}
|
43 | 153 |
|
| 154 | +// Deep sorting of nested objects |
44 | 155 | sortKeys({b: {b: 0, a: 0}, a: 0}, {deep: true});
|
45 | 156 | //=> {a: 0, b: {a: 0, b: 0}}
|
46 | 157 |
|
| 158 | +// Deep sorting of objects inside arrays |
47 | 159 | sortKeys({b: [{b: 0, a: 0}], a: 0}, {deep: true});
|
48 | 160 | //=> {a: 0, b: [{a: 0, b: 0}]}
|
49 | 161 |
|
| 162 | +// Custom key compare (reverse alphabetical) |
50 | 163 | sortKeys({c: 0, a: 0, b: 0}, {
|
51 | 164 | compare: (a, b) => -a.localeCompare(b)
|
52 | 165 | });
|
53 | 166 | //=> {c: 0, b: 0, a: 0}
|
54 | 167 |
|
55 |
| -sortKeys([{b: 0, a:2}], {deep: true}); |
| 168 | +// Deep processing for a top-level array |
| 169 | +sortKeys([{b: 0, a: 2}], {deep: true}); |
56 | 170 | //=> [{a: 2, b: 0}]
|
57 |
| -``` |
58 | 171 | */
|
59 | 172 | export default function sortKeys<T extends Record<string, any>>(
|
60 | 173 | object: T,
|
|
0 commit comments