blob: 337affc102aa899932f35f208d5368f1dbad58be [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Mathias Bynens79e2cf02020-05-29 16:46:17 +02003const configurationError = require('./utils/configurationError');
4const getModulePath = require('./utils/getModulePath');
5const globjoin = require('globjoin');
Tim van der Lippe16b82282021-11-08 13:50:26 +00006const micromatch = require('micromatch');
Tim van der Lippeefb716a2020-12-01 12:54:04 +00007const normalizeAllRuleSettings = require('./normalizeAllRuleSettings');
Tim van der Lippe16b82282021-11-08 13:50:26 +00008const normalizePath = require('normalize-path');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02009const path = require('path');
Mathias Bynens79e2cf02020-05-29 16:46:17 +020010
Tim van der Lippe16b82282021-11-08 13:50:26 +000011/** @typedef {import('stylelint').ConfigPlugins} StylelintConfigPlugins */
12/** @typedef {import('stylelint').ConfigProcessor} StylelintConfigProcessor */
13/** @typedef {import('stylelint').ConfigProcessors} StylelintConfigProcessors */
14/** @typedef {import('stylelint').ConfigRules} StylelintConfigRules */
15/** @typedef {import('stylelint').ConfigOverride} StylelintConfigOverride */
16/** @typedef {import('stylelint').InternalApi} StylelintInternalApi */
17/** @typedef {import('stylelint').Config} StylelintConfig */
18/** @typedef {import('stylelint').CosmiconfigResult} StylelintCosmiconfigResult */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020019
20/**
Tim van der Lippe16b82282021-11-08 13:50:26 +000021 * - Merges config and stylelint options
Mathias Bynens79e2cf02020-05-29 16:46:17 +020022 * - Makes all paths absolute
23 * - Merges extends
24 * @param {StylelintInternalApi} stylelint
25 * @param {StylelintConfig} config
26 * @param {string} configDir
27 * @param {boolean} [allowOverrides]
Tim van der Lippe16b82282021-11-08 13:50:26 +000028 * @param {string} [filePath]
Mathias Bynens79e2cf02020-05-29 16:46:17 +020029 * @returns {Promise<StylelintConfig>}
30 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000031async function augmentConfigBasic(stylelint, config, configDir, allowOverrides, filePath) {
32 let augmentedConfig = config;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020033
Tim van der Lippe16b82282021-11-08 13:50:26 +000034 if (allowOverrides) {
35 augmentedConfig = addOptions(stylelint, augmentedConfig);
36 }
37
38 augmentedConfig = await extendConfig(stylelint, augmentedConfig, configDir);
39
40 if (filePath) {
41 while (augmentedConfig.overrides) {
42 augmentedConfig = applyOverrides(augmentedConfig, configDir, filePath);
43 augmentedConfig = await extendConfig(stylelint, augmentedConfig, configDir);
44 }
45 }
46
47 return absolutizePaths(augmentedConfig, configDir);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020048}
49
50/**
51 * Extended configs need to be run through augmentConfigBasic
52 * but do not need the full treatment. Things like pluginFunctions
53 * will be resolved and added by the parent config.
54 * @param {StylelintInternalApi} stylelint
Tim van der Lippe16b82282021-11-08 13:50:26 +000055 * @param {StylelintCosmiconfigResult} [cosmiconfigResult]
56 * @returns {Promise<StylelintCosmiconfigResult>}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020057 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000058async function augmentConfigExtended(stylelint, cosmiconfigResult) {
59 if (!cosmiconfigResult) {
60 return null;
61 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +020062
63 const configDir = path.dirname(cosmiconfigResult.filepath || '');
Tim van der Lippe16b82282021-11-08 13:50:26 +000064 const { config } = cosmiconfigResult;
Mathias Bynens79e2cf02020-05-29 16:46:17 +020065
Tim van der Lippe16b82282021-11-08 13:50:26 +000066 const augmentedConfig = await augmentConfigBasic(stylelint, config, configDir);
67
68 return {
69 config: augmentedConfig,
70 filepath: cosmiconfigResult.filepath,
71 };
Mathias Bynens79e2cf02020-05-29 16:46:17 +020072}
73
74/**
75 * @param {StylelintInternalApi} stylelint
Tim van der Lippe16b82282021-11-08 13:50:26 +000076 * @param {string} [filePath]
77 * @param {StylelintCosmiconfigResult} [cosmiconfigResult]
78 * @returns {Promise<StylelintCosmiconfigResult>}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020079 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000080async function augmentConfigFull(stylelint, filePath, cosmiconfigResult) {
81 if (!cosmiconfigResult) {
82 return null;
83 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +020084
85 const config = cosmiconfigResult.config;
86 const filepath = cosmiconfigResult.filepath;
87
88 const configDir = stylelint._options.configBasedir || path.dirname(filepath || '');
89
Tim van der Lippe16b82282021-11-08 13:50:26 +000090 let augmentedConfig = await augmentConfigBasic(stylelint, config, configDir, true, filePath);
Mathias Bynens79e2cf02020-05-29 16:46:17 +020091
Tim van der Lippe16b82282021-11-08 13:50:26 +000092 augmentedConfig = addPluginFunctions(augmentedConfig);
93 augmentedConfig = addProcessorFunctions(augmentedConfig);
94
95 if (!augmentedConfig.rules) {
96 throw configurationError(
97 'No rules found within configuration. Have you provided a "rules" property?',
98 );
99 }
100
101 augmentedConfig = normalizeAllRuleSettings(augmentedConfig);
102
103 return {
104 config: augmentedConfig,
105 filepath: cosmiconfigResult.filepath,
106 };
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200107}
108
109/**
110 * Make all paths in the config absolute:
111 * - ignoreFiles
112 * - plugins
113 * - processors
114 * (extends handled elsewhere)
115 * @param {StylelintConfig} config
116 * @param {string} configDir
117 * @returns {StylelintConfig}
118 */
119function absolutizePaths(config, configDir) {
120 if (config.ignoreFiles) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000121 config.ignoreFiles = [config.ignoreFiles].flat().map((glob) => {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200122 if (path.isAbsolute(glob.replace(/^!/, ''))) return glob;
123
124 return globjoin(configDir, glob);
125 });
126 }
127
128 if (config.plugins) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000129 config.plugins = [config.plugins].flat().map((lookup) => getModulePath(configDir, lookup));
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200130 }
131
132 if (config.processors) {
133 config.processors = absolutizeProcessors(config.processors, configDir);
134 }
135
136 return config;
137}
138
139/**
140 * Processors are absolutized in their own way because
141 * they can be and return a string or an array
142 * @param {StylelintConfigProcessors} processors
143 * @param {string} configDir
144 * @return {StylelintConfigProcessors}
145 */
146function absolutizeProcessors(processors, configDir) {
147 const normalizedProcessors = Array.isArray(processors) ? processors : [processors];
148
149 return normalizedProcessors.map((item) => {
150 if (typeof item === 'string') {
151 return getModulePath(configDir, item);
152 }
153
154 return [getModulePath(configDir, item[0]), item[1]];
155 });
156}
157
158/**
159 * @param {StylelintInternalApi} stylelint
160 * @param {StylelintConfig} config
161 * @param {string} configDir
162 * @return {Promise<StylelintConfig>}
163 */
Tim van der Lippe16b82282021-11-08 13:50:26 +0000164async function extendConfig(stylelint, config, configDir) {
165 if (config.extends === undefined) {
166 return config;
167 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200168
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200169 const { extends: configExtends, ...originalWithoutExtends } = config;
Tim van der Lippe16b82282021-11-08 13:50:26 +0000170 const normalizedExtends = [configExtends].flat();
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200171
Tim van der Lippe16b82282021-11-08 13:50:26 +0000172 let resultConfig = originalWithoutExtends;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200173
Tim van der Lippe16b82282021-11-08 13:50:26 +0000174 for (const extendLookup of normalizedExtends) {
175 const extendResult = await loadExtendedConfig(stylelint, configDir, extendLookup);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200176
Tim van der Lippe16b82282021-11-08 13:50:26 +0000177 if (extendResult) {
178 resultConfig = mergeConfigs(resultConfig, extendResult.config);
179 }
180 }
181
182 return mergeConfigs(resultConfig, originalWithoutExtends);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200183}
184
185/**
186 * @param {StylelintInternalApi} stylelint
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200187 * @param {string} configDir
188 * @param {string} extendLookup
Tim van der Lippe16b82282021-11-08 13:50:26 +0000189 * @return {Promise<StylelintCosmiconfigResult>}
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200190 */
Tim van der Lippe16b82282021-11-08 13:50:26 +0000191function loadExtendedConfig(stylelint, configDir, extendLookup) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200192 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
Tim van der Lippe16b82282021-11-08 13:50:26 +0000239 /** @type {{overrides: StylelintConfigOverride[]}} */
240 const overridesMerger = {};
241
242 if (a.overrides || b.overrides) {
243 overridesMerger.overrides = [];
244
245 if (a.overrides) {
246 overridesMerger.overrides = overridesMerger.overrides.concat(a.overrides);
247 }
248
249 if (b.overrides) {
250 overridesMerger.overrides = [...new Set(overridesMerger.overrides.concat(b.overrides))];
251 }
252 }
253
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200254 const rulesMerger = {};
255
256 if (a.rules || b.rules) {
257 rulesMerger.rules = { ...a.rules, ...b.rules };
258 }
259
Tim van der Lippe16b82282021-11-08 13:50:26 +0000260 const result = {
261 ...a,
262 ...b,
263 ...processorMerger,
264 ...pluginMerger,
265 ...overridesMerger,
266 ...rulesMerger,
267 };
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200268
269 return result;
270}
271
272/**
273 * @param {StylelintConfig} config
274 * @returns {StylelintConfig}
275 */
276function addPluginFunctions(config) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000277 if (!config.plugins) {
278 return config;
279 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200280
Tim van der Lippe16b82282021-11-08 13:50:26 +0000281 const normalizedPlugins = [config.plugins].flat();
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200282
Tim van der Lippe16b82282021-11-08 13:50:26 +0000283 /** @type {{[k: string]: Function}} */
284 const pluginFunctions = {};
285
286 for (const pluginLookup of normalizedPlugins) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200287 let pluginImport = require(pluginLookup);
288
289 // Handle either ES6 or CommonJS modules
290 pluginImport = pluginImport.default || pluginImport;
291
292 // A plugin can export either a single rule definition
293 // or an array of them
Tim van der Lippe16b82282021-11-08 13:50:26 +0000294 const normalizedPluginImport = [pluginImport].flat();
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200295
296 normalizedPluginImport.forEach((pluginRuleDefinition) => {
297 if (!pluginRuleDefinition.ruleName) {
298 throw configurationError(
Tim van der Lippe16b82282021-11-08 13:50:26 +0000299 `stylelint requires plugins to expose a ruleName. The plugin "${pluginLookup}" is not doing this, so will not work with stylelint. Please file an issue with the plugin.`,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200300 );
301 }
302
303 if (!pluginRuleDefinition.ruleName.includes('/')) {
304 throw configurationError(
Tim van der Lippe16b82282021-11-08 13:50:26 +0000305 `stylelint requires plugin rules to be namespaced, 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.`,
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200306 );
307 }
308
Tim van der Lippe16b82282021-11-08 13:50:26 +0000309 pluginFunctions[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200310 });
Tim van der Lippe16b82282021-11-08 13:50:26 +0000311 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200312
313 config.pluginFunctions = pluginFunctions;
314
315 return config;
316}
317
318/**
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200319 * Given an array of processors strings, we want to add two
320 * properties to the augmented config:
321 * - codeProcessors: functions that will run on code as it comes in
322 * - resultProcessors: functions that will run on results as they go out
323 *
324 * To create these properties, we need to:
325 * - Find the processor module
326 * - Initialize the processor module by calling its functions with any
327 * provided options
328 * - Push the processor's code and result processors to their respective arrays
329 * @type {Map<string, string | Object>}
330 */
331const processorCache = new Map();
332
333/**
334 * @param {StylelintConfig} config
335 * @return {StylelintConfig}
336 */
337function addProcessorFunctions(config) {
338 if (!config.processors) return config;
339
340 /** @type {Array<Function>} */
341 const codeProcessors = [];
342 /** @type {Array<Function>} */
343 const resultProcessors = [];
344
Tim van der Lippe16b82282021-11-08 13:50:26 +0000345 [config.processors].flat().forEach((processorConfig) => {
346 const processorKey = JSON.stringify(processorConfig);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200347
Tim van der Lippe16b82282021-11-08 13:50:26 +0000348 let initializedProcessor;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200349
Tim van der Lippe16b82282021-11-08 13:50:26 +0000350 if (processorCache.has(processorKey)) {
351 initializedProcessor = processorCache.get(processorKey);
352 } else {
353 const processorLookup =
354 typeof processorConfig === 'string' ? processorConfig : processorConfig[0];
355 const processorOptions = typeof processorConfig === 'string' ? undefined : processorConfig[1];
356 let processor = require(processorLookup);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200357
Tim van der Lippe16b82282021-11-08 13:50:26 +0000358 processor = processor.default || processor;
359 initializedProcessor = processor(processorOptions);
360 processorCache.set(processorKey, initializedProcessor);
361 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200362
Tim van der Lippe16b82282021-11-08 13:50:26 +0000363 if (initializedProcessor && initializedProcessor.code) {
364 codeProcessors.push(initializedProcessor.code);
365 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200366
Tim van der Lippe16b82282021-11-08 13:50:26 +0000367 if (initializedProcessor && initializedProcessor.result) {
368 resultProcessors.push(initializedProcessor.result);
369 }
370 });
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200371
372 config.codeProcessors = codeProcessors;
373 config.resultProcessors = resultProcessors;
374
375 return config;
376}
377
Tim van der Lippe16b82282021-11-08 13:50:26 +0000378/**
379 * @param {StylelintConfig} fullConfig
380 * @param {string} configDir
381 * @param {string} filePath
382 * @return {StylelintConfig}
383 */
384function applyOverrides(fullConfig, configDir, filePath) {
385 let { overrides, ...config } = fullConfig;
386
387 if (!overrides) {
388 return config;
389 }
390
391 if (!Array.isArray(overrides)) {
392 throw new TypeError(
393 'The `overrides` configuration property should be an array, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
394 );
395 }
396
397 for (const override of overrides) {
398 const { files, ...configOverrides } = override;
399
400 if (!files) {
401 throw new Error(
402 'Every object in the `overrides` configuration property should have a `files` property with globs, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
403 );
404 }
405
406 const filesGlobs = [files]
407 .flat()
408 .map((glob) => {
409 if (path.isAbsolute(glob.replace(/^!/, ''))) {
410 return glob;
411 }
412
413 return globjoin(configDir, glob);
414 })
415 // Glob patterns for micromatch should be in POSIX-style
416 .map((s) => normalizePath(s));
417
418 if (micromatch.isMatch(filePath, filesGlobs, { dot: true })) {
419 config = mergeConfigs(config, configOverrides);
420 }
421 }
422
423 return config;
424}
425
426/**
427 * Add options to the config
428 *
429 * @param {StylelintInternalApi} stylelint
430 * @param {StylelintConfig} config
431 *
432 * @returns {StylelintConfig}
433 */
434function addOptions(stylelint, config) {
435 const augmentedConfig = {
436 ...config,
437 };
438
439 if (stylelint._options.ignoreDisables) {
440 augmentedConfig.ignoreDisables = stylelint._options.ignoreDisables;
441 }
442
443 if (stylelint._options.quiet) {
444 augmentedConfig.quiet = stylelint._options.quiet;
445 }
446
447 if (stylelint._options.reportNeedlessDisables) {
448 augmentedConfig.reportNeedlessDisables = stylelint._options.reportNeedlessDisables;
449 }
450
451 if (stylelint._options.reportInvalidScopeDisables) {
452 augmentedConfig.reportInvalidScopeDisables = stylelint._options.reportInvalidScopeDisables;
453 }
454
455 if (stylelint._options.reportDescriptionlessDisables) {
456 augmentedConfig.reportDescriptionlessDisables =
457 stylelint._options.reportDescriptionlessDisables;
458 }
459
460 if (stylelint._options.customSyntax) {
461 augmentedConfig.customSyntax = stylelint._options.customSyntax;
462 }
463
464 return augmentedConfig;
465}
466
467module.exports = { augmentConfigExtended, augmentConfigFull, applyOverrides };