From 553dfe12f3f50a840a6edc1ddde663ae20cfb667 Mon Sep 17 00:00:00 2001 From: Aaron Dill <117116764+aarondill@users.noreply.github.com> Date: Tue, 4 Mar 2025 09:34:56 -0600 Subject: [PATCH] refactor(cli): use `util.parseArgs` to parse arguments (#283) * feat: use util.parseArgs * refactor: destrucure argument in parseCLIArguments This moved argument destructuring to the parseCliArguments function, renames values to options, renames positionals to patterns, and moves the try catch to the run function * refactor: move default pattern to parseCliArgument * fix: typo isQuiet->shouldBeQuiet * fix: show help on illegal option value * test: added tests for illegal argument usage * feat: add support for --no-check and --no-quiet * Revert "feat: add support for --no-check and --no-quiet" This reverts commit 10a60672d5346ae160e6daa6e96f484c42e67b4f. * test: add tests which throw for --no-* arguments * test: update tests * test: update snapshots * style: linting * refactor: use `Object.hasOwn` --------- Co-authored-by: fisker Cheung --- cli.js | 97 ++++++++++++----------- eslint.config.js | 1 + index.js | 11 +-- reporter.js | 4 +- tests/cli.js | 47 +++++++++++ tests/snapshots/cli.js.md | 154 ++++++++++++++++++++++++++++++++++++ tests/snapshots/cli.js.snap | Bin 4121 -> 4672 bytes 7 files changed, 258 insertions(+), 56 deletions(-) diff --git a/cli.js b/cli.js index ef3c9b10..5e24cefc 100755 --- a/cli.js +++ b/cli.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import { globSync } from 'tinyglobby' import fs from 'node:fs' +import { parseArgs } from 'node:util' import getStdin from 'get-stdin' import sortPackageJson from './index.js' import Reporter from './reporter.js' @@ -30,6 +31,32 @@ If file/glob is omitted, './package.json' file will be processed. ) } +function parseCliArguments() { + const { values: options, positionals: patterns } = parseArgs({ + options: { + check: { type: 'boolean', short: 'c', default: false }, + quiet: { type: 'boolean', short: 'q', default: false }, + stdin: { type: 'boolean', default: false }, + ignore: { + type: 'string', + short: 'i', + multiple: true, + default: ['node_modules/**'], + }, + version: { type: 'boolean', short: 'v', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + allowPositionals: true, + strict: true, + }) + + if (patterns.length === 0) { + patterns[0] = 'package.json' + } + + return { options, patterns } +} + function sortPackageJsonFile(file, reporter, isCheck) { const original = fs.readFileSync(file, 'utf8') const sorted = sortPackageJson(original) @@ -46,6 +73,7 @@ function sortPackageJsonFile(file, reporter, isCheck) { function sortPackageJsonFiles(patterns, { ignore, ...options }) { const files = globSync(patterns, { ignore }) + const reporter = new Reporter(files, options) const { isCheck } = options @@ -64,61 +92,38 @@ async function sortPackageJsonFromStdin() { } function run() { - const cliArguments = process.argv - .slice(2) - .map((arg) => arg.split('=')) - .flat() - - if ( - cliArguments.some((argument) => argument === '--help' || argument === '-h') - ) { + let options, patterns + try { + ;({ options, patterns } = parseCliArguments()) + } catch (error) { + process.exitCode = 2 + console.error(error.message) + if ( + error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION' || + error.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' + ) { + console.error(`Try 'sort-package-json --help' for more information.`) + } + return + } + + if (options.help) { return showHelpInformation() } - if ( - cliArguments.some( - (argument) => argument === '--version' || argument === '-v', - ) - ) { + if (options.version) { return showVersion() } - if (cliArguments.some((argument) => argument === '--stdin')) { + if (options.stdin) { return sortPackageJsonFromStdin() } - const patterns = [] - const ignore = [] - let isCheck = false - let shouldBeQuiet = false - - let lastArg - for (const argument of cliArguments) { - if (lastArg === '--ignore' || lastArg === '-i') { - ignore.push(argument) - lastArg = undefined - continue - } - if (argument === '--check' || argument === '-c') { - isCheck = true - } else if (argument === '--quiet' || argument === '-q') { - shouldBeQuiet = true - } else if (argument === '--ignore' || argument === '-i') { - lastArg = argument - } else { - patterns.push(argument) - } - } - - if (!patterns.length) { - patterns[0] = 'package.json' - } - - if (!ignore.length) { - ignore[0] = 'node_modules' - } - - sortPackageJsonFiles(patterns, { ignore, isCheck, shouldBeQuiet }) + sortPackageJsonFiles(patterns, { + ignore: options.ignore, + isCheck: options.check, + shouldBeQuiet: options.quiet, + }) } run() diff --git a/eslint.config.js b/eslint.config.js index ea8730c6..a34199c0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,7 @@ export default [ languageOptions: { globals: { ...globals.builtin, ...globals.node }, }, + settings: { node: { version: '20' } }, }, { ignores: ['index.cjs'] }, ] diff --git a/index.js b/index.js index 4039bdd0..cfc03a93 100755 --- a/index.js +++ b/index.js @@ -5,11 +5,6 @@ import gitHooks from 'git-hooks-list' import isPlainObject from 'is-plain-obj' import semver from 'semver' -const hasOwn = - // eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax -- will enable later - Object.hasOwn || - // TODO: Remove this when we drop supported for Node.js v14 - ((object, property) => Object.prototype.hasOwnProperty.call(object, property)) const pipe = (fns) => (x, ...args) => @@ -51,7 +46,7 @@ const sortDirectories = sortObjectBy([ const overProperty = (property, over) => (object, ...args) => - hasOwn(object, property) + Object.hasOwn(object, property) ? { ...object, [property]: over(object[property], ...args) } : object const sortGitHooks = sortObjectBy(gitHooks) @@ -218,8 +213,8 @@ const defaultNpmScripts = new Set([ const hasDevDependency = (dependency, packageJson) => { return ( - hasOwn(packageJson, 'devDependencies') && - hasOwn(packageJson.devDependencies, dependency) + Object.hasOwn(packageJson, 'devDependencies') && + Object.hasOwn(packageJson.devDependencies, dependency) ) } diff --git a/reporter.js b/reporter.js index d61e1eb2..e09053dc 100644 --- a/reporter.js +++ b/reporter.js @@ -60,7 +60,7 @@ class Reporter { return } - const { isCheck, isQuiet } = this.#options + const { isCheck, shouldBeQuiet } = this.#options if (isCheck && changedFilesCount) { process.exitCode = 1 @@ -70,7 +70,7 @@ class Reporter { process.exitCode = 2 } - if (isQuiet) { + if (shouldBeQuiet) { return } diff --git a/tests/cli.js b/tests/cli.js index a4234d1a..6410e05d 100644 --- a/tests/cli.js +++ b/tests/cli.js @@ -63,6 +63,31 @@ test('run `cli --help` with `--version`', macro.testCLI, { message: 'Should prioritize help over version.', }) +test('run `cli --help=value`', macro.testCLI, { + args: ['--help=value'], + message: 'Should report illegal argument and suggest help.', +}) + +test('run `cli --version=true`', macro.testCLI, { + args: ['--version=true'], + message: 'Should report illegal argument and suggest help.', +}) + +test('run `cli --unknown-option`', macro.testCLI, { + args: ['--unknown-option'], + message: 'Should report unknown option and suggest help.', +}) + +test('run `cli -u` with unknown option', macro.testCLI, { + args: ['-u'], + message: 'Should report unknown option and suggest help.', +}) + +test('run `cli --no-version`', macro.testCLI, { + args: ['--no-version'], + message: 'A snapshot to show how `--no-*` works, not care about result.', +}) + test('run `cli` with no patterns', macro.testCLI, { fixtures: [ { @@ -87,6 +112,11 @@ test('run `cli --quiet` with no patterns', macro.testCLI, { message: 'Should format package.json without message.', }) +test('run `cli --quiet=value`', macro.testCLI, { + args: ['--quiet=value'], + message: 'Should report illegal argument and suggest help.', +}) + test('run `cli -q` with no patterns', macro.testCLI, { fixtures: [ { @@ -111,6 +141,11 @@ test('run `cli --check` with no patterns', macro.testCLI, { message: 'Should not sort package.json', }) +test('run `cli --check=value`', macro.testCLI, { + args: ['--check=value'], + message: 'Should report illegal argument and suggest help.', +}) + test('run `cli --check --quiet` with no patterns', macro.testCLI, { fixtures: [ { @@ -147,6 +182,18 @@ test('run `cli -c -q` with no patterns', macro.testCLI, { message: 'Should support `-q` alias', }) +test('run `cli -cq` with no patterns', macro.testCLI, { + fixtures: [ + { + file: 'package.json', + content: badJson, + expect: badJson, + }, + ], + args: ['-cq'], + message: 'Should support option aggregation', +}) + test('run `cli` on 1 bad file', macro.testCLI, { fixtures: [ { diff --git a/tests/snapshots/cli.js.md b/tests/snapshots/cli.js.md index 160f1778..56074083 100644 --- a/tests/snapshots/cli.js.md +++ b/tests/snapshots/cli.js.md @@ -217,6 +217,96 @@ Generated by [AVA](https://avajs.dev). }, } +## run `cli --help=value` + +> Should report illegal argument and suggest help. + + { + args: [ + '--help=value', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Option '-h, --help' does not take an argument␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + +## run `cli --version=true` + +> Should report illegal argument and suggest help. + + { + args: [ + '--version=true', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Option '-v, --version' does not take an argument␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + +## run `cli --unknown-option` + +> Should report unknown option and suggest help. + + { + args: [ + '--unknown-option', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Unknown option '--unknown-option'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "--unknown-option"␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + +## run `cli -u` with unknown option + +> Should report unknown option and suggest help. + + { + args: [ + '-u', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Unknown option '-u'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "-u"␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + +## run `cli --no-version` + +> A snapshot to show how `--no-*` works, not care about result. + + { + args: [ + '--no-version', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Unknown option '--no-version'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "--no-version"␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + ## run `cli` with no patterns > Should format package.json. @@ -275,6 +365,24 @@ Generated by [AVA](https://avajs.dev). }, } +## run `cli --quiet=value` + +> Should report illegal argument and suggest help. + + { + args: [ + '--quiet=value', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Option '-q, --quiet' does not take an argument␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + ## run `cli -q` with no patterns > Should support -q alias. @@ -335,6 +443,24 @@ Generated by [AVA](https://avajs.dev). }, } +## run `cli --check=value` + +> Should report illegal argument and suggest help. + + { + args: [ + '--check=value', + ], + fixtures: [], + result: { + errorCode: 2, + stderr: `Option '-c, --check' does not take an argument␊ + Try 'sort-package-json --help' for more information.␊ + `, + stdout: '', + }, + } + ## run `cli --check --quiet` with no patterns > Should not sort package.json or report a message. @@ -425,6 +551,34 @@ Generated by [AVA](https://avajs.dev). }, } +## run `cli -cq` with no patterns + +> Should support option aggregation + + { + args: [ + '-cq', + ], + fixtures: [ + { + expect: `{␊ + "version": "1.0.0",␊ + "name": "sort-package-json"␊ + }`, + file: 'package.json', + original: `{␊ + "version": "1.0.0",␊ + "name": "sort-package-json"␊ + }`, + }, + ], + result: { + errorCode: 1, + stderr: '', + stdout: '', + }, + } + ## run `cli` on 1 bad file > Should format 1 file. diff --git a/tests/snapshots/cli.js.snap b/tests/snapshots/cli.js.snap index 077c58d9b20aae025717ddba8f76f34ae3e78092..2faf098c9c67c237162c10ba8ff5451043173407 100644 GIT binary patch literal 4672 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 Cn-rY@ literal 4121 zcmV+!5a#beRzVO z?^i(Nf**?r00000000B+oq22=)g8ybZ)VmuYfKUdG$f>Xq=7)N9h?Lx6euAHG&D_0 z67I|6-LXAnJ+qmabqpa@IZ8`aP*rHf(W)Vp+L8(?P-&^ejS!_sT%xGZ7An-D3J?^8 z+6uL(c{6YJnCq?A-u1>_|Ks(}+w~m3&+q&D{oZx=;6TpIY_o4)wnuYxC#U7Df#2$+Kj%gZ+>WqMQieQ~;X+W*IhhOzJgapBZJJ8PzOX zH;uAy)%+Vo`7zeE$9A&1LA{^NnwkyVMavwaZj&^nX)UJ?Xt}X@TZYX-E=w&f&u+VW z{v(=E=;dgQ%?q&Q-;yZCy6eQ)y|Iiv8_k&f9{`^MIFw9)vt9zMBEVJxSYpnkSkBzH z3Gv@==eMv0U<<&pV`BiM=BQ>_dR8}vsCx^1B6B4mhod0uoIXO=J-+w&o~$;i<;*;L zlKvB4SQ+m4wq+DX1~jYajTeNUcS7Dl9%?!tJZ~aCxR2-d7Z9LOgAXo$_EVL5jLRsN zMjJ&H5%6e~IovjV+`z;m;L+xK)C&WlnE$LRqp@Qm{C$-H8F`}VoWX?4+uVSUOsCH>3r3dqxwhS#_I~@OJ6Oo&uHsF5)9yvSMu0a7aDoI} zE+u=Mh>L2|#`0RGcv>gzOjEkpP)D@Ir_jYA2e7!C9p(ncTJ$mDSe)J!_*R9TwExF} z<(C?0*kM}wkZ!2C$`c6vhwkLlO7G%eEZ|@an%0O~>5qy$EH2Y2FP*7|i#r+~&^{j% z(EbmAoGemAT0np!i2#t}2++r)TC*xZD%Gl#$rSOqxgq#?PvKz#JV}6g60my4oBna} zrtjLfS#W&iE26Nlgu?c!C}@0IHz5iRBft?N3XUc~Hvu*f;K~{(kb1V40r0H`0`MCG zJi^Xc0#2Rr0L%-dwLRMd8Ev`acSFglO#b=(%jC!9_%{&i$Rnnk#qfl3{1QB|h2bJqzi13q-ZOfg0Dol# z3<KYTF}6{a|A37`?+`WghqMJ1pml}X9n zjg*lW3GgbTUjoi+p%6l0dnF1xO66J8pnY^e&ANUj0)Y4SFP&ISfMZ0)zk&dt=L0N8 zYtV^4!(KRbB2_2s8fS=Hh<(pQm1 z;Zm-Mp~iPji$vsT5l3Gnz&a5}oA^W&Hdy7>8bl<-kvKWIzk#JLmY2LuyfT;G0z=D< z9Ti1JY^8Tmxb`I?<&$Yn3R9cg>O)GfIQCA1(wCEhHaMOX=5PsEBmo5p*wd78+Z1UJ zPxJ%YvoyXqCL-(2I+1mMELpEclcl5x(8;^V*NEu4lmG)Fx^jG?7So{n#dOKF)8!qI z$_@kt>V-z8J)aUlk${UO;F~RcwA|QY&W|>xJ2YcvHQHCyCy%M{3`ZB96-9U=$^c4V z!?U0mU6OoVN}@*g3t=aU$XQK*vk5RnfNuoJX{#+0l3WhDB=>f862ihK_Q`rDMf!l6 z?dgk6i`dZ>qNHhsKZ16>=%$vfXMR=!Jm$XB(v~VPC@+kT_3HU_pjyB7{g)(rT4E$x1 z+|lGl4o&Hf>Trou;f{EAk>rj_X+@$tlK(aX+|dmL*i#>OR7xK_cl0>%-O&f6Az_pE z;65t>OZIt=t0mwbvG*V{(7w-eTqXnGX$3PgB$z4A*Q<0IG*MRJ%UHN0N& zdQ83I00PV+Krhdg5`7Dst~FRb!Y$6Xuu1mUa=-Gp*tgWFX5l%VH;PWDt3ff4_gGnG zc%}s0&eJG?*I6V3N6ElWo<_A|mYEQ<*rAHfilT!#Hc-MDGR>?%LCX!D6Ff}Ytqrf~ z#CRCV_b^GGCFKw6IGsl?TSXq`3O>9eGMVbAt+u1MUSfSQXBy-FqLYxHv6r4dd6m7C z>pyGoModD>o1lz6`8Y09#3RW*tO% zP61w0fTbzmf>g3xbP(m2Dd52r(Afrjp)Jt7y%nHMw*YObF7XiNacg>vwVcOPUSloK zas1~|Rn@RBCQTog`W@5oy#0-syxm{xVueTnzs#o%UczJS?IH!dh5*;a#n@p5oLK&@ z#Dvm*4~Qp?+ZrQ{-ebK}0(SH6iKE1lv`YrA=iL(>VoB;(fI$U#SOMM(HV;eE+7xh3 z3fP?jUJNQ&OOjfW)RLs>AxY&OS>uwVAIu<0VhPz3^^v4f50@uN8;aT5&XOeUElJXA zRZG$Y`@M4G>h}ueWubmAaZc7D0|&`KcAxm&Di*+ZW#B^@I9mZm#RB-e0=%LC%TmC_ z;+*V(6!1_AIH(OceUfsrR{5%V%U3M{oQ?t*({*~XpsQnCLU>lg`##=^>HF}voL~dV zHi^V=m;eR=#`v7FTSQ`b2LXP}bF(+ZVpyj>5KkrlTTI-JE2-qgC6&CXK`QAz^oJzi zQ3;qU18c;>b(aj>Ed&3SfuqI3^)&^!K>=P@fV5b+22#Lq3V1XHygx~qTT8fF!qpP4 zMhI6@|JlNd{xicgG}g>^?6_G3^;%z}PP0LmL<|QCsn9CE-1a5at*sI2CyQ8VwU)Is zq(spmhio+Ii0Wj9*|NwyGpa&JTI*|+4`88{L7ZXokj%fak=sI)!ni5`d8O$lumxvB zhag!UF|H*9$+>)8au(a|tziP2csb&Txir6q5G=&3I~ z!3m%(diGCxdJ;UVXEhNND@!>{QFcR zHdg}XOF&)%?wIk2tr8$cEp14(vN^UOyE$07xkzcX0vEGvA)nJTs-q<=4wd?Soj`kV zymh*iU#DYv%;mg`DG4@(4T<U0| zBa^T`NA+6r$2w`rZ)3IO?Px7&V|ynr-~-Ls(i=At;HM%zd2C;V_cQ_6@)xg*^n`^? z?{Y0!Cf1WwQ|XC!T!~D)JfYcL6q*-GLi1_WLNmcp78gW}vWStF;F3IIW$BTDl`?RX z3_K)Ome~q$*gm<)DZmfK%JQ)Se4+qfO#xq@BxRWa9ZfA|*$L+T9Bmo@~aI6HJ zE!LQ}{X=8C1F6W8geN{)QG9MIiO*9F5+BdHK9Yd{NB~>%W|vqb{vZQ?l7T)2P{ktg zTLpMb0S-+8>slhw5{YRf606HZB4ZYe>QsWRD=l{BE5?$TwKomZ@U~TqsU1JwbUiSW zzX0b;jah*62x+p4!>6ue?Wt*;!Ytz)+cYY}`|}Ks1}#f9Bg;yg=WN-uk?Kai;3QX( zNq6*OotdH;4rtW9uwn}EWeGS}0!%)+XhJ3Dpqx|yI5+BI1q6bV)a}n#Q1u+!C)2%# zZaPqK+^b@@NgLBL1*d%1%5K5(nj(1WHKV-Q*4V59pt`QH`N5=hyDSorRYkPpFP*~{ zvap+$2Y;tNzAdeLoBRb^N?5p>Q^etjz%BtI^WU?74GtUERsk0g;8&BnRlui{X2f-r zhvTY{hspwx1gzu=ab45WP?I_=iCOqxgvIC#2&t+-V=`9%f4P|)KaqfTHnUm+&Tm>% zUy$JVqq;5JZNK@}NUnz{+YLNy++=UVeSY-T-O<}~2Mjyeo)2c*`f_)Fe$bv3v8!+A zH?RJk$tP4~nH$+}H?hoWcE-~44o@!Emi?LDl@4qU#dT)AV*%pj_RhonM?xohH zQHF6}%P<;m7$@)S3R^~(z%;tjySjYiIC|@_MsvEM(LqhiQrk0?0gbNNyx}aW8{BHR zrHqytiaunRmUfytkm--GhFhkOHLzhe3wifwXdl}utl_}&do@L4$N1Aero6YCg+iu5 X)ohl1jdzNEhr<5>yV2WPdYb?M9%bGg