cli.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. 'use strict';
  2. const checkInvalidCLIOptions = require('./utils/checkInvalidCLIOptions');
  3. const EOL = require('os').EOL;
  4. const getFormatterOptionsText = require('./utils/getFormatterOptionsText');
  5. const getModulePath = require('./utils/getModulePath');
  6. const getStdin = require('get-stdin');
  7. const meow = require('meow');
  8. const path = require('path');
  9. const printConfig = require('./printConfig');
  10. const resolveFrom = require('resolve-from');
  11. const standalone = require('./standalone');
  12. const writeOutputFile = require('./writeOutputFile');
  13. const { red, dim } = require('picocolors');
  14. const EXIT_CODE_ERROR = 2;
  15. /**
  16. * @typedef {object} CLIFlags
  17. * @property {boolean} [cache]
  18. * @property {string} [cacheLocation]
  19. * @property {string | false} config
  20. * @property {string} [configBasedir]
  21. * @property {string} [customSyntax]
  22. * @property {string} [printConfig]
  23. * @property {string} [color]
  24. * @property {string} [customFormatter]
  25. * @property {boolean} [disableDefaultIgnores]
  26. * @property {boolean} [fix]
  27. * @property {string} [formatter="string"]
  28. * @property {string} [help]
  29. * @property {boolean} [ignoreDisables]
  30. * @property {string} [ignorePath]
  31. * @property {string[]} [ignorePattern]
  32. * @property {string} [noColor]
  33. * @property {string} [outputFile]
  34. * @property {boolean} [stdin]
  35. * @property {string} [stdinFilename]
  36. * @property {boolean} [reportNeedlessDisables]
  37. * @property {boolean} [reportInvalidScopeDisables]
  38. * @property {boolean} [reportDescriptionlessDisables]
  39. * @property {number} [maxWarnings]
  40. * @property {boolean} quiet
  41. * @property {string} [syntax]
  42. * @property {string} [version]
  43. * @property {boolean} [allowEmptyInput]
  44. */
  45. /**
  46. * @typedef {object} CLIOptions
  47. * @property {any} input
  48. * @property {any} help
  49. * @property {any} pkg
  50. * @property {Function} showHelp
  51. * @property {Function} showVersion
  52. * @property {CLIFlags} flags
  53. */
  54. /**
  55. * @typedef {object} OptionBaseType
  56. * @property {any} formatter
  57. * @property {boolean} [cache]
  58. * @property {string} [configFile]
  59. * @property {string} [cacheLocation]
  60. * @property {string} [customSyntax]
  61. * @property {string} [codeFilename]
  62. * @property {string} [configBasedir]
  63. * @property {boolean} [quiet]
  64. * @property {any} [printConfig]
  65. * @property {boolean} [fix]
  66. * @property {boolean} [ignoreDisables]
  67. * @property {any} [ignorePath]
  68. * @property {string} [outputFile]
  69. * @property {boolean} [reportNeedlessDisables]
  70. * @property {boolean} [reportInvalidScopeDisables]
  71. * @property {boolean} [reportDescriptionlessDisables]
  72. * @property {boolean} [disableDefaultIgnores]
  73. * @property {number} [maxWarnings]
  74. * @property {string} [syntax]
  75. * @property {string[]} [ignorePattern]
  76. * @property {boolean} [allowEmptyInput]
  77. * @property {string} [files]
  78. * @property {string} [code]
  79. */
  80. const meowOptions = {
  81. autoHelp: false,
  82. autoVersion: false,
  83. help: `
  84. Usage: stylelint [input] [options]
  85. Input: Files(s), glob(s), or nothing to use stdin.
  86. If an input argument is wrapped in quotation marks, it will be passed to
  87. globby for cross-platform glob support. node_modules are always ignored.
  88. You can also pass no input and use stdin, instead.
  89. Options:
  90. --config
  91. Path to a specific configuration file (JSON, YAML, or CommonJS), or the
  92. name of a module in node_modules that points to one. If no --config
  93. argument is provided, stylelint will search for configuration files in
  94. the following places, in this order:
  95. - a stylelint property in package.json
  96. - a .stylelintrc file (with or without filename extension:
  97. .json, .yaml, .yml, and .js are available)
  98. - a stylelint.config.js file exporting a JS object
  99. The search will begin in the working directory and move up the directory
  100. tree until a configuration file is found.
  101. --config-basedir
  102. An absolute path to the directory that relative paths defining "extends"
  103. and "plugins" are *relative to*. Only necessary if these values are
  104. relative paths.
  105. --print-config
  106. Print the configuration for the given path.
  107. --ignore-path, -i
  108. Path to a file containing patterns that describe files to ignore. The
  109. path can be absolute or relative to process.cwd(). By default, stylelint
  110. looks for .stylelintignore in process.cwd().
  111. --ignore-pattern, --ip
  112. Pattern of files to ignore (in addition to those in .stylelintignore)
  113. --fix
  114. Automatically fix problems of certain rules.
  115. --custom-syntax
  116. Module name or path to a JS file exporting a PostCSS-compatible syntax.
  117. --stdin
  118. Accept stdin input even if it is empty.
  119. --stdin-filename
  120. A filename to assign stdin input.
  121. --ignore-disables, --id
  122. Ignore stylelint-disable comments.
  123. --disable-default-ignores, --di
  124. Allow linting of node_modules.
  125. --cache [default: false]
  126. Store the info about processed files in order to only operate on the
  127. changed ones the next time you run stylelint. By default, the cache
  128. is stored in "./.stylelintcache". To adjust this, use --cache-location.
  129. --cache-location [default: '.stylelintcache']
  130. Path to a file or directory to be used for the cache location.
  131. Default is "./.stylelintcache". If a directory is specified, a cache
  132. file will be created inside the specified folder, with a name derived
  133. from a hash of the current working directory.
  134. If the directory for the cache does not exist, make sure you add a trailing "/"
  135. on *nix systems or "\\" on Windows. Otherwise the path will be assumed to be a file.
  136. --formatter, -f [default: "string"]
  137. The output formatter: ${getFormatterOptionsText({ useOr: true })}.
  138. --custom-formatter
  139. Path to a JS file exporting a custom formatting function.
  140. --quiet, -q
  141. Only register problems for rules with an "error"-level severity (ignore
  142. "warning"-level).
  143. --color
  144. --no-color
  145. Force enabling/disabling of color.
  146. --report-needless-disables, --rd
  147. Also report errors for stylelint-disable comments that are not blocking a lint warning.
  148. The process will exit with code ${EXIT_CODE_ERROR} if needless disables are found.
  149. --report-invalid-scope-disables, --risd
  150. Report stylelint-disable comments that used for rules that don't exist within the configuration object.
  151. The process will exit with code ${EXIT_CODE_ERROR} if invalid scope disables are found.
  152. --report-descriptionless-disables, --rdd
  153. Report stylelint-disable comments without a description.
  154. The process will exit with code ${EXIT_CODE_ERROR} if descriptionless disables are found.
  155. --max-warnings, --mw
  156. Number of warnings above which the process will exit with code ${EXIT_CODE_ERROR}.
  157. Useful when setting "defaultSeverity" to "warning" and expecting the
  158. process to fail on warnings (e.g. CI build).
  159. --output-file, -o
  160. Path of file to write report.
  161. --version, -v
  162. Show the currently installed version of stylelint.
  163. --allow-empty-input, --aei
  164. When glob pattern matches no files, the process will exit without throwing an error.
  165. `,
  166. flags: {
  167. allowEmptyInput: {
  168. alias: 'aei',
  169. type: 'boolean',
  170. },
  171. cache: {
  172. type: 'boolean',
  173. },
  174. cacheLocation: {
  175. type: 'string',
  176. },
  177. color: {
  178. type: 'boolean',
  179. },
  180. config: {
  181. type: 'string',
  182. },
  183. configBasedir: {
  184. type: 'string',
  185. },
  186. customFormatter: {
  187. type: 'string',
  188. },
  189. customSyntax: {
  190. type: 'string',
  191. },
  192. disableDefaultIgnores: {
  193. alias: 'di',
  194. type: 'boolean',
  195. },
  196. fix: {
  197. type: 'boolean',
  198. },
  199. formatter: {
  200. alias: 'f',
  201. default: 'string',
  202. type: 'string',
  203. },
  204. help: {
  205. alias: 'h',
  206. type: 'boolean',
  207. },
  208. ignoreDisables: {
  209. alias: 'id',
  210. type: 'boolean',
  211. },
  212. ignorePath: {
  213. alias: 'i',
  214. type: 'string',
  215. },
  216. ignorePattern: {
  217. alias: 'ip',
  218. type: 'string',
  219. isMultiple: true,
  220. },
  221. maxWarnings: {
  222. alias: 'mw',
  223. type: 'number',
  224. },
  225. outputFile: {
  226. alias: 'o',
  227. type: 'string',
  228. },
  229. printConfig: {
  230. type: 'boolean',
  231. },
  232. quiet: {
  233. alias: 'q',
  234. type: 'boolean',
  235. },
  236. reportDescriptionlessDisables: {
  237. alias: 'rdd',
  238. type: 'boolean',
  239. },
  240. reportInvalidScopeDisables: {
  241. alias: 'risd',
  242. type: 'boolean',
  243. },
  244. reportNeedlessDisables: {
  245. alias: 'rd',
  246. type: 'boolean',
  247. },
  248. stdin: {
  249. type: 'boolean',
  250. },
  251. stdinFilename: {
  252. type: 'string',
  253. },
  254. syntax: {
  255. alias: 's',
  256. type: 'string',
  257. },
  258. version: {
  259. alias: 'v',
  260. type: 'boolean',
  261. },
  262. },
  263. };
  264. /**
  265. * @param {string[]} argv
  266. * @returns {Promise<any>}
  267. */
  268. module.exports = async (argv) => {
  269. const cli = buildCLI(argv);
  270. const invalidOptionsMessage = checkInvalidCLIOptions(meowOptions.flags, cli.flags);
  271. if (invalidOptionsMessage) {
  272. process.stderr.write(invalidOptionsMessage);
  273. process.exit(EXIT_CODE_ERROR); // eslint-disable-line no-process-exit
  274. }
  275. let formatter = cli.flags.formatter;
  276. if (cli.flags.customFormatter) {
  277. const customFormatter = path.isAbsolute(cli.flags.customFormatter)
  278. ? cli.flags.customFormatter
  279. : path.join(process.cwd(), cli.flags.customFormatter);
  280. formatter = require(customFormatter);
  281. }
  282. /** @type {OptionBaseType} */
  283. const optionsBase = {
  284. formatter,
  285. };
  286. if (cli.flags.quiet) {
  287. optionsBase.quiet = cli.flags.quiet;
  288. }
  289. if (cli.flags.syntax) {
  290. optionsBase.syntax = cli.flags.syntax;
  291. }
  292. if (cli.flags.customSyntax) {
  293. optionsBase.customSyntax = getModulePath(process.cwd(), cli.flags.customSyntax);
  294. }
  295. if (cli.flags.config) {
  296. // Should check these possibilities:
  297. // a. name of a node_module
  298. // b. absolute path
  299. // c. relative path relative to `process.cwd()`.
  300. // If none of the above work, we'll try a relative path starting
  301. // in `process.cwd()`.
  302. optionsBase.configFile =
  303. resolveFrom.silent(process.cwd(), cli.flags.config) ||
  304. path.join(process.cwd(), cli.flags.config);
  305. }
  306. if (cli.flags.configBasedir) {
  307. optionsBase.configBasedir = path.isAbsolute(cli.flags.configBasedir)
  308. ? cli.flags.configBasedir
  309. : path.resolve(process.cwd(), cli.flags.configBasedir);
  310. }
  311. if (cli.flags.stdinFilename) {
  312. optionsBase.codeFilename = cli.flags.stdinFilename;
  313. }
  314. if (cli.flags.ignorePath) {
  315. optionsBase.ignorePath = cli.flags.ignorePath;
  316. }
  317. if (cli.flags.ignorePattern) {
  318. optionsBase.ignorePattern = cli.flags.ignorePattern;
  319. }
  320. if (cli.flags.ignoreDisables) {
  321. optionsBase.ignoreDisables = cli.flags.ignoreDisables;
  322. }
  323. if (cli.flags.disableDefaultIgnores) {
  324. optionsBase.disableDefaultIgnores = cli.flags.disableDefaultIgnores;
  325. }
  326. if (cli.flags.cache) {
  327. optionsBase.cache = true;
  328. }
  329. if (cli.flags.cacheLocation) {
  330. optionsBase.cacheLocation = cli.flags.cacheLocation;
  331. }
  332. if (cli.flags.fix) {
  333. optionsBase.fix = cli.flags.fix;
  334. }
  335. if (cli.flags.outputFile) {
  336. optionsBase.outputFile = cli.flags.outputFile;
  337. }
  338. const reportNeedlessDisables = cli.flags.reportNeedlessDisables;
  339. const reportInvalidScopeDisables = cli.flags.reportInvalidScopeDisables;
  340. const reportDescriptionlessDisables = cli.flags.reportDescriptionlessDisables;
  341. if (reportNeedlessDisables) {
  342. optionsBase.reportNeedlessDisables = reportNeedlessDisables;
  343. }
  344. if (reportInvalidScopeDisables) {
  345. optionsBase.reportInvalidScopeDisables = reportInvalidScopeDisables;
  346. }
  347. if (reportDescriptionlessDisables) {
  348. optionsBase.reportDescriptionlessDisables = reportDescriptionlessDisables;
  349. }
  350. const maxWarnings = cli.flags.maxWarnings;
  351. if (maxWarnings !== undefined) {
  352. optionsBase.maxWarnings = maxWarnings;
  353. }
  354. if (cli.flags.help) {
  355. cli.showHelp(0);
  356. return;
  357. }
  358. if (cli.flags.version) {
  359. cli.showVersion();
  360. return;
  361. }
  362. if (cli.flags.allowEmptyInput) {
  363. optionsBase.allowEmptyInput = cli.flags.allowEmptyInput;
  364. }
  365. // Add input/code into options
  366. /** @type {OptionBaseType} */
  367. const options = cli.input.length
  368. ? {
  369. ...optionsBase,
  370. files: /** @type {string} */ (cli.input),
  371. }
  372. : await getStdin().then((stdin) => {
  373. return {
  374. ...optionsBase,
  375. code: stdin,
  376. };
  377. });
  378. if (cli.flags.printConfig) {
  379. return printConfig(options)
  380. .then((config) => {
  381. process.stdout.write(JSON.stringify(config, null, ' '));
  382. })
  383. .catch(handleError);
  384. }
  385. if (!options.files && !options.code && !cli.flags.stdin) {
  386. cli.showHelp();
  387. return;
  388. }
  389. return standalone(options)
  390. .then((linted) => {
  391. if (!linted.output) {
  392. return;
  393. }
  394. process.stdout.write(linted.output);
  395. if (options.outputFile) {
  396. writeOutputFile(linted.output, options.outputFile).catch(handleError);
  397. }
  398. if (linted.errored) {
  399. process.exitCode = EXIT_CODE_ERROR;
  400. } else if (maxWarnings !== undefined && linted.maxWarningsExceeded) {
  401. const foundWarnings = linted.maxWarningsExceeded.foundWarnings;
  402. process.stderr.write(
  403. `${EOL}${red(`Max warnings exceeded: `)}${foundWarnings} found. ${dim(
  404. `${maxWarnings} allowed${EOL}${EOL}`,
  405. )}`,
  406. );
  407. process.exitCode = EXIT_CODE_ERROR;
  408. }
  409. })
  410. .catch(handleError);
  411. };
  412. /**
  413. * @param {{ stack: any, code: any }} err
  414. * @returns {void}
  415. */
  416. function handleError(err) {
  417. process.stderr.write(err.stack + EOL);
  418. const exitCode = typeof err.code === 'number' ? err.code : 1;
  419. process.exitCode = exitCode;
  420. }
  421. /**
  422. * @param {string[]} argv
  423. * @returns {CLIOptions}
  424. */
  425. function buildCLI(argv) {
  426. // @ts-expect-error -- TS2322: Type 'Result<AnyFlags>' is not assignable to type 'CLIOptions'.
  427. return meow({ ...meowOptions, argv });
  428. }
  429. module.exports.buildCLI = buildCLI;