UNPKG

stylelint

Version:

A mighty, modern CSS linter.

354 lines (283 loc) 10.7 kB
/* @flow */ 'use strict'; const _ = require('lodash'); const configurationError = require('./utils/configurationError'); const dynamicRequire = require('./dynamicRequire'); const getModulePath = require('./utils/getModulePath'); const globjoin = require('globjoin'); const normalizeRuleSettings = require('./normalizeRuleSettings'); const path = require('path'); const requireRule = require('./requireRule'); // - Merges config and configOverrides // - Makes all paths absolute // - Merges extends function augmentConfigBasic( stylelint /*: stylelint$internalApi*/, config /*: stylelint$config*/, configDir /*: string*/, allowOverrides /*:: ?: boolean*/, ) /*: Promise<stylelint$config>*/ { 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<?{ config: stylelint$config, filepath: string }>*/ { 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<?{ config: stylelint$config, filepath: string }>*/ { const cosmiconfigResult = cosmiconfigResultArg; // Lock in for Flow if (!cosmiconfigResult) return Promise.resolve(null); const config = cosmiconfigResult.config; const filepath = cosmiconfigResult.filepath; const configDir = stylelint._options.configBasedir || path.dirname(filepath || ''); return augmentConfigBasic(stylelint, config, configDir, true) .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, }; }); } // 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<stylelint$config>*/ { 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<?{ config: stylelint$config, filepath: string }>*/ { const extendPath = getModulePath(configDir, extendLookup); return stylelint._extendExplorer.load(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 = dynamicRequire(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 = requireRule(ruleName) || _.get(config, ['pluginFunctions', ruleName]); if (!rule) { normalizedRules[ruleName] = []; } else { 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 = dynamicRequire(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 };