|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- '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;
- }
|