UNPKG

11.1 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const configurationError = require('./utils/configurationError');
5const getModulePath = require('./utils/getModulePath');
6const globjoin = require('globjoin');
7const normalizeAllRuleSettings = require('./normalizeAllRuleSettings');
8const path = require('path');
9
10/** @typedef {import('stylelint').StylelintConfigPlugins} StylelintConfigPlugins */
11/** @typedef {import('stylelint').StylelintConfigProcessor} StylelintConfigProcessor */
12/** @typedef {import('stylelint').StylelintConfigProcessors} StylelintConfigProcessors */
13/** @typedef {import('stylelint').StylelintConfigRules} StylelintConfigRules */
14/** @typedef {import('stylelint').StylelintInternalApi} StylelintInternalApi */
15/** @typedef {import('stylelint').StylelintConfig} StylelintConfig */
16/** @typedef {import('stylelint').CosmiconfigResult} CosmiconfigResult */
17
18/**
19 * - Merges config and configOverrides
20 * - Makes all paths absolute
21 * - Merges extends
22 * @param {StylelintInternalApi} stylelint
23 * @param {StylelintConfig} config
24 * @param {string} configDir
25 * @param {boolean} [allowOverrides]
26 * @returns {Promise<StylelintConfig>}
27 */
28function augmentConfigBasic(stylelint, config, configDir, allowOverrides) {
29 return Promise.resolve()
30 .then(() => {
31 if (!allowOverrides) return config;
32
33 return _.merge(config, stylelint._options.configOverrides);
34 })
35 .then((augmentedConfig) => {
36 return extendConfig(stylelint, augmentedConfig, configDir);
37 })
38 .then((augmentedConfig) => {
39 return absolutizePaths(augmentedConfig, configDir);
40 });
41}
42
43/**
44 * Extended configs need to be run through augmentConfigBasic
45 * but do not need the full treatment. Things like pluginFunctions
46 * will be resolved and added by the parent config.
47 * @param {StylelintInternalApi} stylelint
48 * @param {CosmiconfigResult} [cosmiconfigResult]
49 * @returns {Promise<CosmiconfigResult | null>}
50 */
51function augmentConfigExtended(stylelint, cosmiconfigResult) {
52 if (!cosmiconfigResult) return Promise.resolve(null);
53
54 const configDir = path.dirname(cosmiconfigResult.filepath || '');
55 const { ignoreFiles, ...cleanedConfig } = cosmiconfigResult.config;
56
57 return augmentConfigBasic(stylelint, cleanedConfig, configDir).then((augmentedConfig) => {
58 return {
59 config: augmentedConfig,
60 filepath: cosmiconfigResult.filepath,
61 };
62 });
63}
64
65/**
66 * @param {StylelintInternalApi} stylelint
67 * @param {CosmiconfigResult} [cosmiconfigResult]
68 * @returns {Promise<CosmiconfigResult | null>}
69 */
70function augmentConfigFull(stylelint, cosmiconfigResult) {
71 if (!cosmiconfigResult) return Promise.resolve(null);
72
73 const config = cosmiconfigResult.config;
74 const filepath = cosmiconfigResult.filepath;
75
76 const configDir = stylelint._options.configBasedir || path.dirname(filepath || '');
77
78 return augmentConfigBasic(stylelint, config, configDir, true)
79 .then((augmentedConfig) => {
80 return addPluginFunctions(augmentedConfig);
81 })
82 .then((augmentedConfig) => {
83 return addProcessorFunctions(augmentedConfig);
84 })
85 .then((augmentedConfig) => {
86 if (!augmentedConfig.rules) {
87 throw configurationError(
88 'No rules found within configuration. Have you provided a "rules" property?',
89 );
90 }
91
92 return normalizeAllRuleSettings(augmentedConfig);
93 })
94 .then((augmentedConfig) => {
95 return {
96 config: augmentedConfig,
97 filepath: cosmiconfigResult.filepath,
98 };
99 });
100}
101
102/**
103 * Make all paths in the config absolute:
104 * - ignoreFiles
105 * - plugins
106 * - processors
107 * (extends handled elsewhere)
108 * @param {StylelintConfig} config
109 * @param {string} configDir
110 * @returns {StylelintConfig}
111 */
112function absolutizePaths(config, configDir) {
113 if (config.ignoreFiles) {
114 config.ignoreFiles = /** @type {string[]} */ ([]).concat(config.ignoreFiles).map((glob) => {
115 if (path.isAbsolute(glob.replace(/^!/, ''))) return glob;
116
117 return globjoin(configDir, glob);
118 });
119 }
120
121 if (config.plugins) {
122 config.plugins = /** @type {string[]} */ ([]).concat(config.plugins).map((lookup) => {
123 return getModulePath(configDir, lookup);
124 });
125 }
126
127 if (config.processors) {
128 config.processors = absolutizeProcessors(config.processors, configDir);
129 }
130
131 return config;
132}
133
134/**
135 * Processors are absolutized in their own way because
136 * they can be and return a string or an array
137 * @param {StylelintConfigProcessors} processors
138 * @param {string} configDir
139 * @return {StylelintConfigProcessors}
140 */
141function absolutizeProcessors(processors, configDir) {
142 const normalizedProcessors = Array.isArray(processors) ? processors : [processors];
143
144 return normalizedProcessors.map((item) => {
145 if (typeof item === 'string') {
146 return getModulePath(configDir, item);
147 }
148
149 return [getModulePath(configDir, item[0]), item[1]];
150 });
151}
152
153/**
154 * @param {StylelintInternalApi} stylelint
155 * @param {StylelintConfig} config
156 * @param {string} configDir
157 * @return {Promise<StylelintConfig>}
158 */
159function extendConfig(stylelint, config, configDir) {
160 if (config.extends === undefined) return Promise.resolve(config);
161
162 const normalizedExtends = Array.isArray(config.extends) ? config.extends : [config.extends];
163 const { extends: configExtends, ...originalWithoutExtends } = config;
164
165 const loadExtends = normalizedExtends.reduce((resultPromise, extendLookup) => {
166 return resultPromise.then((resultConfig) => {
167 return loadExtendedConfig(stylelint, resultConfig, configDir, extendLookup).then(
168 (extendResult) => {
169 if (!extendResult) return resultConfig;
170
171 return mergeConfigs(resultConfig, extendResult.config);
172 },
173 );
174 });
175 }, Promise.resolve(originalWithoutExtends));
176
177 return loadExtends.then((resultConfig) => {
178 return mergeConfigs(resultConfig, originalWithoutExtends);
179 });
180}
181
182/**
183 * @param {StylelintInternalApi} stylelint
184 * @param {StylelintConfig} config
185 * @param {string} configDir
186 * @param {string} extendLookup
187 * @return {Promise<CosmiconfigResult | null>}
188 */
189function loadExtendedConfig(stylelint, config, configDir, extendLookup) {
190 const extendPath = getModulePath(configDir, extendLookup);
191
192 return stylelint._extendExplorer.load(extendPath);
193}
194
195/**
196 * When merging configs (via extends)
197 * - plugin and processor arrays are joined
198 * - rules are merged via Object.assign, so there is no attempt made to
199 * merge any given rule's settings. If b contains the same rule as a,
200 * b's rule settings will override a's rule settings entirely.
201 * - Everything else is merged via Object.assign
202 * @param {StylelintConfig} a
203 * @param {StylelintConfig} b
204 * @returns {StylelintConfig}
205 */
206function mergeConfigs(a, b) {
207 /** @type {{plugins: StylelintConfigPlugins}} */
208 const pluginMerger = {};
209
210 if (a.plugins || b.plugins) {
211 pluginMerger.plugins = [];
212
213 if (a.plugins) {
214 pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins);
215 }
216
217 if (b.plugins) {
218 pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))];
219 }
220 }
221
222 /** @type {{processors: StylelintConfigProcessors}} */
223 const processorMerger = {};
224
225 if (a.processors || b.processors) {
226 processorMerger.processors = [];
227
228 if (a.processors) {
229 processorMerger.processors = processorMerger.processors.concat(a.processors);
230 }
231
232 if (b.processors) {
233 processorMerger.processors = [...new Set(processorMerger.processors.concat(b.processors))];
234 }
235 }
236
237 const rulesMerger = {};
238
239 if (a.rules || b.rules) {
240 rulesMerger.rules = { ...a.rules, ...b.rules };
241 }
242
243 const result = { ...a, ...b, ...processorMerger, ...pluginMerger, ...rulesMerger };
244
245 return result;
246}
247
248/**
249 * @param {StylelintConfig} config
250 * @returns {StylelintConfig}
251 */
252function addPluginFunctions(config) {
253 if (!config.plugins) return config;
254
255 const normalizedPlugins = Array.isArray(config.plugins) ? config.plugins : [config.plugins];
256
257 const pluginFunctions = normalizedPlugins.reduce((result, pluginLookup) => {
258 let pluginImport = require(pluginLookup);
259
260 // Handle either ES6 or CommonJS modules
261 pluginImport = pluginImport.default || pluginImport;
262
263 // A plugin can export either a single rule definition
264 // or an array of them
265 const normalizedPluginImport = Array.isArray(pluginImport) ? pluginImport : [pluginImport];
266
267 normalizedPluginImport.forEach((pluginRuleDefinition) => {
268 if (!pluginRuleDefinition.ruleName) {
269 throw configurationError(
270 'stylelint v3+ requires plugins to expose a ruleName. ' +
271 `The plugin "${pluginLookup}" is not doing this, so will not work ` +
272 'with stylelint v3+. Please file an issue with the plugin.',
273 );
274 }
275
276 if (!pluginRuleDefinition.ruleName.includes('/')) {
277 throw configurationError(
278 'stylelint v7+ requires plugin rules to be namespaced, ' +
279 'i.e. only `plugin-namespace/plugin-rule-name` plugin rule names are supported. ' +
280 `The plugin rule "${pluginRuleDefinition.ruleName}" does not do this, so will not work. ` +
281 'Please file an issue with the plugin.',
282 );
283 }
284
285 result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
286 });
287
288 return result;
289 }, /** @type {{[k: string]: Function}} */ ({}));
290
291 config.pluginFunctions = pluginFunctions;
292
293 return config;
294}
295
296/**
297 * Given an array of processors strings, we want to add two
298 * properties to the augmented config:
299 * - codeProcessors: functions that will run on code as it comes in
300 * - resultProcessors: functions that will run on results as they go out
301 *
302 * To create these properties, we need to:
303 * - Find the processor module
304 * - Initialize the processor module by calling its functions with any
305 * provided options
306 * - Push the processor's code and result processors to their respective arrays
307 * @type {Map<string, string | Object>}
308 */
309const processorCache = new Map();
310
311/**
312 * @param {StylelintConfig} config
313 * @return {StylelintConfig}
314 */
315function addProcessorFunctions(config) {
316 if (!config.processors) return config;
317
318 /** @type {Array<Function>} */
319 const codeProcessors = [];
320 /** @type {Array<Function>} */
321 const resultProcessors = [];
322
323 /** @type {Array<StylelintConfigProcessor>} */ ([])
324 .concat(config.processors)
325 .forEach((processorConfig) => {
326 const processorKey = JSON.stringify(processorConfig);
327
328 let initializedProcessor;
329
330 if (processorCache.has(processorKey)) {
331 initializedProcessor = processorCache.get(processorKey);
332 } else {
333 const processorLookup =
334 typeof processorConfig === 'string' ? processorConfig : processorConfig[0];
335 const processorOptions =
336 typeof processorConfig === 'string' ? undefined : processorConfig[1];
337 let processor = require(processorLookup);
338
339 processor = processor.default || processor;
340 initializedProcessor = processor(processorOptions);
341 processorCache.set(processorKey, initializedProcessor);
342 }
343
344 if (initializedProcessor && initializedProcessor.code) {
345 codeProcessors.push(initializedProcessor.code);
346 }
347
348 if (initializedProcessor && initializedProcessor.result) {
349 resultProcessors.push(initializedProcessor.result);
350 }
351 });
352
353 config.codeProcessors = codeProcessors;
354 config.resultProcessors = resultProcessors;
355
356 return config;
357}
358
359module.exports = { augmentConfigExtended, augmentConfigFull };