--- /dev/null
+/* @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<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 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$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);
+ }
+ 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 || "<input css 1>",
+ deprecations: [],
+ invalidOptionWarnings: [],
+ parseErrors: [],
+ errored: true,
+ warnings: [
+ {
+ line: error.line,
+ column: error.column,
+ rule: error.name,
+ severity: "error",
+ text: error.reason + " (" + error.name + ")"
+ }
+ ]
+ };
+}