You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
9.8 KiB

const sortObjectKeys = require('sort-object-keys')
const detectIndent = require('detect-indent')
const detectNewline = require('detect-newline').graceful
const gitHooks = require('git-hooks-list')
const isPlainObject = require('is-plain-obj')
const hasOwnProperty = (object, property) =>
Object.prototype.hasOwnProperty.call(object, property)
const pipe = (fns) => (x) => fns.reduce((result, fn) => fn(result), x)
const onArray = (fn) => (x) => (Array.isArray(x) ? fn(x) : x)
const onStringArray = (fn) => (x) =>
Array.isArray(x) && x.every((item) => typeof item === 'string') ? fn(x) : x
const uniq = onStringArray((xs) => xs.filter((x, i) => i === xs.indexOf(x)))
const sortArray = onStringArray((array) => [...array].sort())
const uniqAndSortArray = pipe([uniq, sortArray])
const onObject = (fn) => (x) => (isPlainObject(x) ? fn(x) : x)
const sortObjectBy = (comparator, deep) => {
const over = onObject((object) => {
object = sortObjectKeys(object, comparator)
if (deep) {
for (const [key, value] of Object.entries(object)) {
object[key] = over(value)
}
}
return object
})
return over
}
const sortObject = sortObjectBy()
const sortURLObject = sortObjectBy(['type', 'url'])
const sortPeopleObject = sortObjectBy(['name', 'email', 'url'])
const sortDirectories = sortObjectBy([
'lib',
'bin',
'man',
'doc',
'example',
'test',
])
const overProperty = (property, over) => (object) =>
hasOwnProperty(object, property)
? Object.assign(object, { [property]: over(object[property]) })
: object
const sortGitHooks = sortObjectBy(gitHooks)
// https://github.com/eslint/eslint/blob/acc0e47572a9390292b4e313b4a4bf360d236358/conf/config-schema.js
const eslintBaseConfigProperties = [
// `files` and `excludedFiles` are only on `overrides[]`
// for easier sort `overrides[]`,
// add them to here, so we don't need sort `overrides[]` twice
'files',
'excludedFiles',
// baseConfig
'env',
'parser',
'parserOptions',
'settings',
'plugins',
'extends',
'rules',
'overrides',
'globals',
'processor',
'noInlineConfig',
'reportUnusedDisableDirectives',
]
const sortEslintConfig = onObject(
pipe([
sortObjectBy(eslintBaseConfigProperties),
overProperty('env', sortObject),
overProperty('globals', sortObject),
overProperty(
'overrides',
onArray((overrides) => overrides.map(sortEslintConfig)),
),
overProperty('parserOptions', sortObject),
overProperty(
'rules',
sortObjectBy(
(rule1, rule2) =>
rule1.split('/').length - rule2.split('/').length ||
rule1.localeCompare(rule2),
),
),
overProperty('settings', sortObject),
]),
)
const sortVSCodeBadgeObject = sortObjectBy(['description', 'url', 'href'])
const sortPrettierConfig = onObject(
pipe([
// sort keys alphabetically, but put `overrides` at bottom
(config) =>
sortObjectKeys(config, [
...Object.keys(config)
.filter((key) => key !== 'overrides')
.sort(),
'overrides',
]),
// if `config.overrides` exists
overProperty(
'overrides',
// and `config.overrides` is an array
onArray((overrides) =>
overrides.map(
pipe([
// sort `config.overrides[]` alphabetically
sortObject,
// sort `config.overrides[].options` alphabetically
overProperty('options', sortObject),
]),
),
),
),
]),
)
// See https://docs.npmjs.com/misc/scripts
const defaultNpmScripts = new Set([
'install',
'pack',
'prepare',
'publish',
'restart',
'shrinkwrap',
'start',
'stop',
'test',
'uninstall',
'version',
])
const sortScripts = onObject((scripts) => {
const names = Object.keys(scripts)
const prefixable = new Set()
const keys = names
.map((name) => {
const omitted = name.replace(/^(?:pre|post)/, '')
if (defaultNpmScripts.has(omitted) || names.includes(omitted)) {
prefixable.add(omitted)
return omitted
}
return name
})
.sort()
const order = keys.reduce(
(order, key) =>
order.concat(
prefixable.has(key) ? [`pre${key}`, key, `post${key}`] : [key],
),
[],
)
return sortObjectKeys(scripts, order)
})
// fields marked `vscode` are for `Visual Studio Code extension manifest` only
// https://code.visualstudio.com/api/references/extension-manifest
// Supported fields:
// publisher, displayName, categories, galleryBanner, preview, contributes,
// activationEvents, badges, markdown, qna, extensionPack,
// extensionDependencies, icon
// field.key{string}: field name
// field.over{function}: sort field subKey
const fields = [
{ key: 'name' },
/* vscode */ { key: 'displayName' },
{ key: 'version' },
{ key: 'private' },
{ key: 'description' },
/* vscode */ { key: 'categories', over: uniq },
{ key: 'keywords', over: uniq },
{ key: 'homepage' },
{ key: 'bugs', over: sortObjectBy(['url', 'email']) },
{ key: 'repository', over: sortURLObject },
{ key: 'funding', over: sortURLObject },
{ key: 'license', over: sortURLObject },
/* vscode */ { key: 'qna' },
{ key: 'author', over: sortPeopleObject },
{
key: 'contributors',
over: onArray((contributors) => contributors.map(sortPeopleObject)),
},
/* vscode */ { key: 'publisher' },
{ key: 'sideEffects' },
{ key: 'type' },
{ key: 'exports', over: sortObject },
{ key: 'main' },
{ key: 'umd:main' },
{ key: 'jsdelivr' },
{ key: 'unpkg' },
{ key: 'module' },
{ key: 'source' },
{ key: 'jsnext:main' },
{ key: 'browser' },
{ key: 'types' },
{ key: 'typings' },
{ key: 'style' },
{ key: 'example' },
{ key: 'examplestyle' },
{ key: 'assets' },
{ key: 'bin', over: sortObject },
{ key: 'man' },
{ key: 'directories', over: sortDirectories },
{ key: 'files', over: uniq },
{ key: 'workspaces' },
// node-pre-gyp https://www.npmjs.com/package/node-pre-gyp#1-add-new-entries-to-your-packagejson
{
key: 'binary',
over: sortObjectBy([
'module_name',
'module_path',
'remote_path',
'package_name',
'host',
]),
},
{ key: 'scripts', over: sortScripts },
{ key: 'betterScripts', over: sortScripts },
/* vscode */ { key: 'contributes', over: sortObject },
/* vscode */ { key: 'activationEvents', over: uniq },
{ key: 'husky', over: overProperty('hooks', sortGitHooks) },
{ key: 'pre-commit' },
{ key: 'commitlint', over: sortObject },
{ key: 'lint-staged', over: sortObject },
{ key: 'config', over: sortObject },
{ key: 'nodemonConfig', over: sortObject },
{ key: 'browserify', over: sortObject },
{ key: 'babel', over: sortObject },
{ key: 'browserslist' },
{ key: 'xo', over: sortObject },
{ key: 'prettier', over: sortPrettierConfig },
{ key: 'eslintConfig', over: sortEslintConfig },
{ key: 'eslintIgnore' },
{ key: 'npmpkgjsonlint', over: sortObject },
{ key: 'remarkConfig', over: sortObject },
{ key: 'stylelint' },
{ key: 'ava', over: sortObject },
{ key: 'jest', over: sortObject },
{ key: 'mocha', over: sortObject },
{ key: 'nyc', over: sortObject },
{ key: 'resolutions', over: sortObject },
{ key: 'dependencies', over: sortObject },
{ key: 'devDependencies', over: sortObject },
{ key: 'dependenciesMeta', over: sortObjectBy(undefined, true) },
{ key: 'peerDependencies', over: sortObject },
// TODO: only sort depth = 2
{ key: 'peerDependenciesMeta', over: sortObjectBy(undefined, true) },
{ key: 'optionalDependencies', over: sortObject },
{ key: 'bundledDependencies', over: uniqAndSortArray },
{ key: 'bundleDependencies', over: uniqAndSortArray },
/* vscode */ { key: 'extensionPack', over: uniqAndSortArray },
/* vscode */ { key: 'extensionDependencies', over: uniqAndSortArray },
{ key: 'flat' },
{ key: 'engines', over: sortObject },
{ key: 'engineStrict', over: sortObject },
{ key: 'languageName' },
{ key: 'os' },
{ key: 'cpu' },
{ key: 'preferGlobal', over: sortObject },
{ key: 'publishConfig', over: sortObject },
/* vscode */ { key: 'icon' },
/* vscode */ {
key: 'badges',
over: onArray((badge) => badge.map(sortVSCodeBadgeObject)),
},
/* vscode */ { key: 'galleryBanner', over: sortObject },
/* vscode */ { key: 'preview' },
/* vscode */ { key: 'markdown' },
]
const defaultSortOrder = fields.map(({ key }) => key)
const overFields = pipe(
fields.reduce((fns, { key, over }) => {
if (over) {
fns.push(overProperty(key, over))
}
return fns
}, []),
)
function editStringJSON(json, over) {
if (typeof json === 'string') {
const { indent } = detectIndent(json)
const endCharacters = json.slice(-1) === '\n' ? '\n' : ''
const newline = detectNewline(json)
json = JSON.parse(json)
let result = JSON.stringify(over(json), null, indent) + endCharacters
if (newline === '\r\n') {
result = result.replace(/\n/g, newline)
}
return result
}
return over(json)
}
const isPrivateKey = (key) => key[0] === '_'
const partition = (array, predicate) =>
array.reduce(
(result, value) => {
result[predicate(value) ? 0 : 1].push(value)
return result
},
[[], []],
)
function sortPackageJson(jsonIsh, options = {}) {
return editStringJSON(
jsonIsh,
onObject((json) => {
let sortOrder = options.sortOrder ? options.sortOrder : defaultSortOrder
if (Array.isArray(sortOrder)) {
const keys = Object.keys(json)
const [privateKeys, publicKeys] = partition(keys, isPrivateKey)
sortOrder = [
...sortOrder,
...defaultSortOrder,
...publicKeys.sort(),
...privateKeys.sort(),
]
}
return overFields(sortObjectKeys(json, sortOrder))
}),
)
}
module.exports = sortPackageJson
module.exports.sortPackageJson = sortPackageJson
module.exports.sortOrder = defaultSortOrder
module.exports.default = sortPackageJson