sort_package_json/
lib.rs

1use serde_json::{Map, Value};
2
3/// Options for controlling JSON formatting when sorting
4#[derive(Debug, Clone)]
5pub struct SortOptions {
6    /// Whether to pretty-print the output JSON
7    pub pretty: bool,
8    /// Whether to sort the scripts field alphabetically
9    pub sort_scripts: bool,
10}
11
12impl Default for SortOptions {
13    fn default() -> Self {
14        Self { pretty: true, sort_scripts: false }
15    }
16}
17
18/// Sorts a package.json string with custom options
19pub fn sort_package_json_with_options(
20    input: &str,
21    options: &SortOptions,
22) -> Result<String, serde_json::Error> {
23    // Check for UTF-8 BOM and strip it for parsing
24    const BOM: char = '\u{FEFF}';
25    let input_without_bom = input.strip_prefix(BOM).unwrap_or(input);
26    let has_bom = input_without_bom.len() != input.len();
27
28    let value: Value = serde_json::from_str(input_without_bom)?;
29
30    let sorted_value = if let Value::Object(obj) = value {
31        Value::Object(sort_object_keys(obj, options))
32    } else {
33        value
34    };
35
36    let result = if options.pretty {
37        let mut s = serde_json::to_string_pretty(&sorted_value)?;
38        s.push('\n');
39        s
40    } else {
41        serde_json::to_string(&sorted_value)?
42    };
43
44    // Preserve BOM if it was present in the input
45    if has_bom {
46        let mut output = String::with_capacity(BOM.len_utf8() + result.len());
47        output.push(BOM);
48        output.push_str(&result);
49        Ok(output)
50    } else {
51        Ok(result)
52    }
53}
54
55/// Sorts a package.json string with default options (pretty-printed)
56pub fn sort_package_json(input: &str) -> Result<String, serde_json::Error> {
57    sort_package_json_with_options(input, &SortOptions::default())
58}
59
60/// Declares package.json field ordering with transformations.
61///
62/// This macro generates a match statement that handles known package.json fields
63/// in a specific order using explicit indices. It supports optional transformation
64/// expressions for fields that need special processing.
65///
66/// # Usage
67///
68/// ```ignore
69/// declare_field_order!(key, value, known, non_private, private; [
70///     0 => "$schema",
71///     1 => "name",
72///     7 => "categories" => transform_array(&value, sort_array_unique),
73/// ]);
74/// ```
75///
76/// # Parameters
77///
78/// - `key`: The field name identifier
79/// - `value`: The field value identifier
80/// - `known`: The vector to push known fields to
81/// - `non_private`: The vector to push non-private unknown fields to
82/// - `private`: The vector to push private (underscore-prefixed) fields to
83/// - Followed by an array of field declarations in the format:
84///   - `index => "field_name"` for fields without transformation
85///   - `index => "field_name" => transformation_expr` for fields with transformation
86macro_rules! declare_field_order {
87    (
88        $key:ident, $value:ident, $known:ident, $non_private:ident, $private:ident;
89        [
90            $( $idx:literal => $field_name:literal $( => $transform:expr )? ),* $(,)?
91        ]
92    ) => {
93        {
94            // Compile-time validation: ensure indices are literals
95            $( let _ = $idx; )*
96
97            // Generate the match statement
98            match $key.as_str() {
99                $(
100                    $field_name => {
101                        $known.push((
102                            $idx,
103                            $key,
104                            declare_field_order!(@value $value $(, $transform)?)
105                        ));
106                    },
107                )*
108                _ => {
109                    // Unknown field - check if private
110                    if $key.starts_with('_') {
111                        $private.push(($key, $value));
112                    } else {
113                        $non_private.push(($key, $value));
114                    }
115                }
116            }
117        }
118    };
119
120    // Helper: extract value without transformation
121    (@value $value:ident) => { $value };
122
123    // Helper: extract value with transformation
124    (@value $value:ident, $transform:expr) => { $transform };
125}
126
127fn transform_value<F>(value: Value, transform: F) -> Value
128where
129    F: FnOnce(Map<String, Value>) -> Map<String, Value>,
130{
131    match value {
132        Value::Object(o) => Value::Object(transform(o)),
133        _ => value,
134    }
135}
136
137fn transform_array<F>(value: Value, transform: F) -> Value
138where
139    F: FnOnce(Vec<Value>) -> Vec<Value>,
140{
141    match value {
142        Value::Array(arr) => Value::Array(transform(arr)),
143        _ => value,
144    }
145}
146
147fn transform_with_key_order(value: Value, key_order: &[&str]) -> Value {
148    transform_value(value, |o| sort_object_by_key_order(o, key_order))
149}
150
151fn sort_object_alphabetically(obj: Map<String, Value>) -> Map<String, Value> {
152    let mut entries: Vec<(String, Value)> = obj.into_iter().collect();
153    entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
154    entries.into_iter().collect()
155}
156
157fn sort_object_recursive(obj: Map<String, Value>) -> Map<String, Value> {
158    let mut entries: Vec<(String, Value)> = obj.into_iter().collect();
159    entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
160
161    entries
162        .into_iter()
163        .map(|(key, value)| {
164            let transformed_value = match value {
165                Value::Object(nested) => Value::Object(sort_object_recursive(nested)),
166                _ => value,
167            };
168            (key, transformed_value)
169        })
170        .collect()
171}
172
173fn sort_array_unique(mut arr: Vec<Value>) -> Vec<Value> {
174    // Filter non-strings in-place (same behavior as filter_map)
175    arr.retain(|v| v.is_string());
176
177    // Sort in-place by comparing string values (zero allocations)
178    arr.sort_unstable_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
179
180    // Remove consecutive duplicates in-place
181    arr.dedup_by(|a, b| a.as_str() == b.as_str());
182
183    arr
184}
185
186fn sort_paths_naturally(mut arr: Vec<Value>) -> Vec<Value> {
187    // Filter and deduplicate in-place
188    arr.retain(|v| v.is_string());
189    arr.sort_unstable_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
190    arr.dedup_by(|a, b| a.as_str() == b.as_str());
191
192    // Pre-compute depth and lowercase ONCE per string (not on every comparison)
193    // Move Values from arr into tuples (no copying)
194    let mut with_keys: Vec<(usize, String, Value)> = arr
195        .into_iter()
196        .map(|v| {
197            let s = v.as_str().unwrap();
198            let depth = s.matches('/').count();
199            let lowercase = s.to_lowercase();
200            (depth, lowercase, v)
201        })
202        .collect();
203
204    // Sort using pre-computed keys (zero allocations during comparison)
205    with_keys.sort_unstable_by(|(depth_a, lower_a, _), (depth_b, lower_b, _)| {
206        depth_a.cmp(depth_b).then_with(|| lower_a.cmp(lower_b))
207    });
208
209    // Extract Values (move out of tuples, no copying)
210    with_keys.into_iter().map(|(_, _, v)| v).collect()
211}
212
213fn sort_object_by_key_order(mut obj: Map<String, Value>, key_order: &[&str]) -> Map<String, Value> {
214    // Pre-allocate capacity to avoid reallocations
215    let mut result = Map::with_capacity(obj.len());
216
217    // Add keys in specified order
218    for &key in key_order {
219        if let Some(value) = obj.remove(key) {
220            result.insert(key.into(), value);
221        }
222    }
223
224    // Add remaining keys alphabetically
225    let mut remaining: Vec<(String, Value)> = obj.into_iter().collect();
226    remaining.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
227
228    for (key, value) in remaining {
229        result.insert(key, value);
230    }
231
232    result
233}
234
235fn sort_people_object(obj: Map<String, Value>) -> Map<String, Value> {
236    sort_object_by_key_order(obj, &["name", "email", "url"])
237}
238
239fn sort_exports(obj: Map<String, Value>) -> Map<String, Value> {
240    let mut paths = Vec::new();
241    let mut types_conds = Vec::new();
242    let mut other_conds = Vec::new();
243    let mut default_cond = None;
244
245    for (key, value) in obj {
246        if key.starts_with('.') {
247            paths.push((key, value));
248        } else if key == "default" {
249            default_cond = Some((key, value));
250        } else if key == "types" || key.starts_with("types@") {
251            types_conds.push((key, value));
252        } else {
253            other_conds.push((key, value));
254        }
255    }
256
257    let mut result = Map::new();
258
259    // Add in order: paths, types, others, default
260    for (key, value) in paths {
261        let transformed = match value {
262            Value::Object(nested) => Value::Object(sort_exports(nested)),
263            _ => value,
264        };
265        result.insert(key, transformed);
266    }
267
268    for (key, value) in types_conds {
269        let transformed = match value {
270            Value::Object(nested) => Value::Object(sort_exports(nested)),
271            _ => value,
272        };
273        result.insert(key, transformed);
274    }
275
276    for (key, value) in other_conds {
277        let transformed = match value {
278            Value::Object(nested) => Value::Object(sort_exports(nested)),
279            _ => value,
280        };
281        result.insert(key, transformed);
282    }
283
284    if let Some((key, value)) = default_cond {
285        let transformed = match value {
286            Value::Object(nested) => Value::Object(sort_exports(nested)),
287            _ => value,
288        };
289        result.insert(key, transformed);
290    }
291
292    result
293}
294
295fn sort_object_keys(obj: Map<String, Value>, options: &SortOptions) -> Map<String, Value> {
296    // Storage for categorized keys with their values and ordering information
297    let mut known: Vec<(usize, String, Value)> = Vec::new(); // (order_index, key, value)
298    let mut non_private: Vec<(String, Value)> = Vec::new();
299    let mut private: Vec<(String, Value)> = Vec::new();
300
301    // Single pass through all keys using into_iter()
302    for (key, value) in obj {
303        declare_field_order!(key, value, known, non_private, private; [
304            // Core Package Metadata
305            0 => "$schema",
306            1 => "name",
307            2 => "displayName",
308            3 => "version",
309            4 => "stableVersion",
310            5 => "gitHead",
311            6 => "private",
312            7 => "description",
313            8 => "categories" => transform_array(value, sort_array_unique),
314            9 => "keywords" => transform_array(value, sort_array_unique),
315            10 => "homepage",
316            11 => "bugs" => transform_with_key_order(value, &["url", "email"]),
317            // License & People
318            12 => "license",
319            13 => "author" => transform_value(value, sort_people_object),
320            14 => "maintainers",
321            15 => "contributors",
322            // Repository & Funding
323            16 => "repository" => transform_with_key_order(value, &["type", "url"]),
324            17 => "funding" => transform_with_key_order(value, &["type", "url"]),
325            18 => "donate" => transform_with_key_order(value, &["type", "url"]),
326            19 => "sponsor" => transform_with_key_order(value, &["type", "url"]),
327            20 => "qna",
328            21 => "publisher",
329            // Package Content & Distribution
330            22 => "man",
331            23 => "style",
332            24 => "example",
333            25 => "examplestyle",
334            26 => "assets",
335            27 => "bin" => transform_value(value, sort_object_alphabetically),
336            28 => "source",
337            29 => "directories" => transform_with_key_order(value, &["lib", "bin", "man", "doc", "example", "test"]),
338            30 => "workspaces",
339            31 => "binary" => transform_with_key_order(value, &["module_name", "module_path", "remote_path", "package_name", "host"]),
340            32 => "files" => transform_array(value, sort_paths_naturally),
341            33 => "os",
342            34 => "cpu",
343            35 => "libc" => transform_array(value, sort_array_unique),
344            // Package Entry Points
345            36 => "type",
346            37 => "sideEffects",
347            38 => "main",
348            39 => "module",
349            40 => "browser",
350            41 => "types",
351            42 => "typings",
352            43 => "typesVersions",
353            44 => "typeScriptVersion",
354            45 => "typesPublisherContentHash",
355            46 => "react-native",
356            47 => "svelte",
357            48 => "unpkg",
358            49 => "jsdelivr",
359            50 => "jsnext:main",
360            51 => "umd",
361            52 => "umd:main",
362            53 => "es5",
363            54 => "esm5",
364            55 => "fesm5",
365            56 => "es2015",
366            57 => "esm2015",
367            58 => "fesm2015",
368            59 => "es2020",
369            60 => "esm2020",
370            61 => "fesm2020",
371            62 => "esnext",
372            63 => "imports",
373            64 => "exports" => transform_value(value, sort_exports),
374            65 => "publishConfig" => transform_value(value, sort_object_alphabetically),
375            // Scripts
376            66 => "scripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
377            67 => "betterScripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
378            // Dependencies
379            68 => "dependencies" => transform_value(value, sort_object_alphabetically),
380            69 => "devDependencies" => transform_value(value, sort_object_alphabetically),
381            70 => "dependenciesMeta",
382            71 => "peerDependencies" => transform_value(value, sort_object_alphabetically),
383            72 => "peerDependenciesMeta",
384            73 => "optionalDependencies" => transform_value(value, sort_object_alphabetically),
385            74 => "bundledDependencies" => transform_array(value, sort_array_unique),
386            75 => "bundleDependencies" => transform_array(value, sort_array_unique),
387            76 => "resolutions" => transform_value(value, sort_object_alphabetically),
388            77 => "overrides" => transform_value(value, sort_object_alphabetically),
389            // Git Hooks & Commit Tools
390            78 => "husky" => transform_value(value, sort_object_recursive),
391            79 => "simple-git-hooks",
392            80 => "pre-commit",
393            81 => "lint-staged",
394            82 => "nano-staged",
395            83 => "commitlint" => transform_value(value, sort_object_recursive),
396            // VSCode Extension Specific
397            84 => "l10n",
398            85 => "contributes",
399            86 => "activationEvents" => transform_array(value, sort_array_unique),
400            87 => "extensionPack" => transform_array(value, sort_array_unique),
401            88 => "extensionDependencies" => transform_array(value, sort_array_unique),
402            89 => "extensionKind" => transform_array(value, sort_array_unique),
403            90 => "icon",
404            91 => "badges",
405            92 => "galleryBanner",
406            93 => "preview",
407            94 => "markdown",
408            // Build & Tool Configuration
409            95 => "napi" => transform_value(value, sort_object_alphabetically),
410            96 => "flat",
411            97 => "config" => transform_value(value, sort_object_alphabetically),
412            98 => "nodemonConfig" => transform_value(value, sort_object_recursive),
413            99 => "browserify" => transform_value(value, sort_object_recursive),
414            100 => "babel" => transform_value(value, sort_object_recursive),
415            101 => "browserslist",
416            102 => "xo" => transform_value(value, sort_object_recursive),
417            103 => "prettier" => transform_value(value, sort_object_recursive),
418            104 => "eslintConfig" => transform_value(value, sort_object_recursive),
419            105 => "eslintIgnore",
420            106 => "standard" => transform_value(value, sort_object_recursive),
421            107 => "npmpkgjsonlint",
422            108 => "npmPackageJsonLintConfig",
423            109 => "npmpackagejsonlint",
424            110 => "release",
425            111 => "auto-changelog" => transform_value(value, sort_object_recursive),
426            112 => "remarkConfig" => transform_value(value, sort_object_recursive),
427            113 => "stylelint" => transform_value(value, sort_object_recursive),
428            114 => "typescript" => transform_value(value, sort_object_recursive),
429            115 => "typedoc" => transform_value(value, sort_object_recursive),
430            116 => "tshy" => transform_value(value, sort_object_recursive),
431            117 => "tsdown" => transform_value(value, sort_object_recursive),
432            118 => "size-limit",
433            // Testing
434            119 => "ava" => transform_value(value, sort_object_recursive),
435            120 => "jest" => transform_value(value, sort_object_recursive),
436            121 => "jest-junit",
437            122 => "jest-stare",
438            123 => "mocha" => transform_value(value, sort_object_recursive),
439            124 => "nyc" => transform_value(value, sort_object_recursive),
440            125 => "c8" => transform_value(value, sort_object_recursive),
441            126 => "tap",
442            127 => "tsd" => transform_value(value, sort_object_recursive),
443            128 => "typeCoverage" => transform_value(value, sort_object_recursive),
444            129 => "oclif" => transform_value(value, sort_object_recursive),
445            // Runtime & Package Manager
446            130 => "languageName",
447            131 => "preferGlobal",
448            132 => "devEngines" => transform_value(value, sort_object_alphabetically),
449            133 => "engines" => transform_value(value, sort_object_alphabetically),
450            134 => "engineStrict",
451            135 => "volta" => transform_value(value, sort_object_recursive),
452            136 => "packageManager",
453            137 => "pnpm",
454        ]);
455    }
456
457    // Sort each category (using unstable sort for better performance)
458    known.sort_unstable_by_key(|(index, _, _)| *index);
459    non_private.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
460    private.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
461
462    // Build result map
463    let mut result = Map::new();
464
465    // Insert known fields (already transformed)
466    for (_index, key, value) in known {
467        result.insert(key, value);
468    }
469
470    // Insert non-private unknown fields
471    for (key, value) in non_private {
472        result.insert(key, value);
473    }
474
475    // Insert private fields
476    for (key, value) in private {
477        result.insert(key, value);
478    }
479
480    result
481}