getPostcssResult.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. 'use strict';
  2. const fs = require('fs');
  3. const LazyResult = require('postcss/lib/lazy-result');
  4. const postcss = require('postcss');
  5. const syntaxes = require('./syntaxes');
  6. /** @typedef {import('postcss').Result} Result */
  7. /** @typedef {import('postcss').Syntax} Syntax */
  8. /** @typedef {import('stylelint').CustomSyntax} CustomSyntax */
  9. /** @typedef {import('stylelint').GetPostcssOptions} GetPostcssOptions */
  10. /** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */
  11. const postcssProcessor = postcss();
  12. /**
  13. * @param {StylelintInternalApi} stylelint
  14. * @param {GetPostcssOptions} options
  15. *
  16. * @returns {Promise<Result>}
  17. */
  18. module.exports = function (stylelint, options = {}) {
  19. const cached = options.filePath ? stylelint._postcssResultCache.get(options.filePath) : undefined;
  20. if (cached) return Promise.resolve(cached);
  21. /** @type {Promise<string> | undefined} */
  22. let getCode;
  23. if (options.code !== undefined) {
  24. getCode = Promise.resolve(options.code);
  25. } else if (options.filePath) {
  26. getCode = readFile(options.filePath);
  27. }
  28. if (!getCode) {
  29. throw new Error('code or filePath required');
  30. }
  31. return getCode
  32. .then((code) => {
  33. /** @type {Syntax | null} */
  34. let syntax = null;
  35. if (stylelint._options.customSyntax) {
  36. syntax = getCustomSyntax(stylelint._options.customSyntax);
  37. } else if (stylelint._options.syntax) {
  38. if (stylelint._options.syntax === 'css') {
  39. syntax = cssSyntax(stylelint);
  40. } else {
  41. const keys = Object.keys(syntaxes);
  42. if (!keys.includes(stylelint._options.syntax)) {
  43. throw new Error(
  44. `You must use a valid syntax option, either: css, ${keys
  45. .slice(0, -1)
  46. .join(', ')} or ${keys.slice(-1)}`,
  47. );
  48. }
  49. syntax = syntaxes[stylelint._options.syntax];
  50. }
  51. } else if (!(options.codeProcessors && options.codeProcessors.length)) {
  52. const autoSyntax = require('postcss-syntax');
  53. // TODO: investigate why lazy import HTML syntax causes
  54. // JS files with the word "html" to throw TypeError
  55. // https://github.com/stylelint/stylelint/issues/4793
  56. const { html, ...rest } = syntaxes;
  57. syntax = autoSyntax({
  58. css: cssSyntax(stylelint),
  59. jsx: syntaxes['css-in-js'],
  60. ...rest,
  61. });
  62. }
  63. const postcssOptions = {
  64. from: options.filePath,
  65. syntax,
  66. };
  67. const source = options.code ? options.codeFilename : options.filePath;
  68. let preProcessedCode = code;
  69. if (options.codeProcessors && options.codeProcessors.length) {
  70. if (stylelint._options.fix) {
  71. // eslint-disable-next-line no-console
  72. console.warn(
  73. 'Autofix is incompatible with processors and will be disabled. Are you sure you need a processor?',
  74. );
  75. stylelint._options.fix = false;
  76. }
  77. options.codeProcessors.forEach((codeProcessor) => {
  78. preProcessedCode = codeProcessor(preProcessedCode, source);
  79. });
  80. }
  81. const result = new LazyResult(postcssProcessor, preProcessedCode, postcssOptions);
  82. return result;
  83. })
  84. .then((postcssResult) => {
  85. if (options.filePath) {
  86. stylelint._postcssResultCache.set(options.filePath, postcssResult);
  87. }
  88. return postcssResult;
  89. });
  90. };
  91. /**
  92. * @param {CustomSyntax} customSyntax
  93. * @returns {Syntax}
  94. */
  95. function getCustomSyntax(customSyntax) {
  96. let resolved;
  97. if (typeof customSyntax === 'string') {
  98. try {
  99. resolved = require(customSyntax);
  100. } catch (error) {
  101. throw new Error(
  102. `Cannot resolve custom syntax module ${customSyntax}. Check that module ${customSyntax} is available and spelled correctly.`,
  103. );
  104. }
  105. /*
  106. * PostCSS allows for syntaxes that only contain a parser, however,
  107. * it then expects the syntax to be set as the `parse` option.
  108. */
  109. if (!resolved.parse) {
  110. resolved = {
  111. parse: resolved,
  112. stringify: postcss.stringify,
  113. };
  114. }
  115. return resolved;
  116. }
  117. if (typeof customSyntax === 'object') {
  118. if (typeof customSyntax.parse === 'function') {
  119. resolved = { ...customSyntax };
  120. } else {
  121. throw new Error(
  122. `An object provided to the "customSyntax" option must have a "parse" property. Ensure the "parse" property exists and its value is a function.`,
  123. );
  124. }
  125. return resolved;
  126. }
  127. throw new Error(`Custom syntax must be a string or a Syntax object`);
  128. }
  129. /**
  130. * @param {string} filePath
  131. * @returns {Promise<string>}
  132. */
  133. function readFile(filePath) {
  134. return new Promise((resolve, reject) => {
  135. fs.readFile(filePath, 'utf8', (err, content) => {
  136. if (err) {
  137. return reject(err);
  138. }
  139. resolve(content);
  140. });
  141. });
  142. }
  143. /**
  144. * @param {StylelintInternalApi} stylelint
  145. * @returns {Syntax}
  146. */
  147. function cssSyntax(stylelint) {
  148. return {
  149. parse: stylelint._options.fix ? require('postcss-safe-parser') : postcss.parse,
  150. stringify: postcss.stringify,
  151. };
  152. }