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