UNPKG

11.3 kBJavaScriptView Raw
1/* @flow */
2"use strict";
3
4const _ = require("lodash");
5const configurationError = require("./utils/configurationError");
6const dynamicRequire = require("./dynamicRequire");
7const getModulePath = require("./utils/getModulePath");
8const globjoin = require("globjoin");
9const normalizeRuleSettings = require("./normalizeRuleSettings");
10const path = require("path");
11const requireRule = require("./requireRule");
12
13// - Merges config and configOverrides
14// - Makes all paths absolute
15// - Merges extends
16function augmentConfigBasic(
17 stylelint /*: stylelint$internalApi*/,
18 config /*: stylelint$config*/,
19 configDir /*: string*/,
20 allowOverrides /*:: ?: boolean*/
21) /*: Promise<stylelint$config>*/ {
22 return Promise.resolve()
23 .then(() => {
24 if (!allowOverrides) return config;
25
26 return _.merge(config, stylelint._options.configOverrides);
27 })
28 .then(augmentedConfig => {
29 return extendConfig(stylelint, augmentedConfig, configDir);
30 })
31 .then(augmentedConfig => {
32 return absolutizePaths(augmentedConfig, configDir);
33 });
34}
35
36// Extended configs need to be run through augmentConfigBasic
37// but do not need the full treatment. Things like pluginFunctions
38// will be resolved and added by the parent config.
39function augmentConfigExtended(
40 stylelint /*: stylelint$internalApi*/,
41 cosmiconfigResultArg /*: ?{
42 config: stylelint$config,
43 filepath: string,
44 }*/
45) /*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
46 const cosmiconfigResult = cosmiconfigResultArg; // Lock in for Flow
47
48 if (!cosmiconfigResult) return Promise.resolve(null);
49
50 const configDir = path.dirname(cosmiconfigResult.filepath || "");
51 const cleanedConfig = _.omit(cosmiconfigResult.config, "ignoreFiles");
52
53 return augmentConfigBasic(stylelint, cleanedConfig, configDir).then(
54 augmentedConfig => {
55 return {
56 config: augmentedConfig,
57 filepath: cosmiconfigResult.filepath
58 };
59 }
60 );
61}
62
63function augmentConfigFull(
64 stylelint /*: stylelint$internalApi*/,
65 cosmiconfigResultArg /*: ?{
66 config: stylelint$config,
67 filepath: string,
68 }*/
69) /*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
70 const cosmiconfigResult = cosmiconfigResultArg; // Lock in for Flow
71
72 if (!cosmiconfigResult) return Promise.resolve(null);
73
74 const config = cosmiconfigResult.config;
75 const filepath = cosmiconfigResult.filepath;
76
77 const configDir =
78 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// Make all paths in the config absolute:
105// - ignoreFiles
106// - plugins
107// - processors
108// (extends handled elsewhere)
109function absolutizePaths(
110 config /*: stylelint$config*/,
111 configDir /*: string*/
112) /*: stylelint$config*/ {
113 if (config.ignoreFiles) {
114 config.ignoreFiles = [].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 = [].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// Processors are absolutized in their own way because
135// they can be and return a string or an array
136function absolutizeProcessors(
137 processors /*: stylelint$configProcessors*/,
138 configDir /*: string*/
139) /*: stylelint$configProcessors*/ {
140 const normalizedProcessors = Array.isArray(processors)
141 ? processors
142 : [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
153function extendConfig(
154 stylelint /*: stylelint$internalApi*/,
155 config /*: stylelint$config*/,
156 configDir /*: string*/
157) /*: Promise<stylelint$config>*/ {
158 if (config.extends === undefined) return Promise.resolve(config);
159
160 const normalizedExtends = Array.isArray(config.extends)
161 ? config.extends
162 : [config.extends];
163
164 const originalWithoutExtends = _.omit(config, "extends");
165 const loadExtends = normalizedExtends.reduce(
166 (resultPromise, extendLookup) => {
167 return resultPromise.then(resultConfig => {
168 return loadExtendedConfig(
169 stylelint,
170 resultConfig,
171 configDir,
172 extendLookup
173 ).then(extendResult => {
174 if (!extendResult) return resultConfig;
175
176 return mergeConfigs(resultConfig, extendResult.config);
177 });
178 });
179 },
180 Promise.resolve(originalWithoutExtends)
181 );
182
183 return loadExtends.then(resultConfig => {
184 return mergeConfigs(resultConfig, originalWithoutExtends);
185 });
186}
187
188function loadExtendedConfig(
189 stylelint /*: stylelint$internalApi*/,
190 config /*: stylelint$config*/,
191 configDir /*: string*/,
192 extendLookup /*: string*/
193) /*: Promise<?{ config: stylelint$config, filepath: string }>*/ {
194 const extendPath = getModulePath(configDir, extendLookup);
195
196 return stylelint._extendExplorer.load(extendPath);
197}
198
199// When merging configs (via extends)
200// - plugin and processor arrays are joined
201// - rules are merged via Object.assign, so there is no attempt made to
202// merge any given rule's settings. If b contains the same rule as a,
203// b's rule settings will override a's rule settings entirely.
204// - Everything else is merged via Object.assign
205function mergeConfigs(
206 a /*: stylelint$config*/,
207 b /*: stylelint$config*/
208) /*: stylelint$config*/ {
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 = _.uniq(pluginMerger.plugins.concat(b.plugins));
220 }
221 }
222
223 const processorMerger = {};
224
225 if (a.processors || b.processors) {
226 processorMerger.processors = [];
227
228 if (a.processors) {
229 processorMerger.processors = processorMerger.processors.concat(
230 a.processors
231 );
232 }
233
234 if (b.processors) {
235 processorMerger.processors = _.uniq(
236 processorMerger.processors.concat(b.processors)
237 );
238 }
239 }
240
241 const rulesMerger = {};
242
243 if (a.rules || b.rules) {
244 rulesMerger.rules = Object.assign({}, a.rules, b.rules);
245 }
246
247 const result = Object.assign(
248 {},
249 a,
250 b,
251 processorMerger,
252 pluginMerger,
253 rulesMerger
254 );
255
256 return result;
257}
258
259function addPluginFunctions(
260 config /*: stylelint$config*/
261) /*: stylelint$config*/ {
262 if (!config.plugins) return config;
263
264 const normalizedPlugins = Array.isArray(config.plugins)
265 ? config.plugins
266 : [config.plugins];
267
268 const pluginFunctions = normalizedPlugins.reduce((result, pluginLookup) => {
269 let pluginImport = dynamicRequire(pluginLookup);
270
271 // Handle either ES6 or CommonJS modules
272 pluginImport = pluginImport.default || pluginImport;
273
274 // A plugin can export either a single rule definition
275 // or an array of them
276 const normalizedPluginImport = Array.isArray(pluginImport)
277 ? pluginImport
278 : [pluginImport];
279
280 normalizedPluginImport.forEach(pluginRuleDefinition => {
281 if (!pluginRuleDefinition.ruleName) {
282 throw configurationError(
283 "stylelint v3+ requires plugins to expose a ruleName. " +
284 `The plugin "${pluginLookup}" is not doing this, so will not work ` +
285 "with stylelint v3+. Please file an issue with the plugin."
286 );
287 }
288
289 if (!_.includes(pluginRuleDefinition.ruleName, "/")) {
290 throw configurationError(
291 "stylelint v7+ requires plugin rules to be namspaced, " +
292 "i.e. only `plugin-namespace/plugin-rule-name` plugin rule names are supported. " +
293 `The plugin rule "${
294 pluginRuleDefinition.ruleName
295 }" does not do this, so will not work. ` +
296 "Please file an issue with the plugin."
297 );
298 }
299
300 result[pluginRuleDefinition.ruleName] = pluginRuleDefinition.rule;
301 });
302
303 return result;
304 }, {});
305
306 config.pluginFunctions = pluginFunctions;
307
308 return config;
309}
310
311function normalizeAllRuleSettings(
312 config /*: stylelint$config*/
313) /*: stylelint$config*/ {
314 const normalizedRules = {};
315
316 if (!config.rules) return config;
317
318 Object.keys(config.rules).forEach(ruleName => {
319 const rawRuleSettings = _.get(config, ["rules", ruleName]);
320
321 const rule =
322 requireRule(ruleName) || _.get(config, ["pluginFunctions", ruleName]);
323
324 if (!rule) {
325 throw configurationError(`Undefined rule ${ruleName}`);
326 }
327
328 normalizedRules[ruleName] = normalizeRuleSettings(
329 rawRuleSettings,
330 ruleName,
331 _.get(rule, "primaryOptionArray")
332 );
333 });
334 config.rules = normalizedRules;
335
336 return config;
337}
338
339// Given an array of processors strings, we want to add two
340// properties to the augmented config:
341// - codeProcessors: functions that will run on code as it comes in
342// - resultProcessors: functions that will run on results as they go out
343//
344// To create these properties, we need to:
345// - Find the processor module
346// - Intialize the processor module by calling its functions with any
347// provided options
348// - Push the processor's code and result processors to their respective arrays
349const processorCache = new Map();
350
351function addProcessorFunctions(
352 config /*: stylelint$config*/
353) /*: stylelint$config*/ {
354 if (!config.processors) return config;
355
356 const codeProcessors = [];
357 const resultProcessors = [];
358
359 [].concat(config.processors).forEach(processorConfig => {
360 const processorKey = JSON.stringify(processorConfig);
361
362 let initializedProcessor;
363
364 if (processorCache.has(processorKey)) {
365 initializedProcessor = processorCache.get(processorKey);
366 } else {
367 processorConfig = [].concat(processorConfig);
368 const processorLookup = processorConfig[0];
369 const processorOptions = 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 };