forked from lix-project/lix-website
335 lines
9.9 KiB
JavaScript
335 lines
9.9 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const createStylelint = require('./createStylelint');
|
|
const createStylelintResult = require('./createStylelintResult');
|
|
const debug = require('debug')('stylelint:standalone');
|
|
const fastGlob = require('fast-glob');
|
|
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 = require('globby');
|
|
const hash = require('./utils/hash');
|
|
const NoFilesFoundError = require('./utils/noFilesFoundError');
|
|
const path = require('path');
|
|
const pkg = require('../package.json');
|
|
const prepareReturnValue = require('./prepareReturnValue');
|
|
const { default: ignore } = require('ignore');
|
|
const DEFAULT_IGNORE_FILENAME = '.stylelintignore';
|
|
const FILE_NOT_FOUND_ERROR_CODE = 'ENOENT';
|
|
const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
|
|
const writeFileAtomic = require('write-file-atomic');
|
|
|
|
/** @typedef {import('stylelint').StylelintStandaloneOptions} StylelintStandaloneOptions */
|
|
/** @typedef {import('stylelint').StylelintStandaloneReturnValue} StylelintStandaloneReturnValue */
|
|
/** @typedef {import('stylelint').StylelintResult} StylelintResult */
|
|
/** @typedef {import('stylelint').Formatter} Formatter */
|
|
/** @typedef {import('stylelint').FormatterIdentifier} FormatterIdentifier */
|
|
|
|
/**
|
|
* @param {StylelintStandaloneOptions} options
|
|
* @returns {Promise<StylelintStandaloneReturnValue>}
|
|
*/
|
|
module.exports = function (options) {
|
|
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 reportInvalidScopeDisables = options.reportInvalidScopeDisables;
|
|
const reportDescriptionlessDisables = options.reportDescriptionlessDisables;
|
|
const syntax = options.syntax;
|
|
const allowEmptyInput = options.allowEmptyInput || false;
|
|
const useCache = options.cache || false;
|
|
/** @type {FileCache} */
|
|
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');
|
|
}
|
|
|
|
/** @type {Formatter} */
|
|
let formatterFunction;
|
|
|
|
try {
|
|
formatterFunction = getFormatterFunction(formatter);
|
|
} catch (error) {
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
const stylelint = createStylelint({
|
|
config,
|
|
configFile,
|
|
configBasedir,
|
|
configOverrides,
|
|
ignoreDisables,
|
|
ignorePath: ignoreFilePath,
|
|
reportNeedlessDisables,
|
|
reportInvalidScopeDisables,
|
|
reportDescriptionlessDisables,
|
|
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([], options, formatterFunction));
|
|
}
|
|
|
|
return stylelint
|
|
._lintSource({
|
|
code,
|
|
codeFilename: absoluteCodeFilename,
|
|
})
|
|
.then((postcssResult) => {
|
|
// Check for file existence
|
|
return /** @type {Promise<void>} */ (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], options, formatterFunction);
|
|
|
|
if (options.fix && postcssResult && !postcssResult.stylelint.ignored) {
|
|
if (!postcssResult.stylelint.disableWritingFix) {
|
|
// If we're fixing, the output should be the fixed code
|
|
returnValue.output = postcssResult.root.toString(postcssResult.opts.syntax);
|
|
} else {
|
|
// If the writing of the fix is disabled, the input code is returned as-is
|
|
returnValue.output = code;
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
});
|
|
}
|
|
|
|
let fileList = files;
|
|
|
|
if (typeof fileList === 'string') {
|
|
fileList = [fileList];
|
|
}
|
|
|
|
fileList = fileList.map((entry) => {
|
|
const cwd = _.get(globbyOptions, 'cwd', process.cwd());
|
|
const absolutePath = !path.isAbsolute(entry) ? path.join(cwd, entry) : path.normalize(entry);
|
|
|
|
if (fs.existsSync(absolutePath)) {
|
|
// This path points to a file. Return an escaped path to avoid globbing
|
|
return fastGlob.escapePath(entry);
|
|
}
|
|
|
|
return entry;
|
|
});
|
|
|
|
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
|
|
* @type {Promise<Error | void>}
|
|
*/
|
|
let fixFile = Promise.resolve();
|
|
|
|
if (
|
|
postcssResult.root &&
|
|
postcssResult.opts &&
|
|
!postcssResult.stylelint.ignored &&
|
|
options.fix &&
|
|
!postcssResult.stylelint.disableWritingFix
|
|
) {
|
|
// @ts-ignore TODO TYPES toString accepts 0 arguments
|
|
const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
|
|
|
|
if (
|
|
postcssResult.root &&
|
|
postcssResult.root.source &&
|
|
// @ts-ignore TODO TYPES css is unknown property
|
|
postcssResult.root.source.input.css !== fixedCss
|
|
) {
|
|
fixFile = 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, absoluteFilepath);
|
|
});
|
|
});
|
|
|
|
return Promise.all(getStylelintResults);
|
|
})
|
|
.then((stylelintResults) => {
|
|
if (useCache) {
|
|
fileCache.reconcile();
|
|
}
|
|
|
|
const rtn = prepareReturnValue(stylelintResults, options, formatterFunction);
|
|
|
|
debug(`Linting complete in ${Date.now() - startTime}ms`);
|
|
|
|
return rtn;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {FormatterIdentifier | undefined} selected
|
|
* @returns {Formatter}
|
|
*/
|
|
function getFormatterFunction(selected) {
|
|
/** @type {Formatter} */
|
|
let formatterFunction;
|
|
|
|
if (typeof selected === 'string') {
|
|
formatterFunction = formatters[selected];
|
|
|
|
if (formatterFunction === undefined) {
|
|
throw new Error(
|
|
`You must use a valid formatter option: ${getFormatterOptionsText()} or a function`,
|
|
);
|
|
}
|
|
} else if (typeof selected === 'function') {
|
|
formatterFunction = selected;
|
|
} else {
|
|
formatterFunction = formatters.json;
|
|
}
|
|
|
|
return formatterFunction;
|
|
}
|
|
|
|
/**
|
|
* @param {import('stylelint').StylelintInternalApi} stylelint
|
|
* @param {any} error
|
|
* @param {string} [filePath]
|
|
* @return {Promise<StylelintResult>}
|
|
*/
|
|
function handleError(stylelint, error, filePath = undefined) {
|
|
if (error.name === 'CssSyntaxError') {
|
|
return createStylelintResult(stylelint, undefined, filePath, error);
|
|
}
|
|
|
|
throw error;
|
|
}
|