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