@lntvow/sort-package-json 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ type ComparatorFunction = (left: string, right: string) => number
4
+
5
+ interface Options {
6
+ readonly sortOrder?: readonly string[] | ComparatorFunction
7
+ readonly sortScripts?: boolean
8
+ }
9
+
10
+ export interface SortPackageJson {
11
+ /**
12
+ * Sort packageJson object.
13
+ *
14
+ * @param packageJson - A packageJson.
15
+ * @param options - An options object.
16
+ * @returns Sorted packageJson object.
17
+ */
18
+ <T extends Record<any, any>>(packageJson: T, options?: Options): T
19
+
20
+ /**
21
+ * Sort packageJson string.
22
+ *
23
+ * @param packageJson - A packageJson string.
24
+ * @param options - An options object.
25
+ * @returns Sorted packageJson string.
26
+ */
27
+ (packageJson: string, options?: Options): string
28
+ }
29
+
30
+ declare const sortPackageJsonDefault: SortPackageJson
31
+ export default sortPackageJsonDefault
32
+
33
+ export const sortPackageJson: SortPackageJson
34
+ export const sortOrder: string[]
package/index.js ADDED
@@ -0,0 +1,636 @@
1
+ import fs from 'node:fs'
2
+ import sortObjectKeys from 'sort-object-keys'
3
+ import detectIndent from 'detect-indent'
4
+ import { detectNewlineGraceful as detectNewline } from 'detect-newline'
5
+ import gitHooks from 'git-hooks-list'
6
+ import isPlainObject from 'is-plain-obj'
7
+ import semverCompare from 'semver/functions/compare.js'
8
+ import semverMinVersion from 'semver/ranges/min-version.js'
9
+
10
+ const pipe =
11
+ (fns) =>
12
+ (x, ...args) =>
13
+ fns.reduce((result, fn) => fn(result, ...args), x)
14
+ const onArray = (fn) => (x) => (Array.isArray(x) ? fn(x) : x)
15
+ const onStringArray = (fn) => (x) =>
16
+ Array.isArray(x) && x.every((item) => typeof item === 'string') ? fn(x) : x
17
+ const uniq = onStringArray((xs) => [...new Set(xs)])
18
+ const sortArray = onStringArray((array) => array.toSorted())
19
+ const uniqAndSortArray = pipe([uniq, sortArray])
20
+ const onObject =
21
+ (fn) =>
22
+ (x, ...args) =>
23
+ isPlainObject(x) ? fn(x, ...args) : x
24
+ const sortObjectBy = (comparator, deep) => {
25
+ const over = onObject((object) => {
26
+ if (deep) {
27
+ object = Object.fromEntries(
28
+ Object.entries(object).map(([key, value]) => [key, over(value)]),
29
+ )
30
+ }
31
+
32
+ return sortObjectKeys(object, comparator)
33
+ })
34
+
35
+ return over
36
+ }
37
+ const objectGroupBy =
38
+ // eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax -- Safe
39
+ Object.groupBy ||
40
+ // Remove this when we drop support for Node.js 20
41
+ ((array, callback) => {
42
+ const result = Object.create(null)
43
+ for (const value of array) {
44
+ const key = callback(value)
45
+ if (result[key]) {
46
+ result[key].push(value)
47
+ } else {
48
+ result[key] = [value]
49
+ }
50
+ }
51
+ return result
52
+ })
53
+ const sortObject = sortObjectBy()
54
+ const sortURLObject = sortObjectBy(['type', 'url'])
55
+ const sortPeopleObject = sortObjectBy(['name', 'email', 'url'])
56
+ const sortDirectories = sortObjectBy([
57
+ 'lib',
58
+ 'bin',
59
+ 'man',
60
+ 'doc',
61
+ 'example',
62
+ 'test',
63
+ ])
64
+ const overProperty = (property, over) =>
65
+ onObject((object, ...args) =>
66
+ Object.hasOwn(object, property)
67
+ ? { ...object, [property]: over(object[property], ...args) }
68
+ : object,
69
+ )
70
+ const sortGitHooks = sortObjectBy(gitHooks)
71
+
72
+ const parseNameAndVersionRange = (specifier) => {
73
+ // Ignore anything after > & rely on fallback alphanumeric sorting for that
74
+ const [nameAndVersion] = specifier.split('>')
75
+ const atMatches = [...nameAndVersion.matchAll('@')]
76
+ if (
77
+ !atMatches.length ||
78
+ (atMatches.length === 1 && atMatches[0].index === 0)
79
+ ) {
80
+ return { name: specifier }
81
+ }
82
+ const splitIndex = atMatches.pop().index
83
+ return {
84
+ name: nameAndVersion.substring(0, splitIndex),
85
+ range: nameAndVersion.substring(splitIndex + 1),
86
+ }
87
+ }
88
+
89
+ const sortObjectBySemver = sortObjectBy((a, b) => {
90
+ const { name: aName, range: aRange } = parseNameAndVersionRange(a)
91
+ const { name: bName, range: bRange } = parseNameAndVersionRange(b)
92
+
93
+ if (aName !== bName) {
94
+ return aName.localeCompare(bName, 'en')
95
+ }
96
+ if (!aRange) {
97
+ return -1
98
+ }
99
+ if (!bRange) {
100
+ return 1
101
+ }
102
+ return semverCompare(semverMinVersion(aRange), semverMinVersion(bRange))
103
+ })
104
+
105
+ const getPackageName = (ident) => {
106
+ const index = ident.indexOf('@', ident.startsWith('@') ? 1 : 0)
107
+ // This should not happen, unless user manually edit the package.json file
108
+ return index === -1 ? ident : ident.slice(0, index)
109
+ }
110
+
111
+ const sortObjectByIdent = (a, b) => {
112
+ const packageNameA = getPackageName(a)
113
+ const packageNameB = getPackageName(b)
114
+
115
+ if (packageNameA < packageNameB) return -1
116
+ if (packageNameA > packageNameB) return 1
117
+ return 0
118
+ }
119
+
120
+ // Cache by `process.cwd()` instead of a variable to allow user call `process.chdir()`
121
+ const cache = new Map()
122
+ const hasYarnOrPnpmFiles = () => {
123
+ const cwd = process.cwd()
124
+ if (!cache.has(cwd)) {
125
+ cache.set(
126
+ cwd,
127
+ fs.existsSync('yarn.lock') ||
128
+ fs.existsSync('.yarn/') ||
129
+ fs.existsSync('.yarnrc.yml') ||
130
+ fs.existsSync('pnpm-lock.yaml') ||
131
+ fs.existsSync('pnpm-workspace.yaml'),
132
+ )
133
+ }
134
+ return cache.get(cwd)
135
+ }
136
+
137
+ /**
138
+ * Detects the package manager from package.json and lock files
139
+ * @param {object} packageJson - The parsed package.json object
140
+ * @returns {boolean} - The detected package manager. Default to npm if not detected.
141
+ */
142
+ function shouldSortDependenciesLikeNpm(packageJson) {
143
+ // https://github.com/nodejs/corepack
144
+ if (typeof packageJson.packageManager === 'string') {
145
+ return packageJson.packageManager.startsWith('npm@')
146
+ }
147
+
148
+ if (packageJson.devEngines?.packageManager?.name) {
149
+ return packageJson.devEngines.packageManager.name === 'npm'
150
+ }
151
+
152
+ if (packageJson.pnpm) {
153
+ return false
154
+ }
155
+
156
+ // Optimisation: Check if npm is explicit before reading FS.
157
+ if (packageJson.engines?.npm) {
158
+ return true
159
+ }
160
+
161
+ if (hasYarnOrPnpmFiles()) {
162
+ return false
163
+ }
164
+
165
+ return true
166
+ }
167
+
168
+ /**
169
+ * Sort dependencies alphabetically, detecting package manager to use the
170
+ * appropriate comparison. npm uses locale-aware comparison, yarn and pnpm use
171
+ * simple string comparison
172
+ */
173
+ const sortDependencies = onObject((dependencies, packageJson) => {
174
+ // Avoid file access
175
+ if (Object.keys(dependencies).length < 2) {
176
+ return dependencies
177
+ }
178
+
179
+ // sort deps like the npm CLI does (via the package @npmcli/package-json)
180
+ // https://github.com/npm/package-json/blob/b6465f44c727d6513db6898c7cbe41dd355cebe8/lib/update-dependencies.js#L8-L21
181
+ if (shouldSortDependenciesLikeNpm(packageJson)) {
182
+ return sortObjectKeys(dependencies, (a, b) => a.localeCompare(b, 'en'))
183
+ }
184
+
185
+ return sortObjectKeys(dependencies)
186
+ })
187
+
188
+ /**
189
+ * "workspaces" can be an array (npm or yarn classic) or an object (pnpm/bun).
190
+ * In the case of an array, we do not want to alphabetically sort it in case
191
+ * scripts need to run in a specific order.
192
+ *
193
+ * @see https://docs.npmjs.com/cli/v7/using-npm/workspaces?v=true#running-commands-in-the-context-of-workspaces
194
+ */
195
+ const sortWorkspaces = pipe([
196
+ sortObjectBy(['packages', 'catalog']),
197
+ overProperty('packages', uniqAndSortArray),
198
+ overProperty('catalog', sortDependencies),
199
+ ])
200
+
201
+ // https://github.com/eslint/eslint/blob/acc0e47572a9390292b4e313b4a4bf360d236358/conf/config-schema.js
202
+ const eslintBaseConfigProperties = [
203
+ // `files` and `excludedFiles` are only on `overrides[]`
204
+ // for easier sort `overrides[]`,
205
+ // add them to here, so we don't need sort `overrides[]` twice
206
+ 'files',
207
+ 'excludedFiles',
208
+ // baseConfig
209
+ 'env',
210
+ 'parser',
211
+ 'parserOptions',
212
+ 'settings',
213
+ 'plugins',
214
+ 'extends',
215
+ 'rules',
216
+ 'overrides',
217
+ 'globals',
218
+ 'processor',
219
+ 'noInlineConfig',
220
+ 'reportUnusedDisableDirectives',
221
+ ]
222
+ const sortEslintConfig = pipe([
223
+ sortObjectBy(eslintBaseConfigProperties),
224
+ overProperty('env', sortObject),
225
+ overProperty('globals', sortObject),
226
+ overProperty(
227
+ 'overrides',
228
+ onArray((overrides) => overrides.map(sortEslintConfig)),
229
+ ),
230
+ overProperty('parserOptions', sortObject),
231
+ overProperty(
232
+ 'rules',
233
+ sortObjectBy(
234
+ (rule1, rule2) =>
235
+ rule1.split('/').length - rule2.split('/').length ||
236
+ rule1.localeCompare(rule2),
237
+ ),
238
+ ),
239
+ overProperty('settings', sortObject),
240
+ ])
241
+ const sortVSCodeBadgeObject = sortObjectBy(['description', 'url', 'href'])
242
+
243
+ const sortPrettierConfig = pipe([
244
+ // sort keys alphabetically, but put `overrides` at bottom
245
+ onObject((config) =>
246
+ sortObjectKeys(config, [
247
+ ...Object.keys(config)
248
+ .filter((key) => key !== 'overrides')
249
+ .sort(),
250
+ 'overrides',
251
+ ]),
252
+ ),
253
+ // if `config.overrides` exists
254
+ overProperty(
255
+ 'overrides',
256
+ // and `config.overrides` is an array
257
+ onArray((overrides) =>
258
+ overrides.map(
259
+ pipe([
260
+ // sort `config.overrides[]` alphabetically
261
+ sortObject,
262
+ // sort `config.overrides[].options` alphabetically
263
+ overProperty('options', sortObject),
264
+ ]),
265
+ ),
266
+ ),
267
+ ),
268
+ ])
269
+
270
+ const sortVolta = sortObjectBy(['node', 'npm', 'yarn'])
271
+ const sortDevEngines = overProperty(
272
+ 'packageManager',
273
+ sortObjectBy(['name', 'version', 'onFail']),
274
+ )
275
+
276
+ const pnpmBaseConfigProperties = [
277
+ 'peerDependencyRules',
278
+ 'neverBuiltDependencies',
279
+ 'onlyBuiltDependencies',
280
+ 'onlyBuiltDependenciesFile',
281
+ 'allowedDeprecatedVersions',
282
+ 'allowNonAppliedPatches',
283
+ 'updateConfig',
284
+ 'auditConfig',
285
+ 'requiredScripts',
286
+ 'supportedArchitectures',
287
+ 'overrides',
288
+ 'patchedDependencies',
289
+ 'packageExtensions',
290
+ ]
291
+
292
+ const sortPnpmConfig = pipe([
293
+ sortObjectBy(pnpmBaseConfigProperties, true),
294
+ overProperty('overrides', sortObjectBySemver),
295
+ ])
296
+
297
+ // See https://docs.npmjs.com/misc/scripts
298
+ const defaultNpmScripts = new Set([
299
+ 'install',
300
+ 'pack',
301
+ 'prepare',
302
+ 'publish',
303
+ 'restart',
304
+ 'shrinkwrap',
305
+ 'start',
306
+ 'stop',
307
+ 'test',
308
+ 'uninstall',
309
+ 'version',
310
+ ])
311
+
312
+ const hasDevDependency = (dependency, packageJson) => {
313
+ return (
314
+ Object.hasOwn(packageJson, 'devDependencies') &&
315
+ Object.hasOwn(packageJson.devDependencies, dependency)
316
+ )
317
+ }
318
+
319
+ const runSRegExp =
320
+ /(?<=^|[\s&;<>|(])(?:run-s|npm-run-all2? .*(?:--sequential|--serial|-s))(?=$|[\s&;<>|)])/
321
+
322
+ const isSequentialScript = (command) =>
323
+ command.includes('*') && runSRegExp.test(command)
324
+
325
+ const hasSequentialScript = (packageJson) => {
326
+ if (
327
+ !hasDevDependency('npm-run-all', packageJson) &&
328
+ !hasDevDependency('npm-run-all2', packageJson)
329
+ ) {
330
+ return false
331
+ }
332
+ const scripts = ['scripts', 'betterScripts'].flatMap((field) =>
333
+ packageJson[field] ? Object.values(packageJson[field]) : [],
334
+ )
335
+ return scripts.some((script) => isSequentialScript(script))
336
+ }
337
+
338
+ function sortScriptNames(keys, prefix = '') {
339
+ const groupMap = new Map()
340
+ for (const key of keys) {
341
+ const rest = prefix ? key.slice(prefix.length + 1) : key
342
+ const idx = rest.indexOf(':')
343
+ if (idx > 0) {
344
+ const base = key.slice(0, (prefix ? prefix.length + 1 : 0) + idx)
345
+ if (!groupMap.has(base)) groupMap.set(base, [])
346
+ groupMap.get(base).push(key)
347
+ } else {
348
+ if (!groupMap.has(key)) groupMap.set(key, [])
349
+ groupMap.get(key).push(key)
350
+ }
351
+ }
352
+ return Array.from(groupMap.keys())
353
+ .sort()
354
+ .flatMap((groupKey) => {
355
+ const children = groupMap.get(groupKey)
356
+ if (
357
+ children.length > 1 &&
358
+ children.some((k) => k !== groupKey && k.startsWith(groupKey + ':'))
359
+ ) {
360
+ const direct = children
361
+ .filter((k) => k === groupKey || !k.startsWith(groupKey + ':'))
362
+ .sort()
363
+ const nested = children.filter((k) => k.startsWith(groupKey + ':'))
364
+ return [...direct, ...sortScriptNames(nested, groupKey)]
365
+ }
366
+ return children.sort()
367
+ })
368
+ }
369
+
370
+ const sortScripts = onObject((scripts, packageJson) => {
371
+ let names = Object.keys(scripts)
372
+ const prefixable = new Set()
373
+
374
+ names = names.map((name) => {
375
+ const omitted = name.replace(/^(?:pre|post)/, '')
376
+ if (defaultNpmScripts.has(omitted) || names.includes(omitted)) {
377
+ prefixable.add(omitted)
378
+ return omitted
379
+ }
380
+ return name
381
+ })
382
+
383
+ if (!hasSequentialScript(packageJson)) {
384
+ names = sortScriptNames(names)
385
+ }
386
+ names = names.flatMap((key) =>
387
+ prefixable.has(key) ? [`pre${key}`, key, `post${key}`] : [key],
388
+ )
389
+ return sortObjectKeys(scripts, names)
390
+ })
391
+
392
+ /*
393
+ - Move `default` condition to bottom
394
+ */
395
+ const sortConditions = (conditions) => {
396
+ const { defaultConditions = [], restConditions = [] } = objectGroupBy(
397
+ conditions,
398
+ (condition) => {
399
+ if (condition === 'default') {
400
+ return 'defaultConditions'
401
+ }
402
+
403
+ return 'restConditions'
404
+ },
405
+ )
406
+
407
+ return [...restConditions, ...defaultConditions]
408
+ }
409
+
410
+ const sortExports = onObject((exports) => {
411
+ const { paths = [], conditions = [] } = objectGroupBy(
412
+ Object.keys(exports),
413
+ (key) => (key.startsWith('.') ? 'paths' : 'conditions'),
414
+ )
415
+
416
+ return Object.fromEntries(
417
+ [...paths, ...sortConditions(conditions)].map((key) => [
418
+ key,
419
+ sortExports(exports[key]),
420
+ ]),
421
+ )
422
+ })
423
+
424
+ // fields marked `vscode` are for `Visual Studio Code extension manifest` only
425
+ // https://code.visualstudio.com/api/references/extension-manifest
426
+ // Supported fields:
427
+ // publisher, displayName, categories, galleryBanner, preview, contributes,
428
+ // activationEvents, badges, markdown, qna, extensionPack,
429
+ // extensionDependencies, icon
430
+
431
+ // field.key{string}: field name
432
+ // field.over{function}: sort field subKey
433
+ const fields = [
434
+ { key: '$schema' },
435
+ { key: 'name' },
436
+ /* vscode */ { key: 'displayName' },
437
+ { key: 'version' },
438
+ /* yarn */ { key: 'stableVersion' },
439
+ { key: 'private' },
440
+ { key: 'description' },
441
+ /* vscode */ { key: 'categories', over: uniq },
442
+ { key: 'keywords', over: uniq },
443
+ { key: 'homepage' },
444
+ { key: 'bugs', over: sortObjectBy(['url', 'email']) },
445
+ { key: 'repository', over: sortURLObject },
446
+ { key: 'funding', over: sortURLObject },
447
+ { key: 'license', over: sortURLObject },
448
+ /* vscode */ { key: 'qna' },
449
+ { key: 'author', over: sortPeopleObject },
450
+ {
451
+ key: 'maintainers',
452
+ over: onArray((maintainers) => maintainers.map(sortPeopleObject)),
453
+ },
454
+ {
455
+ key: 'contributors',
456
+ over: onArray((contributors) => contributors.map(sortPeopleObject)),
457
+ },
458
+ /* vscode */ { key: 'publisher' },
459
+ { key: 'sideEffects' },
460
+ { key: 'type' },
461
+ { key: 'imports' },
462
+ { key: 'exports', over: sortExports },
463
+ { key: 'main' },
464
+ { key: 'svelte' },
465
+ { key: 'umd:main' },
466
+ { key: 'jsdelivr' },
467
+ { key: 'unpkg' },
468
+ { key: 'module' },
469
+ { key: 'source' },
470
+ { key: 'jsnext:main' },
471
+ { key: 'browser' },
472
+ { key: 'react-native' },
473
+ { key: 'types' },
474
+ { key: 'typesVersions' },
475
+ { key: 'typings' },
476
+ { key: 'style' },
477
+ { key: 'example' },
478
+ { key: 'examplestyle' },
479
+ { key: 'assets' },
480
+ { key: 'bin', over: sortObject },
481
+ { key: 'man' },
482
+ { key: 'directories', over: sortDirectories },
483
+ { key: 'files', over: uniq },
484
+ { key: 'workspaces', over: sortWorkspaces },
485
+ // node-pre-gyp https://www.npmjs.com/package/node-pre-gyp#1-add-new-entries-to-your-packagejson
486
+ {
487
+ key: 'binary',
488
+ over: sortObjectBy([
489
+ 'module_name',
490
+ 'module_path',
491
+ 'remote_path',
492
+ 'package_name',
493
+ 'host',
494
+ ]),
495
+ },
496
+ { key: 'scripts', over: sortScripts },
497
+ { key: 'betterScripts', over: sortScripts },
498
+ /* vscode */ { key: 'l10n' },
499
+ /* vscode */ { key: 'contributes', over: sortObject },
500
+ /* vscode */ { key: 'activationEvents', over: uniq },
501
+ { key: 'husky', over: overProperty('hooks', sortGitHooks) },
502
+ { key: 'simple-git-hooks', over: sortGitHooks },
503
+ { key: 'pre-commit' },
504
+ { key: 'commitlint', over: sortObject },
505
+ { key: 'lint-staged' },
506
+ { key: 'nano-staged' },
507
+ { key: 'config', over: sortObject },
508
+ { key: 'nodemonConfig', over: sortObject },
509
+ { key: 'browserify', over: sortObject },
510
+ { key: 'babel', over: sortObject },
511
+ { key: 'browserslist' },
512
+ { key: 'xo', over: sortObject },
513
+ { key: 'prettier', over: sortPrettierConfig },
514
+ { key: 'eslintConfig', over: sortEslintConfig },
515
+ { key: 'eslintIgnore' },
516
+ { key: 'npmpkgjsonlint', over: sortObject },
517
+ { key: 'npmPackageJsonLintConfig', over: sortObject },
518
+ { key: 'npmpackagejsonlint', over: sortObject },
519
+ { key: 'release', over: sortObject },
520
+ { key: 'remarkConfig', over: sortObject },
521
+ { key: 'stylelint' },
522
+ { key: 'ava', over: sortObject },
523
+ { key: 'jest', over: sortObject },
524
+ { key: 'jest-junit', over: sortObject },
525
+ { key: 'jest-stare', over: sortObject },
526
+ { key: 'mocha', over: sortObject },
527
+ { key: 'nyc', over: sortObject },
528
+ { key: 'c8', over: sortObject },
529
+ { key: 'tap', over: sortObject },
530
+ { key: 'oclif', over: sortObjectBy(undefined, true) },
531
+ { key: 'resolutions', over: sortObject },
532
+ { key: 'overrides', over: sortDependencies },
533
+ { key: 'dependencies', over: sortDependencies },
534
+ { key: 'devDependencies', over: sortDependencies },
535
+ { key: 'dependenciesMeta', over: sortObjectBy(sortObjectByIdent, true) },
536
+ { key: 'peerDependencies', over: sortDependencies },
537
+ // TODO: only sort depth = 2
538
+ { key: 'peerDependenciesMeta', over: sortObjectBy(undefined, true) },
539
+ { key: 'optionalDependencies', over: sortDependencies },
540
+ { key: 'bundledDependencies', over: uniqAndSortArray },
541
+ { key: 'bundleDependencies', over: uniqAndSortArray },
542
+ /* vscode */ { key: 'extensionPack', over: uniqAndSortArray },
543
+ /* vscode */ { key: 'extensionDependencies', over: uniqAndSortArray },
544
+ { key: 'flat' },
545
+ { key: 'packageManager' },
546
+ { key: 'engines', over: sortObject },
547
+ { key: 'engineStrict', over: sortObject },
548
+ { key: 'devEngines', over: sortDevEngines },
549
+ { key: 'volta', over: sortVolta },
550
+ { key: 'languageName' },
551
+ { key: 'os' },
552
+ { key: 'cpu' },
553
+ { key: 'preferGlobal', over: sortObject },
554
+ { key: 'publishConfig', over: sortObject },
555
+ /* vscode */ { key: 'icon' },
556
+ /* vscode */ {
557
+ key: 'badges',
558
+ over: onArray((badge) => badge.map(sortVSCodeBadgeObject)),
559
+ },
560
+ /* vscode */ { key: 'galleryBanner', over: sortObject },
561
+ /* vscode */ { key: 'preview' },
562
+ /* vscode */ { key: 'markdown' },
563
+ { key: 'pnpm', over: sortPnpmConfig },
564
+ ]
565
+
566
+ const defaultSortOrder = fields.map(({ key }) => key)
567
+ const overFields = pipe(
568
+ fields
569
+ .map(({ key, over }) => (over ? overProperty(key, over) : undefined))
570
+ .filter(Boolean),
571
+ )
572
+ const overFieldsWithoutScripts = pipe(
573
+ fields
574
+ .map(({ key, over }) => {
575
+ if (!over) {
576
+ return undefined
577
+ }
578
+ if (key === 'scripts' || key === 'betterScripts') {
579
+ return undefined
580
+ }
581
+ return overProperty(key, over)
582
+ })
583
+ .filter(Boolean),
584
+ )
585
+
586
+ function editStringJSON(json, over) {
587
+ if (typeof json === 'string') {
588
+ const { indent, type } = detectIndent(json)
589
+ const endCharacters = json.slice(-1) === '\n' ? '\n' : ''
590
+ const newline = detectNewline(json)
591
+ json = JSON.parse(json)
592
+
593
+ let result =
594
+ JSON.stringify(over(json), null, type === 'tab' ? '\t' : indent) +
595
+ endCharacters
596
+ if (newline === '\r\n') {
597
+ result = result.replace(/\n/g, newline)
598
+ }
599
+ return result
600
+ }
601
+
602
+ return over(json)
603
+ }
604
+
605
+ function sortPackageJson(jsonIsh, options = {}) {
606
+ const shouldSortScripts = options.sortScripts ?? false
607
+ const overFieldsForOptions = shouldSortScripts
608
+ ? overFields
609
+ : overFieldsWithoutScripts
610
+
611
+ return editStringJSON(
612
+ jsonIsh,
613
+ onObject((json) => {
614
+ let sortOrder = options.sortOrder || defaultSortOrder
615
+
616
+ if (Array.isArray(sortOrder)) {
617
+ const keys = Object.keys(json)
618
+ const { privateKeys = [], publicKeys = [] } = objectGroupBy(
619
+ keys,
620
+ (key) => (key[0] === '_' ? 'privateKeys' : 'publicKeys'),
621
+ )
622
+ sortOrder = [
623
+ ...sortOrder,
624
+ ...defaultSortOrder,
625
+ ...publicKeys.sort(),
626
+ ...privateKeys.sort(),
627
+ ]
628
+ }
629
+
630
+ return overFieldsForOptions(sortObjectKeys(json, sortOrder), json)
631
+ }),
632
+ )
633
+ }
634
+
635
+ export default sortPackageJson
636
+ export { sortPackageJson, defaultSortOrder as sortOrder }