| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748 |
- import { isAbsolute, join, resolve } from 'node:path';
- import { EOL } from 'node:os';
- import { existsSync } from 'node:fs';
- import process from 'node:process';
- import picocolors from 'picocolors';
- const { dim, red } = picocolors;
- import meow from 'meow';
- import { isBoolean, isNumber, isObject, isPlainObject, isString } from './utils/validateTypes.mjs';
- import checkInvalidCLIOptions from './utils/checkInvalidCLIOptions.mjs';
- import dynamicImport from './utils/dynamicImport.mjs';
- import printConfig from './printConfig.mjs';
- import resolveSilent from './utils/resolveSilent.mjs';
- import standalone from './standalone.mjs';
- import writeOutputFile from './writeOutputFile.mjs';
- import {
- DEFAULT_CACHE_LOCATION,
- DEFAULT_FIX_MODE,
- DEFAULT_FORMATTER,
- DEFAULT_IGNORE_FILENAME,
- DEFAULT_SUPPRESSION_FILENAME,
- EXIT_CODE_FATAL_ERROR,
- EXIT_CODE_INVALID_USAGE,
- EXIT_CODE_LINT_PROBLEM,
- EXIT_CODE_SUCCESS,
- } from './constants.mjs';
- import normalizeFixMode from './utils/normalizeFixMode.mjs';
- const helpText = `
- Usage: stylelint [input] [options]
- Input: Files(s), glob(s), or nothing to use stdin.
- If an input argument is wrapped in quotation marks, it will be passed to
- globby for cross-platform glob support. "node_modules" are always ignored.
- You can also pass no input and use stdin, instead.
- Options:
- --config, -c <path_or_module>
- A path to a specific configuration file (JSON, YAML, CommonJS, or ES module),
- or a module name in "node_modules" that points to one. If no argument is
- provided, Stylelint will search for configuration files in the following
- places, in this order:
- - a "stylelint" property in "package.json"
- - a ".stylelintrc" file
- - a ".stylelintrc.{cjs,mjs,js,json,yaml,yml}" file
- - a "stylelint.config.{cjs,mjs,js}" file
- The search will begin in the working directory and move up the directory
- tree until a configuration file is found.
- --config-basedir <path>
- An absolute path to the directory that relative paths defining "extends",
- "plugins", and "customSyntax" are *relative to*. Only necessary if these
- values are relative paths.
- --print-config
- Print the configuration for the given input file path. Globs are unsupported.
- --ignore-path, -i <path>
- A path to a file containing patterns that describe files to ignore. The
- path can be absolute or relative to "process.cwd()". You can repeat the
- option to provide multiple paths. By default, Stylelint looks for
- "${DEFAULT_IGNORE_FILENAME}" in "process.cwd()". Multiple can be set.
- --ignore-pattern, --ip <pattern>
- A pattern of files to ignore (in addition to those in "${DEFAULT_IGNORE_FILENAME}").
- Multiple can be set.
- --fix [<mode>]
- Automatically fix problems of certain rules. The modes are as follows:
- lax attempting to fix as much as possible even with syntax errors (default)
- strict only fixing when there are no syntax errors
- --compute-edit-info, --cei
- Compute edit information for fixable problems.
- --custom-syntax <name_or_path>
- A module name or path to a JS file exporting a PostCSS-compatible syntax.
- --stdin
- Accept stdin input even if it is empty.
- --stdin-filename <name>
- A filename to assign stdin input.
- --ignore-disables, --id
- Ignore "stylelint-disable" comments.
- --disable-default-ignores, --di
- Allow linting of "node_modules".
- --[no-]cache
- Store the info about processed files in order to only operate on the
- changed ones the next time you run Stylelint. By default, the cache is
- stored in "${DEFAULT_CACHE_LOCATION}". To adjust this, use "--cache-location".
- Cache is disabled by default.
- --cache-location <path>
- A path to a file or directory to be used for the cache location. If a
- directory is specified, a cache file will be created inside the specified
- folder, with a name derived from a hash of the current working directory.
- If the directory for the cache does not exist, make sure you add a trailing "/"
- on *nix systems or "\\" on Windows. Otherwise the path will be assumed to
- be a file.
- --cache-strategy <strategy>
- A strategy for the cache to use for detecting changed files. Either one of:
- metadata by metadata of a file (default)
- content by content of a file
- The "content" strategy can be useful in cases where the modification time
- of your files changes even if their contents have not. For example, this can
- happen during git operations like "git clone" because git does not track file
- modification time.
- --formatter, -f <formatter>
- An output formatter. The variants are as follows:
- string human-readable strings (default)
- compact similar to ESLint's compact formatter
- github workflow commands for GitHub Actions (DEPRECATED)
- json JSON format
- tap TAP format
- unix C compiler-like format
- verbose extend "string" to include a file list and a tally
- --custom-formatter <path_or_module>
- A path to a JS file or module name exporting a custom formatting function.
- --quiet, -q
- Only register problems for rules with an "error"-level severity (ignore
- "warning"-level).
- --quiet-deprecation-warnings
- Ignore deprecations warnings.
- --[no-]color
- Force enabling/disabling of color.
- --[no-]validate
- Force enable/disable the validation of the rules' options.
- --report-needless-disables, --rd
- Also report errors for "stylelint-disable" comments that are not blocking
- a lint warning. The process will exit with code ${EXIT_CODE_LINT_PROBLEM} if needless disables are found.
- --report-invalid-scope-disables, --risd
- Report "stylelint-disable" comments that used for rules that don't exist
- within the configuration object. The process will exit with code ${EXIT_CODE_LINT_PROBLEM} if invalid
- scope disables are found.
- --report-descriptionless-disables, --rdd
- Report "stylelint-disable" comments without a description. The process will
- exit with code ${EXIT_CODE_LINT_PROBLEM} if descriptionless disables are found.
- --max-warnings, --mw <number>
- The number of warnings above which the process will exit with code ${EXIT_CODE_LINT_PROBLEM}.
- Useful when setting "defaultSeverity" to "warning" and expecting the process
- to fail on warnings (e.g. CI build).
- --output-file, -o <path>
- A file path to write a report.
- --allow-empty-input, --aei
- When a glob pattern matches no files, the process will exit without throwing an error.
- --suppress [<rule>]
- Suppress problems and record them in a file. If no rule is specified,
- all problems are suppressed. Otherwise, only problems with the given
- rules are suppressed, e.g., "--suppress rule1 --suppress rule2".
- See also "--suppress-location".
- --suppress-location <path>
- A path to a file or directory to be used for the suppressions file location. If a
- directory is specified, a suppressions file will be created inside the specified
- folder, with the name "${DEFAULT_SUPPRESSION_FILENAME}".
- --globby-options, --go <json>
- Options in JSON format passed to globby.
- --version, -v
- Show the version.
- --help, -h
- Show the help.
- `;
- const flags = {
- allowEmptyInput: {
- aliases: ['aei'],
- type: 'boolean',
- },
- cache: {
- type: 'boolean',
- },
- cacheLocation: {
- type: 'string',
- },
- cacheStrategy: {
- type: 'string',
- },
- color: {
- type: 'boolean',
- },
- computeEditInfo: {
- shortFlag: 'cei',
- type: 'boolean',
- default: false,
- },
- config: {
- shortFlag: 'c',
- type: 'string',
- },
- configBasedir: {
- type: 'string',
- },
- customFormatter: {
- type: 'string',
- },
- customSyntax: {
- type: 'string',
- },
- disableDefaultIgnores: {
- aliases: ['di'],
- type: 'boolean',
- },
- fix: {
- type: 'string',
- },
- formatter: {
- shortFlag: 'f',
- type: 'string',
- },
- globbyOptions: {
- aliases: ['go'],
- type: 'string',
- },
- help: {
- shortFlag: 'h',
- type: 'boolean',
- },
- ignoreDisables: {
- aliases: ['id'],
- type: 'boolean',
- },
- ignorePath: {
- shortFlag: 'i',
- type: 'string',
- isMultiple: true,
- },
- ignorePattern: {
- aliases: ['ip'],
- type: 'string',
- isMultiple: true,
- },
- maxWarnings: {
- aliases: ['mw'],
- type: 'number',
- },
- outputFile: {
- shortFlag: 'o',
- type: 'string',
- },
- printConfig: {
- type: 'boolean',
- },
- quiet: {
- shortFlag: 'q',
- type: 'boolean',
- },
- quietDeprecationWarnings: {
- type: 'boolean',
- },
- reportDescriptionlessDisables: {
- aliases: ['rdd'],
- type: 'boolean',
- },
- reportInvalidScopeDisables: {
- aliases: ['risd'],
- type: 'boolean',
- },
- reportNeedlessDisables: {
- aliases: ['rd'],
- type: 'boolean',
- },
- reportUnscopedDisables: {
- aliases: ['rud'],
- type: 'boolean',
- },
- stdin: {
- type: 'boolean',
- },
- stdinFilename: {
- type: 'string',
- },
- suppress: {
- type: 'string',
- isMultiple: true,
- },
- suppressLocation: {
- type: 'string',
- },
- validate: {
- type: 'boolean',
- default: true,
- },
- version: {
- shortFlag: 'v',
- type: 'boolean',
- },
- };
- /**
- * @param {string[]} argv
- * @returns {Promise<void>}
- */
- export default async function main(argv) {
- const cli = buildCLI(argv);
- const invalidOptionsMessage = checkInvalidCLIOptions(flags, cli.flags);
- if (invalidOptionsMessage) {
- process.stderr.write(invalidOptionsMessage);
- process.exitCode = EXIT_CODE_INVALID_USAGE;
- return;
- }
- const {
- // Sort alphabetically
- allowEmptyInput,
- cache,
- cacheLocation,
- cacheStrategy,
- computeEditInfo,
- config: configFile,
- configBasedir,
- customFormatter,
- customSyntax,
- disableDefaultIgnores,
- formatter: formatterInput,
- fix,
- globbyOptions,
- help,
- ignoreDisables,
- ignorePath,
- ignorePattern,
- maxWarnings,
- outputFile,
- printConfig: printConfigFlag,
- quiet,
- quietDeprecationWarnings,
- reportDescriptionlessDisables,
- reportInvalidScopeDisables,
- reportNeedlessDisables,
- reportUnscopedDisables,
- stdin,
- stdinFilename,
- suppress,
- suppressLocation,
- validate,
- version,
- } = cli.flags;
- const showHelp = () => cli.showHelp(EXIT_CODE_SUCCESS);
- if (help) {
- showHelp();
- return;
- }
- if (version) {
- cli.showVersion();
- return;
- }
- let formatter = undefined;
- if (isString(customFormatter)) {
- formatter = await importCustomFormatter(customFormatter);
- } else if (isString(formatterInput)) {
- formatter = /** @type {import('stylelint').FormatterType} */ (formatterInput);
- }
- /** @type {import('stylelint').LinterOptions} */
- const options = {
- formatter,
- _defaultFormatter: DEFAULT_FORMATTER,
- };
- if (isBoolean(quiet)) {
- options.quiet = quiet;
- }
- if (isBoolean(quietDeprecationWarnings)) {
- options.quietDeprecationWarnings = quietDeprecationWarnings;
- }
- if (isString(customSyntax)) {
- options.customSyntax = customSyntax;
- }
- const cwd = process.cwd();
- if (isString(configFile)) {
- // Should check these possibilities:
- // a. name of a node_module
- // b. absolute path
- // c. relative path relative to `process.cwd()`.
- // If none of the above work, we'll try a relative path starting
- // in `process.cwd()`.
- options.configFile = resolveSilent(cwd, configFile) || join(cwd, configFile);
- }
- if (isString(configBasedir)) {
- options.configBasedir = isAbsolute(configBasedir) ? configBasedir : resolve(cwd, configBasedir);
- }
- if (isString(globbyOptions)) {
- try {
- options.globbyOptions = await parseGlobbyOptions(globbyOptions);
- } catch (error) {
- if (typeof error === 'string') {
- process.stderr.write(`${error}${EOL}`);
- process.exitCode = EXIT_CODE_INVALID_USAGE;
- return;
- }
- throw error;
- }
- }
- if (isString(stdinFilename)) {
- options.codeFilename = stdinFilename;
- }
- if (Array.isArray(ignorePath)) {
- options.ignorePath = ignorePath;
- }
- if (Array.isArray(ignorePattern)) {
- options.ignorePattern = ignorePattern;
- }
- if (isBoolean(ignoreDisables)) {
- options.ignoreDisables = ignoreDisables;
- }
- if (isBoolean(disableDefaultIgnores)) {
- options.disableDefaultIgnores = disableDefaultIgnores;
- }
- if (isBoolean(cache)) {
- options.cache = cache;
- }
- if (isString(cacheLocation)) {
- options.cacheLocation = cacheLocation;
- }
- if (isString(cacheStrategy)) {
- options.cacheStrategy = cacheStrategy;
- }
- if (isString(fix)) {
- options.fix = normalizeFixMode(fix);
- // If the fix argument is unknown, consider it as an input path.
- // TODO: `false` is for backward compatibility. We can remove it in the next major (17.0.0).
- if (options.fix === undefined && fix !== 'false') {
- options.fix = DEFAULT_FIX_MODE;
- cli.input.push(fix);
- }
- }
- if (isBoolean(validate)) {
- options.validate = validate;
- }
- if (isBoolean(reportNeedlessDisables)) {
- options.reportNeedlessDisables = reportNeedlessDisables;
- }
- if (isBoolean(reportInvalidScopeDisables)) {
- options.reportInvalidScopeDisables = reportInvalidScopeDisables;
- }
- if (isBoolean(reportDescriptionlessDisables)) {
- options.reportDescriptionlessDisables = reportDescriptionlessDisables;
- }
- if (isBoolean(reportUnscopedDisables)) {
- options.reportUnscopedDisables = reportUnscopedDisables;
- }
- if (isNumber(maxWarnings)) {
- options.maxWarnings = maxWarnings;
- }
- if (isBoolean(allowEmptyInput)) {
- options.allowEmptyInput = allowEmptyInput;
- }
- if (isBoolean(computeEditInfo)) {
- options.computeEditInfo = computeEditInfo;
- }
- // Handle suppress logic - detect both --suppress (all) and --suppress=rule cases
- const hasPlainSuppress = argv.includes('--suppress');
- const hasRuleSuppress = Array.isArray(suppress) && suppress.length > 0;
- if (hasPlainSuppress) {
- options.suppressAll = true;
- }
- if (hasRuleSuppress) {
- options.suppressRule = suppress;
- }
- if (isString(suppressLocation)) {
- options.suppressLocation = suppressLocation;
- }
- if (options.suppressAll && options.suppressRule) {
- process.stderr.write(
- `${red('Error: The --suppress and --suppress=<rule> options cannot be used together.')}${EOL}`,
- );
- process.exitCode = EXIT_CODE_INVALID_USAGE;
- return;
- }
- // Add input/code into options
- if (cli.input.length > 0) {
- options.files = cli.input;
- } else {
- options.code = await getStdin();
- }
- const hasStdin = stdin || isString(options.code);
- if (hasStdin && (options.suppressAll || options.suppressRule)) {
- process.stderr.write(
- `${red(
- 'Error: The --suppress and --suppress=<rule> options cannot be used with piped-in code.',
- )}${EOL}`,
- );
- process.exitCode = EXIT_CODE_INVALID_USAGE;
- return;
- }
- if (printConfigFlag) {
- await printConfig(options)
- .then((config) => {
- process.stdout.write(JSON.stringify(config, null, 2));
- })
- .catch(handleError);
- return;
- }
- if (!options.files && !isString(options.code) && !stdin) {
- showHelp();
- return;
- }
- return standalone(options)
- .then(({ report, code, errored, maxWarningsExceeded }) => {
- if (!report && !code) {
- return;
- }
- if (code) {
- process.stdout.write(code);
- }
- if (report) {
- process.stderr.write(report);
- }
- if (isString(outputFile)) {
- writeOutputFile(report, outputFile).catch(handleError);
- }
- if (errored) {
- process.exitCode = EXIT_CODE_LINT_PROBLEM;
- } else if (isNumber(maxWarnings) && maxWarningsExceeded) {
- const foundWarnings = maxWarningsExceeded.foundWarnings;
- process.stderr.write(
- `${EOL}${red(`Max warnings exceeded: `)}${foundWarnings} found. ${dim(
- `${maxWarnings} allowed${EOL}${EOL}`,
- )}`,
- );
- process.exitCode = EXIT_CODE_LINT_PROBLEM;
- }
- })
- .catch(handleError);
- }
- /**
- * @param {unknown} err
- * @returns {void}
- */
- function handleError(err) {
- if (!isObject(err)) {
- throw err;
- }
- if ('stack' in err && isString(err.stack)) {
- process.stderr.write(err.stack + EOL);
- }
- const exitCode = 'code' in err && isNumber(err.code) ? err.code : EXIT_CODE_FATAL_ERROR;
- process.exitCode = exitCode;
- }
- /**
- * @param {string} value
- * @returns {Promise<Record<string, unknown>>}
- */
- function parseGlobbyOptions(value) {
- const errorMessage = () =>
- `Invalid option ${red('"--globby-options"')}.` +
- ` The value ${red(`"${value}"`)} is not valid JSON object.`;
- let options;
- try {
- options = JSON.parse(value);
- } catch {
- return Promise.reject(errorMessage());
- }
- if (isPlainObject(options)) {
- return Promise.resolve(options);
- }
- return Promise.reject(errorMessage());
- }
- /**
- * @returns {Promise<string | undefined>}
- */
- async function getStdin() {
- const { stdin } = process;
- if (stdin.isTTY) {
- return undefined;
- }
- const chunks = [];
- for await (const chunk of stdin) {
- chunks.push(chunk);
- }
- return Buffer.concat(chunks).toString();
- }
- /**
- * @param {string} fileOrModulePath
- * @returns {Promise<import('stylelint').Formatter>}
- */
- function importCustomFormatter(fileOrModulePath) {
- let modulePath = fileOrModulePath;
- if (existsSync(modulePath)) {
- modulePath = resolve(modulePath); // to absolute path
- }
- return dynamicImport(modulePath).then((m) => m.default);
- }
- /**
- * @param {string[]} argv
- */
- export function buildCLI(argv) {
- return meow(helpText, {
- autoHelp: false,
- autoVersion: false,
- argv,
- // NOTE: `meow()` infers flag types when passing a flag object with inline.
- // However, `checkInvalidCLIOptions()` also needs this flag object.
- // So, unfortunately, the return value type inference by `meow()` is unavailable here.
- //
- // @ts-expect-error -- TS2322: Type '{ allowEmptyInput: {...} }' is not assignable to type 'AnyFlags'.
- flags,
- // NOTE: We must enable `allowUnknownFlags` because meow exits with `2` if `allowUnknownFlags` is disabled.
- // Instead, we use our different exit code with `checkInvalidCLIOptions()`.
- // See also https://github.com/sindresorhus/meow/blob/v12.0.1/source/validate.js#L75
- allowUnknownFlags: true,
- // NOTE: If CLI flags are unspecified (e.g., `--cache` nor `--no-cache`), we want to fall back to config properties (e.g., `cache`).
- booleanDefault: undefined,
- importMeta: import.meta,
- });
- }
|