UNPKG

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