UNPKG

6.47 kBJavaScriptView Raw
1const stylelint = require('stylelint')
2const createPostcssModulesValuesResolver = require('postcss-modules-values')
3const createPostcssCustomPropertiesResolver = require('postcss-custom-properties')
4const createPostcssCustomMediaResolver = require('postcss-custom-media')
5const createPostcssCustomSelectorsResolver = require('postcss-custom-selectors')
6const createPostcssCalcResolver = require('postcss-calc')
7const { findCssGlobalFiles } = require('../lib/findCssGlobalFiles')
8const { getNormalizedRoots } = require('./machinery/ast')
9
10const postcssModulesValuesResolver = createPostcssModulesValuesResolver()
11const postcssCalcResolver = createPostcssCalcResolver()
12
13/*
14 Motivation
15
16 Without these (and some eslint) rules html and css will be tied together in a way
17 that prevents reuse. Every html element in the code is a potential component, without
18 these rules it becomes quite tricky to turn a select set of tags into a component. The
19 css often ties it together in a way that makes it quite hard to extract the correct parts
20 for the component. This results in people copy/pasting large sections and adjusting them
21 to their needs.
22*/
23
24const rules = toStyleLintPlugins(
25 require('./rules/color-schemes'),
26 require('./rules/css-global'),
27 require('./rules/layout-related-properties'),
28 require('./rules/naming-policy'),
29 require('./rules/selector-policy'),
30 require('./rules/parent-child-policy'),
31 require('./rules/root-policy'),
32 require('./rules/at-rule-restrictions'),
33 require('./rules/index'),
34 require('./rules/reset'),
35)
36module.exports = rules
37
38function toStyleLintPlugins(...rules) {
39 const ruleInteraction = determineRuleInteraction(rules)
40 const ruleConfiguration = convertToConfiguration(ruleInteraction)
41
42 return rules.map(({ ruleName, cssRequirements, create }) =>
43 createPlugin({
44 ...(cssRequirements || {}),
45 ruleName: `kaliber/${ruleName}`,
46 plugin: create(ruleConfiguration[ruleName] || {}),
47 })
48 )
49}
50
51function determineRuleInteraction(rules) {
52 /*
53 [{ ruleInteraction: { ruleA: { b: c } } }, { ruleInteraction: { ruleA: { c: d } } }, ...]
54
55 to
56
57 { ruleA: [{ b: c }, { c: d }], ... }
58 */
59 return rules.reduce(
60 (result, rule) => ({
61 ...result,
62 ...Object.entries(rule.ruleInteraction || {}).reduce(
63 (result, [rule, config]) => ({
64 ...result,
65 [rule]: [...(result[rule] || []), config]
66 }),
67 result
68 )
69 }),
70 {}
71 )
72}
73
74function convertToConfiguration(ruleInteraction) {
75 /*
76 { ruleA: [{ a: b }, { a: c }, { b: d }], ... }
77
78 to
79
80 { ruleA" { a: b or c, b: d }, ... }
81 */
82 return Object.entries(ruleInteraction).reduce(
83 (result, [rule, configs]) => ({ ...result, [rule]: merge(configs) }),
84 {}
85 )
86
87 function merge(configs) {
88 return configs.reduce(
89 (result, config) => Object.entries(config).reduce(
90 (result, [key, value]) => {
91 if (typeof value !== 'function') throw new Error(`don't know how to handle config value`)
92 const existing = result[key]
93 return { ...result, [key]: existing ? x => existing(x) || value(x) : value }
94 },
95 result
96 ),
97 {}
98 )
99 }
100}
101
102function createPlugin({
103 ruleName, plugin,
104 normalizedCss = false,
105 resolvedCustomProperties = false,
106 resolvedCustomMedia = false,
107 resolvedCustomSelectors = false,
108 resolvedModuleValues = false,
109 resolvedCalc = false,
110}) {
111 const stylelintPlugin = stylelint.createPlugin(ruleName, pluginWrapper)
112
113 return {
114 ...stylelintPlugin,
115 }
116
117 function pluginWrapper(primaryOption, secondaryOptionObject, context) {
118 return async (originalRoot, result) => {
119 const check = { actual: primaryOption, possible: [true] }
120 if (!stylelint.utils.validateOptions(result, ruleName, check)) return
121
122 const reported = {}
123 const importFrom = findCssGlobalFiles(originalRoot.source.input.file)
124
125 const modifiedRoot = originalRoot.clone()
126 if (resolvedModuleValues) {
127 await postcssModulesValuesResolver(modifiedRoot, result)
128 }
129 if (resolvedCustomProperties) {
130 const postcssCustomPropertiesResolver = createPostcssCustomPropertiesResolver({ preserve: false, importFrom })
131 await postcssCustomPropertiesResolver(modifiedRoot, result)
132 }
133 if (resolvedCustomMedia) {
134 const postcssCustomMediaResolver = createPostcssCustomMediaResolver({ preserve: false, importFrom })
135 await postcssCustomMediaResolver(modifiedRoot, result)
136 }
137 if (resolvedCustomSelectors) {
138 const postcssCustomSelectorsResolver = createPostcssCustomSelectorsResolver({ preserve: false, importFrom })
139 await postcssCustomSelectorsResolver(modifiedRoot, result)
140 }
141 if (resolvedCalc) {
142 await postcssCalcResolver(modifiedRoot, result)
143 }
144 callPlugin(modifiedRoot)
145
146 /*
147 This implementation splits it for each plugin. This might be a performance problem. The easy
148 solution would be to create a `kaliber/style-lint` plugin/rule. That rule would be the only
149 rule that is configured in .stylelintrc. It would split the root once and then run the
150 different rules manually (stylelint.rules['kaliber/xyz'](...)(splitRoot, result)).
151 */
152 if (normalizedCss)
153 Object.entries(getNormalizedRoots(modifiedRoot)).forEach(([mediaQuery, modifiedRoot]) => {
154 callPlugin(modifiedRoot)
155 })
156
157 function callPlugin(modifiedRoot) {
158 plugin({ modifiedRoot, originalRoot, report, context })
159 }
160
161 function report(node, message, index) {
162 const id = getId(node, message, index)
163 if (reported[id]) return
164 else reported[id] = true
165 if (!node.source) stylelint.utils.report({
166 message: `A generated node (${getNodeId(node)}) caused a problem\n ${node.toString().split('\n').join('\n ')}\n${message}`,
167 node: node.parent, result, ruleName
168 })
169 else stylelint.utils.report({ message, index, node, result, ruleName })
170 }
171 }
172 }
173}
174
175function getId(node, message, index) {
176 return `${getNodeId(node)}-${message}${index}`
177}
178
179function getNodeId({ type, prop, selector, name, params, parent }) {
180 const nodeId =
181 type === 'decl' ? `decl-${prop}` :
182 type === 'rule' ? `rule-${selector}` :
183 type === 'atrule' ? `atrule-${name}-${params}` :
184 type
185
186 const parentId = parent
187 ? `${getNodeId(parent)}-`
188 : ''
189
190 return `${parentId}${nodeId}`
191}