Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 1 | 'use strict'; |
| 2 | |
| 3 | const _ = require('lodash'); |
| 4 | const createStylelint = require('./createStylelint'); |
| 5 | const createStylelintResult = require('./createStylelintResult'); |
| 6 | const debug = require('debug')('stylelint:standalone'); |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 7 | const fastGlob = require('fast-glob'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 8 | const FileCache = require('./utils/FileCache'); |
| 9 | const filterFilePaths = require('./utils/filterFilePaths'); |
| 10 | const formatters = require('./formatters'); |
| 11 | const fs = require('fs'); |
| 12 | const getFormatterOptionsText = require('./utils/getFormatterOptionsText'); |
| 13 | const globby = require('globby'); |
| 14 | const hash = require('./utils/hash'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 15 | const NoFilesFoundError = require('./utils/noFilesFoundError'); |
| 16 | const path = require('path'); |
| 17 | const pkg = require('../package.json'); |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 18 | const prepareReturnValue = require('./prepareReturnValue'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 19 | const { default: ignore } = require('ignore'); |
| 20 | const DEFAULT_IGNORE_FILENAME = '.stylelintignore'; |
| 21 | const FILE_NOT_FOUND_ERROR_CODE = 'ENOENT'; |
| 22 | const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**']; |
| 23 | const writeFileAtomic = require('write-file-atomic'); |
| 24 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 25 | /** @typedef {import('stylelint').StylelintStandaloneOptions} StylelintStandaloneOptions */ |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 26 | /** @typedef {import('stylelint').StylelintStandaloneReturnValue} StylelintStandaloneReturnValue */ |
| 27 | /** @typedef {import('stylelint').StylelintResult} StylelintResult */ |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 28 | /** @typedef {import('stylelint').Formatter} Formatter */ |
| 29 | /** @typedef {import('stylelint').FormatterIdentifier} FormatterIdentifier */ |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 30 | |
| 31 | /** |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 32 | * @param {StylelintStandaloneOptions} options |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 33 | * @returns {Promise<StylelintStandaloneReturnValue>} |
| 34 | */ |
| 35 | module.exports = function (options) { |
| 36 | const cacheLocation = options.cacheLocation; |
| 37 | const code = options.code; |
| 38 | const codeFilename = options.codeFilename; |
| 39 | const config = options.config; |
| 40 | const configBasedir = options.configBasedir; |
| 41 | const configFile = options.configFile; |
| 42 | const configOverrides = options.configOverrides; |
| 43 | const customSyntax = options.customSyntax; |
| 44 | const globbyOptions = options.globbyOptions; |
| 45 | const files = options.files; |
| 46 | const fix = options.fix; |
| 47 | const formatter = options.formatter; |
| 48 | const ignoreDisables = options.ignoreDisables; |
| 49 | const reportNeedlessDisables = options.reportNeedlessDisables; |
| 50 | const reportInvalidScopeDisables = options.reportInvalidScopeDisables; |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 51 | const reportDescriptionlessDisables = options.reportDescriptionlessDisables; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 52 | const syntax = options.syntax; |
| 53 | const allowEmptyInput = options.allowEmptyInput || false; |
| 54 | const useCache = options.cache || false; |
| 55 | /** @type {FileCache} */ |
| 56 | let fileCache; |
| 57 | const startTime = Date.now(); |
| 58 | |
| 59 | // The ignorer will be used to filter file paths after the glob is checked, |
| 60 | // before any files are actually read |
| 61 | const ignoreFilePath = options.ignorePath || DEFAULT_IGNORE_FILENAME; |
| 62 | const absoluteIgnoreFilePath = path.isAbsolute(ignoreFilePath) |
| 63 | ? ignoreFilePath |
| 64 | : path.resolve(process.cwd(), ignoreFilePath); |
| 65 | let ignoreText = ''; |
| 66 | |
| 67 | try { |
| 68 | ignoreText = fs.readFileSync(absoluteIgnoreFilePath, 'utf8'); |
| 69 | } catch (readError) { |
| 70 | if (readError.code !== FILE_NOT_FOUND_ERROR_CODE) throw readError; |
| 71 | } |
| 72 | |
| 73 | const ignorePattern = options.ignorePattern || []; |
| 74 | const ignorer = ignore().add(ignoreText).add(ignorePattern); |
| 75 | |
| 76 | const isValidCode = typeof code === 'string'; |
| 77 | |
| 78 | if ((!files && !isValidCode) || (files && (code || isValidCode))) { |
| 79 | throw new Error('You must pass stylelint a `files` glob or a `code` string, though not both'); |
| 80 | } |
| 81 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 82 | /** @type {Formatter} */ |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 83 | let formatterFunction; |
| 84 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 85 | try { |
| 86 | formatterFunction = getFormatterFunction(formatter); |
| 87 | } catch (error) { |
| 88 | return Promise.reject(error); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 89 | } |
| 90 | |
| 91 | const stylelint = createStylelint({ |
| 92 | config, |
| 93 | configFile, |
| 94 | configBasedir, |
| 95 | configOverrides, |
| 96 | ignoreDisables, |
| 97 | ignorePath: ignoreFilePath, |
| 98 | reportNeedlessDisables, |
| 99 | reportInvalidScopeDisables, |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 100 | reportDescriptionlessDisables, |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 101 | syntax, |
| 102 | customSyntax, |
| 103 | fix, |
| 104 | }); |
| 105 | |
| 106 | if (!files) { |
| 107 | const absoluteCodeFilename = |
| 108 | codeFilename !== undefined && !path.isAbsolute(codeFilename) |
| 109 | ? path.join(process.cwd(), codeFilename) |
| 110 | : codeFilename; |
| 111 | |
| 112 | // if file is ignored, return nothing |
| 113 | if ( |
| 114 | absoluteCodeFilename && |
| 115 | !filterFilePaths(ignorer, [path.relative(process.cwd(), absoluteCodeFilename)]).length |
| 116 | ) { |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 117 | return Promise.resolve(prepareReturnValue([], options, formatterFunction)); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 118 | } |
| 119 | |
| 120 | return stylelint |
| 121 | ._lintSource({ |
| 122 | code, |
| 123 | codeFilename: absoluteCodeFilename, |
| 124 | }) |
| 125 | .then((postcssResult) => { |
| 126 | // Check for file existence |
| 127 | return new Promise((resolve, reject) => { |
| 128 | if (!absoluteCodeFilename) { |
| 129 | reject(); |
| 130 | |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | fs.stat(absoluteCodeFilename, (err) => { |
| 135 | if (err) { |
| 136 | reject(); |
| 137 | } else { |
| 138 | resolve(); |
| 139 | } |
| 140 | }); |
| 141 | }) |
| 142 | .then(() => { |
| 143 | return stylelint._createStylelintResult(postcssResult, absoluteCodeFilename); |
| 144 | }) |
| 145 | .catch(() => { |
| 146 | return stylelint._createStylelintResult(postcssResult); |
| 147 | }); |
| 148 | }) |
| 149 | .catch(_.partial(handleError, stylelint)) |
| 150 | .then((stylelintResult) => { |
| 151 | const postcssResult = stylelintResult._postcssResult; |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 152 | const returnValue = prepareReturnValue([stylelintResult], options, formatterFunction); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 153 | |
| 154 | if (options.fix && postcssResult && !postcssResult.stylelint.ignored) { |
| 155 | if (!postcssResult.stylelint.disableWritingFix) { |
| 156 | // If we're fixing, the output should be the fixed code |
| 157 | returnValue.output = postcssResult.root.toString(postcssResult.opts.syntax); |
| 158 | } else { |
| 159 | // If the writing of the fix is disabled, the input code is returned as-is |
| 160 | returnValue.output = code; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | return returnValue; |
| 165 | }); |
| 166 | } |
| 167 | |
| 168 | let fileList = files; |
| 169 | |
| 170 | if (typeof fileList === 'string') { |
| 171 | fileList = [fileList]; |
| 172 | } |
| 173 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 174 | fileList = fileList.map((entry) => { |
| 175 | const cwd = _.get(globbyOptions, 'cwd', process.cwd()); |
| 176 | const absolutePath = !path.isAbsolute(entry) ? path.join(cwd, entry) : path.normalize(entry); |
| 177 | |
| 178 | if (fs.existsSync(absolutePath)) { |
| 179 | // This path points to a file. Return an escaped path to avoid globbing |
| 180 | return fastGlob.escapePath(entry); |
| 181 | } |
| 182 | |
| 183 | return entry; |
| 184 | }); |
| 185 | |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 186 | if (!options.disableDefaultIgnores) { |
| 187 | fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`)); |
| 188 | } |
| 189 | |
| 190 | if (useCache) { |
| 191 | const stylelintVersion = pkg.version; |
| 192 | const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config || {})}`); |
| 193 | |
| 194 | fileCache = new FileCache(cacheLocation, hashOfConfig); |
| 195 | } else { |
| 196 | // No need to calculate hash here, we just want to delete cache file. |
| 197 | fileCache = new FileCache(cacheLocation); |
| 198 | // Remove cache file if cache option is disabled |
| 199 | fileCache.destroy(); |
| 200 | } |
| 201 | |
| 202 | return globby(fileList, globbyOptions) |
| 203 | .then((filePaths) => { |
| 204 | // The ignorer filter needs to check paths relative to cwd |
| 205 | filePaths = filterFilePaths( |
| 206 | ignorer, |
| 207 | filePaths.map((p) => path.relative(process.cwd(), p)), |
| 208 | ); |
| 209 | |
| 210 | if (!filePaths.length) { |
| 211 | if (!allowEmptyInput) { |
| 212 | throw new NoFilesFoundError(fileList); |
| 213 | } |
| 214 | |
| 215 | return Promise.all([]); |
| 216 | } |
| 217 | |
| 218 | const cwd = _.get(globbyOptions, 'cwd', process.cwd()); |
| 219 | let absoluteFilePaths = filePaths.map((filePath) => { |
| 220 | const absoluteFilepath = !path.isAbsolute(filePath) |
| 221 | ? path.join(cwd, filePath) |
| 222 | : path.normalize(filePath); |
| 223 | |
| 224 | return absoluteFilepath; |
| 225 | }); |
| 226 | |
| 227 | if (useCache) { |
| 228 | absoluteFilePaths = absoluteFilePaths.filter(fileCache.hasFileChanged.bind(fileCache)); |
| 229 | } |
| 230 | |
| 231 | const getStylelintResults = absoluteFilePaths.map((absoluteFilepath) => { |
| 232 | debug(`Processing ${absoluteFilepath}`); |
| 233 | |
| 234 | return stylelint |
| 235 | ._lintSource({ |
| 236 | filePath: absoluteFilepath, |
| 237 | }) |
| 238 | .then((postcssResult) => { |
| 239 | if (postcssResult.stylelint.stylelintError && useCache) { |
| 240 | debug(`${absoluteFilepath} contains linting errors and will not be cached.`); |
| 241 | fileCache.removeEntry(absoluteFilepath); |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * If we're fixing, save the file with changed code |
| 246 | * @type {Promise<Error | void>} |
| 247 | */ |
| 248 | let fixFile = Promise.resolve(); |
| 249 | |
| 250 | if ( |
| 251 | postcssResult.root && |
| 252 | postcssResult.opts && |
| 253 | !postcssResult.stylelint.ignored && |
| 254 | options.fix && |
| 255 | !postcssResult.stylelint.disableWritingFix |
| 256 | ) { |
| 257 | // @ts-ignore TODO TYPES toString accepts 0 arguments |
| 258 | const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax); |
| 259 | |
| 260 | if ( |
| 261 | postcssResult.root && |
| 262 | postcssResult.root.source && |
| 263 | // @ts-ignore TODO TYPES css is unknown property |
| 264 | postcssResult.root.source.input.css !== fixedCss |
| 265 | ) { |
| 266 | fixFile = writeFileAtomic(absoluteFilepath, fixedCss); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | return fixFile.then(() => |
| 271 | stylelint._createStylelintResult(postcssResult, absoluteFilepath), |
| 272 | ); |
| 273 | }) |
| 274 | .catch((error) => { |
| 275 | // On any error, we should not cache the lint result |
| 276 | fileCache.removeEntry(absoluteFilepath); |
| 277 | |
| 278 | return handleError(stylelint, error, absoluteFilepath); |
| 279 | }); |
| 280 | }); |
| 281 | |
| 282 | return Promise.all(getStylelintResults); |
| 283 | }) |
| 284 | .then((stylelintResults) => { |
| 285 | if (useCache) { |
| 286 | fileCache.reconcile(); |
| 287 | } |
| 288 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 289 | const rtn = prepareReturnValue(stylelintResults, options, formatterFunction); |
| 290 | |
| 291 | debug(`Linting complete in ${Date.now() - startTime}ms`); |
| 292 | |
| 293 | return rtn; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 294 | }); |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 295 | }; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 296 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 297 | /** |
| 298 | * @param {FormatterIdentifier | undefined} selected |
| 299 | * @returns {Formatter} |
| 300 | */ |
| 301 | function getFormatterFunction(selected) { |
| 302 | /** @type {Formatter} */ |
| 303 | let formatterFunction; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 304 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 305 | if (typeof selected === 'string') { |
| 306 | formatterFunction = formatters[selected]; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 307 | |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 308 | if (formatterFunction === undefined) { |
| 309 | throw new Error( |
| 310 | `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`, |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 311 | ); |
| 312 | } |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 313 | } else if (typeof selected === 'function') { |
| 314 | formatterFunction = selected; |
| 315 | } else { |
| 316 | formatterFunction = formatters.json; |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 317 | } |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 318 | |
| 319 | return formatterFunction; |
| 320 | } |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 321 | |
| 322 | /** |
| 323 | * @param {import('stylelint').StylelintInternalApi} stylelint |
| 324 | * @param {any} error |
| 325 | * @param {string} [filePath] |
| 326 | * @return {Promise<StylelintResult>} |
| 327 | */ |
| 328 | function handleError(stylelint, error, filePath = undefined) { |
| 329 | if (error.name === 'CssSyntaxError') { |
| 330 | return createStylelintResult(stylelint, undefined, filePath, error); |
| 331 | } |
| 332 | |
| 333 | throw error; |
| 334 | } |