UNPKG

13.7 kBJavaScriptView Raw
1'use strict';
2
3const checkInvalidCLIOptions = require('./utils/checkInvalidCLIOptions');
4const EOL = require('os').EOL;
5const getFormatterOptionsText = require('./utils/getFormatterOptionsText');
6const getModulePath = require('./utils/getModulePath');
7const getStdin = require('get-stdin');
8const meow = require('meow');
9const path = require('path');
10const printConfig = require('./printConfig');
11const resolveFrom = require('resolve-from');
12const standalone = require('./standalone');
13const writeOutputFile = require('./writeOutputFile');
14const { red, dim } = require('picocolors');
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]
41 * @property {boolean} [reportDescriptionlessDisables]
42 * @property {number} [maxWarnings]
43 * @property {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 {boolean} [quiet]
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} [reportDescriptionlessDisables]
77 * @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 --fix
134
135 Automatically fix problems of certain rules.
136
137 --custom-syntax
138
139 Module name or path to a JS file exporting a PostCSS-compatible syntax.
140
141 --stdin
142
143 Accept stdin input even if it is empty.
144
145 --stdin-filename
146
147 A filename to assign stdin input.
148
149 --ignore-disables, --id
150
151 Ignore stylelint-disable comments.
152
153 --disable-default-ignores, --di
154
155 Allow linting of node_modules.
156
157 --cache [default: false]
158
159 Store the info about processed files in order to only operate on the
160 changed ones the next time you run stylelint. By default, the cache
161 is stored in "./.stylelintcache". To adjust this, use --cache-location.
162
163 --cache-location [default: '.stylelintcache']
164
165 Path to a file or directory to be used for the cache location.
166 Default is "./.stylelintcache". If a directory is specified, a cache
167 file will be created inside the specified folder, with a name derived
168 from a hash of the current working directory.
169
170 If the directory for the cache does not exist, make sure you add a trailing "/"
171 on *nix systems or "\\" on Windows. Otherwise the path will be assumed to be a file.
172
173 --formatter, -f [default: "string"]
174
175 The output formatter: ${getFormatterOptionsText({ useOr: true })}.
176
177 --custom-formatter
178
179 Path to a JS file exporting a custom formatting function.
180
181 --quiet, -q
182
183 Only register problems for rules with an "error"-level severity (ignore
184 "warning"-level).
185
186 --color
187 --no-color
188
189 Force enabling/disabling of color.
190
191 --report-needless-disables, --rd
192
193 Also report errors for stylelint-disable comments that are not blocking a lint warning.
194 The process will exit with code ${EXIT_CODE_ERROR} if needless disables are found.
195
196 --report-invalid-scope-disables, --risd
197
198 Report stylelint-disable comments that used for rules that don't exist within the configuration object.
199 The process will exit with code ${EXIT_CODE_ERROR} if invalid scope disables are found.
200
201 --report-descriptionless-disables, --rdd
202
203 Report stylelint-disable comments without a description.
204 The process will exit with code ${EXIT_CODE_ERROR} if descriptionless disables are found.
205
206 --max-warnings, --mw
207
208 Number of warnings above which the process will exit with code ${EXIT_CODE_ERROR}.
209 Useful when setting "defaultSeverity" to "warning" and expecting the
210 process to fail on warnings (e.g. CI build).
211
212 --output-file, -o
213
214 Path of file to write report.
215
216 --version, -v
217
218 Show the currently installed version of stylelint.
219
220 --allow-empty-input, --aei
221
222 When glob pattern matches no files, the process will exit without throwing an error.
223 `,
224 flags: {
225 allowEmptyInput: {
226 alias: 'aei',
227 type: 'boolean',
228 },
229 cache: {
230 type: 'boolean',
231 },
232 cacheLocation: {
233 type: 'string',
234 },
235 color: {
236 type: 'boolean',
237 },
238 config: {
239 type: 'string',
240 },
241 configBasedir: {
242 type: 'string',
243 },
244 customFormatter: {
245 type: 'string',
246 },
247 customSyntax: {
248 type: 'string',
249 },
250 disableDefaultIgnores: {
251 alias: 'di',
252 type: 'boolean',
253 },
254 fix: {
255 type: 'boolean',
256 },
257 formatter: {
258 alias: 'f',
259 default: 'string',
260 type: 'string',
261 },
262 help: {
263 alias: 'h',
264 type: 'boolean',
265 },
266 ignoreDisables: {
267 alias: 'id',
268 type: 'boolean',
269 },
270 ignorePath: {
271 alias: 'i',
272 type: 'string',
273 },
274 ignorePattern: {
275 alias: 'ip',
276 type: 'string',
277 isMultiple: true,
278 },
279 maxWarnings: {
280 alias: 'mw',
281 type: 'number',
282 },
283 outputFile: {
284 alias: 'o',
285 type: 'string',
286 },
287 printConfig: {
288 type: 'boolean',
289 },
290 quiet: {
291 alias: 'q',
292 type: 'boolean',
293 },
294 reportDescriptionlessDisables: {
295 alias: 'rdd',
296 type: 'boolean',
297 },
298 reportInvalidScopeDisables: {
299 alias: 'risd',
300 type: 'boolean',
301 },
302 reportNeedlessDisables: {
303 alias: 'rd',
304 type: 'boolean',
305 },
306 stdin: {
307 type: 'boolean',
308 },
309 stdinFilename: {
310 type: 'string',
311 },
312 syntax: {
313 alias: 's',
314 type: 'string',
315 },
316 version: {
317 alias: 'v',
318 type: 'boolean',
319 },
320 },
321};
322
323/**
324 * @param {string[]} argv
325 * @returns {Promise<any>}
326 */
327module.exports = async (argv) => {
328 const cli = buildCLI(argv);
329
330 const invalidOptionsMessage = checkInvalidCLIOptions(meowOptions.flags, cli.flags);
331
332 if (invalidOptionsMessage) {
333 process.stderr.write(invalidOptionsMessage);
334 process.exit(EXIT_CODE_ERROR); // eslint-disable-line no-process-exit
335 }
336
337 let formatter = cli.flags.formatter;
338
339 if (cli.flags.customFormatter) {
340 const customFormatter = path.isAbsolute(cli.flags.customFormatter)
341 ? cli.flags.customFormatter
342 : path.join(process.cwd(), cli.flags.customFormatter);
343
344 formatter = require(customFormatter);
345 }
346
347 /** @type {OptionBaseType} */
348 const optionsBase = {
349 formatter,
350 };
351
352 if (cli.flags.quiet) {
353 optionsBase.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 const reportDescriptionlessDisables = cli.flags.reportDescriptionlessDisables;
421
422 if (reportNeedlessDisables) {
423 optionsBase.reportNeedlessDisables = reportNeedlessDisables;
424 }
425
426 if (reportInvalidScopeDisables) {
427 optionsBase.reportInvalidScopeDisables = reportInvalidScopeDisables;
428 }
429
430 if (reportDescriptionlessDisables) {
431 optionsBase.reportDescriptionlessDisables = reportDescriptionlessDisables;
432 }
433
434 const maxWarnings = cli.flags.maxWarnings;
435
436 if (maxWarnings !== undefined) {
437 optionsBase.maxWarnings = maxWarnings;
438 }
439
440 if (cli.flags.help) {
441 cli.showHelp(0);
442
443 return;
444 }
445
446 if (cli.flags.version) {
447 cli.showVersion();
448
449 return;
450 }
451
452 if (cli.flags.allowEmptyInput) {
453 optionsBase.allowEmptyInput = cli.flags.allowEmptyInput;
454 }
455
456 // Add input/code into options
457 /** @type {OptionBaseType} */
458 const options = cli.input.length
459 ? {
460 ...optionsBase,
461 files: /** @type {string} */ (cli.input),
462 }
463 : await getStdin().then((stdin) => {
464 return {
465 ...optionsBase,
466 code: stdin,
467 };
468 });
469
470 if (cli.flags.printConfig) {
471 return printConfig(options)
472 .then((config) => {
473 process.stdout.write(JSON.stringify(config, null, ' '));
474 })
475 .catch(handleError);
476 }
477
478 if (!options.files && !options.code && !cli.flags.stdin) {
479 cli.showHelp();
480
481 return;
482 }
483
484 return standalone(options)
485 .then((linted) => {
486 if (!linted.output) {
487 return;
488 }
489
490 process.stdout.write(linted.output);
491
492 if (options.outputFile) {
493 writeOutputFile(linted.output, options.outputFile).catch(handleError);
494 }
495
496 if (linted.errored) {
497 process.exitCode = EXIT_CODE_ERROR;
498 } else if (maxWarnings !== undefined && linted.maxWarningsExceeded) {
499 const foundWarnings = linted.maxWarningsExceeded.foundWarnings;
500
501 process.stderr.write(
502 `${EOL}${red(`Max warnings exceeded: `)}${foundWarnings} found. ${dim(
503 `${maxWarnings} allowed${EOL}${EOL}`,
504 )}`,
505 );
506 process.exitCode = EXIT_CODE_ERROR;
507 }
508 })
509 .catch(handleError);
510};
511
512/**
513 * @param {{ stack: any, code: any }} err
514 * @returns {void}
515 */
516function handleError(err) {
517 process.stderr.write(err.stack + EOL);
518 const exitCode = typeof err.code === 'number' ? err.code : 1;
519
520 process.exitCode = exitCode;
521}
522
523/**
524 * @param {string[]} argv
525 * @returns {CLIOptions}
526 */
527function buildCLI(argv) {
528 // @ts-expect-error -- TODO TYPES
529 return meow({ ...meowOptions, argv });
530}
531
532module.exports.buildCLI = buildCLI;