1mod value;
2
3use std::borrow::Cow;
4
5use value::{Object, Value};
6
7const BOM_STR: &str = "\u{FEFF}";
9
10#[derive(Debug, Clone)]
12pub struct SortOptions {
13 pub pretty: bool,
15 pub sort_scripts: bool,
17}
18
19impl Default for SortOptions {
20 fn default() -> Self {
21 Self { pretty: true, sort_scripts: false }
22 }
23}
24
25pub 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 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 Ok(unsafe { String::from_utf8_unchecked(buf) })
62}
63
64pub fn sort_package_json(input: &str) -> Result<String, serde_json::Error> {
66 sort_package_json_with_options(input, &SortOptions::default())
67}
68
69#[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
117fn sort_array_unique(mut arr: Vec<Value<'_>>) -> Vec<Value<'_>> {
119 arr.retain(Value::is_string);
120 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
126fn 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
146fn 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
196macro_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 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 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 12 => "license",
245 13 => "author" => transform_value(value, sort_people_object),
246 14 => "maintainers",
247 15 => "contributors",
248 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 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 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 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 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 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 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 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 113 => "auto-changelog" => transform_value(value, sort_object_alphabetically),
355 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 118 => "tshy" => transform_value(value, sort_object_alphabetically),
362 119 => "tsdown" => transform_value(value, sort_object_recursive),
363 120 => "size-limit",
364 121 => "ava" => transform_value(value, sort_object_recursive),
366 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 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 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}