plugins/no-deprecated-colors.js (83 lines of code) (raw):

const stylelint = require('stylelint') const kebabCase = require('lodash.kebabcase') const matchAll = require('string.prototype.matchall') const ruleName = 'primer/no-deprecated-colors' const messages = stylelint.utils.ruleMessages(ruleName, { rejected: (varName, replacement) => { if (replacement === null) { return `${varName} is a deprecated color variable. Please consult the primer color docs for a replacement. https://primer.style/primitives` } if (Array.isArray(replacement)) { replacement = replacement.map(r => `--color-${kebabCase(r)}`) return `${varName} is a deprecated color variable. Please use one of (${replacement.join(', ')}).` } replacement = `--color-${kebabCase(replacement)}` return `${varName} is a deprecated color variable. Please use the replacement ${replacement}.` } }) // Match CSS variable references (e.g var(--color-text-primary)) // eslint-disable-next-line no-useless-escape const variableReferenceRegex = /var\(([^\),]+)(,.*)?\)/g const replacedVars = {} const newVars = {} module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}, context) => { if (!enabled) { return noop } const {verbose = false} = options // eslint-disable-next-line no-console const log = verbose ? (...args) => console.warn(...args) : noop // Keep track of declarations we've already seen const seen = new WeakMap() // eslint-disable-next-line import/no-dynamic-require const deprecatedColors = require(options.deprecatedFile || '@primer/primitives/dist/deprecated/colors.json') // eslint-disable-next-line import/no-dynamic-require const removedColors = require(options.removedFile || '@primer/primitives/dist/removed/colors.json') const variableChecks = Object.assign(deprecatedColors, removedColors) const convertedCSSVars = Object.entries(variableChecks) .map(([k, v]) => { return [`--color-${kebabCase(k)}`, v] }) .reduce((acc, [key, value]) => { acc[key] = value return acc }, {}) const lintResult = (root, result) => { // Walk all declarartions root.walk(node => { if (seen.has(node)) { return } else { seen.set(node, true) } // walk these nodes if (!(node.type === 'decl' || node.type === 'atrule')) { return } for (const [, variableName] of matchAll( node.type === 'atrule' ? node.params : node.value, variableReferenceRegex )) { if (variableName in convertedCSSVars) { let replacement = convertedCSSVars[variableName] if (context.fix && replacement !== null && !Array.isArray(replacement)) { replacement = `--color-${kebabCase(replacement)}` replacedVars[variableName] = true newVars[replacement] = true if (node.type === 'atrule') { node.params = node.params.replace(variableName, replacement) } else { node.value = node.value.replace(variableName, replacement) } continue } stylelint.utils.report({ message: messages.rejected(variableName, replacement), node, ruleName, result }) } } }) } log( `${Object.keys(replacedVars).length} deprecated variables replaced with ${Object.keys(newVars).length} variables.` ) return lintResult }) function noop() {} module.exports.ruleName = ruleName module.exports.messages = messages