UNPKG

14 kBJavaScriptView Raw
1'use strict';
2
3const configurationError = require('./utils/configurationError');
4const getModulePath = require('./utils/getModulePath');
5const globjoin = require('globjoin');
6const micromatch = require('micromatch');
7const normalizeAllRuleSettings = require('./normalizeAllRuleSettings');
8const normalizePath = require('normalize-path');
9const path = require('path');
10
11/** @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 */
19/** @typedef {import('stylelint').CodeProcessor} StylelintCodeProcessor */
20/** @typedef {import('stylelint').ResultProcessor} StylelintResultProcessor */
21
22/**
23 * - Merges config and stylelint options
24 * - Makes all paths absolute
25 * - Merges extends
26 * @param {StylelintInternalApi} stylelint
27 * @param {StylelintConfig} config
28 * @param {string} configDir
29 * @param {boolean} allowOverrides
30 * @param {string} rootConfigDir
31 * @param {string} [filePath]
32 * @returns {Promise<StylelintConfig>}
33 */
34async function augmentConfigBasic(
35 stylelint,
36 config,
37 configDir,
38 allowOverrides,
39 rootConfigDir,
40 filePath,
41) {
42 let augmentedConfig = config;
43
44 if (allowOverrides) {
45 augmentedConfig = addOptions(stylelint, augmentedConfig);
46 }
47
48 if (filePath) {
49 augmentedConfig = applyOverrides(augmentedConfig, rootConfigDir, filePath);
50 }
51
52 augmentedConfig = await extendConfig(
53 stylelint,
54 augmentedConfig,
55 configDir,
56 rootConfigDir,
57 filePath,
58 );
59
60 const cwd = stylelint._options.cwd;
61
62 return absolutizePaths(augmentedConfig, configDir, cwd);
63}
64
65/**
66 * Extended configs need to be run through augmentConfigBasic
67 * but do not need the full treatment. Things like pluginFunctions
68 * will be resolved and added by the parent config.
69 * @param {string} cwd
70 * @returns {(cosmiconfigResult?: StylelintCosmiconfigResult) => Promise<StylelintCosmiconfigResult>}
71 */
72function augmentConfigExtended(cwd) {
73 return async (cosmiconfigResult) => {
74 if (!cosmiconfigResult) {
75 return null;
76 }
77
78 const configDir = path.dirname(cosmiconfigResult.filepath || '');
79 const { config } = cosmiconfigResult;
80
81 const augmentedConfig = absolutizePaths(config, configDir, cwd);
82
83 return {
84 config: augmentedConfig,
85 filepath: cosmiconfigResult.filepath,
86 };
87 };
88}
89
90/**
91 * @param {StylelintInternalApi} stylelint
92 * @param {string} [filePath]
93 * @param {StylelintCosmiconfigResult} [cosmiconfigResult]
94 * @returns {Promise<StylelintCosmiconfigResult>}
95 */
96async function augmentConfigFull(stylelint, filePath, cosmiconfigResult) {
97 if (!cosmiconfigResult) {
98 return null;
99 }
100
101 const config = cosmiconfigResult.config;
102 const filepath = cosmiconfigResult.filepath;
103
104 const configDir = stylelint._options.configBasedir || path.dirname(filepath || '');
105
106 let augmentedConfig = await augmentConfigBasic(
107 stylelint,
108 config,
109 configDir,
110 true,
111 configDir,
112 filePath,
113 );
114
115 augmentedConfig = addPluginFunctions(augmentedConfig);
116 augmentedConfig = addProcessorFunctions(augmentedConfig);
117
118 if (!augmentedConfig.rules) {
119 throw configurationError(
120 'No rules found within configuration. Have you provided a "rules" property?',
121 );
122 }
123
124 augmentedConfig = normalizeAllRuleSettings(augmentedConfig);
125
126 return {
127 config: augmentedConfig,
128 filepath: cosmiconfigResult.filepath,
129 };
130}
131
132/**
133 * Make all paths in the config absolute:
134 * - ignoreFiles
135 * - plugins
136 * - processors
137 * (extends handled elsewhere)
138 * @param {StylelintConfig} config
139 * @param {string} configDir
140 * @param {string} cwd
141 * @returns {StylelintConfig}
142 */
143function absolutizePaths(config, configDir, cwd) {
144 if (config.ignoreFiles) {
145 config.ignoreFiles = [config.ignoreFiles].flat().map((glob) => {
146 if (path.isAbsolute(glob.replace(/^!/, ''))) return glob;
147
148 return globjoin(configDir, glob);
149 });
150 }
151
152 if (config.plugins) {
153 config.plugins = [config.plugins].flat().map((lookup) => getModulePath(configDir, lookup, cwd));
154 }
155
156 if (config.processors) {
157 config.processors = absolutizeProcessors(config.processors, configDir);
158 }
159
160 return config;
161}
162
163/**
164 * Processors are absolutized in their own way because
165 * they can be and return a string or an array
166 * @param {StylelintConfigProcessors} processors
167 * @param {string} configDir
168 * @return {StylelintConfigProcessors}
169 */
170function absolutizeProcessors(processors, configDir) {
171 const normalizedProcessors = Array.isArray(processors) ? processors : [processors];
172
173 return normalizedProcessors.map((item) => {
174 if (typeof item === 'string') {
175 return getModulePath(configDir, item);
176 }
177
178 return [getModulePath(configDir, item[0]), item[1]];
179 });
180}
181
182/**
183 * @param {StylelintInternalApi} stylelint
184 * @param {StylelintConfig} config
185 * @param {string} configDir
186 * @param {string} rootConfigDir
187 * @param {string} [filePath]
188 * @return {Promise<StylelintConfig>}
189 */
190async function extendConfig(stylelint, config, configDir, rootConfigDir, filePath) {
191 if (config.extends === undefined) {
192 return config;
193 }
194
195 const { extends: configExtends, ...originalWithoutExtends } = config;
196 const normalizedExtends = [configExtends].flat();
197
198 let resultConfig = originalWithoutExtends;
199
200 for (const extendLookup of normalizedExtends) {
201 const extendResult = await loadExtendedConfig(stylelint, configDir, extendLookup);
202
203 if (extendResult) {
204 let extendResultConfig = extendResult.config;
205 const extendConfigDir = path.dirname(extendResult.filepath || '');
206
207 extendResultConfig = await augmentConfigBasic(
208 stylelint,
209 extendResultConfig,
210 extendConfigDir,
211 false,
212 rootConfigDir,
213 filePath,
214 );
215
216 resultConfig = mergeConfigs(resultConfig, extendResultConfig);
217 }
218 }
219
220 return mergeConfigs(resultConfig, originalWithoutExtends);
221}
222
223/**
224 * @param {StylelintInternalApi} stylelint
225 * @param {string} configDir
226 * @param {string} extendLookup
227 * @return {Promise<StylelintCosmiconfigResult>}
228 */
229function loadExtendedConfig(stylelint, configDir, extendLookup) {
230 const extendPath = getModulePath(configDir, extendLookup, stylelint._options.cwd);
231
232 return stylelint._extendExplorer.load(extendPath);
233}
234
235/**
236 * When merging configs (via extends)
237 * - plugin and processor arrays are joined
238 * - rules are merged via Object.assign, so there is no attempt made to
239 * merge any given rule's settings. If b contains the same rule as a,
240 * b's rule settings will override a's rule settings entirely.
241 * - Everything else is merged via Object.assign
242 * @param {StylelintConfig} a
243 * @param {StylelintConfig} b
244 * @returns {StylelintConfig}
245 */
246function mergeConfigs(a, b) {
247 /** @type {{plugins: StylelintConfigPlugins}} */
248 const pluginMerger = {};
249
250 if (a.plugins || b.plugins) {
251 pluginMerger.plugins = [];
252
253 if (a.plugins) {
254 pluginMerger.plugins = pluginMerger.plugins.concat(a.plugins);
255 }
256
257 if (b.plugins) {
258 pluginMerger.plugins = [...new Set(pluginMerger.plugins.concat(b.plugins))];
259 }
260 }
261
262 /** @type {{processors: StylelintConfigProcessors}} */
263 const processorMerger = {};
264
265 if (a.processors || b.processors) {
266 processorMerger.processors = [];
267
268 if (a.processors) {
269 processorMerger.processors = processorMerger.processors.concat(a.processors);
270 }
271
272 if (b.processors) {
273 processorMerger.processors = [...new Set(processorMerger.processors.concat(b.processors))];
274 }
275 }
276
277 /** @type {{overrides: StylelintConfigOverride[]}} */
278 const overridesMerger = {};
279
280 if (a.overrides || b.overrides) {
281 overridesMerger.overrides = [];
282
283 if (a.overrides) {
284 overridesMerger.overrides = overridesMerger.overrides.concat(a.overrides);
285 }
286
287 if (b.overrides) {
288 overridesMerger.overrides = [...new Set(overridesMerger.overrides.concat(b.overrides))];
289 }
290 }
291
292 const rulesMerger = {};
293
294 if (a.rules || b.rules) {
295 rulesMerger.rules = { ...a.rules, ...b.rules };
296 }
297
298 const result = {
299 ...a,
300 ...b,
301 ...processorMerger,
302 ...pluginMerger,
303 ...overridesMerger,
304 ...rulesMerger,
305 };
306
307 return result;
308}
309
310/**
311 * @param {StylelintConfig} config
312 * @returns {StylelintConfig}
313 */
314function addPluginFunctions(config) {
315 if (!config.plugins) {
316 return config;
317 }
318
319 const normalizedPlugins = [config.plugins].flat();
320
321 /** @type {StylelintConfig['pluginFunctions']} */
322 const pluginFunctions = {};
323
324 for (const pluginLookup of normalizedPlugins) {
325 let pluginImport = require(pluginLookup);
326
327 // Handle either ES6 or CommonJS modules
328 pluginImport = pluginImport.default || pluginImport;
329
330 // A plugin can export either a single rule definition
331 // or an array of them
332 const normalizedPluginImport = [pluginImport].flat();
333
334 for (const pluginRuleDefinition of normalizedPluginImport) {
335 if (!pluginRuleDefinition.ruleName) {
336 throw configurationError(
337 `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.`,
338 );
339 }
340
341 if (!pluginRuleDefinition.ruleName.includes('/')) {
342 throw configurationError(
343 `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.`,
344 );
345 }
346
347 pluginFunctions[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
348 }
349 }
350
351 config.pluginFunctions = pluginFunctions;
352
353 return config;
354}
355
356/**
357 * Given an array of processors strings, we want to add two
358 * properties to the augmented config:
359 * - codeProcessors: functions that will run on code as it comes in
360 * - resultProcessors: functions that will run on results as they go out
361 *
362 * To create these properties, we need to:
363 * - Find the processor module
364 * - Initialize the processor module by calling its functions with any
365 * provided options
366 * - Push the processor's code and result processors to their respective arrays
367 * @type {Map<string, string | Object>}
368 */
369const processorCache = new Map();
370
371/**
372 * @param {StylelintConfig} config
373 * @return {StylelintConfig}
374 */
375function addProcessorFunctions(config) {
376 if (!config.processors) return config;
377
378 /** @type {StylelintCodeProcessor[]} */
379 const codeProcessors = [];
380 /** @type {StylelintResultProcessor[]} */
381 const resultProcessors = [];
382
383 for (const processorConfig of [config.processors].flat()) {
384 const processorKey = JSON.stringify(processorConfig);
385
386 let initializedProcessor;
387
388 if (processorCache.has(processorKey)) {
389 initializedProcessor = processorCache.get(processorKey);
390 } else {
391 const processorLookup =
392 typeof processorConfig === 'string' ? processorConfig : processorConfig[0];
393 const processorOptions = typeof processorConfig === 'string' ? undefined : processorConfig[1];
394 let processor = require(processorLookup);
395
396 processor = processor.default || processor;
397 initializedProcessor = processor(processorOptions);
398 processorCache.set(processorKey, initializedProcessor);
399 }
400
401 if (initializedProcessor && initializedProcessor.code) {
402 codeProcessors.push(initializedProcessor.code);
403 }
404
405 if (initializedProcessor && initializedProcessor.result) {
406 resultProcessors.push(initializedProcessor.result);
407 }
408 }
409
410 config.codeProcessors = codeProcessors;
411 config.resultProcessors = resultProcessors;
412
413 return config;
414}
415
416/**
417 * @param {StylelintConfig} fullConfig
418 * @param {string} rootConfigDir
419 * @param {string} filePath
420 * @return {StylelintConfig}
421 */
422function applyOverrides(fullConfig, rootConfigDir, filePath) {
423 let { overrides, ...config } = fullConfig;
424
425 if (!overrides) {
426 return config;
427 }
428
429 if (!Array.isArray(overrides)) {
430 throw new TypeError(
431 'The `overrides` configuration property should be an array, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
432 );
433 }
434
435 for (const override of overrides) {
436 const { files, ...configOverrides } = override;
437
438 if (!files) {
439 throw new Error(
440 'Every object in the `overrides` configuration property should have a `files` property with globs, e.g. { "overrides": [{ "files": "*.css", "rules": {} }] }.',
441 );
442 }
443
444 const filesGlobs = [files]
445 .flat()
446 .map((glob) => {
447 if (path.isAbsolute(glob.replace(/^!/, ''))) {
448 return glob;
449 }
450
451 return globjoin(rootConfigDir, glob);
452 })
453 // Glob patterns for micromatch should be in POSIX-style
454 .map((s) => normalizePath(s));
455
456 if (micromatch.isMatch(filePath, filesGlobs, { dot: true })) {
457 config = mergeConfigs(config, configOverrides);
458 }
459 }
460
461 return config;
462}
463
464/**
465 * Add options to the config
466 *
467 * @param {StylelintInternalApi} stylelint
468 * @param {StylelintConfig} config
469 *
470 * @returns {StylelintConfig}
471 */
472function addOptions(stylelint, config) {
473 const augmentedConfig = {
474 ...config,
475 };
476
477 if (stylelint._options.ignoreDisables) {
478 augmentedConfig.ignoreDisables = stylelint._options.ignoreDisables;
479 }
480
481 if (stylelint._options.quiet) {
482 augmentedConfig.quiet = stylelint._options.quiet;
483 }
484
485 if (stylelint._options.reportNeedlessDisables) {
486 augmentedConfig.reportNeedlessDisables = stylelint._options.reportNeedlessDisables;
487 }
488
489 if (stylelint._options.reportInvalidScopeDisables) {
490 augmentedConfig.reportInvalidScopeDisables = stylelint._options.reportInvalidScopeDisables;
491 }
492
493 if (stylelint._options.reportDescriptionlessDisables) {
494 augmentedConfig.reportDescriptionlessDisables =
495 stylelint._options.reportDescriptionlessDisables;
496 }
497
498 if (stylelint._options.customSyntax) {
499 augmentedConfig.customSyntax = stylelint._options.customSyntax;
500 }
501
502 return augmentedConfig;
503}
504
505module.exports = { augmentConfigExtended, augmentConfigFull, applyOverrides };