blob: 10371313c4af312c65a3325c6fdbc33e50a5bcd4 [file] [log] [blame]
'use strict';
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 isPathNotFoundError = require('./utils/isPathNotFoundError');
const NoFilesFoundError = require('./utils/noFilesFoundError');
const normalizePath = require('normalize-path');
const path = require('path');
const pkg = require('../package.json');
const prepareReturnValue = require('./prepareReturnValue');
const { default: ignore } = require('ignore');
const DEFAULT_IGNORE_FILENAME = '.stylelintignore';
const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
const writeFileAtomic = require('write-file-atomic');
/** @typedef {import('stylelint').LinterOptions} LinterOptions */
/** @typedef {import('stylelint').LinterResult} LinterResult */
/** @typedef {import('stylelint').LintResult} StylelintResult */
/** @typedef {import('stylelint').Formatter} Formatter */
/** @typedef {import('stylelint').FormatterType} FormatterType */
/**
*
* @param {LinterOptions} options
* @returns {Promise<LinterResult>}
*/
async function standalone({
allowEmptyInput = false,
cache: useCache = false,
cacheLocation,
code,
codeFilename,
config,
configBasedir,
configFile,
customSyntax,
disableDefaultIgnores,
files,
fix,
formatter,
globbyOptions,
ignoreDisables,
ignorePath = DEFAULT_IGNORE_FILENAME,
ignorePattern = [],
maxWarnings,
quiet,
reportDescriptionlessDisables,
reportInvalidScopeDisables,
reportNeedlessDisables,
syntax,
}) {
/** @type {FileCache} */
let fileCache;
const startTime = Date.now();
const isValidCode = typeof code === 'string';
if ((!files && !isValidCode) || (files && (code || isValidCode))) {
return Promise.reject(
new Error('You must pass stylelint a `files` glob or a `code` string, though not both'),
);
}
// The ignorer will be used to filter file paths after the glob is checked,
// before any files are actually read
const absoluteIgnoreFilePath = path.isAbsolute(ignorePath)
? ignorePath
: path.resolve(process.cwd(), ignorePath);
let ignoreText = '';
try {
ignoreText = fs.readFileSync(absoluteIgnoreFilePath, 'utf8');
} catch (readError) {
if (!isPathNotFoundError(readError)) {
return Promise.reject(readError);
}
}
const ignorer = ignore().add(ignoreText).add(ignorePattern);
/** @type {Formatter} */
let formatterFunction;
try {
formatterFunction = getFormatterFunction(formatter);
} catch (error) {
return Promise.reject(error);
}
const stylelint = createStylelint({
config,
configFile,
configBasedir,
ignoreDisables,
ignorePath,
reportNeedlessDisables,
reportInvalidScopeDisables,
reportDescriptionlessDisables,
syntax,
customSyntax,
fix,
quiet,
});
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 prepareReturnValue([], maxWarnings, formatterFunction);
}
let stylelintResult;
try {
const postcssResult = await stylelint._lintSource({
code,
codeFilename: absoluteCodeFilename,
});
stylelintResult = await stylelint._createStylelintResult(postcssResult, absoluteCodeFilename);
} catch (error) {
stylelintResult = await handleError(stylelint, error);
}
const postcssResult = stylelintResult._postcssResult;
const returnValue = prepareReturnValue([stylelintResult], maxWarnings, formatterFunction);
if (
fix &&
postcssResult &&
!postcssResult.stylelint.ignored &&
!postcssResult.stylelint.ruleDisableFix
) {
returnValue.output =
!postcssResult.stylelint.disableWritingFix && postcssResult.opts
? // If we're fixing, the output should be the fixed code
postcssResult.root.toString(postcssResult.opts.syntax)
: // If the writing of the fix is disabled, the input code is returned as-is
code;
}
return returnValue;
}
let fileList = [files].flat().map((entry) => {
const cwd = (globbyOptions && 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(normalizePath(entry));
}
return entry;
});
if (!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();
}
let filePaths = await globby(fileList, globbyOptions);
// The ignorer filter needs to check paths relative to cwd
filePaths = filterFilePaths(
ignorer,
filePaths.map((p) => path.relative(process.cwd(), p)),
);
let stylelintResults;
if (filePaths.length) {
const cwd = (globbyOptions && 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(async (absoluteFilepath) => {
debug(`Processing ${absoluteFilepath}`);
try {
const postcssResult = await stylelint._lintSource({
filePath: absoluteFilepath,
});
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
*/
if (
postcssResult.root &&
postcssResult.opts &&
!postcssResult.stylelint.ignored &&
fix &&
!postcssResult.stylelint.disableWritingFix
) {
const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
if (
postcssResult.root &&
postcssResult.root.source &&
postcssResult.root.source.input.css !== fixedCss
) {
await writeFileAtomic(absoluteFilepath, fixedCss);
}
}
return stylelint._createStylelintResult(postcssResult, absoluteFilepath);
} catch (error) {
// On any error, we should not cache the lint result
fileCache.removeEntry(absoluteFilepath);
return handleError(stylelint, error, absoluteFilepath);
}
});
stylelintResults = await Promise.all(getStylelintResults);
} else if (allowEmptyInput) {
stylelintResults = await Promise.all([]);
} else {
stylelintResults = await Promise.reject(new NoFilesFoundError(fileList));
}
if (useCache) {
fileCache.reconcile();
}
const result = prepareReturnValue(stylelintResults, maxWarnings, formatterFunction);
debug(`Linting complete in ${Date.now() - startTime}ms`);
return result;
}
/**
* @param {FormatterType | Formatter | 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').InternalApi} 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;
}
module.exports = /** @type {typeof import('stylelint').lint} */ (standalone);