augmentConfig.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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').ConfigPlugins} StylelintConfigPlugins */
  10. /** @typedef {import('stylelint').ConfigProcessor} StylelintConfigProcessor */
  11. /** @typedef {import('stylelint').ConfigProcessors} StylelintConfigProcessors */
  12. /** @typedef {import('stylelint').ConfigRules} StylelintConfigRules */
  13. /** @typedef {import('stylelint').ConfigOverride} StylelintConfigOverride */
  14. /** @typedef {import('stylelint').InternalApi} StylelintInternalApi */
  15. /** @typedef {import('stylelint').Config} StylelintConfig */
  16. /** @typedef {import('stylelint').CosmiconfigResult} StylelintCosmiconfigResult */
  17. /** @typedef {import('stylelint').CodeProcessor} StylelintCodeProcessor */
  18. /** @typedef {import('stylelint').ResultProcessor} StylelintResultProcessor */
  19. /**
  20. * - Merges config and stylelint options
  21. * - Makes all paths absolute
  22. * - Merges extends
  23. * @param {StylelintInternalApi} stylelint
  24. * @param {StylelintConfig} config
  25. * @param {string} configDir
  26. * @param {boolean} allowOverrides
  27. * @param {string} rootConfigDir
  28. * @param {string} [filePath]
  29. * @returns {Promise<StylelintConfig>}
  30. */
  31. async function augmentConfigBasic(
  32. stylelint,
  33. config,
  34. configDir,
  35. allowOverrides,
  36. rootConfigDir,
  37. filePath,
  38. ) {
  39. let augmentedConfig = config;
  40. if (allowOverrides) {
  41. augmentedConfig = addOptions(stylelint, augmentedConfig);
  42. }
  43. if (filePath) {
  44. augmentedConfig = applyOverrides(augmentedConfig, rootConfigDir, filePath);
  45. }
  46. augmentedConfig = await extendConfig(
  47. stylelint,
  48. augmentedConfig,
  49. configDir,
  50. rootConfigDir,
  51. filePath,
  52. );
  53. const cwd = stylelint._options.cwd;
  54. return absolutizePaths(augmentedConfig, configDir, cwd);
  55. }
  56. /**
  57. * Extended configs need to be run through augmentConfigBasic
  58. * but do not need the full treatment. Things like pluginFunctions
  59. * will be resolved and added by the parent config.
  60. * @param {string} cwd
  61. * @returns {(cosmiconfigResult?: StylelintCosmiconfigResult) => Promise<StylelintCosmiconfigResult>}
  62. */
  63. function augmentConfigExtended(cwd) {
  64. return async (cosmiconfigResult) => {
  65. if (!cosmiconfigResult) {
  66. return null;
  67. }
  68. const configDir = path.dirname(cosmiconfigResult.filepath || '');
  69. const { config } = cosmiconfigResult;
  70. const augmentedConfig = absolutizePaths(config, configDir, cwd);
  71. return {
  72. config: augmentedConfig,
  73. filepath: cosmiconfigResult.filepath,
  74. };
  75. };
  76. }
  77. /**
  78. * @param {StylelintInternalApi} stylelint
  79. * @param {string} [filePath]
  80. * @param {StylelintCosmiconfigResult} [cosmiconfigResult]
  81. * @returns {Promise<StylelintCosmiconfigResult>}
  82. */
  83. async function augmentConfigFull(stylelint, filePath, cosmiconfigResult) {
  84. if (!cosmiconfigResult) {
  85. return null;
  86. }
  87. const config = cosmiconfigResult.config;
  88. const filepath = cosmiconfigResult.filepath;
  89. const configDir = stylelint._options.configBasedir || path.dirname(filepath || '');
  90. let augmentedConfig = await augmentConfigBasic(
  91. stylelint,
  92. config,
  93. configDir,
  94. true,
  95. configDir,
  96. filePath,
  97. );
  98. augmentedConfig = addPluginFunctions(augmentedConfig);
  99. augmentedConfig = addProcessorFunctions(augmentedConfig);
  100. if (!augmentedConfig.rules) {
  101. throw configurationError(
  102. 'No rules found within configuration. Have you provided a "rules" property?',
  103. );
  104. }
  105. augmentedConfig = normalizeAllRuleSettings(augmentedConfig);
  106. return {
  107. config: augmentedConfig,
  108. filepath: cosmiconfigResult.filepath,
  109. };
  110. }
  111. /**
  112. * Make all paths in the config absolute:
  113. * - ignoreFiles
  114. * - plugins
  115. * - processors
  116. * (extends handled elsewhere)
  117. * @param {StylelintConfig} config
  118. * @param {string} configDir
  119. * @param {string} cwd
  120. * @returns {StylelintConfig}
  121. */
  122. function absolutizePaths(config, configDir, cwd) {
  123. if (config.ignoreFiles) {
  124. config.ignoreFiles = [config.ignoreFiles].flat().map((glob) => {
  125. if (path.isAbsolute(glob.replace(/^!/, ''))) return glob;
  126. return globjoin(configDir, glob);
  127. });
  128. }
  129. if (config.plugins) {
  130. config.plugins = [config.plugins].flat().map((lookup) => {
  131. if (typeof lookup === 'string') {
  132. return getModulePath(configDir, lookup, cwd);
  133. }
  134. return lookup;
  135. });
  136. }
  137. if (config.processors) {
  138. config.processors = absolutizeProcessors(config.processors, configDir);
  139. }
  140. return config;
  141. }
  142. /**
  143. * Processors are absolutized in their own way because
  144. * they can be and return a string or an array
  145. * @param {StylelintConfigProcessors} processors
  146. * @param {string} configDir
  147. * @return {StylelintConfigProcessors}
  148. */
  149. function absolutizeProcessors(processors, configDir) {
  150. const normalizedProcessors = Array.isArray(processors) ? processors : [processors];
  151. return normalizedProcessors.map((item) => {
  152. if (typeof item === 'string') {
  153. return getModulePath(configDir, item);
  154. }
  155. return [getModulePath(configDir, item[0]), item[1]];
  156. });
  157. }
  158. /**
  159. * @param {StylelintInternalApi} stylelint
  160. * @param {StylelintConfig} config
  161. * @param {string} configDir
  162. * @param {string} rootConfigDir
  163. * @param {string} [filePath]
  164. * @return {Promise<StylelintConfig>}
  165. */
  166. async function extendConfig(stylelint, config, configDir, rootConfigDir, filePath) {
  167. if (config.extends === undefined) {
  168. return config;
  169. }
  170. const { extends: configExtends, ...originalWithoutExtends } = config;
  171. const normalizedExtends = [configExtends].flat();
  172. let resultConfig = originalWithoutExtends;
  173. for (const extendLookup of normalizedExtends) {
  174. const extendResult = await loadExtendedConfig(stylelint, configDir, extendLookup);
  175. if (extendResult) {
  176. let extendResultConfig = extendResult.config;
  177. const extendConfigDir = path.dirname(extendResult.filepath || '');
  178. extendResultConfig = await augmentConfigBasic(
  179. stylelint,
  180. extendResultConfig,
  181. extendConfigDir,
  182. false,
  183. rootConfigDir,
  184. filePath,
  185. );
  186. resultConfig = mergeConfigs(resultConfig, extendResultConfig);
  187. }
  188. }
  189. return mergeConfigs(resultConfig, originalWithoutExtends);
  190. }
  191. /**
  192. * @param {StylelintInternalApi} stylelint
  193. * @param {string} configDir
  194. * @param {string} extendLookup
  195. * @return {Promise<StylelintCosmiconfigResult>}
  196. */
  197. function loadExtendedConfig(stylelint, configDir, extendLookup) {
  198. const extendPath = getModulePath(configDir, extendLookup, stylelint._options.cwd);
  199. return stylelint._extendExplorer.load(extendPath);
  200. }
  201. /**
  202. * When merging configs (via extends)
  203. * - plugin and processor arrays are joined
  204. * - rules are merged via Object.assign, so there is no attempt made to
  205. * merge any given rule's settings. If b contains the same rule as a,
  206. * b's rule settings will override a's rule settings entirely.
  207. * - Everything else is merged via Object.assign
  208. * @param {StylelintConfig} a
  209. * @param {StylelintConfig} b
  210. * @returns {StylelintConfig}
  211. */
  212. function mergeConfigs(a, b) {
  213. /** @type {{plugins: StylelintConfigPlugins}} */
  214. const pluginMerger = {};
  215. if (a.plugins || b.plugins) {
  216. pluginMerger.plugins = [];
  217. if (a.plugins) {
  218. pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins);
  219. }
  220. if (b.plugins) {
  221. pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))];
  222. }
  223. }
  224. /** @type {{processors: StylelintConfigProcessors}} */
  225. const processorMerger = {};
  226. if (a.processors || b.processors) {
  227. processorMerger.processors = [];
  228. if (a.processors) {
  229. processorMerger.processors = processorMerger.processors.concat(a.processors);
  230. }
  231. if (b.processors) {
  232. processorMerger.processors = [...new Set(processorMerger.processors.concat(b.processors))];
  233. }
  234. }
  235. /** @type {{overrides: StylelintConfigOverride[]}} */
  236. const overridesMerger = {};
  237. if (a.overrides || b.overrides) {
  238. overridesMerger.overrides = [];
  239. if (a.overrides) {
  240. overridesMerger.overrides = overridesMerger.overrides.concat(a.overrides);
  241. }
  242. if (b.overrides) {
  243. overridesMerger.overrides = [...new Set(overridesMerger.overrides.concat(b.overrides))];
  244. }
  245. }
  246. const rulesMerger = {};
  247. if (a.rules || b.rules) {
  248. rulesMerger.rules = { ...a.rules, ...b.rules };
  249. }
  250. const result = {
  251. ...a,
  252. ...b,
  253. ...processorMerger,
  254. ...pluginMerger,
  255. ...overridesMerger,
  256. ...rulesMerger,
  257. };
  258. return result;
  259. }
  260. /**
  261. * @param {StylelintConfig} config
  262. * @returns {StylelintConfig}
  263. */
  264. function addPluginFunctions(config) {
  265. if (!config.plugins) {
  266. return config;
  267. }
  268. const normalizedPlugins = [config.plugins].flat();
  269. /** @type {StylelintConfig['pluginFunctions']} */
  270. const pluginFunctions = {};
  271. for (const pluginLookup of normalizedPlugins) {
  272. let pluginImport;
  273. if (typeof pluginLookup === 'string') {
  274. pluginImport = require(pluginLookup);
  275. } else {
  276. pluginImport = pluginLookup;
  277. }
  278. // Handle either ES6 or CommonJS modules
  279. pluginImport = pluginImport.default || pluginImport;
  280. // A plugin can export either a single rule definition
  281. // or an array of them
  282. const normalizedPluginImport = [pluginImport].flat();
  283. for (const pluginRuleDefinition of normalizedPluginImport) {
  284. if (!pluginRuleDefinition.ruleName) {
  285. throw configurationError(
  286. `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.`,
  287. );
  288. }
  289. if (!pluginRuleDefinition.ruleName.includes('/')) {
  290. throw configurationError(
  291. `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.`,
  292. );
  293. }
  294. pluginFunctions[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
  295. }
  296. }
  297. config.pluginFunctions = pluginFunctions;
  298. return config;
  299. }
  300. /**
  301. * Given an array of processors strings, we want to add two
  302. * properties to the augmented config:
  303. * - codeProcessors: functions that will run on code as it comes in
  304. * - resultProcessors: functions that will run on results as they go out
  305. *
  306. * To create these properties, we need to:
  307. * - Find the processor module
  308. * - Initialize the processor module by calling its functions with any
  309. * provided options
  310. * - Push the processor's code and result processors to their respective arrays
  311. * @type {Map<string, string | Object>}
  312. */
  313. const processorCache = new Map();
  314. /**
  315. * @param {StylelintConfig} config
  316. * @return {StylelintConfig}
  317. */
  318. function addProcessorFunctions(config) {
  319. if (!config.processors) return config;
  320. /** @type {StylelintCodeProcessor[]} */
  321. const codeProcessors = [];
  322. /** @type {StylelintResultProcessor[]} */
  323. const resultProcessors = [];
  324. for (const processorConfig of [config.processors].flat()) {
  325. const processorKey = JSON.stringify(processorConfig);
  326. let initializedProcessor;
  327. if (processorCache.has(processorKey)) {
  328. initializedProcessor = processorCache.get(processorKey);
  329. } else {
  330. const processorLookup =
  331. typeof processorConfig === 'string' ? processorConfig : processorConfig[0];
  332. const processorOptions = typeof processorConfig === 'string' ? undefined : processorConfig[1];
  333. let processor = require(processorLookup);
  334. processor = processor.default || processor;
  335. initializedProcessor = processor(processorOptions);
  336. processorCache.set(processorKey, initializedProcessor);
  337. }
  338. if (initializedProcessor && initializedProcessor.code) {
  339. codeProcessors.push(initializedProcessor.code);
  340. }
  341. if (initializedProcessor && initializedProcessor.result) {
  342. resultProcessors.push(initializedProcessor.result);
  343. }
  344. }
  345. config.codeProcessors = codeProcessors;
  346. config.resultProcessors = resultProcessors;
  347. return config;
  348. }
  349. /**
  350. * @param {StylelintConfig} fullConfig
  351. * @param {string} rootConfigDir
  352. * @param {string} filePath
  353. * @return {StylelintConfig}
  354. */
  355. function applyOverrides(fullConfig, rootConfigDir, filePath) {
  356. let { overrides, ...config } = fullConfig;
  357. if (!overrides) {
  358. return config;
  359. }
  360. if (!Array.isArray(overrides)) {
  361. throw new TypeError(
  362. 'The `overrides` configuration property should be an array, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
  363. );
  364. }
  365. for (const override of overrides) {
  366. const { files, ...configOverrides } = override;
  367. if (!files) {
  368. throw new Error(
  369. 'Every object in the `overrides` configuration property should have a `files` property with globs, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
  370. );
  371. }
  372. const filesGlobs = [files]
  373. .flat()
  374. .map((glob) => {
  375. if (path.isAbsolute(glob.replace(/^!/, ''))) {
  376. return glob;
  377. }
  378. return globjoin(rootConfigDir, glob);
  379. })
  380. // Glob patterns for micromatch should be in POSIX-style
  381. .map((s) => normalizePath(s));
  382. if (micromatch.isMatch(filePath, filesGlobs, { dot: true })) {
  383. config = mergeConfigs(config, configOverrides);
  384. }
  385. }
  386. return config;
  387. }
  388. /**
  389. * Add options to the config
  390. *
  391. * @param {StylelintInternalApi} stylelint
  392. * @param {StylelintConfig} config
  393. *
  394. * @returns {StylelintConfig}
  395. */
  396. function addOptions(stylelint, config) {
  397. const augmentedConfig = {
  398. ...config,
  399. };
  400. if (stylelint._options.ignoreDisables) {
  401. augmentedConfig.ignoreDisables = stylelint._options.ignoreDisables;
  402. }
  403. if (stylelint._options.quiet) {
  404. augmentedConfig.quiet = stylelint._options.quiet;
  405. }
  406. if (stylelint._options.reportNeedlessDisables) {
  407. augmentedConfig.reportNeedlessDisables = stylelint._options.reportNeedlessDisables;
  408. }
  409. if (stylelint._options.reportInvalidScopeDisables) {
  410. augmentedConfig.reportInvalidScopeDisables = stylelint._options.reportInvalidScopeDisables;
  411. }
  412. if (stylelint._options.reportDescriptionlessDisables) {
  413. augmentedConfig.reportDescriptionlessDisables =
  414. stylelint._options.reportDescriptionlessDisables;
  415. }
  416. if (stylelint._options.customSyntax) {
  417. augmentedConfig.customSyntax = stylelint._options.customSyntax;
  418. }
  419. return augmentedConfig;
  420. }
  421. module.exports = { augmentConfigExtended, augmentConfigFull, applyOverrides };