UNPKG

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