1+ import fs from 'node:fs'
12import sortObjectKeys from 'sort-object-keys'
23import detectIndent from 'detect-indent'
34import { 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+ )
6970const sortGitHooks = sortObjectBy ( gitHooks )
7071
7172const 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
139202const 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+ ] )
180241const 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
210270const sortVolta = sortObjectBy ( [ 'node' , 'npm' , 'yarn' ] )
271+ const sortDevEngines = overProperty (
272+ 'packageManager' ,
273+ sortObjectBy ( [ 'name' , 'version' , 'onFail' ] ) ,
274+ )
211275
212276const 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
236298const 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