'use strict'; const _ = require('lodash'); const configurationError = require('./utils/configurationError'); const getModulePath = require('./utils/getModulePath'); const globjoin = require('globjoin'); const normalizeAllRuleSettings = require('./normalizeAllRuleSettings'); const path = require('path'); /** @typedef {import('stylelint').StylelintConfigPlugins} StylelintConfigPlugins */ /** @typedef {import('stylelint').StylelintConfigProcessor} StylelintConfigProcessor */ /** @typedef {import('stylelint').StylelintConfigProcessors} StylelintConfigProcessors */ /** @typedef {import('stylelint').StylelintConfigRules} StylelintConfigRules */ /** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */ /** @typedef {import('stylelint').StylelintConfig} StylelintConfig */ /** @typedef {import('stylelint').CosmiconfigResult} CosmiconfigResult */ /** * - Merges config and configOverrides * - Makes all paths absolute * - Merges extends * @param {StylelintInternalApi} stylelint * @param {StylelintConfig} config * @param {string} configDir * @param {boolean} [allowOverrides] * @returns {Promise} */ function augmentConfigBasic(stylelint, config, configDir, allowOverrides) { 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. * @param {StylelintInternalApi} stylelint * @param {CosmiconfigResult} [cosmiconfigResult] * @returns {Promise} */ function augmentConfigExtended(stylelint, cosmiconfigResult) { if (!cosmiconfigResult) return Promise.resolve(null); const configDir = path.dirname(cosmiconfigResult.filepath || ''); const { ignoreFiles, ...cleanedConfig } = cosmiconfigResult.config; return augmentConfigBasic(stylelint, cleanedConfig, configDir).then((augmentedConfig) => { return { config: augmentedConfig, filepath: cosmiconfigResult.filepath, }; }); } /** * @param {StylelintInternalApi} stylelint * @param {CosmiconfigResult} [cosmiconfigResult] * @returns {Promise} */ function augmentConfigFull(stylelint, cosmiconfigResult) { 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) * @param {StylelintConfig} config * @param {string} configDir * @returns {StylelintConfig} */ function absolutizePaths(config, configDir) { if (config.ignoreFiles) { config.ignoreFiles = /** @type {string[]} */ ([]).concat(config.ignoreFiles).map((glob) => { if (path.isAbsolute(glob.replace(/^!/, ''))) return glob; return globjoin(configDir, glob); }); } if (config.plugins) { config.plugins = /** @type {string[]} */ ([]).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 * @param {StylelintConfigProcessors} processors * @param {string} configDir * @return {StylelintConfigProcessors} */ function absolutizeProcessors(processors, configDir) { 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]]; }); } /** * @param {StylelintInternalApi} stylelint * @param {StylelintConfig} config * @param {string} configDir * @return {Promise} */ function extendConfig(stylelint, config, configDir) { if (config.extends === undefined) return Promise.resolve(config); const normalizedExtends = Array.isArray(config.extends) ? config.extends : [config.extends]; const { extends: configExtends, ...originalWithoutExtends } = config; 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); }); } /** * @param {StylelintInternalApi} stylelint * @param {StylelintConfig} config * @param {string} configDir * @param {string} extendLookup * @return {Promise} */ function loadExtendedConfig(stylelint, config, configDir, extendLookup) { 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 * @param {StylelintConfig} a * @param {StylelintConfig} b * @returns {StylelintConfig} */ function mergeConfigs(a, b) { /** @type {{plugins: StylelintConfigPlugins}} */ const pluginMerger = {}; if (a.plugins || b.plugins) { pluginMerger.plugins = []; if (a.plugins) { pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins); } if (b.plugins) { pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))]; } } /** @type {{processors: StylelintConfigProcessors}} */ const processorMerger = {}; if (a.processors || b.processors) { processorMerger.processors = []; if (a.processors) { processorMerger.processors = processorMerger.processors.concat(a.processors); } if (b.processors) { processorMerger.processors = [...new Set(processorMerger.processors.concat(b.processors))]; } } const rulesMerger = {}; if (a.rules || b.rules) { rulesMerger.rules = { ...a.rules, ...b.rules }; } const result = { ...a, ...b, ...processorMerger, ...pluginMerger, ...rulesMerger }; return result; } /** * @param {StylelintConfig} config * @returns {StylelintConfig} */ function addPluginFunctions(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 (!pluginRuleDefinition.ruleName.includes('/')) { throw configurationError( 'stylelint v7+ 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.', ); } result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule; }); return result; }, /** @type {{[k: string]: Function}} */ ({})); config.pluginFunctions = pluginFunctions; 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 * - Initialize the processor module by calling its functions with any * provided options * - Push the processor's code and result processors to their respective arrays * @type {Map} */ const processorCache = new Map(); /** * @param {StylelintConfig} config * @return {StylelintConfig} */ function addProcessorFunctions(config) { if (!config.processors) return config; /** @type {Array} */ const codeProcessors = []; /** @type {Array} */ const resultProcessors = []; /** @type {Array} */ ([]) .concat(config.processors) .forEach((processorConfig) => { const processorKey = JSON.stringify(processorConfig); let initializedProcessor; if (processorCache.has(processorKey)) { initializedProcessor = processorCache.get(processorKey); } else { const processorLookup = typeof processorConfig === 'string' ? processorConfig : processorConfig[0]; const processorOptions = typeof processorConfig === 'string' ? undefined : 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 };