plugins/no-override.js (100 lines of code) (raw):

const stylelint = require('stylelint') const {requirePrimerFile} = require('./lib/primer') const ruleName = 'primer/no-override' const CLASS_PATTERN = /(\.[-\w]+)/ const CLASS_PATTERN_ALL = new RegExp(CLASS_PATTERN, 'g') const CLASS_PATTERN_ONLY = /^\.[-\w]+(:{1,2}[-\w]+)?$/ module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}) => { if (!enabled) { return noop } const {bundles = ['utilities'], ignoreSelectors = []} = options const isSelectorIgnored = typeof ignoreSelectors === 'function' ? ignoreSelectors : selector => { return ignoreSelectors.some(pattern => { return pattern instanceof RegExp ? pattern.test(selector) : selector.includes(pattern) }) } const primerMeta = requirePrimerFile('dist/meta.json') const availableBundles = Object.keys(primerMeta.bundles) // These map selectors to the bundle in which they're defined. // If there's no entry for a given selector, it means that it's not defined // in one of the *specified* bundles, since we're iterating over the list of // bundle names in the options. const immutableSelectors = new Map() const immutableClassSelectors = new Map() for (const bundle of bundles) { if (!availableBundles.includes(bundle)) { continue } const stats = requirePrimerFile(`dist/stats/${bundle}.json`) const selectors = stats.selectors.values for (const selector of selectors) { immutableSelectors.set(selector, bundle) for (const classSelector of getClassSelectors(selector)) { immutableClassSelectors.set(classSelector, bundle) } } } const messages = stylelint.utils.ruleMessages(ruleName, { rejected: ({rule, selector, bundle}) => { const definedIn = ` (defined in @primer/css/${bundle})` const ruleSelector = collapseWhitespace(rule.selector) const context = selector === rule.selector ? '' : ` in "${ruleSelector}"` return `"${collapseWhitespace(selector)}" should not be overridden${context}${definedIn}.` } }) return (root, result) => { if (!Array.isArray(bundles) || bundles.some(bundle => !availableBundles.includes(bundle))) { const invalidBundles = Array.isArray(bundles) ? `"${bundles.filter(bundle => !availableBundles.includes(bundle)).join('", "')}"` : '(not an array)' result.warn(`The "bundles" option must be an array of valid bundles; got: ${invalidBundles}`, { stylelintType: 'invalidOption', stylelintReference: 'https://github.com/primer/stylelint-config#options' }) } const report = subject => stylelint.utils.report({ message: messages.rejected(subject), node: subject.rule, result, ruleName }) root.walkRules(rule => { const {selector} = rule if (immutableSelectors.has(selector)) { if (isClassSelector(selector)) { if (!isSelectorIgnored(selector)) { return report({ rule, bundle: immutableSelectors.get(selector), selector }) } } else { // console.log(`not a class selector: "${selector}"`) } } for (const classSelector of getClassSelectors(selector)) { if (immutableClassSelectors.has(classSelector)) { if (!isSelectorIgnored(classSelector)) { return report({ rule, bundle: immutableClassSelectors.get(classSelector), selector: classSelector }) } } } }) } }) function getClassSelectors(selector) { const match = selector.match(CLASS_PATTERN_ALL) return match ? [...match] : [] } function isClassSelector(selector) { return CLASS_PATTERN_ONLY.test(selector) } function collapseWhitespace(str) { return str.trim().replace(/\s+/g, ' ') } function noop() {}