Skip to content

Commit e3f2370

Browse files
Tobbefisker
andauthored
feat: sort dependencies according to detected package manager, support devEngines field (#382)
Co-authored-by: fisker <lionkay@gmail.com>
1 parent 0b528c2 commit e3f2370

File tree

5 files changed

+291
-69
lines changed

5 files changed

+291
-69
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ The default order is exported as a `sortOrder` object.
407407
1. `packageManager`
408408
1. `engines`
409409
1. `engineStrict`
410+
1. `devEngines`
410411
1. `volta`
411412
1. `languageName`
412413
1. `os`

index.js

Lines changed: 132 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'node:fs'
12
import sortObjectKeys from 'sort-object-keys'
23
import detectIndent from 'detect-indent'
34
import { detectNewlineGraceful as detectNewline } from 'detect-newline'
@@ -60,12 +61,12 @@ const sortDirectories = sortObjectBy([
6061
'example',
6162
'test',
6263
])
63-
const overProperty =
64-
(property, over) =>
65-
(object, ...args) =>
64+
const overProperty = (property, over) =>
65+
onObject((object, ...args) =>
6666
Object.hasOwn(object, property)
6767
? { ...object, [property]: over(object[property], ...args) }
68-
: object
68+
: object,
69+
)
6970
const sortGitHooks = sortObjectBy(gitHooks)
7071

7172
const parseNameAndVersionRange = (specifier) => {
@@ -116,9 +117,73 @@ const sortObjectByIdent = (a, b) => {
116117
return 0
117118
}
118119

119-
// sort deps like the npm CLI does (via the package @npmcli/package-json)
120-
// https://github.com/npm/package-json/blob/b6465f44c727d6513db6898c7cbe41dd355cebe8/lib/update-dependencies.js#L8-L21
121-
const sortDependenciesLikeNpm = sortObjectBy((a, b) => a.localeCompare(b, 'en'))
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+
})
122187

123188
/**
124189
* "workspaces" can be an array (npm or yarn classic) or an object (pnpm/bun).
@@ -127,13 +192,11 @@ const sortDependenciesLikeNpm = sortObjectBy((a, b) => a.localeCompare(b, 'en'))
127192
*
128193
* @see https://docs.npmjs.com/cli/v7/using-npm/workspaces?v=true#running-commands-in-the-context-of-workspaces
129194
*/
130-
const sortWorkspaces = onObject(
131-
pipe([
132-
sortObjectBy(['packages', 'catalog']),
133-
overProperty('packages', uniqAndSortArray),
134-
overProperty('catalog', sortDependenciesLikeNpm),
135-
]),
136-
)
195+
const sortWorkspaces = pipe([
196+
sortObjectBy(['packages', 'catalog']),
197+
overProperty('packages', uniqAndSortArray),
198+
overProperty('catalog', sortDependencies),
199+
])
137200

138201
// https://github.com/eslint/eslint/blob/acc0e47572a9390292b4e313b4a4bf360d236358/conf/config-schema.js
139202
const eslintBaseConfigProperties = [
@@ -156,58 +219,59 @@ const eslintBaseConfigProperties = [
156219
'noInlineConfig',
157220
'reportUnusedDisableDirectives',
158221
]
159-
const sortEslintConfig = onObject(
160-
pipe([
161-
sortObjectBy(eslintBaseConfigProperties),
162-
overProperty('env', sortObject),
163-
overProperty('globals', sortObject),
164-
overProperty(
165-
'overrides',
166-
onArray((overrides) => overrides.map(sortEslintConfig)),
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),
167237
),
168-
overProperty('parserOptions', sortObject),
169-
overProperty(
170-
'rules',
171-
sortObjectBy(
172-
(rule1, rule2) =>
173-
rule1.split('/').length - rule2.split('/').length ||
174-
rule1.localeCompare(rule2),
175-
),
176-
),
177-
overProperty('settings', sortObject),
178-
]),
179-
)
238+
),
239+
overProperty('settings', sortObject),
240+
])
180241
const sortVSCodeBadgeObject = sortObjectBy(['description', 'url', 'href'])
181242

