/* @flow */ "use strict" const configurationError = require("./utils/configurationError") const getModulePath = require("./utils/getModulePath") const _ = require("lodash") const fs = require("fs") const globjoin = require("globjoin") const normalizeRuleSettings = require("./normalizeRuleSettings") const path = require("path") const rules = require("./rules") const DEFAULT_IGNORE_FILENAME = ".stylelintignore" const FILE_NOT_FOUND_ERROR_CODE = "ENOENT" // - Merges config and configOverrides // - Makes all paths absolute // - Merges extends function augmentConfigBasic( stylelint/*: stylelint$internalApi*/, config/*: stylelint$config*/, configDir/*: string*/, allowOverrides/*:: ?: boolean*/ )/*: Promise*/ { return Promise.resolve().then(() => { if (!allowOverrides) return config return _.merge(config, stylelint._options.configOverrides) }).then(augmentedConfig => { return extendConfig(stylelint, augmentedConfig, configDir) }).then(augmentedConfig => { return absolutizePaths(augmentedConfig, configDir) }) } // Extended configs need to be run through augmentConfigBasic // but do not need the full treatment. Things like pluginFunctions // will be resolved and added by the parent config. function augmentConfigExtended( stylelint/*: stylelint$internalApi*/, cosmiconfigResultArg/*: ?{ config: stylelint$config, filepath: string, }*/ )/*: Promise*/ { const cosmiconfigResult = cosmiconfigResultArg // Lock in for Flow if (!cosmiconfigResult) return Promise.resolve(null) const configDir = path.dirname(cosmiconfigResult.filepath || "") const cleanedConfig = _.omit(cosmiconfigResult.config, "ignoreFiles") return augmentConfigBasic(stylelint, cleanedConfig, configDir).then(augmentedConfig => { return { config: augmentedConfig, filepath: cosmiconfigResult.filepath, } }) } function augmentConfigFull( stylelint/*: stylelint$internalApi*/, cosmiconfigResultArg/*: ?{ config: stylelint$config, filepath: string, }*/ )/*: Promise*/ { const cosmiconfigResult = cosmiconfigResultArg // Lock in for Flow if (!cosmiconfigResult) return Promise.resolve(null) const config = cosmiconfigResult.config, filepath = cosmiconfigResult.filepath const configDir = stylelint._options.configBasedir || path.dirname(filepath || "") return augmentConfigBasic(stylelint, config, configDir, true).then(augmentedConfig => { return addIgnorePatterns(stylelint, augmentedConfig) }).then(augmentedConfig => { return addPluginFunctions(augmentedConfig) }).then(augmentedConfig => { return addProcessorFunctions(augmentedConfig) }).then(augmentedConfig => { if (!augmentedConfig.rules) { throw configurationError("No rules found within configuration. Have you provided a \"rules\" property?") } return normalizeAllRuleSettings(augmentedConfig) }).then(augmentedConfig => { return { config: augmentedConfig, filepath: cosmiconfigResult.filepath, } }) } // Load a file ignore ignore patterns, if there is one; // then add them to the config as an ignorePatterns property function addIgnorePatterns( stylelint/*: stylelint$internalApi*/, config/*: stylelint$config*/ )/*: Promise*/ { const ignoreFilePath = stylelint._options.ignorePath || DEFAULT_IGNORE_FILENAME const absoluteIgnoreFilePath = path.isAbsolute(ignoreFilePath) ? ignoreFilePath : path.resolve(process.cwd(), ignoreFilePath) return new Promise((resolve, reject) => { fs.readFile(absoluteIgnoreFilePath, "utf8", (err, data) => { if (err) { // If the file's not found, fine, we'll just // consider it an empty array of globs if (err.code === FILE_NOT_FOUND_ERROR_CODE) { return resolve(config) } return reject(err) } // Add an ignorePatterns property to the config, containing the // .gitignore-patterned globs loaded from .stylelintignore const augmentedConfig/*: stylelint$config*/ = Object.assign({}, config, { ignorePatterns: data, }) resolve(augmentedConfig) }) }) } // Make all paths in the config absolute: // - ignoreFiles // - plugins // - processors // (extends handled elsewhere) function absolutizePaths( config/*: stylelint$config*/, configDir/*: string*/ )/*: stylelint$config*/ { if (config.ignoreFiles) { config.ignoreFiles = [].concat(config.ignoreFiles).map(glob => { if (path.isAbsolute(glob.replace(/^!/, ""))) return glob return globjoin(configDir, glob) }) } if (config.plugins) { config.plugins = [].concat(config.plugins).map(lookup => { return getModulePath(configDir, lookup) }) } if (config.processors) { config.processors = absolutizeProcessors(config.processors, configDir) } return config } // Processors are absolutized in their own way because // they can be and return a string or an array function absolutizeProcessors( processors/*: stylelint$configProcessors*/, configDir/*: string*/ )/*: stylelint$configProcessors*/ { const normalizedProcessors = Array.isArray(processors) ? processors : [processors] return normalizedProcessors.map(item => { if (typeof item === "string") { return getModulePath(configDir, item) } return [ getModulePath(configDir, item[0]), item[1] ] }) } function extendConfig( stylelint/*: stylelint$internalApi*/, config/*: stylelint$config*/, configDir/*: string*/ )/*: Promise*/ { if (config.extends === undefined) return Promise.resolve(config) const normalizedExtends = Array.isArray(config.extends) ? config.extends : [config.extends] const originalWithoutExtends = _.omit(config, "extends") const loadExtends = normalizedExtends.reduce((resultPromise, extendLookup) => { return resultPromise.then(resultConfig => { return loadExtendedConfig(stylelint, resultConfig, configDir, extendLookup).then(extendResult => { if (!extendResult) return resultConfig return mergeConfigs(resultConfig, extendResult.config) }) }) }, Promise.resolve(originalWithoutExtends)) return loadExtends.then(resultConfig => { return mergeConfigs(resultConfig, originalWithoutExtends) }) } function loadExtendedConfig( stylelint/*: stylelint$internalApi*/, config/*: stylelint$config*/, configDir/*: string*/, extendLookup/*: string*/ )/*: Promise*/ { const extendPath = getModulePath(configDir, extendLookup) return stylelint._extendExplorer.load(null, extendPath) } // When merging configs (via extends) // - plugin and processor arrays are joined // - rules are merged via Object.assign, so there is no attempt made to // merge any given rule's settings. If b contains the same rule as a, // b's rule settings will override a's rule settings entirely. // - Everything else is merged via Object.assign function mergeConfigs(a/*: stylelint$config*/, b/*: stylelint$config*/)/*: stylelint$config*/ { const pluginMerger = {} if (a.plugins || b.plugins) { pluginMerger.plugins = [] if (a.plugins) { pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins) } if (b.plugins) { pluginMerger.plugins = _.uniq(pluginMerger.plugins.concat(b.plugins)) } } const processorMerger = {} if (a.processors || b.processors) { processorMerger.processors = [] if (a.processors) { processorMerger.processors = processorMerger.processors.concat(a.processors) } if (b.processors) { processorMerger.processors = _.uniq(processorMerger.processors.concat(b.processors)) } } const rulesMerger = {} if (a.rules || b.rules) { rulesMerger.rules = Object.assign({}, a.rules, b.rules) } const result = Object.assign({}, a, b, processorMerger, pluginMerger, rulesMerger) return result } function addPluginFunctions(config/*: stylelint$config*/)/*: stylelint$config*/ { if (!config.plugins) return config const normalizedPlugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins] const pluginFunctions = normalizedPlugins.reduce((result, pluginLookup) => { let pluginImport = require(pluginLookup) // Handle either ES6 or CommonJS modules pluginImport = pluginImport.default || pluginImport // A plugin can export either a single rule definition // or an array of them const normalizedPluginImport = Array.isArray(pluginImport) ? pluginImport : [pluginImport] normalizedPluginImport.forEach(pluginRuleDefinition => { if (!pluginRuleDefinition.ruleName) { throw configurationError("stylelint v3+ requires plugins to expose a ruleName. " + `The plugin "${pluginLookup}" is not doing this, so will not work ` + "with stylelint v3+. Please file an issue with the plugin.") } if (!_.includes(pluginRuleDefinition.ruleName, "/")) { throw configurationError("stylelint v7+ requires plugin rules to be namspaced, " + "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.") } result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule }) return result }, {}) config.pluginFunctions = pluginFunctions return config } function normalizeAllRuleSettings(config/*: stylelint$config*/)/*: stylelint$config*/ { const normalizedRules = {} if (!config.rules) return config Object.keys(config.rules).forEach(ruleName => { const rawRuleSettings = _.get(config, [ "rules", ruleName ]) const rule = rules[ruleName] || _.get(config, [ "pluginFunctions", ruleName ]) if (!rule) { throw configurationError(`Undefined rule ${ruleName}`) } normalizedRules[ruleName] = normalizeRuleSettings(rawRuleSettings, ruleName, _.get(rule, "primaryOptionArray")) }) config.rules = normalizedRules return config } // Given an array of processors strings, we want to add two // properties to the augmented config: // - codeProcessors: functions that will run on code as it comes in // - resultProcessors: functions that will run on results as they go out // // To create these properties, we need to: // - Find the processor module // - Intialize the processor module by calling its functions with any // provided options // - Push the processor's code and result processors to their respective arrays const processorCache = new Map() function addProcessorFunctions(config/*: stylelint$config*/)/*: stylelint$config*/ { if (!config.processors) return config const codeProcessors = [] const resultProcessors = [] ;[].concat(config.processors).forEach(processorConfig => { const processorKey = JSON.stringify(processorConfig) let initializedProcessor if (processorCache.has(processorKey)) { initializedProcessor = processorCache.get(processorKey) } else { processorConfig = [].concat(processorConfig) const processorLookup = processorConfig[0] const processorOptions = processorConfig[1] let processor = require(processorLookup) processor = processor.default || processor initializedProcessor = processor(processorOptions) processorCache.set(processorKey, initializedProcessor) } if (initializedProcessor && initializedProcessor.code) { codeProcessors.push(initializedProcessor.code) } if (initializedProcessor && initializedProcessor.result) { resultProcessors.push(initializedProcessor.result) } }) config.codeProcessors = codeProcessors config.resultProcessors = resultProcessors return config } module.exports = { augmentConfigExtended, augmentConfigFull }