UNPKG

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