standalone.js 9.9 KB

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