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
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
|