Skip to main content

sort_package_json/
lib.rs

1use serde_json::{Map, Value};
2
3/// UTF-8 BOM (`U+FEFF`).
4const BOM_STR: &str = "\u{FEFF}";
5
6/// Options for controlling JSON formatting when sorting.
7#[derive(Debug, Clone)]
8pub struct SortOptions {
9    /// Whether to pretty-print the output JSON.
10    pub pretty: bool,
11    /// Whether to sort the scripts field alphabetically.
12    pub sort_scripts: bool,
13}
14
15impl Default for SortOptions {
16    fn default() -> Self {
17        Self { pretty: true, sort_scripts: false }
18    }
19}
20
21/// Sorts a `package.json` string with custom options.
22pub fn sort_package_json_with_options(
23    input: &str,
24    options: &SortOptions,
25) -> Result<String, serde_json::Error> {
26    let (has_bom, body) =
27        input.strip_prefix(BOM_STR).map_or((false, input), |stripped| (true, stripped));
28
29    let value: Value = serde_json::from_str(body)?;
30
31    let sorted = match value {
32        Value::Object(obj) => Value::Object(sort_object_keys(obj, options)),
33        other => other,
34    };
35
36    // Serialize directly into a byte buffer so the (optional) BOM, the JSON body, and the
37    // trailing newline are all written into a single allocation. This skips the extra
38    // String allocation + copy that `to_string_pretty` followed by manual BOM-prepending
39    // would incur.
40    //
41    // Sized for the common case where the input is already pretty-printed: output ≈ input
42    // in length. The `+ 16` absorbs the trailing `'\n'` push and minor reformatting slop
43    // without forcing a final realloc.
44    let mut buf: Vec<u8> = Vec::with_capacity(input.len() + 16);
45    if has_bom {
46        buf.extend_from_slice(BOM_STR.as_bytes());
47    }
48    if options.pretty {
49        serde_json::to_writer_pretty(&mut buf, &sorted)?;
50        buf.push(b'\n');
51    } else {
52        serde_json::to_writer(&mut buf, &sorted)?;
53    }
54    // SAFETY: `serde_json::to_writer{,_pretty}` are contractually required to emit valid
55    // UTF-8 (this is also what `serde_json::to_string_pretty` itself relies on). The BOM
56    // bytes and the trailing `\n` are also valid UTF-8.
57    Ok(unsafe { String::from_utf8_unchecked(buf) })
58}
59
60/// Sorts a `package.json` string with default options (pretty-printed).
61pub fn sort_package_json(input: &str) -> Result<String, serde_json::Error> {
62    sort_package_json_with_options(input, &SortOptions::default())
63}
64
65// ===== Value-level transformations ==========================================
66
67#[inline]
68fn transform_value<F>(value: Value, f: F) -> Value
69where
70    F: FnOnce(Map<String, Value>) -> Map<String, Value>,
71{
72    match value {
73        Value::Object(o) => Value::Object(f(o)),
74        other => other,
75    }
76}
77
78#[inline]
79fn transform_array<F>(value: Value, f: F) -> Value
80where
81    F: FnOnce(Vec<Value>) -> Vec<Value>,
82{
83    match value {
84        Value::Array(arr) => Value::Array(f(arr)),
85        other => other,
86    }
87}
88
89#[inline]
90fn transform_with_key_order(value: Value, key_order: &[&str]) -> Value {
91    transform_value(value, |o| sort_object_by_key_order(o, key_order))
92}
93
94fn sort_object_alphabetically(mut obj: Map<String, Value>) -> Map<String, Value> {
95    obj.sort_keys();
96    obj
97}
98
99fn sort_object_recursive(mut obj: Map<String, Value>) -> Map<String, Value> {
100    sort_object_recursive_in_place(&mut obj);
101    obj
102}
103
104fn sort_object_recursive_in_place(obj: &mut Map<String, Value>) {
105    for value in obj.values_mut() {
106        if let Value::Object(nested) = value {
107            sort_object_recursive_in_place(nested);
108        }
109    }
110    obj.sort_keys();
111}
112
113/// Filters non-strings, sorts ascending, and removes duplicates.
114fn sort_array_unique(mut arr: Vec<Value>) -> Vec<Value> {
115    arr.retain(Value::is_string);
116    // `unwrap` is sound: `retain` above guarantees every element is a string.
117    arr.sort_unstable_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
118    arr.dedup_by(|a, b| a.as_str() == b.as_str());
119    arr
120}
121
122/// Removes duplicate string entries while preserving original order. Used for fields
123/// where order matters (e.g., `files` with `!` negation patterns).
124fn dedupe_array(mut arr: Vec<Value>) -> Vec<Value> {
125    let mut write = 0;
126    for read in 0..arr.len() {
127        let keep = match arr[read].as_str() {
128            Some(s) => !arr[..write].iter().any(|seen| seen.as_str() == Some(s)),
129            None => false,
130        };
131        if keep {
132            if write != read {
133                arr.swap(write, read);
134            }
135            write += 1;
136        }
137    }
138    arr.truncate(write);
139    arr
140}
141
142/// Reorders `obj` so that any keys present in `key_order` appear first (in the given
143/// order), with the remaining keys following alphabetically.
144///
145/// Single-pass classification + merge — avoids `IndexMap::shift_remove`'s O(n) tail-shift
146/// per requested key.
147fn sort_object_by_key_order(obj: Map<String, Value>, key_order: &[&str]) -> Map<String, Value> {
148    let mut known: Vec<Option<(String, Value)>> = (0..key_order.len()).map(|_| None).collect();
149    let mut others: Vec<(String, Value)> = Vec::new();
150
151    for (key, value) in obj {
152        match key_order.iter().position(|kn| *kn == key.as_str()) {
153            Some(idx) => known[idx] = Some((key, value)),
154            None => others.push((key, value)),
155        }
156    }
157
158    others.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
159
160    let mut result = Map::with_capacity(known.len() + others.len());
161    for (key, value) in known.into_iter().flatten() {
162        result.insert(key, value);
163    }
164    for (key, value) in others {
165        result.insert(key, value);
166    }
167    result
168}
169
170fn sort_people_object(obj: Map<String, Value>) -> Map<String, Value> {
171    sort_object_by_key_order(obj, &["name", "email", "url"])
172}
173
174// ===== Top-level field ordering =============================================
175
176/// Declares the canonical order for known top-level `package.json` fields. For each
177/// matched key, the field is bucketed with its order index; an optional transformation
178/// expression (with `value` and `options` in scope) rewrites the value before storage.
179/// Unknown fields fall through to the catch-all arm.
180macro_rules! declare_field_order {
181    (
182        $key:ident, $value:ident, $known:ident, $unknown:ident;
183        [ $( $idx:literal => $field_name:literal $( => $transform:expr )? ),* $(,)? ]
184    ) => {
185        match $key.as_str() {
186            $(
187                $field_name => $known.push((
188                    $idx,
189                    $key,
190                    declare_field_order!(@value $value $(, $transform)?),
191                )),
192            )*
193            _ => $unknown.push(($key, $value)),
194        }
195    };
196    (@value $value:ident) => { $value };
197    (@value $value:ident, $transform:expr) => { $transform };
198}
199
200fn sort_object_keys(obj: Map<String, Value>, options: &SortOptions) -> Map<String, Value> {
201    // `known` collects fields with a canonical position; `unknown` collects everything
202    // else, sorted with private (`_`-prefixed) keys after non-private ones.
203    let mut known: Vec<(usize, String, Value)> = Vec::new();
204    let mut unknown: Vec<(String, Value)> = Vec::new();
205
206    for (key, value) in obj {
207        declare_field_order!(key, value, known, unknown; [
208            // Core Package Metadata
209            0 => "$schema",
210            1 => "name",
211            2 => "displayName",
212            3 => "version",
213            4 => "stableVersion",
214            5 => "gitHead",
215            6 => "private",
216            7 => "description",
217            8 => "categories" => transform_array(value, sort_array_unique),
218            9 => "keywords" => transform_array(value, sort_array_unique),
219            10 => "homepage",
220            11 => "bugs" => transform_with_key_order(value, &["url", "email"]),
221            // License & People
222            12 => "license",
223            13 => "author" => transform_value(value, sort_people_object),
224            14 => "maintainers",
225            15 => "contributors",
226            // Repository & Funding
227            16 => "repository" => transform_with_key_order(value, &["type", "url"]),
228            17 => "funding" => transform_with_key_order(value, &["type", "url"]),
229            18 => "donate" => transform_with_key_order(value, &["type", "url"]),
230            19 => "sponsor" => transform_with_key_order(value, &["type", "url"]),
231            20 => "qna",
232            21 => "publisher",
233            // Package Content & Distribution
234            22 => "man",
235            23 => "style",
236            24 => "example",
237            25 => "examplestyle",
238            26 => "assets",
239            27 => "bin" => transform_value(value, sort_object_alphabetically),
240            28 => "source",
241            29 => "directories" => transform_with_key_order(value, &["lib", "bin", "man", "doc", "example", "test"]),
242            30 => "workspaces",
243            31 => "binary" => transform_with_key_order(value, &["module_name", "module_path", "remote_path", "package_name", "host"]),
244            32 => "files" => transform_array(value, dedupe_array),
245            33 => "os",
246            34 => "cpu",
247            35 => "libc" => transform_array(value, sort_array_unique),
248            // Package Entry Points
249            36 => "type",
250            37 => "sideEffects",
251            38 => "main",
252            39 => "module",
253            40 => "browser",
254            41 => "types",
255            42 => "typings",
256            43 => "typesVersions",
257            44 => "typeScriptVersion",
258            45 => "typesPublisherContentHash",
259            46 => "react-native",
260            47 => "svelte",
261            48 => "unpkg",
262            49 => "jsdelivr",
263            50 => "jsnext:main",
264            51 => "umd",
265            52 => "umd:main",
266            53 => "es5",
267            54 => "esm5",
268            55 => "fesm5",
269            56 => "es2015",
270            57 => "esm2015",
271            58 => "fesm2015",
272            59 => "es2020",
273            60 => "esm2020",
274            61 => "fesm2020",
275            62 => "esnext",
276            63 => "imports",
277            64 => "exports",
278            65 => "publishConfig" => transform_value(value, |o| sort_object_keys(o, options)),
279            // Scripts
280            66 => "scripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
281            67 => "betterScripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
282            // Dependencies
283            68 => "dependencies" => transform_value(value, sort_object_alphabetically),
284            69 => "devDependencies" => transform_value(value, sort_object_alphabetically),
285            70 => "dependenciesMeta",
286            71 => "peerDependencies" => transform_value(value, sort_object_alphabetically),
287            72 => "peerDependenciesMeta",
288            73 => "optionalDependencies" => transform_value(value, sort_object_alphabetically),
289            74 => "bundledDependencies" => transform_array(value, sort_array_unique),
290            75 => "bundleDependencies" => transform_array(value, sort_array_unique),
291            76 => "resolutions" => transform_value(value, sort_object_alphabetically),
292            77 => "overrides" => transform_value(value, sort_object_alphabetically),
293            // Git Hooks & Commit Tools
294            78 => "husky" => transform_value(value, sort_object_recursive),
295            79 => "simple-git-hooks",
296            80 => "vite-staged",
297            81 => "lint-staged",
298            82 => "nano-staged",
299            83 => "pre-commit",
300            84 => "commitlint" => transform_value(value, sort_object_recursive),
301            // VSCode Extension Specific
302            85 => "l10n",
303            86 => "contributes",
304            87 => "activationEvents" => transform_array(value, sort_array_unique),
305            88 => "extensionPack" => transform_array(value, sort_array_unique),
306            89 => "extensionDependencies" => transform_array(value, sort_array_unique),
307            90 => "extensionKind" => transform_array(value, sort_array_unique),
308            91 => "icon",
309            92 => "badges",
310            93 => "galleryBanner",
311            94 => "preview",
312            95 => "markdown",
313            // Build & Tool Configuration
314            96 => "napi" => transform_value(value, sort_object_alphabetically),
315            97 => "flat",
316            98 => "config" => transform_value(value, sort_object_alphabetically),
317            99 => "nodemonConfig" => transform_value(value, sort_object_recursive),
318            100 => "browserify" => transform_value(value, sort_object_recursive),
319            101 => "babel" => transform_value(value, sort_object_recursive),
320            102 => "browserslist",
321            103 => "xo" => transform_value(value, sort_object_recursive),
322            104 => "prettier" => transform_value(value, sort_object_recursive),
323            105 => "eslintConfig" => transform_value(value, sort_object_recursive),
324            106 => "eslintIgnore",
325            107 => "standard" => transform_value(value, sort_object_recursive),
326            108 => "npmpkgjsonlint",
327            109 => "npmPackageJsonLintConfig",
328            110 => "npmpackagejsonlint",
329            111 => "release",
330            112 => "auto-changelog" => transform_value(value, sort_object_recursive),
331            113 => "remarkConfig" => transform_value(value, sort_object_recursive),
332            114 => "stylelint" => transform_value(value, sort_object_recursive),
333            115 => "typescript" => transform_value(value, sort_object_recursive),
334            116 => "typedoc" => transform_value(value, sort_object_recursive),
335            117 => "tshy" => transform_value(value, sort_object_recursive),
336            118 => "tsdown" => transform_value(value, sort_object_recursive),
337            119 => "size-limit",
338            // Testing
339            120 => "ava" => transform_value(value, sort_object_recursive),
340            121 => "jest" => transform_value(value, sort_object_recursive),
341            122 => "jest-junit",
342            123 => "jest-stare",
343            124 => "mocha" => transform_value(value, sort_object_recursive),
344            125 => "nyc" => transform_value(value, sort_object_recursive),
345            126 => "c8" => transform_value(value, sort_object_recursive),
346            127 => "tap",
347            128 => "tsd" => transform_value(value, sort_object_recursive),
348            129 => "typeCoverage" => transform_value(value, sort_object_recursive),
349            130 => "oclif" => transform_value(value, sort_object_recursive),
350            // Runtime & Package Manager
351            131 => "languageName",
352            132 => "preferGlobal",
353            133 => "devEngines" => transform_value(value, sort_object_alphabetically),
354            134 => "engines" => transform_value(value, sort_object_alphabetically),
355            135 => "engineStrict",
356            136 => "volta" => transform_value(value, sort_object_recursive),
357            137 => "packageManager",
358            138 => "pnpm",
359        ]);
360    }
361
362    known.sort_unstable_by_key(|(idx, _, _)| *idx);
363    // Single sort over all unknowns: non-private (`!_`) before private (`_`-prefixed),
364    // each group alphabetical.
365    unknown.sort_unstable_by(|(a, _), (b, _)| {
366        let a_priv = a.starts_with('_');
367        let b_priv = b.starts_with('_');
368        a_priv.cmp(&b_priv).then_with(|| a.cmp(b))
369    });
370
371    let mut result = Map::with_capacity(known.len() + unknown.len());
372    for (_, key, value) in known {
373        result.insert(key, value);
374    }
375    for (key, value) in unknown {
376        result.insert(key, value);
377    }
378    result
379}