augmentConfig.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. 'use strict';
  2. const configurationError = require('./utils/configurationError');
  3. const getModulePath = require('./utils/getModulePath');
  4. const globjoin = require('globjoin');
  5. const micromatch = require('micromatch');
  6. const normalizeAllRuleSettings = require('./normalizeAllRuleSettings');
  7. const normalizePath = require('normalize-path');
  8. const path = require('path');
  9. /** @typedef {import('stylelint').InternalApi} StylelintInternalApi */
  10. /** @typedef {import('stylelint').Config} StylelintConfig */
  11. /** @typedef {import('stylelint').CosmiconfigResult} StylelintCosmiconfigResult */
  12. /**
  13. * @param {string} glob
  14. * @param {string} basedir
  15. * @returns {string}
  16. */
  17. function absolutizeGlob(glob, basedir) {
  18. const result = path.isAbsolute(glob.replace(/^!/, '')) ? glob : globjoin(basedir, glob);
  19. // Glob patterns for micromatch should be in POSIX-style
  20. return normalizePath(result);
  21. }
  22. /**
  23. * - Merges config and stylelint options
  24. * - Makes all paths absolute
  25. * - Merges extends
  26. * @param {StylelintInternalApi} stylelint
  27. * @param {StylelintConfig} config
  28. * @param {string} configDir
  29. * @param {boolean} allowOverrides
  30. * @param {string} rootConfigDir
  31. * @param {string} [filePath]
  32. * @returns {Promise<StylelintConfig>}
  33. */
  34. async function augmentConfigBasic(
  35. stylelint,
  36. config,
  37. configDir,
  38. allowOverrides,
  39. rootConfigDir,
  40. filePath,
  41. ) {
  42. let augmentedConfig = config;
  43. if (allowOverrides) {
  44. augmentedConfig = addOptions(stylelint, augmentedConfig);
  45. }
  46. if (filePath) {
  47. augmentedConfig = applyOverrides(augmentedConfig, rootConfigDir, filePath);
  48. }
  49. augmentedConfig = await extendConfig(
  50. stylelint,
  51. augmentedConfig,
  52. configDir,
  53. rootConfigDir,
  54. filePath,
  55. );
  56. const cwd = stylelint._options.cwd;
  57. return absolutizePaths(augmentedConfig, configDir, cwd);
  58. }
  59. /**
  60. * Extended configs need to be run through augmentConfigBasic
  61. * but do not need the full treatment. Things like pluginFunctions
  62. * will be resolved and added by the parent config.
  63. * @param {string} cwd
  64. * @returns {(cosmiconfigResult?: StylelintCosmiconfigResult) => Promise<StylelintCosmiconfigResult>}
  65. */
  66. function augmentConfigExtended(cwd) {
  67. return async (cosmiconfigResult) => {
  68. if (!cosmiconfigResult) {
  69. return null;
  70. }
  71. const configDir = path.dirname(cosmiconfigResult.filepath || '');
  72. const { config } = cosmiconfigResult;
  73. const augmentedConfig = absolutizePaths(config, configDir, cwd);
  74. return {
  75. config: augmentedConfig,
  76. filepath: cosmiconfigResult.filepath,
  77. };
  78. };
  79. }
  80. /**
  81. * @param {StylelintInternalApi} stylelint
  82. * @param {string} [filePath]
  83. * @param {StylelintCosmiconfigResult} [cosmiconfigResult]
  84. * @returns {Promise<StylelintCosmiconfigResult>}
  85. */
  86. async function augmentConfigFull(stylelint, filePath, cosmiconfigResult) {
  87. if (!cosmiconfigResult) {
  88. return null;
  89. }
  90. const config = cosmiconfigResult.config;
  91. const filepath = cosmiconfigResult.filepath;
  92. const configDir = stylelint._options.configBasedir || path.dirname(filepath || '');
  93. let augmentedConfig = await augmentConfigBasic(
  94. stylelint,
  95. config,
  96. configDir,
  97. true,
  98. configDir,
  99. filePath,
  100. );
  101. augmentedConfig = addPluginFunctions(augmentedConfig);
  102. if (!augmentedConfig.rules) {
  103. throw configurationError(
  104. 'No rules found within configuration. Have you provided a "rules" property?',
  105. );
  106. }
  107. augmentedConfig = normalizeAllRuleSettings(augmentedConfig);
  108. return {
  109. config: augmentedConfig,
  110. filepath: cosmiconfigResult.filepath,
  111. };
  112. }
  113. /**
  114. * Make all paths in the config absolute.
  115. *
  116. * @param {StylelintConfig} config
  117. * @param {string} configDir
  118. * @param {string} cwd
  119. * @returns {StylelintConfig}
  120. */
  121. function absolutizePaths(config, configDir, cwd) {
  122. if (config.ignoreFiles) {
  123. config.ignoreFiles = [config.ignoreFiles].flat().map((glob) => absolutizeGlob(glob, configDir));
  124. }
  125. if (config.plugins) {
  126. config.plugins = [config.plugins].flat().map((lookup) => {
  127. if (typeof lookup === 'string') {
  128. return getModulePath(configDir, lookup, cwd);
  129. }
  130. return lookup;
  131. });
  132. }
  133. return config;
  134. }
  135. /**
  136. * @param {StylelintInternalApi} stylelint
  137. * @param {StylelintConfig} config
  138. * @param {string} configDir
  139. * @param {string} rootConfigDir
  140. * @param {string} [filePath]
  141. * @return {Promise<StylelintConfig>}
  142. */
  143. async function extendConfig(stylelint, config, configDir, rootConfigDir, filePath) {
  144. if (config.extends === undefined) {
  145. return config;
  146. }
  147. const { extends: configExtends, ...originalWithoutExtends } = config;
  148. const normalizedExtends = [configExtends].flat();
  149. let resultConfig = originalWithoutExtends;
  150. for (const extendLookup of normalizedExtends) {
  151. const extendResult = await loadExtendedConfig(stylelint, configDir, extendLookup);
  152. if (extendResult) {
  153. let extendResultConfig = extendResult.config;
  154. const extendConfigDir = path.dirname(extendResult.filepath || '');
  155. extendResultConfig = await augmentConfigBasic(
  156. stylelint,
  157. extendResultConfig,
  158. extendConfigDir,
  159. false,
  160. rootConfigDir,
  161. filePath,
  162. );
  163. resultConfig = mergeConfigs(resultConfig, extendResultConfig);
  164. }
  165. }
  166. return mergeConfigs(resultConfig, originalWithoutExtends);
  167. }
  168. /**
  169. * @param {StylelintInternalApi} stylelint
  170. * @param {string} configDir
  171. * @param {string} extendLookup
  172. * @return {Promise<StylelintCosmiconfigResult>}
  173. */
  174. function loadExtendedConfig(stylelint, configDir, extendLookup) {
  175. const extendPath = getModulePath(configDir, extendLookup, stylelint._options.cwd);
  176. return stylelint._extendExplorer.load(extendPath);
  177. }
  178. /**
  179. * When merging configs (via extends)
  180. * - plugin, extends, overrides arrays are joined
  181. * - rules are merged via Object.assign, so there is no attempt made to
  182. * merge any given rule's settings. If b contains the same rule as a,
  183. * b's rule settings will override a's rule settings entirely.
  184. * - Everything else is merged via Object.assign
  185. * @param {StylelintConfig} a
  186. * @param {StylelintConfig} b
  187. * @returns {StylelintConfig}
  188. */
  189. function mergeConfigs(a, b) {
  190. /** @type {Pick<StylelintConfig, 'plugins'>} */
  191. const pluginMerger = {};
  192. if (a.plugins || b.plugins) {
  193. pluginMerger.plugins = [];
  194. if (a.plugins) {
  195. pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins);
  196. }
  197. if (b.plugins) {
  198. pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))];
  199. }
  200. }
  201. /** @type {Pick<StylelintConfig, 'overrides'>} */
  202. const overridesMerger = {};
  203. if (a.overrides || b.overrides) {
  204. overridesMerger.overrides = [];
  205. if (a.overrides) {
  206. overridesMerger.overrides = overridesMerger.overrides.concat(a.overrides);
  207. }
  208. if (b.overrides) {
  209. overridesMerger.overrides = [...new Set(overridesMerger.overrides.concat(b.overrides))];
  210. }
  211. }
  212. /** @type {Pick<StylelintConfig, 'extends'>} */
  213. const extendsMerger = {};
  214. if (a.extends || b.extends) {
  215. extendsMerger.extends = [];
  216. if (a.extends) {
  217. extendsMerger.extends = extendsMerger.extends.concat(a.extends);
  218. }
  219. if (b.extends) {
  220. extendsMerger.extends = extendsMerger.extends.concat(b.extends);
  221. }
  222. // Remove duplicates from the array, the last item takes precedence
  223. extendsMerger.extends = extendsMerger.extends.filter(
  224. (item, index, arr) => arr.lastIndexOf(item) === index,
  225. );
  226. }
  227. const rulesMerger = {};
  228. if (a.rules || b.rules) {
  229. rulesMerger.rules = { ...a.rules, ...b.rules };
  230. }
  231. const result = {
  232. ...a,
  233. ...b,
  234. ...extendsMerger,
  235. ...pluginMerger,
  236. ...overridesMerger,
  237. ...rulesMerger,
  238. };
  239. return result;
  240. }
  241. /**
  242. * @param {StylelintConfig} config
  243. * @returns {StylelintConfig}
  244. */
  245. function addPluginFunctions(config) {
  246. if (!config.plugins) {
  247. return config;
  248. }
  249. const normalizedPlugins = [config.plugins].flat();
  250. /** @type {StylelintConfig['pluginFunctions']} */
  251. const pluginFunctions = {};
  252. for (const pluginLookup of normalizedPlugins) {
  253. let pluginImport;
  254. if (typeof pluginLookup === 'string') {
  255. pluginImport = require(pluginLookup);
  256. } else {
  257. pluginImport = pluginLookup;
  258. }
  259. // Handle either ES6 or CommonJS modules
  260. pluginImport = pluginImport.default || pluginImport;
  261. // A plugin can export either a single rule definition
  262. // or an array of them
  263. const normalizedPluginImport = [pluginImport].flat();
  264. for (const pluginRuleDefinition of normalizedPluginImport) {
  265. if (!pluginRuleDefinition.ruleName) {
  266. throw configurationError(
  267. `stylelint requires plugins to expose a ruleName. The plugin "${pluginLookup}" is not doing this, so will not work with stylelint. Please file an issue with the plugin.`,
  268. );
  269. }
  270. if (!pluginRuleDefinition.ruleName.includes('/')) {
  271. throw configurationError(
  272. `stylelint requires plugin rules to be namespaced, i.e. only \`plugin-namespace/plugin-rule-name\` plugin rule names are supported. The plugin rule "${pluginRuleDefinition.ruleName}" does not do this, so will not work. Please file an issue with the plugin.`,
  273. );
  274. }
  275. pluginFunctions[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
  276. }
  277. }
  278. config.pluginFunctions = pluginFunctions;
  279. return config;
  280. }
  281. /**
  282. * @param {StylelintConfig} fullConfig
  283. * @param {string} rootConfigDir
  284. * @param {string} filePath
  285. * @return {StylelintConfig}
  286. */
  287. function applyOverrides(fullConfig, rootConfigDir, filePath) {
  288. let { overrides, ...config } = fullConfig;
  289. if (!overrides) {
  290. return config;
  291. }
  292. if (!Array.isArray(overrides)) {
  293. throw new TypeError(
  294. 'The `overrides` configuration property should be an array, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
  295. );
  296. }
  297. for (const override of overrides) {
  298. const { files, ...configOverrides } = override;
  299. if (!files) {
  300. throw new Error(
  301. 'Every object in the `overrides` configuration property should have a `files` property with globs, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
  302. );
  303. }
  304. const absoluteGlobs = [files].flat().map((glob) => absolutizeGlob(glob, rootConfigDir));
  305. if (
  306. micromatch.isMatch(filePath, absoluteGlobs, { dot: true }) ||
  307. // E.g. `*.css` matches any CSS files in any directories.
  308. micromatch.isMatch(filePath, files, { dot: true, basename: true })
  309. ) {
  310. config = mergeConfigs(config, configOverrides);
  311. }
  312. }
  313. return config;
  314. }
  315. /**
  316. * Add options to the config
  317. *
  318. * @param {StylelintInternalApi} stylelint
  319. * @param {StylelintConfig} config
  320. *
  321. * @returns {StylelintConfig}
  322. */
  323. function addOptions(stylelint, config) {
  324. const augmentedConfig = {
  325. ...config,
  326. };
  327. if (stylelint._options.ignoreDisables) {
  328. augmentedConfig.ignoreDisables = stylelint._options.ignoreDisables;
  329. }
  330. if (stylelint._options.quiet) {
  331. augmentedConfig.quiet = stylelint._options.quiet;
  332. }
  333. if (stylelint._options.reportNeedlessDisables) {
  334. augmentedConfig.reportNeedlessDisables = stylelint._options.reportNeedlessDisables;
  335. }
  336. if (stylelint._options.reportInvalidScopeDisables) {
  337. augmentedConfig.reportInvalidScopeDisables = stylelint._options.reportInvalidScopeDisables;
  338. }
  339. if (stylelint._options.reportDescriptionlessDisables) {
  340. augmentedConfig.reportDescriptionlessDisables =
  341. stylelint._options.reportDescriptionlessDisables;
  342. }
  343. if (stylelint._options.customSyntax) {
  344. augmentedConfig.customSyntax = stylelint._options.customSyntax;
  345. }
  346. return augmentedConfig;
  347. }
  348. module.exports = { augmentConfigExtended, augmentConfigFull, applyOverrides };