Skip to main content

sort_package_json/
lib.rs

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