blob: 13b1605981a76c2abc580a4c3c612f16415efd77 [file] [log] [blame]
/* @flow */
"use strict";
const _ = require("lodash");
const createStylelint = require("./createStylelint");
const createStylelintResult = require("./createStylelintResult");
const debug = require("debug")("stylelint:standalone");
const FileCache = require("./utils/FileCache");
const filterFilePaths = require("./utils/filterFilePaths");
const formatters = require("./formatters");
const fs = require("fs");
const getFormatterOptionsText = require("./utils/getFormatterOptionsText");
const globby /*: Function*/ = require("globby");
const hash = require("./utils/hash");
const ignore = require("ignore");
const needlessDisables /*: Function*/ = require("./needlessDisables");
const NoFilesFoundError = require("./utils/noFilesFoundError");
const path = require("path");
const pify = require("pify");
const pkg = require("../package.json");
const writeFileAtomic /*: Function*/ = require("./vendor/writeFileAtomic");
const DEFAULT_IGNORE_FILENAME = ".stylelintignore";
const FILE_NOT_FOUND_ERROR_CODE = "ENOENT";
const ALWAYS_IGNORED_GLOBS = ["**/node_modules/**", "**/bower_components/**"];
module.exports = function(
options /*: stylelint$standaloneOptions */
) /*: Promise<stylelint$standaloneReturnValue>*/ {
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 globbyOptions = options.globbyOptions;
const files = options.files;
const fix = options.fix;
const formatter = options.formatter;
const ignoreDisables = options.ignoreDisables;
const reportNeedlessDisables = options.reportNeedlessDisables;
const maxWarnings = options.maxWarnings;
const syntax = options.syntax;
const allowEmptyInput = options.allowEmptyInput || false;
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: ${getFormatterOptionsText()} 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;
// if file is ignored, return nothing
if (
absoluteCodeFilename &&
!filterFilePaths(ignorer, [
path.relative(process.cwd(), absoluteCodeFilename)
]).length
) {
return Promise.resolve(prepareReturnValue([]));
}
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(_.partial(handleError, stylelint))
.then(stylelintResult => {
const postcssResult = stylelintResult._postcssResult;
const returnValue = prepareReturnValue([stylelintResult]);
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, globbyOptions)
.then(filePaths => {
// The ignorer filter needs to check paths relative to cwd
filePaths = filterFilePaths(
ignorer,
filePaths.map(p => path.relative(process.cwd(), p))
);
if (!filePaths.length) {
if (!allowEmptyInput) {
throw new NoFilesFoundError(fileList);
}
return Promise.all([]);
}
const cwd = _.get(globbyOptions, "cwd", process.cwd());
let absoluteFilePaths = filePaths.map(filePath => {
const absoluteFilepath = !path.isAbsolute(filePath)
? path.join(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
);
if (postcssResult.root.source.input.css !== fixedCss) {
fixFile = pify(writeFileAtomic)(absoluteFilepath, fixedCss);
}
}
return fixFile.then(() =>
stylelint._createStylelintResult(postcssResult, absoluteFilepath)
);
})
.catch(error => {
// On any error, we should not cache the lint result
fileCache.removeEntry(absoluteFilepath);
return handleError(stylelint, error);
});
});
return Promise.all(getStylelintResults);
})
.then(stylelintResults => {
if (useCache) {
fileCache.reconcile();
}
return prepareReturnValue(stylelintResults);
});
function prepareReturnValue(
stylelintResults /*: Array<stylelint$result>*/
) /*: 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);
}
if (maxWarnings !== undefined) {
const foundWarnings = stylelintResults.reduce((count, file) => {
return count + file.warnings.length;
}, 0);
if (foundWarnings > maxWarnings) {
returnValue.maxWarningsExceeded = { maxWarnings, foundWarnings };
}
}
debug(`Linting complete in ${Date.now() - startTime}ms`);
return returnValue;
}
};
function handleError(
stylelint /*: stylelint$internalApi */,
error /*: Object */
) /*: Promise<stylelint$result> */ {
if (error.name === "CssSyntaxError") {
return createStylelintResult(stylelint, null, null, error);
} else {
throw error;
}
}