@primer/stylelint-config 13.0.1 → 13.1.0-rc.437e7b4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/stylelint-config",
3
- "version": "13.0.1",
3
+ "version": "13.1.0-rc.437e7b4",
4
4
  "description": "Sharable stylelint config used by GitHub's CSS",
5
5
  "author": "GitHub, Inc.",
6
6
  "license": "MIT",
@@ -44,9 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@github/browserslist-config": "^1.0.0",
47
- "@primer/css": "^21.0.8",
48
47
  "@primer/primitives": "^9.0.1",
49
- "anymatch": "^3.1.1",
50
48
  "postcss-scss": "^4.0.2",
51
49
  "postcss-styled-syntax": "^0.6.4",
52
50
  "postcss-value-parser": "^4.0.2",
@@ -56,8 +54,7 @@
56
54
  "stylelint-no-unsupported-browser-features": "^8.0.0",
57
55
  "stylelint-order": "^6.0.4",
58
56
  "stylelint-scss": "^6.2.0",
59
- "stylelint-value-no-unknown-custom-properties": "^6.0.1",
60
- "tap-map": "^1.0.0"
57
+ "stylelint-value-no-unknown-custom-properties": "^6.0.1"
61
58
  },
62
59
  "prettier": "@github/prettier-config",
63
60
  "devDependencies": {
package/plugins/colors.js CHANGED
@@ -1,43 +1,168 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- const bgVars = [
4
- '$bg-*',
5
- '$tooltip-background-color',
6
- // Match variables in any of the following formats: --color-bg-*, --color-*-bg-*, --color-*-bg, *bgColor*, *fgColor*, *borderColor*, *iconColor*
7
- /var\(--color-(.+-)*bg(-.+)*\)/,
8
- /var\(--color-[^)]+\)/,
9
- /var\((.+-)*bgColor(-.+)*\)/,
10
- /var\((.+-)*fgColor(-.+)*\)/,
11
- /var\((.+-)*borderColor(-.+)*\)/,
12
- /var\((.+-)*iconColor(-.+)*\)/,
13
- ]
1
+ import stylelint from 'stylelint'
2
+ import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
3
+ import {primitivesVariables, hasValidColor} from './lib/utils.js'
4
+ import valueParser from 'postcss-value-parser'
5
+
6
+ const {
7
+ createPlugin,
8
+ utils: {report, ruleMessages, validateOptions},
9
+ } = stylelint
14
10
 
