Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 1 | 'use strict'; |
| 2 | |
| 3 | const _ = require('lodash'); |
| 4 | const configurationError = require('./utils/configurationError'); |
| 5 | const getModulePath = require('./utils/getModulePath'); |
| 6 | const globjoin = require('globjoin'); |
Tim van der Lippe | efb716a | 2020-12-01 12:54:04 +0000 | [diff] [blame^] | 7 | const normalizeAllRuleSettings = require('./normalizeAllRuleSettings'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 8 | const path = require('path'); |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 9 | |
| 10 | /** @typedef {import('stylelint').StylelintConfigPlugins} StylelintConfigPlugins */ |
| 11 | /** @typedef {import('stylelint').StylelintConfigProcessor} StylelintConfigProcessor */ |
| 12 | /** @typedef {import('stylelint').StylelintConfigProcessors} StylelintConfigProcessors */ |
| 13 | /** @typedef {import('stylelint').StylelintConfigRules} StylelintConfigRules */ |
| 14 | /** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */ |
| 15 | /** @typedef {import('stylelint').StylelintConfig} StylelintConfig */ |
| 16 | /** @typedef {import('stylelint').CosmiconfigResult} CosmiconfigResult */ |
| 17 | |
| 18 | /** |
| 19 | * - Merges config and configOverrides |
| 20 | * - Makes all paths absolute |
| 21 | * - Merges extends |
| 22 | * @param {StylelintInternalApi} stylelint |
| 23 | * @param {StylelintConfig} config |
| 24 | * @param {string} configDir |
| 25 | * @param {boolean} [allowOverrides] |
| 26 | * @returns {Promise<StylelintConfig>} |
| 27 | */ |
| 28 | function augmentConfigBasic(stylelint, config, configDir, allowOverrides) { |
| 29 | return Promise.resolve() |
| 30 | .then(() => { |
| 31 | if (!allowOverrides) return config; |
| 32 | |
| 33 | return _.merge(config, stylelint._options.configOverrides); |
| 34 | }) |
| 35 | .then((augmentedConfig) => { |
| 36 | return extendConfig(stylelint, augmentedConfig, configDir); |
| 37 | }) |
| 38 | .then((augmentedConfig) => { |
| 39 | return absolutizePaths(augmentedConfig, configDir); |
| 40 | }); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Extended configs need to be run through augmentConfigBasic |
| 45 | * but do not need the full treatment. Things like pluginFunctions |
| 46 | * will be resolved and added by the parent config. |
| 47 | * @param {StylelintInternalApi} stylelint |
| 48 | * @param {CosmiconfigResult} [cosmiconfigResult] |
| 49 | * @returns {Promise<CosmiconfigResult | null>} |
| 50 | */ |
| 51 | function augmentConfigExtended(stylelint, cosmiconfigResult) { |
| 52 | if (!cosmiconfigResult) return Promise.resolve(null); |
| 53 | |
| 54 | const configDir = path.dirname(cosmiconfigResult.filepath || ''); |
| 55 | const { ignoreFiles, ...cleanedConfig } = cosmiconfigResult.config; |
| 56 | |
| 57 | return augmentConfigBasic(stylelint, cleanedConfig, configDir).then((augmentedConfig) => { |
| 58 | return { |
| 59 | config: augmentedConfig, |
| 60 | filepath: cosmiconfigResult.filepath, |
| 61 | }; |
| 62 | }); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * @param {StylelintInternalApi} stylelint |
| 67 | * @param {CosmiconfigResult} [cosmiconfigResult] |
| 68 | * @returns {Promise<CosmiconfigResult | null>} |
| 69 | */ |
| 70 | function augmentConfigFull(stylelint, cosmiconfigResult) { |
| 71 | if (!cosmiconfigResult) return Promise.resolve(null); |
| 72 | |
| 73 | const config = cosmiconfigResult.config; |
| 74 | const filepath = cosmiconfigResult.filepath; |
| 75 | |
| 76 | const configDir = stylelint._options.configBasedir || path.dirname(filepath || ''); |
| 77 | |
| 78 | return augmentConfigBasic(stylelint, config, configDir, true) |
| 79 | .then((augmentedConfig) => { |
| 80 | return addPluginFunctions(augmentedConfig); |
| 81 | }) |
| 82 | .then((augmentedConfig) => { |
| 83 | return addProcessorFunctions(augmentedConfig); |
| 84 | }) |
| 85 | .then((augmentedConfig) => { |
| 86 | if (!augmentedConfig.rules) { |
| 87 | throw configurationError( |
| 88 | 'No rules found within configuration. Have you provided a "rules" property?', |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | return normalizeAllRuleSettings(augmentedConfig); |
| 93 | }) |
| 94 | .then((augmentedConfig) => { |
| 95 | return { |
| 96 | config: augmentedConfig, |
| 97 | filepath: cosmiconfigResult.filepath, |
| 98 | }; |
| 99 | }); |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Make all paths in the config absolute: |
| 104 | * - ignoreFiles |
| 105 | * - plugins |
| 106 | * - processors |
| 107 | * (extends handled elsewhere) |
| 108 | * @param {StylelintConfig} config |
| 109 | * @param {string} configDir |
| 110 | * @returns {StylelintConfig} |
| 111 | */ |
| 112 | function absolutizePaths(config, configDir) { |
| 113 | if (config.ignoreFiles) { |
| 114 | config.ignoreFiles = /** @type {string[]} */ ([]).concat(config.ignoreFiles).map((glob) => { |
| 115 | if (path.isAbsolute(glob.replace(/^!/, ''))) return glob; |
| 116 | |
| 117 | return globjoin(configDir, glob); |
| 118 | }); |
| 119 | } |
| 120 | |
| 121 | if (config.plugins) { |
| 122 | config.plugins = /** @type {string[]} */ ([]).concat(config.plugins).map((lookup) => { |
| 123 | return getModulePath(configDir, lookup); |
| 124 | }); |
| 125 | } |
| 126 | |
| 127 | if (config.processors) { |
| 128 | config.processors = absolutizeProcessors(config.processors, configDir); |
| 129 | } |
| 130 | |
| 131 | return config; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Processors are absolutized in their own way because |
| 136 | * they can be and return a string or an array |
| 137 | * @param {StylelintConfigProcessors} processors |
| 138 | * @param {string} configDir |
| 139 | * @return {StylelintConfigProcessors} |
| 140 | */ |
| 141 | function absolutizeProcessors(processors, configDir) { |
| 142 | const normalizedProcessors = Array.isArray(processors) ? processors : [processors]; |
| 143 | |
| 144 | return normalizedProcessors.map((item) => { |
| 145 | if (typeof item === 'string') { |
| 146 | return getModulePath(configDir, item); |
| 147 | } |
| 148 | |
| 149 | return [getModulePath(configDir, item[0]), item[1]]; |
| 150 | }); |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * @param {StylelintInternalApi} stylelint |
| 155 | * @param {StylelintConfig} config |
| 156 | * @param {string} configDir |
| 157 | * @return {Promise<StylelintConfig>} |
| 158 | */ |
| 159 | function extendConfig(stylelint, config, configDir) { |
| 160 | if (config.extends === undefined) return Promise.resolve(config); |
| 161 | |
| 162 | const normalizedExtends = Array.isArray(config.extends) ? config.extends : [config.extends]; |
| 163 | const { extends: configExtends, ...originalWithoutExtends } = config; |
| 164 | |
| 165 | const loadExtends = normalizedExtends.reduce((resultPromise, extendLookup) => { |
| 166 | return resultPromise.then((resultConfig) => { |
| 167 | return loadExtendedConfig(stylelint, resultConfig, configDir, extendLookup).then( |
| 168 | (extendResult) => { |
| 169 | if (!extendResult) return resultConfig; |
| 170 | |
| 171 | return mergeConfigs(resultConfig, extendResult.config); |
| 172 | }, |
| 173 | ); |
| 174 | }); |
| 175 | }, Promise.resolve(originalWithoutExtends)); |
| 176 | |
| 177 | return loadExtends.then((resultConfig) => { |
| 178 | return mergeConfigs(resultConfig, originalWithoutExtends); |
| 179 | }); |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * @param {StylelintInternalApi} stylelint |
| 184 | * @param {StylelintConfig} config |
| 185 | * @param {string} configDir |
| 186 | * @param {string} extendLookup |
| 187 | * @return {Promise<CosmiconfigResult | null>} |
| 188 | */ |
| 189 | function loadExtendedConfig(stylelint, config, configDir, extendLookup) { |
| 190 | const extendPath = getModulePath(configDir, extendLookup); |
| 191 | |
| 192 | return stylelint._extendExplorer.load(extendPath); |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * When merging configs (via extends) |
| 197 | * - plugin and processor arrays are joined |
| 198 | * - rules are merged via Object.assign, so there is no attempt made to |
| 199 | * merge any given rule's settings. If b contains the same rule as a, |
| 200 | * b's rule settings will override a's rule settings entirely. |
| 201 | * - Everything else is merged via Object.assign |
| 202 | * @param {StylelintConfig} a |
| 203 | * @param {StylelintConfig} b |
| 204 | * @returns {StylelintConfig} |
| 205 | */ |
| 206 | function mergeConfigs(a, b) { |
| 207 | /** @type {{plugins: StylelintConfigPlugins}} */ |
| 208 | const pluginMerger = {}; |
| 209 | |
| 210 | if (a.plugins || b.plugins) { |
| 211 | pluginMerger.plugins = []; |
| 212 | |
| 213 | if (a.plugins) { |
| 214 | pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins); |
| 215 | } |
| 216 | |
| 217 | if (b.plugins) { |
| 218 | pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))]; |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | /** @type {{processors: StylelintConfigProcessors}} */ |
| 223 | const processorMerger = {}; |
| 224 | |
| 225 | if (a.processors || b.processors) { |
| 226 | processorMerger.processors = []; |
| 227 | |
| 228 | if (a.processors) { |
| 229 | processorMerger.processors = processorMerger.processors.concat(a.processors); |
| 230 | } |
| 231 | |
| 232 | if (b.processors) { |
| 233 | processorMerger.processors = [...new Set(processorMerger.processors.concat(b.processors))]; |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | const rulesMerger = {}; |
| 238 | |
| 239 | if (a.rules || b.rules) { |
| 240 | rulesMerger.rules = { ...a.rules, ...b.rules }; |
| 241 | } |
| 242 | |
| 243 | const result = { ...a, ...b, ...processorMerger, ...pluginMerger, ...rulesMerger }; |
| 244 | |
| 245 | return result; |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * @param {StylelintConfig} config |
| 250 | * @returns {StylelintConfig} |
| 251 | */ |
| 252 | function addPluginFunctions(config) { |
| 253 | if (!config.plugins) return config; |
| 254 | |
| 255 | const normalizedPlugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins]; |
| 256 | |
| 257 | const pluginFunctions = normalizedPlugins.reduce((result, pluginLookup) => { |
| 258 | let pluginImport = require(pluginLookup); |
| 259 | |
| 260 | // Handle either ES6 or CommonJS modules |
| 261 | pluginImport = pluginImport.default || pluginImport; |
| 262 | |
| 263 | // A plugin can export either a single rule definition |
| 264 | // or an array of them |
| 265 | const normalizedPluginImport = Array.isArray(pluginImport) ? pluginImport : [pluginImport]; |
| 266 | |
| 267 | normalizedPluginImport.forEach((pluginRuleDefinition) => { |
| 268 | if (!pluginRuleDefinition.ruleName) { |
| 269 | throw configurationError( |
| 270 | 'stylelint v3+ requires plugins to expose a ruleName. ' + |
| 271 | `The plugin "${pluginLookup}" is not doing this, so will not work ` + |
| 272 | 'with stylelint v3+. Please file an issue with the plugin.', |
| 273 | ); |
| 274 | } |
| 275 | |
| 276 | if (!pluginRuleDefinition.ruleName.includes('/')) { |
| 277 | throw configurationError( |
| 278 | 'stylelint v7+ requires plugin rules to be namespaced, ' + |
| 279 | 'i.e. only `plugin-namespace/plugin-rule-name` plugin rule names are supported. ' + |
| 280 | `The plugin rule "${pluginRuleDefinition.ruleName}" does not do this, so will not work. ` + |
| 281 | 'Please file an issue with the plugin.', |
| 282 | ); |
| 283 | } |
| 284 | |
| 285 | result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule; |
| 286 | }); |
| 287 | |
| 288 | return result; |
| 289 | }, /** @type {{[k: string]: Function}} */ ({})); |
| 290 | |
| 291 | config.pluginFunctions = pluginFunctions; |
| 292 | |
| 293 | return config; |
| 294 | } |
| 295 | |
| 296 | /** |
Mathias Bynens | 79e2cf0 | 2020-05-29 16:46:17 +0200 | [diff] [blame] | 297 | * Given an array of processors strings, we want to add two |
| 298 | * properties to the augmented config: |
| 299 | * - codeProcessors: functions that will run on code as it comes in |
| 300 | * - resultProcessors: functions that will run on results as they go out |
| 301 | * |
| 302 | * To create these properties, we need to: |
| 303 | * - Find the processor module |
| 304 | * - Initialize the processor module by calling its functions with any |
| 305 | * provided options |
| 306 | * - Push the processor's code and result processors to their respective arrays |
| 307 | * @type {Map<string, string | Object>} |
| 308 | */ |
| 309 | const processorCache = new Map(); |
| 310 | |
| 311 | /** |
| 312 | * @param {StylelintConfig} config |
| 313 | * @return {StylelintConfig} |
| 314 | */ |
| 315 | function addProcessorFunctions(config) { |
| 316 | if (!config.processors) return config; |
| 317 | |
| 318 | /** @type {Array<Function>} */ |
| 319 | const codeProcessors = []; |
| 320 | /** @type {Array<Function>} */ |
| 321 | const resultProcessors = []; |
| 322 | |
| 323 | /** @type {Array<StylelintConfigProcessor>} */ ([]) |
| 324 | .concat(config.processors) |
| 325 | .forEach((processorConfig) => { |
| 326 | const processorKey = JSON.stringify(processorConfig); |
| 327 | |
| 328 | let initializedProcessor; |
| 329 | |
| 330 | if (processorCache.has(processorKey)) { |
| 331 | initializedProcessor = processorCache.get(processorKey); |
| 332 | } else { |
| 333 | const processorLookup = |
| 334 | typeof processorConfig === 'string' ? processorConfig : processorConfig[0]; |
| 335 | const processorOptions = |
| 336 | typeof processorConfig === 'string' ? undefined : processorConfig[1]; |
| 337 | let processor = require(processorLookup); |
| 338 | |
| 339 | processor = processor.default || processor; |
| 340 | initializedProcessor = processor(processorOptions); |
| 341 | processorCache.set(processorKey, initializedProcessor); |
| 342 | } |
| 343 | |
| 344 | if (initializedProcessor && initializedProcessor.code) { |
| 345 | codeProcessors.push(initializedProcessor.code); |
| 346 | } |
| 347 | |
| 348 | if (initializedProcessor && initializedProcessor.result) { |
| 349 | resultProcessors.push(initializedProcessor.result); |
| 350 | } |
| 351 | }); |
| 352 | |
| 353 | config.codeProcessors = codeProcessors; |
| 354 | config.resultProcessors = resultProcessors; |
| 355 | |
| 356 | return config; |
| 357 | } |
| 358 | |
| 359 | module.exports = { augmentConfigExtended, augmentConfigFull }; |