182-
const sortPrettierConfig = onObject(
183-
pipe([
184-
// sort keys alphabetically, but put `overrides` at bottom
185-
(config) =>
186-
sortObjectKeys(config, [
187-
...Object.keys(config)
188-
.filter((key) => key !== 'overrides')
189-
.sort(),
190-
'overrides',
191-
]),
192-
// if `config.overrides` exists
193-
overProperty(
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(),
194250
'overrides',
195-
// and `config.overrides` is an array
196-
onArray((overrides) =>
197-
overrides.map(
198-
pipe([
199-
// sort `config.overrides[]` alphabetically
200-
sortObject,
201-
// sort `config.overrides[].options` alphabetically
202-
overProperty('options', sortObject),
203-
]),
204-
),
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+
]),
205265
),
206266
),
207-
]),
208-
)
267+
),
268+
])
209269

210270
const sortVolta = sortObjectBy(['node', 'npm', 'yarn'])
271+
const sortDevEngines = overProperty(
272+
'packageManager',
273+
sortObjectBy(['name', 'version', 'onFail']),
274+
)
211275

212276
const pnpmBaseConfigProperties = [
213277
'peerDependencyRules',
@@ -225,12 +289,10 @@ const pnpmBaseConfigProperties = [
225289
'packageExtensions',
226290
]
227291

228-
const sortPnpmConfig = onObject(
229-
pipe([
230-
sortObjectBy(pnpmBaseConfigProperties, true),
231-
overProperty('overrides', sortObjectBySemver),
232-
]),
233-
)
292+
const sortPnpmConfig = pipe([
293+
sortObjectBy(pnpmBaseConfigProperties, true),
294+
overProperty('overrides', sortObjectBySemver),
295+
])
234296

235297
// See https://docs.npmjs.com/misc/scripts
236298
const defaultNpmScripts = new Set([
@@ -473,14 +535,14 @@ const fields = [
473535
{ key: 'tap', over: sortObject },
474536
{ key: 'oclif', over: sortObjectBy(undefined, true) },
475537
{ key: 'resolutions', over: sortObject },
476-
{ key: 'overrides', over: sortDependenciesLikeNpm },
477-
{ key: 'dependencies', over: sortDependenciesLikeNpm },
478-
{ key: 'devDependencies', over: sortDependenciesLikeNpm },
538+
{ key: 'overrides', over: sortDependencies },
539+
{ key: 'dependencies', over: sortDependencies },
540+
{ key: 'devDependencies', over: sortDependencies },
479541
{ key: 'dependenciesMeta', over: sortObjectBy(sortObjectByIdent, true) },
480-
{ key: 'peerDependencies', over: sortDependenciesLikeNpm },
542+
{ key: 'peerDependencies', over: sortDependencies },
481543
// TODO: only sort depth = 2
482544
{ key: 'peerDependenciesMeta', over: sortObjectBy(undefined, true) },
483-
{ key: 'optionalDependencies', over: sortDependenciesLikeNpm },
545+
{ key: 'optionalDependencies', over: sortDependencies },
484546
{ key: 'bundledDependencies', over: uniqAndSortArray },
485547
{ key: 'bundleDependencies', over: uniqAndSortArray },
486548
/* vscode */ { key: 'extensionPack', over: uniqAndSortArray },
@@ -489,6 +551,7 @@ const fields = [
489551
{ key: 'packageManager' },
490552
{ key: 'engines', over: sortObject },
491553
{ key: 'engineStrict', over: sortObject },
554+
{ key: 'devEngines', over: sortDevEngines },
492555
{ key: 'volta', over: sortVolta },
493556
{ key: 'languageName' },
494557
{ key: 'os' },

0 commit comments

Comments
 (0)