15
- export default createVariableRule(
16
- 'primer/colors',
17
- {
18
- 'background-color': {
19
- expects: 'a background color variable',
20
- values: bgVars.concat('none', 'transparent'),
21
- },
22
- background: {
23
- expects: 'a background color variable',
24
- values: bgVars.concat('none', 'transparent', 'top', 'right', 'bottom', 'left', 'center', '*px', 'url(*)'),
25
- },
26
- 'text color': {
27
- expects: 'a text color variable',
28
- props: 'color',
29
- values: [
30
- '$text-*',
31
- '$tooltip-text-color',
32
- 'inherit',
33
- // Match variables in any of the following formats: --color-text-*, --color-*-text-*, --color-*-text, *fgColor*, *iconColor*
34
- /var\(--color-(.+-)*text(-.+)*\)/,
35
- /var\(--color-(.+-)*fg(-.+)*\)/,
36
- /var\(--color-[^)]+\)/,
37
- /var\((.+-)*fgColor(-.+)*\)/,
38
- /var\((.+-)*iconColor(-.+)*\)/,
39
- ],
40
- },
11
+ export const ruleName = 'primer/colors'
12
+ export const messages = ruleMessages(ruleName, {
13
+ rejected: (value, type) => {
14
+ if (type === 'fg') {
15
+ return `Please use a Primer foreground color variable instead of '${value}'. https://primer.style/foundations/primitives/color#foreground`
16
+ } else if (type === 'bg') {
17
+ return `Please use a Primer background color variable instead of '${value}'. https://primer.style/foundations/primitives/color#background`
18
+ } else if (type === 'border') {
19
+ return `Please use a Primer border color variable instead of '${value}'. https://primer.style/foundations/primitives/color#border`
20
+ }
21
+ return `Please use with a Primer color variable instead of '${value}'. https://primer.style/foundations/primitives/color`
41
22
  },
42
- 'https://primer.style/primitives/colors',
43
- )
23
+ })
24
+
25
+ let variables = primitivesVariables('colors')
26
+ const validProps = {
27
+ '^color$': ['fgColor', 'iconColor'],
28
+ '^background(-color)?$': ['bgColor'],
29
+ '^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$': ['borderColor'],
30
+ '^fill$': ['fgColor', 'iconColor', 'bgColor'],
31
+ '^stroke$': ['fgColor', 'iconColor', 'bgColor', 'borderColor'],
32
+ }
33
+
34
+ const validValues = [
35
+ 'none',
36
+ 'currentcolor',
37
+ 'inherit',
38
+ 'initial',
39
+ 'unset',
40
+ 'revert',
41
+ 'revert-layer',
42
+ 'transparent',
43
+ '0',
44
+ ]
45
+
46
+ const propType = prop => {
47
+ if (/^color/.test(prop)) {
48
+ return 'fg'
49
+ } else if (/^background(-color)?$/.test(prop)) {
50
+ return 'bg'
51
+ } else if (/^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$/.test(prop)) {
52
+ return 'border'
53
+ } else if (/^fill$/.test(prop)) {
54
+ return 'fg'
55
+ } else if (/^stroke$/.test(prop)) {
56
+ return 'fg'
57
+ }
58
+ return undefined
59
+ }
60
+
61
+ variables = variables.filter(variable => {
62
+ const name = variable['name']
63
+ // remove shadow and boxShadow variables
64
+ return !(name.includes('shadow') || name.includes('boxShadow'))
65
+ })
66
+
67
+ /** @type {import('stylelint').Rule} */
68
+ const ruleFunction = primary => {
69
+ return (root, result) => {
70
+ const validOptions = validateOptions(result, ruleName, {
71
+ actual: primary,
72
+ possible: [true],
73
+ })
74
+ if (!validOptions) return
75
+
76
+ const valueIsCorrectType = (value, types) => types.some(type => value.includes(type))
77
+
78
+ root.walkDecls(declNode => {
79
+ const {prop, value} = declNode
80
+
81
+ // Skip if prop is not a valid color prop
82
+ if (!Object.keys(validProps).some(validProp => new RegExp(validProp).test(prop))) return
83
+
84
+ // Get the valid types for the prop
85
+ const types = validProps[Object.keys(validProps).find(re => new RegExp(re).test(prop))]
86
+
87
+ // Walk the value split
88
+ valueParser(value).walk(valueNode => {
89
+ // Skip if value is not a word or function
90
+ if (valueNode.type !== 'word' && valueNode.type !== 'function') return
91
+
92
+ // Skip if value is a valid value
93
+ if (validValues.includes(valueNode.value)) return
94
+
95
+ if (hasValidColor(valueNode.value) || /^\$/.test(valueNode.value)) {
96
+ const rejectedValue =
97
+ valueNode.type === 'function'
98
+ ? `${valueNode.value}(${valueParser.stringify(valueNode.nodes)})`
99
+ : valueNode.value
100
+
101
+ report({
102
+ index: declarationValueIndex(declNode) + valueNode.sourceIndex,
103
+ endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex,
104
+ message: messages.rejected(rejectedValue, propType(prop)),
105
+ node: declNode,
106
+ result,
107
+ ruleName,
108
+ })
109
+ return
110
+ }
111
+
112
+ // Skip functions
113
+ if (valueNode.type === 'function') {
114
+ return
115
+ }
116
+
117
+ // Variable exists and is the correct type (fg, bg, border)
118
+ if (
119
+ variables.some(variable => new RegExp(variable['name']).test(valueNode.value)) &&
120
+ valueIsCorrectType(valueNode.value, types)
121
+ ) {
122
+ return
123
+ }
124
+
125
+ // Value doesn't start with variable --
126
+ if (!valueNode.value.startsWith('--')) {
127
+ return
128
+ }
129
+
130
+ // Ignore old system colors --color-*
131
+ if (
132
+ [
133
+ /^--color-(?:[a-zA-Z0-9-]+-)*text(?:-[a-zA-Z0-9-]+)*$/,
134
+ /^--color-(?:[a-zA-Z0-9-](?!-))*-fg(?:-[a-zA-Z0-9-]+)*$/,
135
+ /^--color-[^)]+$/,
136
+ ].some(oldSysRe => oldSysRe.test(valueNode.value))
137
+ ) {
138
+ return
139
+ }
140
+
141
+ // Property is shortand and value doesn't include color
142
+ if (
143
+ (/^border(-top|-right|-bottom|-left|-inline|-block)*$/.test(prop) || /^background$/.test(prop)) &&
144
+ !valueNode.value.toLowerCase().includes('color')
145
+ ) {
146
+ return
147
+ }
148
+
149
+ report({
150
+ index: declarationValueIndex(declNode) + valueNode.sourceIndex,
151
+ endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex,
152
+ message: messages.rejected(`var(${valueNode.value})`, propType(prop)),
153
+ node: declNode,
154
+ result,
155
+ ruleName,
156
+ })
157
+ })
158
+ })
159
+ }
160
+ }
161
+
162
+ ruleFunction.ruleName = ruleName
163
+ ruleFunction.messages = messages
164
+ ruleFunction.meta = {
165
+ fixable: false,
166
+ }
167
+
168
+ export default createPlugin(ruleName, ruleFunction)
@@ -21,6 +21,9 @@ export function primitivesVariables(type) {
21
21
  files.push('functional/themes/light.json')
22
22
  files.push('functional/size/border.json')
23
23
  break
24
+ case 'colors':
25
+ files.push('functional/themes/light.json')
26
+ break
24
27
  }
