UNPKG

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