1mod value;
14
15use std::borrow::Cow;
16
17use value::{Object, Value};
18
19const BOM_STR: &str = "\u{FEFF}";
21
22#[derive(Debug, Clone, Copy, Eq, PartialEq)]
24#[non_exhaustive]
25pub struct SortOptions {
26 pub pretty: bool,
28 pub sort_scripts: bool,
30}
31
32impl SortOptions {
33 #[must_use]
35 pub const fn new() -> Self {
36 Self { pretty: true, sort_scripts: false }
37 }
38
39 #[must_use]
41 pub const fn with_pretty(mut self, pretty: bool) -> Self {
42 self.pretty = pretty;
43 self
44 }
45
46 #[must_use]
48 pub const fn with_sort_scripts(mut self, sort_scripts: bool) -> Self {
49 self.sort_scripts = sort_scripts;
50 self
51 }
52}
53
54impl Default for SortOptions {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60pub fn sort_package_json_with_options(
69 input: &str,
70 options: &SortOptions,
71) -> Result<String, serde_json::Error> {
72 let (has_bom, body) =
73 input.strip_prefix(BOM_STR).map_or((false, input), |stripped| (true, stripped));
74
75 let value: Value<'_> = serde_json::from_str(body)?;
76
77 let sorted = match value {
78 Value::Object(obj) => Value::Object(sort_object_keys(obj, options)),
79 other => other,
80 };
81
82 let mut buf: Vec<u8> = Vec::with_capacity(input.len() + 16);
91 if has_bom {
92 buf.extend_from_slice(BOM_STR.as_bytes());
93 }
94 if options.pretty {
95 serde_json::to_writer_pretty(&mut buf, &sorted)?;
96 buf.push(b'\n');
97 } else {
98 serde_json::to_writer(&mut buf, &sorted)?;
99 }
100 Ok(unsafe { String::from_utf8_unchecked(buf) })
104}
105
106pub fn sort_package_json(input: &str) -> Result<String, serde_json::Error> {
114 sort_package_json_with_options(input, &SortOptions::default())
115}
116
117#[inline]
120fn transform_value<'a, F>(value: Value<'a>, f: F) -> Value<'a>
121where
122 F: FnOnce(Object<'a>) -> Object<'a>,
123{
124 match value {
125 Value::Object(o) => Value::Object(f(o)),
126 other => other,
127 }
128}
129
130#[inline]
131fn transform_array<'a, F>(value: Value<'a>, f: F) -> Value<'a>
132where
133 F: FnOnce(Vec<Value<'a>>) -> Vec<Value<'a>>,
134{
135 match value {
136 Value::Array(arr) => Value::Array(f(arr)),
137 other => other,
138 }
139}
140
141#[inline]
142fn transform_with_key_order<'a>(value: Value<'a>, key_order: &[&str]) -> Value<'a> {
143 transform_value(value, |o| sort_object_by_key_order(o, key_order))
144}
145
146fn sort_object_alphabetically(mut obj: Object<'_>) -> Object<'_> {
147 obj.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
148 obj
149}
150
151fn sort_object_recursive(mut obj: Object<'_>) -> Object<'_> {
152 sort_object_recursive_in_place(&mut obj);
153 obj
154}
155
156fn sort_object_recursive_in_place(obj: &mut Object<'_>) {
157 for (_, value) in &mut *obj {
158 if let Value::Object(nested) = value {
159 sort_object_recursive_in_place(nested);
160 }
161 }
162 obj.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
163}
164
165fn sort_array_unique(mut arr: Vec<Value<'_>>) -> Vec<Value<'_>> {
167 arr.retain(Value::is_string);
168 arr.sort_unstable_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
170 arr.dedup_by(|a, b| a.as_str() == b.as_str());
171 arr
172}
173
174fn dedupe_array(mut arr: Vec<Value<'_>>) -> Vec<Value<'_>> {
177 let mut write = 0;
178 for read in 0..arr.len() {
179 let keep = match arr[read].as_str() {
180 Some(s) => !arr[..write].iter().any(|seen| seen.as_str() == Some(s)),
181 None => false,
182 };
183 if keep {
184 if write != read {
185 arr.swap(write, read);
186 }
187 write += 1;
188 }
189 }
190 arr.truncate(write);
191 arr
192}
193
194fn sort_object_by_key_order<'a>(obj: Object<'a>, key_order: &[&str]) -> Object<'a> {
199 let mut known: Vec<Option<(Cow<'a, str>, Value<'a>)>> =
200 (0..key_order.len()).map(|_| None).collect();
201 let mut others: Object<'a> = Vec::new();
202
203 for (key, value) in obj {
204 match key_order.iter().position(|kn| *kn == key.as_ref()) {
205 Some(idx) => known[idx] = Some((key, value)),
206 None => others.push((key, value)),
207 }
208 }
209
210 others.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
211
212 let mut result = Vec::with_capacity(known.len() + others.len());
213 for (key, value) in known.into_iter().flatten() {
214 result.push((key, value));
215 }
216 for (key, value) in others {
217 result.push((key, value));
218 }
219 result
220}
221
222fn sort_people_object(obj: Object<'_>) -> Object<'_> {
223 sort_object_by_key_order(obj, &["name", "email", "url"])
224}
225
226fn sort_dev_engine(value: Value<'_>) -> Value<'_> {
227 const KEY_ORDER: &[&str] = &["name", "version", "onFail"];
228
229 match value {
230 Value::Array(arr) => Value::Array(
231 arr.into_iter().map(|value| transform_with_key_order(value, KEY_ORDER)).collect(),
232 ),
233 other => transform_with_key_order(other, KEY_ORDER),
234 }
235}
236
237fn sort_dev_engines(obj: Object<'_>) -> Object<'_> {
238 let mut obj: Object<'_> =
239 obj.into_iter().map(|(key, value)| (key, sort_dev_engine(value))).collect();
240 obj.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
241 obj
242}
243
244macro_rules! declare_field_order {
251 (
252 $key:ident, $value:ident, $known:ident, $unknown:ident;
253 [ $( $idx:literal => $field_name:literal $( => $transform:expr )? ),* $(,)? ]
254 ) => {
255 match $key.as_ref() {
256 $(
257 $field_name => $known.push((
258 $idx,
259 $key,
260 declare_field_order!(@value $value $(, $transform)?),
261 )),
262 )*
263 _ => $unknown.push(($key, $value)),
264 }
265 };
266 (@value $value:ident) => { $value };
267 (@value $value:ident, $transform:expr) => { $transform };
268}
269
270fn sort_object_keys<'a>(obj: Object<'a>, options: &SortOptions) -> Object<'a> {
271 let mut known: Vec<(usize, Cow<'a, str>, Value<'a>)> = Vec::new();
274 let mut unknown: Object<'a> = Vec::new();
275
276 for (key, value) in obj {
277 declare_field_order!(key, value, known, unknown; [
278 0 => "$schema",
280 1 => "name",
281 2 => "displayName",
282 3 => "version",
283 4 => "stableVersion",
284 5 => "gitHead",
285 6 => "private",
286 7 => "description",
287 8 => "categories" => transform_array(value, sort_array_unique),
288 9 => "keywords" => transform_array(value, sort_array_unique),
289 10 => "homepage",
290 11 => "bugs" => transform_with_key_order(value, &["url", "email"]),
291 12 => "license",
293 13 => "author" => transform_value(value, sort_people_object),
294 14 => "maintainers",
295 15 => "contributors",
296 16 => "repository" => transform_with_key_order(value, &["type", "url"]),
298 17 => "funding" => transform_with_key_order(value, &["type", "url"]),
299 18 => "donate" => transform_with_key_order(value, &["type", "url"]),
300 19 => "sponsor" => transform_with_key_order(value, &["type", "url"]),
301 20 => "qna",
302 21 => "publisher",
303 22 => "man",
305 23 => "style",
306 24 => "example",
307 25 => "examplestyle",
308 26 => "assets",
309 27 => "bin" => transform_value(value, sort_object_alphabetically),
310 28 => "source",
311 29 => "directories" => transform_with_key_order(value, &["lib", "bin", "man", "doc", "example", "test"]),
312 30 => "workspaces",
313 31 => "binary" => transform_with_key_order(value, &["module_name", "module_path", "remote_path", "package_name", "host"]),
314 32 => "files" => transform_array(value, dedupe_array),
315 33 => "os",
316 34 => "cpu",
317 35 => "libc" => transform_array(value, sort_array_unique),
318 36 => "type",
320 37 => "sideEffects",
321 38 => "main",
322 39 => "module",
323 40 => "browser",
324 41 => "types",
325 42 => "typings",
326 43 => "typesVersions",
327 44 => "typeScriptVersion",
328 45 => "typesPublisherContentHash",
329 46 => "react-native",
330 47 => "svelte",
331 48 => "unpkg",
332 49 => "jsdelivr",
333 50 => "jsnext:main",
334 51 => "umd",
335 52 => "umd:main",
336 53 => "es5",
337 54 => "esm5",
338 55 => "fesm5",
339 56 => "es2015",
340 57 => "esm2015",
341 58 => "fesm2015",
342 59 => "es2020",
343 60 => "esm2020",
344 61 => "fesm2020",
345 62 => "esnext",
346 63 => "imports",
347 64 => "exports",
348 65 => "publishConfig" => transform_value(value, |o| sort_object_keys(o, options)),
349 66 => "scripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
351 67 => "betterScripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
352 68 => "wireit" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },
353 69 => "dependencies" => transform_value(value, sort_object_alphabetically),
355 70 => "devDependencies" => transform_value(value, sort_object_alphabetically),
356 71 => "dependenciesMeta",
357 72 => "peerDependencies" => transform_value(value, sort_object_alphabetically),
358 73 => "peerDependenciesMeta",
359 74 => "optionalDependencies" => transform_value(value, sort_object_alphabetically),
360 75 => "bundledDependencies" => transform_array(value, sort_array_unique),
361 76 => "bundleDependencies" => transform_array(value, sort_array_unique),
362 77 => "resolutions" => transform_value(value, sort_object_alphabetically),
363 78 => "overrides" => transform_value(value, sort_object_alphabetically),
364 79 => "husky" => transform_value(value, sort_object_recursive),
366 80 => "simple-git-hooks",
367 81 => "vite-staged",
368 82 => "lint-staged",
369 83 => "nano-staged",
370 84 => "pre-commit",
371 85 => "commitlint" => transform_value(value, sort_object_recursive),
372 86 => "l10n",
374 87 => "contributes",
375 88 => "activationEvents" => transform_array(value, sort_array_unique),
376 89 => "extensionPack" => transform_array(value, sort_array_unique),
377 90 => "extensionDependencies" => transform_array(value, sort_array_unique),
378 91 => "extensionKind" => transform_array(value, sort_array_unique),
379 92 => "icon",
380 93 => "badges",
381 94 => "galleryBanner",
382 95 => "preview",
383 96 => "markdown",
384 97 => "napi" => transform_value(value, sort_object_alphabetically),
386 98 => "flat",
387 99 => "config" => transform_value(value, sort_object_alphabetically),
388 100 => "nodemonConfig" => transform_value(value, sort_object_recursive),
389 101 => "browserify" => transform_value(value, sort_object_recursive),
390 102 => "babel" => transform_value(value, sort_object_recursive),
391 103 => "browserslist",
392 104 => "xo" => transform_value(value, sort_object_recursive),
393 105 => "prettier" => transform_value(value, sort_object_recursive),
394 106 => "eslintConfig" => transform_value(value, sort_object_recursive),
395 107 => "eslintIgnore",
396 108 => "standard" => transform_value(value, sort_object_recursive),
397 109 => "npmpkgjsonlint",
398 110 => "npmPackageJsonLintConfig",
399 111 => "npmpackagejsonlint",
400 112 => "release",
401 113 => "auto-changelog" => transform_value(value, sort_object_alphabetically),
403 114 => "remarkConfig" => transform_value(value, sort_object_alphabetically),
405 115 => "stylelint" => transform_value(value, sort_object_recursive),
406 116 => "typescript" => transform_value(value, sort_object_recursive),
407 117 => "typedoc" => transform_value(value, sort_object_recursive),
408 118 => "tshy" => transform_value(value, sort_object_alphabetically),
410 119 => "tsdown" => transform_value(value, sort_object_recursive),
411 120 => "size-limit",
412 121 => "ava" => transform_value(value, sort_object_recursive),
414 122 => "jest" => transform_value(value, sort_object_alphabetically),
416 123 => "jest-junit",
417 124 => "jest-stare",
418 125 => "mocha" => transform_value(value, sort_object_recursive),
419 126 => "nyc" => transform_value(value, sort_object_recursive),
420 127 => "c8" => transform_value(value, sort_object_recursive),
421 128 => "tap",
422 129 => "tsd" => transform_value(value, sort_object_recursive),
423 130 => "typeCoverage" => transform_value(value, sort_object_recursive),
424 131 => "oclif" => transform_value(value, sort_object_recursive),
425 132 => "languageName",
427 133 => "preferGlobal",
428 134 => "devEngines" => transform_value(value, sort_dev_engines),
429 135 => "engines" => transform_value(value, sort_object_alphabetically),
430 136 => "engineStrict",
431 137 => "volta" => transform_value(value, sort_object_recursive),
432 138 => "packageManager",
433 139 => "pnpm",
434 ]);
435 }
436
437 known.sort_unstable_by_key(|(idx, _, _)| *idx);
438 unknown.sort_unstable_by(|(a, _), (b, _)| {
441 let a_priv = a.starts_with('_');
442 let b_priv = b.starts_with('_');
443 a_priv.cmp(&b_priv).then_with(|| a.cmp(b))
444 });
445
446 let mut result = Vec::with_capacity(known.len() + unknown.len());
447 for (_, key, value) in known {
448 result.push((key, value));
449 }
450 for (key, value) in unknown {
451 result.push((key, value));
452 }
453 result
454}