standalone.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. 'use strict';
  2. const debug = require('debug')('stylelint:standalone');
  3. const fastGlob = require('fast-glob');
  4. const fs = require('fs');
  5. const globby = require('globby');
  6. const normalizePath = require('normalize-path');
  7. const path = require('path');
  8. const createStylelint = require('./createStylelint');
  9. const createPartialStylelintResult = require('./createPartialStylelintResult');
  10. const filterFilePaths = require('./utils/filterFilePaths');
  11. const formatters = require('./formatters');
  12. const getFileIgnorer = require('./utils/getFileIgnorer');
  13. const getFormatterOptionsText = require('./utils/getFormatterOptionsText');
  14. const lintSource = require('./lintSource');
  15. const NoFilesFoundError = require('./utils/noFilesFoundError');
  16. const AllFilesIgnoredError = require('./utils/allFilesIgnoredError');
  17. const { assert } = require('./utils/validateTypes');
  18. const prepareReturnValue = require('./prepareReturnValue');
  19. const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
  20. const writeFileAtomic = require('write-file-atomic');
  21. /** @typedef {import('stylelint').Formatter} Formatter */
  22. /** @typedef {import('stylelint').FormatterType} FormatterType */
  23. /**
  24. * @type {import('stylelint')['lint']}
  25. */
  26. async function standalone({
  27. allowEmptyInput = false,
  28. cache: useCache = false,
  29. cacheLocation,
  30. cacheStrategy,
  31. code,
  32. codeFilename,
  33. config,
  34. configBasedir,
  35. configFile,
  36. customSyntax,
  37. cwd = process.cwd(),
  38. disableDefaultIgnores,
  39. files,
  40. fix,
  41. formatter,
  42. globbyOptions,
  43. ignoreDisables,
  44. ignorePath,
  45. ignorePattern,
  46. maxWarnings,
  47. quiet,
  48. quietDeprecationWarnings = false,
  49. reportDescriptionlessDisables,
  50. reportInvalidScopeDisables,
  51. reportNeedlessDisables,
  52. }) {
  53. const startTime = Date.now();
  54. const isValidCode = typeof code === 'string';
  55. if ((!files && !isValidCode) || (files && (code || isValidCode))) {
  56. return Promise.reject(
  57. new Error('You must pass stylelint a `files` glob or a `code` string, though not both'),
  58. );
  59. }
  60. // The ignorer will be used to filter file paths after the glob is checked,
  61. // before any files are actually read
  62. /** @type {import('ignore').Ignore} */
  63. let ignorer;
  64. try {
  65. ignorer = getFileIgnorer({ cwd, ignorePath, ignorePattern });
  66. } catch (error) {
  67. return Promise.reject(error);
  68. }
  69. /** @type {Formatter} */
  70. let formatterFunction;
  71. try {
  72. formatterFunction = getFormatterFunction(formatter);
  73. } catch (error) {
  74. return Promise.reject(error);
  75. }
  76. const stylelint = createStylelint({
  77. cacheLocation,
  78. cacheStrategy,
  79. config,
  80. configFile,
  81. configBasedir,
  82. cwd,
  83. ignoreDisables,
  84. ignorePath,
  85. reportNeedlessDisables,
  86. reportInvalidScopeDisables,
  87. reportDescriptionlessDisables,
  88. customSyntax,
  89. fix,
  90. quiet,
  91. quietDeprecationWarnings,
  92. });
  93. if (!files) {
  94. const absoluteCodeFilename =
  95. codeFilename !== undefined && !path.isAbsolute(codeFilename)
  96. ? path.join(cwd, codeFilename)
  97. : codeFilename;
  98. // if file is ignored, return nothing
  99. if (
  100. absoluteCodeFilename &&
  101. !filterFilePaths(ignorer, [path.relative(cwd, absoluteCodeFilename)]).length
  102. ) {
  103. return prepareReturnValue([], maxWarnings, formatterFunction, cwd);
  104. }
  105. let stylelintResult;
  106. try {
  107. const postcssResult = await lintSource(stylelint, {
  108. code,
  109. codeFilename: absoluteCodeFilename,
  110. });
  111. stylelintResult = createPartialStylelintResult(postcssResult);
  112. } catch (error) {
  113. stylelintResult = handleError(error);
  114. }
  115. const postcssResult = stylelintResult._postcssResult;
  116. const returnValue = prepareReturnValue([stylelintResult], maxWarnings, formatterFunction, cwd);
  117. if (fix && postcssResult && !postcssResult.stylelint.ignored) {
  118. returnValue.output =
  119. !postcssResult.stylelint.disableWritingFix && postcssResult.opts
  120. ? // If we're fixing, the output should be the fixed code
  121. postcssResult.root.toString(postcssResult.opts.syntax)
  122. : // If the writing of the fix is disabled, the input code is returned as-is
  123. code;
  124. }
  125. return returnValue;
  126. }
  127. let fileList = [files].flat().map((entry) => {
  128. const globCWD = (globbyOptions && globbyOptions.cwd) || cwd;
  129. const absolutePath = !path.isAbsolute(entry)
  130. ? path.join(globCWD, entry)
  131. : path.normalize(entry);
  132. if (fs.existsSync(absolutePath)) {
  133. // This path points to a file. Return an escaped path to avoid globbing
  134. return fastGlob.escapePath(normalizePath(entry));
  135. }
  136. return entry;
  137. });
  138. if (!disableDefaultIgnores) {
  139. fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`));
  140. }
  141. if (!useCache) {
  142. stylelint._fileCache.destroy();
  143. }
  144. const effectiveGlobbyOptions = {
  145. cwd,
  146. ...(globbyOptions || {}),
  147. absolute: true,
  148. };
  149. const globCWD = effectiveGlobbyOptions.cwd;
  150. let filePaths = await globby(fileList, effectiveGlobbyOptions);
  151. // Record the length of filePaths before ignore operation
  152. // Prevent prompting "No files matching the pattern 'xx' were found." when .stylelintignore ignore all input files
  153. const filePathsLengthBeforeIgnore = filePaths.length;
  154. // The ignorer filter needs to check paths relative to cwd
  155. filePaths = filterFilePaths(
  156. ignorer,
  157. filePaths.map((p) => path.relative(globCWD, p)),
  158. );
  159. let stylelintResults;
  160. if (filePaths.length) {
  161. let absoluteFilePaths = filePaths.map((filePath) => {
  162. const absoluteFilepath = !path.isAbsolute(filePath)
  163. ? path.join(globCWD, filePath)
  164. : path.normalize(filePath);
  165. return absoluteFilepath;
  166. });
  167. const getStylelintResults = absoluteFilePaths.map(async (absoluteFilepath) => {
  168. debug(`Processing ${absoluteFilepath}`);
  169. try {
  170. const postcssResult = await lintSource(stylelint, {
  171. filePath: absoluteFilepath,
  172. cache: useCache,
  173. });
  174. if (
  175. (postcssResult.stylelint.stylelintError || postcssResult.stylelint.stylelintWarning) &&
  176. useCache
  177. ) {
  178. debug(`${absoluteFilepath} contains linting errors and will not be cached.`);
  179. stylelint._fileCache.removeEntry(absoluteFilepath);
  180. }
  181. /**
  182. * If we're fixing, save the file with changed code
  183. */
  184. if (
  185. postcssResult.root &&
  186. postcssResult.opts &&
  187. !postcssResult.stylelint.ignored &&
  188. fix &&
  189. !postcssResult.stylelint.disableWritingFix
  190. ) {
  191. const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
  192. if (
  193. postcssResult.root &&
  194. postcssResult.root.source &&
  195. postcssResult.root.source.input.css !== fixedCss
  196. ) {
  197. await writeFileAtomic(absoluteFilepath, fixedCss);
  198. }
  199. }
  200. return createPartialStylelintResult(postcssResult);
  201. } catch (error) {
  202. // On any error, we should not cache the lint result
  203. stylelint._fileCache.removeEntry(absoluteFilepath);
  204. return handleError(error);
  205. }
  206. });
  207. stylelintResults = await Promise.all(getStylelintResults);
  208. } else if (allowEmptyInput) {
  209. stylelintResults = await Promise.all([]);
  210. } else if (filePathsLengthBeforeIgnore) {
  211. // All input files ignored
  212. stylelintResults = await Promise.reject(new AllFilesIgnoredError());
  213. } else {
  214. stylelintResults = await Promise.reject(new NoFilesFoundError(fileList));
  215. }
  216. if (useCache) {
  217. stylelint._fileCache.reconcile();
  218. }
  219. const result = prepareReturnValue(stylelintResults, maxWarnings, formatterFunction, cwd);
  220. debug(`Linting complete in ${Date.now() - startTime}ms`);
  221. return result;
  222. }
  223. /**
  224. * @param {FormatterType | Formatter | undefined} selected
  225. * @returns {Formatter}
  226. */
  227. function getFormatterFunction(selected) {
  228. if (typeof selected === 'string') {
  229. const formatterFunction = formatters[selected];
  230. if (formatterFunction === undefined) {
  231. throw new Error(
  232. `You must use a valid formatter option: ${getFormatterOptionsText()} or a function`,
  233. );
  234. }
  235. return formatterFunction;
  236. }
  237. if (typeof selected === 'function') {
  238. return selected;
  239. }
  240. assert(formatters.json);
  241. return formatters.json;
  242. }
  243. /**
  244. * @typedef {import('stylelint').CssSyntaxError} CssSyntaxError
  245. *
  246. * @param {unknown} error
  247. * @return {import('stylelint').LintResult}
  248. */
  249. function handleError(error) {
  250. if (error instanceof Error && error.name === 'CssSyntaxError') {
  251. return createPartialStylelintResult(undefined, /** @type {CssSyntaxError} */ (error));
  252. }
  253. throw error;
  254. }
  255. module.exports = standalone;