/* @flow */ "use strict"; const createStylelint = require("./createStylelint"); const debug = require("debug")("stylelint:standalone"); const FileCache = require("./utils/FileCache"); const formatters /*: Object*/ = require("./formatters"); const fs = require("fs"); const globby /*: Function*/ = require("globby"); const hash = require("./utils/hash"); const ignore = require("ignore"); const needlessDisables /*: Function*/ = require("./needlessDisables"); const path = require("path"); const pify = require("pify"); const pkg = require("../package.json"); const DEFAULT_IGNORE_FILENAME = ".stylelintignore"; const FILE_NOT_FOUND_ERROR_CODE = "ENOENT"; const ALWAYS_IGNORED_GLOBS = ["**/node_modules/**", "**/bower_components/**"]; /*::type CssSyntaxErrorT = { column: number; file?: string; input: { column: number; file?: string; line: number; source: string; }; line: number; message: string; name: string; reason: string; source: string; }*/ module.exports = function( options /*: stylelint$standaloneOptions */ ) /*: Promise*/ { const cacheLocation = options.cacheLocation; const code = options.code; const codeFilename = options.codeFilename; const config = options.config; const configBasedir = options.configBasedir; const configFile = options.configFile; const configOverrides = options.configOverrides; const customSyntax = options.customSyntax; const files = options.files; const fix = options.fix; const formatter = options.formatter; const ignoreDisables = options.ignoreDisables; const reportNeedlessDisables = options.reportNeedlessDisables; const syntax = options.syntax; const useCache = options.cache || false; let fileCache; const startTime = Date.now(); // The ignorer will be used to filter file paths after the glob is checked, // before any files are actually read const ignoreFilePath = options.ignorePath || DEFAULT_IGNORE_FILENAME; const absoluteIgnoreFilePath = path.isAbsolute(ignoreFilePath) ? ignoreFilePath : path.resolve(process.cwd(), ignoreFilePath); let ignoreText = ""; try { ignoreText = fs.readFileSync(absoluteIgnoreFilePath, "utf8"); } catch (readError) { if (readError.code !== FILE_NOT_FOUND_ERROR_CODE) throw readError; } const ignorePattern = options.ignorePattern || []; const ignorer = ignore() .add(ignoreText) .add(ignorePattern); const isValidCode = typeof code === "string"; if ((!files && !isValidCode) || (files && (code || isValidCode))) { throw new Error( "You must pass stylelint a `files` glob or a `code` string, though not both" ); } let formatterFunction; if (typeof formatter === "string") { formatterFunction = formatters[formatter]; if (formatterFunction === undefined) { return Promise.reject( new Error( "You must use a valid formatter option: 'json', 'string', 'verbose', or a function" ) ); } } else if (typeof formatter === "function") { formatterFunction = formatter; } else { formatterFunction = formatters.json; } const stylelint = createStylelint({ config, configFile, configBasedir, configOverrides, ignoreDisables, reportNeedlessDisables, syntax, customSyntax, fix }); if (!files) { const absoluteCodeFilename = codeFilename !== undefined && !path.isAbsolute(codeFilename) ? path.join(process.cwd(), codeFilename) : codeFilename; return stylelint ._lintSource({ code, codeFilename: absoluteCodeFilename }) .then(postcssResult => { // Check for file existence return new Promise((resolve, reject) => { if (!absoluteCodeFilename) { reject(); return; } fs.stat(absoluteCodeFilename, err => { if (err) { reject(); } else { resolve(); } }); }) .then(() => { return stylelint._createStylelintResult( postcssResult, absoluteCodeFilename ); }) .catch(() => { return stylelint._createStylelintResult(postcssResult); }); }) .catch(handleError) .then(stylelintResult => { const returnValue = prepareReturnValue([stylelintResult]); const postcssResult = stylelintResult._postcssResult; // if file is ignored, return nothing if ( absoluteCodeFilename && !ignorer.filter(path.relative(process.cwd(), absoluteCodeFilename)) .length ) { returnValue.output = ""; } else if ( options.fix && postcssResult && !postcssResult.stylelint.ignored ) { // If we're fixing, the output should be the fixed code returnValue.output = postcssResult.root.toString( postcssResult.opts.syntax ); } return returnValue; }); } let fileList = files; if (typeof fileList === "string") { fileList = [fileList]; } if (!options.disableDefaultIgnores) { fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map(glob => "!" + glob)); } if (useCache) { const stylelintVersion = pkg.version; const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config)}`); fileCache = new FileCache(cacheLocation, hashOfConfig); } else { // No need to calculate hash here, we just want to delete cache file. fileCache = new FileCache(cacheLocation); // Remove cache file if cache option is disabled fileCache.destroy(); } return globby(fileList) .then(filePaths => { // The ignorer filter needs to check paths relative to cwd filePaths = ignorer.filter( filePaths.map(p => path.relative(process.cwd(), p)) ); if (!filePaths.length) { return Promise.all([]); } let absoluteFilePaths = filePaths.map(filePath => { const absoluteFilepath = !path.isAbsolute(filePath) ? path.join(process.cwd(), filePath) : path.normalize(filePath); return absoluteFilepath; }); if (useCache) { absoluteFilePaths = absoluteFilePaths.filter( fileCache.hasFileChanged.bind(fileCache) ); } const getStylelintResults = absoluteFilePaths.map(absoluteFilepath => { debug(`Processing ${absoluteFilepath}`); return stylelint ._lintSource({ filePath: absoluteFilepath }) .then(postcssResult => { if (postcssResult.stylelint.stylelintError && useCache) { debug( `${ absoluteFilepath } contains linting errors and will not be cached.` ); fileCache.removeEntry(absoluteFilepath); } // If we're fixing, save the file with changed code let fixFile = Promise.resolve(); if (!postcssResult.stylelint.ignored && options.fix) { const fixedCss = postcssResult.root.toString( postcssResult.opts.syntax ); fixFile = pify(fs.writeFile)(absoluteFilepath, fixedCss); } return fixFile.then(() => stylelint._createStylelintResult(postcssResult, absoluteFilepath) ); }) .catch(handleError); }); return Promise.all(getStylelintResults); }) .then(stylelintResults => { if (useCache) { fileCache.reconcile(); } return prepareReturnValue(stylelintResults); }); function prepareReturnValue( stylelintResults /*: Array*/ ) /*: stylelint$standaloneReturnValue*/ { const errored = stylelintResults.some( result => result.errored || result.parseErrors.length > 0 ); const returnValue /*: stylelint$standaloneReturnValue*/ = { errored, output: formatterFunction(stylelintResults), results: stylelintResults }; if (reportNeedlessDisables) { returnValue.needlessDisables = needlessDisables(stylelintResults); } debug(`Linting complete in ${Date.now() - startTime}ms`); return returnValue; } }; function handleError(error /*: Object*/) { if (error.name === "CssSyntaxError") { return convertCssSyntaxErrorToResult(error); } else { throw error; } } // By converting syntax errors to stylelint results, // we can control their appearance in the formatted output // and other tools like editor plugins can decide how to // present them, as well function convertCssSyntaxErrorToResult( error /*: CssSyntaxErrorT*/ ) /*: stylelint$result*/ { if (error.name !== "CssSyntaxError") { throw error; } return { source: error.file || "", deprecations: [], invalidOptionWarnings: [], parseErrors: [], errored: true, warnings: [ { line: error.line, column: error.column, rule: error.name, severity: "error", text: error.reason + " (" + error.name + ")" } ] }; }