stylelint
Version:
A mighty, modern CSS linter.
354 lines (283 loc) • 10.7 kB
JavaScript
/* @flow */
;
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 };