use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct SortOptions {
pub pretty: bool,
pub sort_scripts: bool,
}
impl Default for SortOptions {
fn default() -> Self {
Self { pretty: true, sort_scripts: false }
}
}
pub fn sort_package_json_with_options(
input: &str,
options: &SortOptions,
) -> Result<String, serde_json::Error> {
const BOM: char = '\u{FEFF}';
let input_without_bom = input.strip_prefix(BOM).unwrap_or(input);
let has_bom = input_without_bom.len() != input.len();
let value: Value = serde_json::from_str(input_without_bom)?;
let sorted_value = if let Value::Object(obj) = value {
Value::Object(sort_object_keys(obj, options))
} else {
value
};
let result = if options.pretty {
let mut s = serde_json::to_string_pretty(&sorted_value)?;
s.push('\n');
s
} else {
serde_json::to_string(&sorted_value)?
};
if has_bom {
let mut output = String::with_capacity(BOM.len_utf8() + result.len());
output.push(BOM);
output.push_str(&result);
Ok(output)
} else {
Ok(result)
}
}
pub fn sort_package_json(input: &str) -> Result<String, serde_json::Error> {
sort_package_json_with_options(input, &SortOptions::default())
}
macro_rules! declare_field_order {
(
$key:ident, $value:ident, $known:ident, $non_private:ident, $private:ident;
[
$( $idx:literal => $field_name:literal $( => $transform:expr )? ),* $(,)?
]
) => {
{
$( let _ = $idx; )*
match $key.as_str() {
$(
$field_name => {
$known.push((
$idx,
$key,
declare_field_order!(@value $value $(, $transform)?)
));
},
)*
_ => {
if $key.starts_with('_') {
$private.push(($key, $value));
} else {
$non_private.push(($key, $value));
}
}
}
}
};
(@value $value:ident) => { $value };
(@value $value:ident, $transform:expr) => { $transform };
}
fn transform_value<F>(value: Value, transform: F) -> Value
where
F: FnOnce(Map<String, Value>) -> Map<String, Value>,
{
match value {
Value::Object(o) => Value::Object(transform(o)),
_ => value,
}
}
fn transform_array<F>(value: Value, transform: F) -> Value
where
F: FnOnce(Vec<Value>) -> Vec<Value>,
{
match value {
Value::Array(arr) => Value::Array(transform(arr)),
_ => value,
}
}
fn transform_with_key_order(value: Value, key_order: &[&str]) -> Value {
transform_value(value, |o| sort_object_by_key_order(o, key_order))
}
fn sort_object_alphabetically(mut obj: Map<String, Value>) -> Map<String, Value> {
obj.sort_keys();
obj
}
fn sort_object_recursive(obj: Map<String, Value>) -> Map<String, Value> {
let mut obj = obj;
sort_object_recursive_in_place(&mut obj);
obj
}
fn sort_object_recursive_in_place(obj: &mut Map<String, Value>) {
for value in obj.values_mut() {
if let Value::Object(nested) = value {
sort_object_recursive_in_place(nested);
}
}
obj.sort_keys();
}
fn sort_array_unique(mut arr: Vec<Value>) -> Vec<Value> {
arr.retain(|v| v.is_string());
arr.sort_unstable_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
arr.dedup_by(|a, b| a.as_str() == b.as_str());
arr
}
fn dedupe_array(mut arr: Vec<Value>) -> Vec<Value> {
let mut write = 0;
for read in 0..arr.len() {
let keep = match arr[read].as_str() {
Some(s) => !arr[..write].iter().any(|seen| seen.as_str() == Some(s)),
None => false,
};
if keep {
if write != read {
arr.swap(write, read);
}
write += 1;
}
}
arr.truncate(write);
arr
}
fn sort_object_by_key_order(mut obj: Map<String, Value>, key_order: &[&str]) -> Map<String, Value> {
obj.sort_keys();
let mut result = Map::with_capacity(obj.len());
for &key in key_order {
if let Some(value) = obj.shift_remove(key) {
result.insert(key.into(), value);
}
}
for (key, value) in obj {
result.insert(key, value);
}
result
}
fn sort_people_object(obj: Map<String, Value>) -> Map<String, Value> {
sort_object_by_key_order(obj, &["name", "email", "url"])
}
fn sort_object_keys(obj: Map<String, Value>, options: &SortOptions) -> Map<String, Value> {
let mut known: Vec<(usize, String, Value)> = Vec::new(); let mut non_private: Vec<(String, Value)> = Vec::new();
let mut private: Vec<(String, Value)> = Vec::new();
for (key, value) in obj {
declare_field_order!(key, value, known, non_private, private; [
0 => "$schema",
1 => "name",
2 => "displayName",
3 => "version",
4 => "stableVersion",
5 => "gitHead",
6 => "private",
7 => "description",
8 => "categories" => transform_array(value, sort_array_unique),
9 => "keywords" => transform_array(value, sort_array_unique),
10 => "homepage",
11 => "bugs" => transform_with_key_order(value, &["url", "email"]),
12 => "license",
13 => "author" => transform_value(value, sort_people_object),
14 => "maintainers",
15 => "contributors",
16 => "repository" => transform_with_key_order(value, &["type", "url"]),
17 => "funding" => transform_with_key_order(value, &["type", "url"]),
18 => "donate" => transform_with_key_order(value, &["type", "url"]),
19 => "sponsor" => transform_with_key_order(value, &["type", "url"]),
20 => "qna",
21 => "publisher",
22 => "man",
23 => "style",
24 => "example",
25 => "examplestyle",
26 => "assets",
27 => "bin" => transform_value(value, sort_object_alphabetically),
28 => "source",
29 => "directories" => transform_with_key_order(value, &["lib", "bin", "man", "doc", "example", "test"]),
30 => "workspaces",
31 => "binary" => transform_with_key_order(value, &["module_name", "module_path", "remote_path", "package_name", "host"]),
32 => "files" => transform_array(value, dedupe_array),
33 => "os",
34 => "cpu",
35 => "libc" => transform_array(value, sort_array_unique),
36 => "type",
37 => "sideEffects",
38 => "main",
39 => "module",
40 => "browser",
41 => "types",
42 => "typings",
43 => "typesVersions",
44 => "typeScriptVersion",
45 => "typesPublisherContentHash",
46 => "react-native",
47 => "svelte",
48 => "unpkg",
49 => "jsdelivr",
50 => "jsnext:main",
51 => "umd",
52 => "umd:main",
53 => "es5",
54 => "esm5",
55 => "fesm5",
56 => "es2015",
57 => "esm2015",
58 => "fesm2015",
59 => "es2020",
60 => "esm2020",
61 => "fesm2020",
62 => "esnext",
63 => "imports",
64 => "exports",
65 => "publishConfig" => transform_value(value, sort_object_alphabetically),
66 => "scripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
67 => "betterScripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
68 => "dependencies" => transform_value(value, sort_object_alphabetically),
69 => "devDependencies" => transform_value(value, sort_object_alphabetically),
70 => "dependenciesMeta",
71 => "peerDependencies" => transform_value(value, sort_object_alphabetically),
72 => "peerDependenciesMeta",
73 => "optionalDependencies" => transform_value(value, sort_object_alphabetically),
74 => "bundledDependencies" => transform_array(value, sort_array_unique),
75 => "bundleDependencies" => transform_array(value, sort_array_unique),
76 => "resolutions" => transform_value(value, sort_object_alphabetically),
77 => "overrides" => transform_value(value, sort_object_alphabetically),
78 => "husky" => transform_value(value, sort_object_recursive),
79 => "simple-git-hooks",
80 => "vite-staged",
81 => "lint-staged",
82 => "nano-staged",
83 => "pre-commit",
84 => "commitlint" => transform_value(value, sort_object_recursive),
85 => "l10n",
86 => "contributes",
87 => "activationEvents" => transform_array(value, sort_array_unique),
88 => "extensionPack" => transform_array(value, sort_array_unique),
89 => "extensionDependencies" => transform_array(value, sort_array_unique),
90 => "extensionKind" => transform_array(value, sort_array_unique),
91 => "icon",
92 => "badges",
93 => "galleryBanner",
94 => "preview",
95 => "markdown",
96 => "napi" => transform_value(value, sort_object_alphabetically),
97 => "flat",
98 => "config" => transform_value(value, sort_object_alphabetically),
99 => "nodemonConfig" => transform_value(value, sort_object_recursive),
100 => "browserify" => transform_value(value, sort_object_recursive),
101 => "babel" => transform_value(value, sort_object_recursive),
102 => "browserslist",
103 => "xo" => transform_value(value, sort_object_recursive),
104 => "prettier" => transform_value(value, sort_object_recursive),
105 => "eslintConfig" => transform_value(value, sort_object_recursive),
106 => "eslintIgnore",
107 => "standard" => transform_value(value, sort_object_recursive),
108 => "npmpkgjsonlint",
109 => "npmPackageJsonLintConfig",
110 => "npmpackagejsonlint",
111 => "release",
112 => "auto-changelog" => transform_value(value, sort_object_recursive),
113 => "remarkConfig" => transform_value(value, sort_object_recursive),
114 => "stylelint" => transform_value(value, sort_object_recursive),
115 => "typescript" => transform_value(value, sort_object_recursive),
116 => "typedoc" => transform_value(value, sort_object_recursive),
117 => "tshy" => transform_value(value, sort_object_recursive),
118 => "tsdown" => transform_value(value, sort_object_recursive),
119 => "size-limit",
120 => "ava" => transform_value(value, sort_object_recursive),
121 => "jest" => transform_value(value, sort_object_recursive),
122 => "jest-junit",
123 => "jest-stare",
124 => "mocha" => transform_value(value, sort_object_recursive),
125 => "nyc" => transform_value(value, sort_object_recursive),
126 => "c8" => transform_value(value, sort_object_recursive),
127 => "tap",
128 => "tsd" => transform_value(value, sort_object_recursive),
129 => "typeCoverage" => transform_value(value, sort_object_recursive),
130 => "oclif" => transform_value(value, sort_object_recursive),
131 => "languageName",
132 => "preferGlobal",
133 => "devEngines" => transform_value(value, sort_object_alphabetically),
134 => "engines" => transform_value(value, sort_object_alphabetically),
135 => "engineStrict",
136 => "volta" => transform_value(value, sort_object_recursive),
137 => "packageManager",
138 => "pnpm",
]);
}
known.sort_unstable_by_key(|(index, _, _)| *index);
non_private.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
private.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
let mut result = Map::with_capacity(known.len() + non_private.len() + private.len());
for (_index, key, value) in known {
result.insert(key, value);
}
for (key, value) in non_private {
result.insert(key, value);
}
for (key, value) in private {
result.insert(key, value);
}
result
}