UNPKG

14.4 kBJavaScriptView Raw
1'use strict';
2
3const chalk = require('chalk');
4const checkInvalidCLIOptions = require('./utils/checkInvalidCLIOptions');
5const disableOptionsReportStringFormatter = require('./formatters/disableOptionsReportStringFormatter');
6const dynamicRequire = require('./dynamicRequire');
7const EOL = require('os').EOL;
8const getFormatterOptionsText = require('./utils/getFormatterOptionsText');
9const getModulePath = require('./utils/getModulePath');
10const getStdin = require('get-stdin');
11const meow = require('meow');
12const path = require('path');
13const printConfig = require('./printConfig');
14const resolveFrom = require('resolve-from');
15const standalone = require('./standalone');
16const writeOutputFile = require('./writeOutputFile');
17
18const EXIT_CODE_ERROR = 2;
19
20/**
21 * @typedef {object} CLIFlags
22 * @property {boolean} [cache]
23 * @property {string} [cacheLocation]
24 * @property {string | false} config
25 * @property {string} [configBasedir]
26 * @property {string} [customSyntax]
27 * @property {string} [printConfig]
28 * @property {string} [color]
29 * @property {string} [customFormatter]
30 * @property {boolean} [disableDefaultIgnores]
31 * @property {boolean} [fix]
32 * @property {string} [formatter="string"]
33 * @property {string} [help]
34 * @property {boolean} [ignoreDisables]
35 * @property {string} [ignorePath]
36 * @property {string} [ignorePattern]
37 * @property {string} [noColor]
38 * @property {string} [outputFile]
39 * @property {boolean} [stdin]
40 * @property {string} [stdinFilename]
41 * @property {boolean} [reportNeedlessDisables]
42 * @property {boolean} [reportInvalidScopeDisables]
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} [disableDefaultIgnores]
78 * @property {number} [maxWarnings]
79 * @property {string} [syntax]
80 * @property {any} [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
208 --max-warnings, --mw
209
210 Number of warnings above which the process will exit with code ${EXIT_CODE_ERROR}.
211 Useful when setting "defaultSeverity" to "warning" and expecting the
212 process to fail on warnings (e.g. CI build).
213
214 --output-file, -o
215
216 Path of file to write report.
217
218 --version, -v
219
220 Show the currently installed version of stylelint.
221
222 --allow-empty-input, --aei
223
224 When glob pattern matches no files, the process will exit without throwing an error.
225 `,
226 flags: {
227 allowEmptyInput: {
228 alias: 'aei',
229 type: 'boolean',
230 },
231 cache: {
232 type: 'boolean',
233 },
234 cacheLocation: {
235 type: 'string',
236 },
237 color: {
238 type: 'boolean',
239 },
240 config: {
241 type: 'string',
242 },
243 configBasedir: {
244 type: 'string',
245 },
246 customFormatter: {
247 type: 'string',
248 },
249 customSyntax: {
250 type: 'string',
251 },
252 disableDefaultIgnores: {
253 alias: 'di',
254 type: 'boolean',
255 },
256 fix: {
257 type: 'boolean',
258 },
259 formatter: {
260 alias: 'f',
261 default: 'string',
262 type: 'string',
263 },
264 help: {
265 alias: 'h',
266 type: 'boolean',
267 },
268 ignoreDisables: {
269 alias: 'id',
270 type: 'boolean',
271 },
272 ignorePath: {
273 alias: 'i',
274 type: 'string',
275 },
276 ignorePattern: {
277 alias: 'ip',
278 type: 'string',
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 = dynamicRequire(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 console.log(err.stack); // eslint-disable-line no-console
538 const exitCode = typeof err.code === 'number' ? err.code : 1;
539
540 process.exit(exitCode); // eslint-disable-line no-process-exit
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;