25
28
 
26
29
  for (const file of files) {
@@ -41,6 +44,19 @@ export function primitivesVariables(type) {
41
44
  return variables
42
45
  }
43
46
 
47
+ const HAS_VALID_HEX = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?:$|[^\da-f])/i
48
+ const COLOR_FUNCTION_NAMES = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch']
49
+
50
+ /**
51
+ * Check if a value contains a valid 3, 4, 6 or 8 digit hex
52
+ *
53
+ * @param {string} value
54
+ * @returns {boolean}
55
+ */
56
+ export function hasValidColor(value) {
57
+ return HAS_VALID_HEX.test(value) || COLOR_FUNCTION_NAMES.includes(value)
58
+ }
59
+
44
60
  export function walkGroups(root, validate) {
45
61
  for (const node of root.nodes) {
46
62
  if (node.type === 'function') {
@@ -1,238 +0,0 @@
1
- import anymatch from 'anymatch'
2
- import valueParser from 'postcss-value-parser'
3
- import TapMap from 'tap-map'
4
-
5
- const SKIP_VALUE_NODE_TYPES = new Set(['space', 'div'])
6
- const SKIP_AT_RULE_NAMES = new Set(['each', 'for', 'function', 'mixin'])
7
-
8
- export default function declarationValidator(rules, options = {}) {
9
- const {formatMessage = defaultMessageFormatter, variables, verbose = false} = options
10
- const variableReplacements = new TapMap()
11
- if (variables) {
12
- for (const [name, {values}] of Object.entries(variables)) {
13
- for (const value of values) {
14
- variableReplacements.tap(value, () => []).push(name)
15
- }
16
- }
17
- }
18
-
19
- const validators = Object.entries(rules)
20
- .map(([key, rule]) => {
21
- if (rule === false) {
22
- return false
23
- }
24
- const {name = key, props = name, expects = `a ${name} value`} = rule
25
- const replacements = Object.assign({}, rule.replacements, getVariableReplacements(rule.values))
26
- // console.warn(`replacements for "${key}": ${JSON.stringify(replacements)}`)
27
- Object.assign(rule, {name, props, expects, replacements})
28
- return {
29
- rule,
30
- matchesProp: anymatch(props),
31
- validate: Array.isArray(rule.components) ? componentValidator(rule) : valueValidator(rule),
32
- }
33
- })
34
- .filter(Boolean)
35
-
36
- const validatorsByProp = new TapMap()
37
- const validatorsByReplacementValue = new Map()
38
- for (const validator of validators) {
39
- for (const value of Object.keys(validator.rule.replacements)) {
40
- validatorsByReplacementValue.set(value, validator)
41
- }
42
- }
43
-
44
- return decl => {
45
- if (closest(decl, isSkippableAtRule)) {
46
- if (verbose) {
47
- // eslint-disable-next-line no-console
48
- console.warn(`skipping declaration: ${decl.parent.toString()}`)
49
- }
50
- // As a general rule, any rule nested in an at-rule is ignored, since
51
- // @for, @each, @mixin, and @function blocks can use whatever variables
52
- // they want
53
- return {valid: true}
54
- }
55
- const validator = getPropValidator(decl.prop)
56
- if (validator) {
57
- const result = validator.validate(decl)
58
- result.errors = result.errors.map(formatMessage)
59
- return result
60
- } else {
61
- return {valid: true}
62
- }
63
- }
64
-
65
- function getVariableReplacements(values) {
66
- const replacements = {}
67
- const varValues = (Array.isArray(values) ? values : [values]).filter(v => typeof v === 'string' && v.includes('$'))
68
- const matches = anymatch(varValues)
69
- for (const [value, aliases] of variableReplacements.entries()) {
70
- for (const alias of aliases) {
71
- if (matches(alias)) {
72
- replacements[value] = alias
73
- }
74
- }
75
- }
76
- return replacements
77
- }
78
-
79
- function getPropValidator(prop) {
80
- return validatorsByProp.tap(prop, () => validators.find(v => v.matchesProp(prop)))
81
- }
82
-
83
- function valueValidator({expects, values, replacements, singular = false}) {
84
- const matches = anymatch(values)
85
- return function validate({prop, value}, nested) {
86
- if (matches(value)) {
87
- return {
88
- valid: true,
89
- errors: [],
90
- fixable: false,
91
- replacement: undefined,
92
- }
93
- } else if (replacements[value]) {
94
- let replacement = value
95
- do {
96
- replacement = replacements[replacement]
97
- } while (replacements[replacement])
98
- return {
99
- valid: false,
100
- errors: [{expects, prop, value, replacement}],
101
- fixable: true,
102
- replacement,
103
- }
104
- } else {
105
- if (nested || singular) {
106
- return {
107
- valid: false,
108
- errors: [{expects, prop, value}],
109
- fixable: false,
110
- replacement: undefined,
111
- }
112
- }
113
-
114
- const parsed = valueParser(value)
115
- const validations = parsed.nodes
116
- .map((node, index) => Object.assign(node, {index}))
117
- .filter(node => !SKIP_VALUE_NODE_TYPES.has(node.type))
118
- .map(node => {
119
- const validation = validate({prop, value: valueParser.stringify(node)}, true)
120
- validation.index = node.index
121
- return validation
122
- })
123
-
124
- const valid = validations.every(v => v.valid)
125
- if (valid) {
126
- return {valid, errors: [], fixable: false, replacement: undefined}
127
- }
128
-
129
- const fixable = validations.some(v => v.fixable)
130
- const errors = validations.reduce((list, v) => list.concat(v.errors), [])
131
-
132
- let replacement = undefined
133
- for (const validation of validations) {
134
- if (fixable && validation.replacement) {
135
- parsed.nodes[validation.index] = {type: 'word', value: validation.replacement}
136
- }
137
- }
138
-
139
- if (fixable) {
140
- replacement = valueParser.stringify(parsed)
141
- }
142
-
143
- return {
144
- valid,
145
- fixable,
146
- errors,
147
- replacement,
148
- }
149
- }
150
- }
151
- }
152
-
153
- function componentValidator({expects, components, values, replacements}) {
154
- const matchesCompoundValue = anymatch(values)
155
- return decl => {
156
- const {prop, value: compoundValue} = decl
157
- const parsed = valueParser(compoundValue)
158
- if (parsed.nodes.length === 1 && matchesCompoundValue(compoundValue)) {
159
- return {valid: true, errors: []}
160
- }
161
-
162
- const errors = []
163
-
164
- let fixable = false
165
- let componentIndex = 0
166
- for (const [index, node] of Object.entries(parsed.nodes)) {
167
- if (SKIP_VALUE_NODE_TYPES.has(node.type)) {
168
- continue
169
- }
170
-
171
- const value = valueParser.stringify(node)
172
-
173
- let componentProp = components[componentIndex++]
174
- let validator = getPropValidator(componentProp)
175
- if (validatorsByReplacementValue.has(value)) {
176
- validator = validatorsByReplacementValue.get(value)
177
- componentProp = validator.rule.name
178
- }
179
-
180
- const nestedProp = `${componentProp} (in ${prop})`
181
- if (validator) {
182
- const result = validator.validate({prop: nestedProp, value}, true)
183
- if (result.replacement) {
184
- parsed.nodes[index] = {
185
- type: 'word',
186
- value: result.replacement,
187
- }
188
- fixable = true
189
- }
190
- for (const error of result.errors) {
191
- errors.push(error)
192
- }
193
- } else {
194
- errors.push({expects, prop: nestedProp, value})
195
- }
196
- }
197
-
198
- let replacement = fixable ? valueParser.stringify(parsed) : undefined
199
-
200
- // if a compound replacement exists, suggest *that* instead
201
- if (replacement && replacements[replacement]) {
202
- do {
203
- replacement = replacements[replacement]
204
- } while (replacements[replacement])
205
- return {
206
- valid: false,
207
- errors: [{expects, prop, value: compoundValue, replacement}],
208
- fixable: true,
209
- replacement,
210
- }
211
- }
212
-
213
- return {
214
- valid: errors.length === 0,
215
- errors,
216
- fixable,
217
- replacement,
218
- }
219
- }
220
- }
221
-
222
- function isSkippableAtRule(node) {
223
- return node.type === 'atrule' && SKIP_AT_RULE_NAMES.has(node.name)
224
- }
225
- }
226
-
227
- function defaultMessageFormatter(error) {
228
- const {expects, value, replacement} = error
229
- const expected = replacement ? `"${replacement}"` : expects
230
- return `Please use ${expected} instead of "${value}"`
231
- }
232
-
233
- function closest(node, test) {
234
- let ancestor = node
235
- do {
236
- if (test(ancestor)) return ancestor
237
- } while ((ancestor = ancestor.parent))
238
- }