blob: ec84b036c0d2fa3e3c1a76c7c46dd0939c28c61c [file] [log] [blame]
/* @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) {
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 = 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 };