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