forked from primer/stylelint-config
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathno-undefined-vars.js
111 lines (92 loc) · 3.55 KB
/
no-undefined-vars.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
const fs = require('fs')
const stylelint = require('stylelint')
const matchAll = require('string.prototype.matchall')
const globby = require('globby')
const TapMap = require('tap-map')
const ruleName = 'primer/no-undefined-vars'
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: varName => `${varName} is not defined`
})
// Match CSS variable definitions (e.g. --color-text-primary:)
const variableDefinitionRegex = /^\s*(--[\w|-]+):/gm
// Match CSS variables defined with the color-variables mixin
const defaultColorModeVariableDefinitionRegex = /^[^/\n]*\(["']?([^'"\s,]+)["']?,\s*\(light|dark:/gm
// Match CSS variable references (e.g var(--color-text-primary))
// eslint-disable-next-line no-useless-escape
const defaultVariableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g
module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}) => {
if (!enabled) {
return noop
}
const {files = ['**/*.scss', '!node_modules'], verbose = false, regex = {}} = options
const colorModeVariableDefinitionRegex = regex.mixinDefinition
? new RegExp(regex.mixinDefinition, 'gm')
: defaultColorModeVariableDefinitionRegex
const variableReferenceRegex = regex.reference ? new RegExp(regex.reference, 'g') : defaultVariableReferenceRegex
// eslint-disable-next-line no-console
const log = verbose ? (...args) => console.warn(...args) : noop
const definedVariables = getDefinedVariables(files, log, colorModeVariableDefinitionRegex, variableReferenceRegex)
// Keep track of declarations we've already seen
const seen = new WeakMap()
return (root, result) => {
function checkVariable(variableName, node) {
if (!definedVariables.has(variableName)) {
stylelint.utils.report({
message: messages.rejected(variableName),
node,
result,
ruleName
})
}
}
root.walkAtRules(rule => {
if (rule.name === 'include' && rule.params.startsWith('color-variables')) {
const innerMatch = [...matchAll(rule.params, variableReferenceRegex)]
if (!innerMatch.length) {
return
}
for (const [, variableName] of innerMatch) {
checkVariable(variableName, rule)
}
}
})
root.walkRules(rule => {
rule.walkDecls(decl => {
if (seen.has(decl)) {
return
} else {
seen.set(decl, true)
}
for (const [, variableName] of matchAll(decl.value, variableReferenceRegex)) {
checkVariable(variableName, decl)
}
})
})
}
})
const cwd = process.cwd()
const cache = new TapMap()
function getDefinedVariables(globs, log, colorModeVariableDefinitionRegex) {
const cacheKey = JSON.stringify({globs, cwd})
return cache.tap(cacheKey, () => {
const definedVariables = new Set()
const files = globby.sync(globs)
log(`Scanning ${files.length} SCSS files for CSS variables`)
for (const file of files) {
log(`==========\nLooking for CSS variable definitions in ${file}`)
const css = fs.readFileSync(file, 'utf-8')
for (const [, variableName] of matchAll(css, variableDefinitionRegex)) {
log(`${variableName} defined in ${file}`)
definedVariables.add(variableName)
}
for (const [, variableName] of matchAll(css, colorModeVariableDefinitionRegex)) {
log(`--color-${variableName} defined via color-variables in ${file}`)
definedVariables.add(`--color-${variableName}`)
}
}
return definedVariables
})
}
function noop() {}
module.exports.ruleName = ruleName
module.exports.messages = messages