standalone.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { isAbsolute, join, normalize, relative } from 'node:path';
  2. import { existsSync } from 'node:fs';
  3. import process from 'node:process';
  4. import createDebug from 'debug';
  5. const debug = createDebug('stylelint:standalone');
  6. import fastGlob from 'fast-glob';
  7. import globby from 'globby';
  8. import normalizePath from 'normalize-path';
  9. import writeFileAtomic from 'write-file-atomic';
  10. import {
  11. AllFilesIgnoredError,
  12. NoFilesFoundError,
  13. SuppressionFileNotFoundError,
  14. } from './utils/errors.mjs';
  15. import { assertString, isString } from './utils/validateTypes.mjs';
  16. import { emitDeprecationWarning, emitExperimentalWarning } from './utils/emitWarning.mjs';
  17. import { DEFAULT_SUPPRESSION_FILENAME } from './constants.mjs';
  18. import { SuppressionsService } from './utils/suppressionsService.mjs';
  19. import createPartialStylelintResult from './createPartialStylelintResult.mjs';
  20. import createStylelint from './createStylelint.mjs';
  21. import filterFilePaths from './utils/filterFilePaths.mjs';
  22. import getConfigForFile from './getConfigForFile.mjs';
  23. import getFileIgnorer from './utils/getFileIgnorer.mjs';
  24. import getFormatter from './utils/getFormatter.mjs';
  25. import lintSource from './lintSource.mjs';
  26. import normalizeFixMode from './utils/normalizeFixMode.mjs';
  27. import prepareReturnValue from './prepareReturnValue.mjs';
  28. import resolveFilePath from './utils/resolveFilePath.mjs';
  29. import resolveOptionValue from './utils/resolveOptionValue.mjs';
  30. const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**'];
  31. /** @import {InternalApi, LintResult} from 'stylelint' */
  32. /**
  33. * @type {import('stylelint').PublicApi['lint']}
  34. */
  35. export default async function standalone({
  36. allowEmptyInput,
  37. cache,
  38. cacheLocation,
  39. cacheStrategy,
  40. code,
  41. codeFilename,
  42. config,
  43. configBasedir,
  44. configFile,
  45. customSyntax,
  46. cwd = process.cwd(),
  47. disableDefaultIgnores,
  48. files,
  49. fix,
  50. computeEditInfo,
  51. formatter,
  52. _defaultFormatter,
  53. globbyOptions,
  54. ignoreDisables,
  55. ignorePath,
  56. ignorePattern,
  57. maxWarnings,
  58. quiet,
  59. quietDeprecationWarnings = false,
  60. reportDescriptionlessDisables,
  61. reportInvalidScopeDisables,
  62. reportNeedlessDisables,
  63. reportUnscopedDisables,
  64. suppressAll,
  65. suppressRule,
  66. suppressLocation,
  67. validate = true,
  68. }) {
  69. const startTime = Date.now();
  70. const useInputCode = !files && isString(code);
  71. const hasOneValidInput = (files && !isString(code)) || useInputCode;
  72. if (!hasOneValidInput) {
  73. return Promise.reject(
  74. new Error('You must pass stylelint a `files` glob or a `code` string, though not both'),
  75. );
  76. }
  77. // The ignorer will be used to filter file paths after the glob is checked,
  78. // before any files are actually read
  79. /** @type {import('ignore').Ignore} */
  80. let ignorer;
  81. try {
  82. ignorer = getFileIgnorer({ cwd, ignorePath, ignorePattern });
  83. } catch (error) {
  84. return Promise.reject(error);
  85. }
  86. const stylelint = createStylelint({
  87. allowEmptyInput,
  88. cache,
  89. cacheLocation,
  90. cacheStrategy,
  91. config,
  92. configFile,
  93. configBasedir,
  94. cwd,
  95. formatter,
  96. _defaultFormatter,
  97. ignoreDisables,
  98. ignorePath,
  99. reportNeedlessDisables,
  100. reportInvalidScopeDisables,
  101. reportDescriptionlessDisables,
  102. reportUnscopedDisables,
  103. customSyntax,
  104. fix,
  105. computeEditInfo,
  106. quiet,
  107. quietDeprecationWarnings,
  108. validate,
  109. });
  110. /** @see https://github.com/stylelint/stylelint/issues/7447 */
  111. if (!quietDeprecationWarnings && formatter === 'github') {
  112. emitDeprecationWarning(
  113. '"github" formatter is deprecated.',
  114. 'GITHUB_FORMATTER',
  115. 'See https://stylelint.io/awesome-stylelint#formatters for alternative formatters.',
  116. );
  117. }
  118. const formatterFunction = await getFormatter(stylelint);
  119. if (!files) {
  120. assertString(code);
  121. const absoluteCodeFilename =
  122. codeFilename !== undefined && !isAbsolute(codeFilename)
  123. ? join(cwd, codeFilename)
  124. : codeFilename;
  125. // if file is ignored, return nothing
  126. if (
  127. absoluteCodeFilename &&
  128. !filterFilePaths(ignorer, [relative(cwd, absoluteCodeFilename)]).length
  129. ) {
  130. return prepareReturnValue({
  131. results: [],
  132. maxWarnings,
  133. quietDeprecationWarnings,
  134. formatter: formatterFunction,
  135. cwd,
  136. });
  137. }
  138. let stylelintResult;
  139. try {
  140. const postcssResult = await lintSource(stylelint, {
  141. code,
  142. codeFilename: absoluteCodeFilename,
  143. });
  144. stylelintResult = createPartialStylelintResult(postcssResult);
  145. } catch (error) {
  146. stylelintResult = handleError(error);
  147. }
  148. await postProcessStylelintResult(stylelint, stylelintResult, absoluteCodeFilename);
  149. const postcssResult = stylelintResult._postcssResult;
  150. const returnValue = prepareReturnValue({
  151. results: [stylelintResult],
  152. maxWarnings,
  153. quietDeprecationWarnings,
  154. formatter: formatterFunction,
  155. cwd,
  156. });
  157. const autofix = normalizeFixMode(stylelint._options.fix) ?? config?.fix ?? false;
  158. if (autofix && postcssResult && !postcssResult.stylelint.ignored) {
  159. returnValue.code = postcssResult.opts
  160. ? // If we're fixing, the output should be the fixed code
  161. postcssResult.root.toString(postcssResult.opts.syntax)
  162. : // If the writing of the fix is disabled, the input code is returned as-is
  163. code;
  164. returnValue._output = returnValue.code; // TODO: Deprecated. Remove in the next major version.
  165. }
  166. return returnValue;
  167. }
  168. let fileList = [files].flat().map((entry) => {
  169. const globCWD = (globbyOptions && globbyOptions.cwd) || cwd;
  170. const absolutePath = !isAbsolute(entry) ? join(globCWD, entry) : normalize(entry);
  171. if (existsSync(absolutePath)) {
  172. // This path points to a file. Return an escaped path to avoid globbing
  173. return fastGlob.escapePath(normalizePath(entry));
  174. }
  175. return entry;
  176. });
  177. if (!disableDefaultIgnores) {
  178. fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`));
  179. }
  180. const useCache = await resolveOptionValue({ stylelint, name: 'cache', default: false });
  181. if (!useCache) {
  182. stylelint._fileCache.destroy();
  183. }
  184. const effectiveGlobbyOptions = {
  185. cwd,
  186. ...(globbyOptions || {}),
  187. absolute: true,
  188. };
  189. const globCWD = effectiveGlobbyOptions.cwd;
  190. let filePaths = await globby(fileList, effectiveGlobbyOptions);
  191. // Record the length of filePaths before ignore operation
  192. // Prevent prompting "No files matching the pattern 'xx' were found." when .stylelintignore ignore all input files
  193. const filePathsLengthBeforeIgnore = filePaths.length;
  194. // The ignorer filter needs to check paths relative to cwd
  195. filePaths = filterFilePaths(
  196. ignorer,
  197. filePaths.map((p) => relative(globCWD, p)),
  198. );
  199. let stylelintResults;
  200. if (filePaths.length) {
  201. let absoluteFilePaths = filePaths.map((filePath) => {
  202. const absoluteFilepath = !isAbsolute(filePath)
  203. ? join(globCWD, filePath)
  204. : normalize(filePath);
  205. return absoluteFilepath;
  206. });
  207. const getStylelintResults = absoluteFilePaths.map(async (absoluteFilepath) => {
  208. debug(`Processing ${absoluteFilepath}`);
  209. try {
  210. const postcssResult = await lintSource(stylelint, {
  211. filePath: absoluteFilepath,
  212. cache: useCache,
  213. });
  214. if (
  215. (postcssResult.stylelint.stylelintError || postcssResult.stylelint.stylelintWarning) &&
  216. useCache
  217. ) {
  218. debug(`${absoluteFilepath} contains linting errors and will not be cached.`);
  219. stylelint._fileCache.removeEntry(absoluteFilepath);
  220. }
  221. /**
  222. * If we're fixing, save the file with changed code
  223. */
  224. if (postcssResult.root && postcssResult.opts && !postcssResult.stylelint.ignored && fix) {
  225. const fixedCss = postcssResult.root.toString(postcssResult.opts.syntax);
  226. if (
  227. postcssResult.root &&
  228. postcssResult.root.source &&
  229. postcssResult.root.source.input.css !== fixedCss
  230. ) {
  231. await writeFileAtomic(absoluteFilepath, fixedCss);
  232. }
  233. }
  234. const stylelintResult = createPartialStylelintResult(postcssResult);
  235. await postProcessStylelintResult(stylelint, stylelintResult, absoluteFilepath);
  236. return stylelintResult;
  237. } catch (error) {
  238. // On any error, we should not cache the lint result
  239. stylelint._fileCache.removeEntry(absoluteFilepath);
  240. const stylelintResult = handleError(error);
  241. await postProcessStylelintResult(stylelint, stylelintResult, absoluteFilepath);
  242. return stylelintResult;
  243. }
  244. });
  245. stylelintResults = await Promise.all(getStylelintResults);
  246. } else if (await resolveOptionValue({ stylelint, name: 'allowEmptyInput', default: false })) {
  247. stylelintResults = await Promise.all([]);
  248. } else if (filePathsLengthBeforeIgnore) {
  249. // All input files ignored
  250. stylelintResults = await Promise.reject(new AllFilesIgnoredError());
  251. } else {
  252. stylelintResults = await Promise.reject(new NoFilesFoundError(fileList));
  253. }
  254. if (!useInputCode) {
  255. const resolvedSuppressLocation = resolveFilePath(
  256. suppressLocation || DEFAULT_SUPPRESSION_FILENAME,
  257. cwd,
  258. DEFAULT_SUPPRESSION_FILENAME,
  259. );
  260. const existsSuppressionsFile = existsSync(resolvedSuppressLocation);
  261. if (suppressLocation && !existsSuppressionsFile && !suppressAll && !suppressRule) {
  262. throw new SuppressionFileNotFoundError();
  263. }
  264. if (suppressAll || suppressRule || existsSuppressionsFile) {
  265. emitExperimentalWarning(
  266. 'The suppressions feature is experimental.',
  267. 'SUPPRESSIONS',
  268. 'See https://stylelint.io/user-guide/suppressions for more information.',
  269. );
  270. const suppressions = new SuppressionsService({
  271. filePath: resolvedSuppressLocation,
  272. cwd: process.cwd(),
  273. });
  274. if (suppressAll || suppressRule) {
  275. await suppressions.suppress(stylelintResults, suppressRule);
  276. }
  277. const suppressionResults = suppressions.applySuppressions(
  278. stylelintResults,
  279. await suppressions.load(),
  280. );
  281. stylelintResults = suppressionResults.results;
  282. }
  283. }
  284. if (useCache) {
  285. stylelint._fileCache.reconcile();
  286. }
  287. const result = prepareReturnValue({
  288. results: stylelintResults,
  289. maxWarnings,
  290. quietDeprecationWarnings,
  291. formatter: formatterFunction,
  292. cwd,
  293. });
  294. debug(`Linting complete in ${Date.now() - startTime}ms`);
  295. return result;
  296. }
  297. /**
  298. * @import {CssSyntaxError} from 'stylelint'
  299. *
  300. * @param {unknown} error
  301. * @returns {LintResult}
  302. */
  303. function handleError(error) {
  304. if (error instanceof Error && error.name === 'CssSyntaxError') {
  305. return createPartialStylelintResult(undefined, /** @type {CssSyntaxError} */ (error));
  306. }
  307. throw error;
  308. }
  309. /**
  310. * @param {InternalApi} stylelint
  311. * @param {LintResult} stylelintResult
  312. * @param {string} [filePath]
  313. * @returns {Promise<void>}
  314. */
  315. async function postProcessStylelintResult(stylelint, stylelintResult, filePath) {
  316. const configForFile = await getConfigForFile({ stylelint, searchPath: filePath, filePath });
  317. const config = configForFile === null ? {} : configForFile.config;
  318. if (!config._processorFunctions) {
  319. return;
  320. }
  321. for (let postprocess of config._processorFunctions.values()) {
  322. postprocess?.(stylelintResult, stylelintResult._postcssResult?.root);
  323. }
  324. }