UNPKG

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