From 3bc226ad0786f9a7a21ce92a63cbba17b8a5b763 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Wed, 29 Nov 2023 22:11:47 +0100 Subject: [PATCH] New rule: check the new color CSS vars have a fallback (#122) * testing new rule to flag new css without fallback * added css vars to jsomn * rename, add index, add docs, remove scale * lint * lint * lint... * fix import * more lint * format * Create wet-lies-visit.md * fix name * oops --------- Co-authored-by: langermank <18661030+langermank@users.noreply.github.com> --- .changeset/wet-lies-visit.md | 5 + .../rules/new-color-css-vars-have-fallback.md | 25 ++ src/configs/recommended.js | 1 + src/index.js | 1 + .../new-css-vars-have-fallback.test.js | 31 ++ src/rules/new-color-css-vars-have-fallback.js | 87 +++++ src/utils/new-color-css-vars-map.json | 326 ++++++++++++++++++ 7 files changed, 476 insertions(+) create mode 100644 .changeset/wet-lies-visit.md create mode 100644 docs/rules/new-color-css-vars-have-fallback.md create mode 100644 src/rules/__tests__/new-css-vars-have-fallback.test.js create mode 100644 src/rules/new-color-css-vars-have-fallback.js create mode 100644 src/utils/new-color-css-vars-map.json diff --git a/.changeset/wet-lies-visit.md b/.changeset/wet-lies-visit.md new file mode 100644 index 00000000..67193148 --- /dev/null +++ b/.changeset/wet-lies-visit.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-primer-react": patch +--- + +New rule: new-color-css-vars-have-fallback: checks that if a new color var is used, it has a fallback value diff --git a/docs/rules/new-color-css-vars-have-fallback.md b/docs/rules/new-color-css-vars-have-fallback.md new file mode 100644 index 00000000..ad976430 --- /dev/null +++ b/docs/rules/new-color-css-vars-have-fallback.md @@ -0,0 +1,25 @@ +## Ensure new Primitive v8 color CSS vars have a fallback + +This rule is temporary as we begin testing v8 color tokens behind a feature flag. If a color token is used without a fallback, the color will only render if the feature flag is enabled. This rule is an extra safety net to ensure we don't accidentally ship code that relies on the feature flag. + +## Rule Details + +This rule refers to a JSON file that lists all the new color tokens + +```json +["--fgColor-default", "--fgColor-muted", "--fgColor-onEmphasis"] +``` + +If it finds that one of these tokens is used without a fallback, it will throw an error. + +👎 Examples of **incorrect** code for this rule + +```jsx + +``` + +👍 Examples of **correct** code for this rule: + +```jsx + +``` diff --git a/src/configs/recommended.js b/src/configs/recommended.js index 89da981a..793932b1 100644 --- a/src/configs/recommended.js +++ b/src/configs/recommended.js @@ -16,6 +16,7 @@ module.exports = { 'primer-react/a11y-tooltip-interactive-trigger': 'error', 'primer-react/new-color-css-vars': 'error', 'primer-react/a11y-explicit-heading': 'error', + 'primer-react/new-color-css-vars-have-fallback': 'error', }, settings: { github: { diff --git a/src/index.js b/src/index.js index 6be7aab4..9df33fd5 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ module.exports = { 'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger'), 'new-color-css-vars': require('./rules/new-color-css-vars'), 'a11y-explicit-heading': require('./rules/a11y-explicit-heading'), + 'new-color-css-vars-have-fallback': require('./rules/new-color-css-vars-have-fallback'), }, configs: { recommended: require('./configs/recommended'), diff --git a/src/rules/__tests__/new-css-vars-have-fallback.test.js b/src/rules/__tests__/new-css-vars-have-fallback.test.js new file mode 100644 index 00000000..5b4ab9ae --- /dev/null +++ b/src/rules/__tests__/new-css-vars-have-fallback.test.js @@ -0,0 +1,31 @@ +const rule = require('../new-color-css-vars-have-fallback') +const {RuleTester} = require('eslint') + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, +}) + +ruleTester.run('new-color-css-vars-have-fallback', rule, { + valid: [ + { + code: ``, + }, + ], + invalid: [ + { + code: ``, + errors: [ + { + message: + 'Expected a fallback value for CSS variable --fgColor-muted. New color variables fallbacks, check primer.style/primitives to find the correct value.', + }, + ], + }, + ], +}) diff --git a/src/rules/new-color-css-vars-have-fallback.js b/src/rules/new-color-css-vars-have-fallback.js new file mode 100644 index 00000000..7c71eecb --- /dev/null +++ b/src/rules/new-color-css-vars-have-fallback.js @@ -0,0 +1,87 @@ +const cssVars = require('../utils/new-color-css-vars-map') + +const reportError = (propertyName, valueNode, context) => { + // performance optimisation: exit early + if (valueNode.type !== 'Literal' && valueNode.type !== 'TemplateElement') return + // get property value + const value = valueNode.type === 'Literal' ? valueNode.value : valueNode.value.cooked + // return if value is not a string + if (typeof value !== 'string') return + // return if value does not include variable + if (!value.includes('var(')) return + + const varRegex = /var\([^(),)]+\)/g + + const match = value.match(varRegex) + // return if no matches + if (!match) return + const vars = match.flatMap(match => + match + .slice(4, -1) + .trim() + .split(/\s*,\s*/g), + ) + for (const cssVar of vars) { + // return if no repalcement exists + if (!cssVars?.includes(cssVar)) return + // report the error + context.report({ + node: valueNode, + message: `Expected a fallback value for CSS variable ${cssVar}. New color variables fallbacks, check primer.style/primitives to find the correct value.`, + }) + } +} + +const reportOnObject = (node, context) => { + const propertyName = node.key.name + if (node.value?.type === 'Literal') { + reportError(propertyName, node.value, context) + } else if (node.value?.type === 'ConditionalExpression') { + reportError(propertyName, node.value.consequent, context) + reportError(propertyName, node.value.alternate, context) + } +} + +const reportOnProperty = (node, context) => { + const propertyName = node.name.name + if (node.value?.type === 'Literal') { + reportError(propertyName, node.value, context) + } else if (node.value?.type === 'JSXExpressionContainer' && node.value.expression?.type === 'ConditionalExpression') { + reportError(propertyName, node.value.expression.consequent, context) + reportError(propertyName, node.value.expression.alternate, context) + } +} + +const reportOnValue = (node, context) => { + if (node?.type === 'Literal') { + reportError(undefined, node, context) + } else if (node?.type === 'JSXExpressionContainer' && node.expression?.type === 'ConditionalExpression') { + reportError(undefined, node.value.expression.consequent, context) + reportError(undefined, node.value.expression.alternate, context) + } +} + +const reportOnTemplateElement = (node, context) => { + reportError(undefined, node, context) +} + +module.exports = { + meta: { + type: 'suggestion', + }, + /** @param {import('eslint').Rule.RuleContext} context */ + create(context) { + return { + // sx OR style property on elements + ['JSXAttribute:matches([name.name=sx], [name.name=style]) ObjectExpression Property']: node => + reportOnObject(node, context), + // property on element like stroke or fill + ['JSXAttribute[name.name!=sx][name.name!=style]']: node => reportOnProperty(node, context), + // variable that is a value + [':matches(VariableDeclarator, ReturnStatement) > Literal']: node => reportOnValue(node, context), + // variable that is a value + [':matches(VariableDeclarator, ReturnStatement) > TemplateElement']: node => + reportOnTemplateElement(node, context), + } + }, +} diff --git a/src/utils/new-color-css-vars-map.json b/src/utils/new-color-css-vars-map.json new file mode 100644 index 00000000..32ab93a9 --- /dev/null +++ b/src/utils/new-color-css-vars-map.json @@ -0,0 +1,326 @@ +[ + "--topicTag-borderColor", + "--highlight-neutral-bgColor", + "--page-header-bgColor", + "--diffBlob-addition-fgColor-text", + "--diffBlob-addition-fgColor-num", + "--diffBlob-addition-bgColor-num", + "--diffBlob-addition-bgColor-line", + "--diffBlob-addition-bgColor-word", + "--diffBlob-deletion-fgColor-text", + "--diffBlob-deletion-fgColor-num", + "--diffBlob-deletion-bgColor-num", + "--diffBlob-deletion-bgColor-line", + "--diffBlob-deletion-bgColor-word", + "--diffBlob-hunk-bgColor-num", + "--diffBlob-expander-iconColor", + "--codeMirror-fgColor", + "--codeMirror-bgColor", + "--codeMirror-gutters-bgColor", + "--codeMirror-gutterMarker-fgColor-default", + "--codeMirror-gutterMarker-fgColor-muted", + "--codeMirror-lineNumber-fgColor", + "--codeMirror-cursor-fgColor", + "--codeMirror-selection-bgColor", + "--codeMirror-activeline-bgColor", + "--codeMirror-matchingBracket-fgColor", + "--codeMirror-lines-bgColor", + "--codeMirror-syntax-fgColor-comment", + "--codeMirror-syntax-fgColor-constant", + "--codeMirror-syntax-fgColor-entity", + "--codeMirror-syntax-fgColor-keyword", + "--codeMirror-syntax-fgColor-storage", + "--codeMirror-syntax-fgColor-string", + "--codeMirror-syntax-fgColor-support", + "--codeMirror-syntax-fgColor-variable", + "--header-fgColor-default", + "--header-fgColor-logo", + "--header-bgColor", + "--header-borderColor-divider", + "--headerSearch-bgColor", + "--headerSearch-borderColor", + "--avatar-bgColor", + "--avatar-borderColor", + "--avatar-shadow", + "--avatarStack-fade-bgColor-default", + "--avatarStack-fade-bgColor-muted", + "--control-bgColor-rest", + "--control-bgColor-hover", + "--control-bgColor-active", + "--control-bgColor-disabled", + "--control-bgColor-selected", + "--control-fgColor-rest", + "--control-fgColor-placeholder", + "--control-fgColor-disabled", + "--control-borderColor-rest", + "--control-borderColor-emphasis", + "--control-borderColor-disabled", + "--control-borderColor-selected", + "--control-borderColor-success", + "--control-borderColor-danger", + "--control-borderColor-warning", + "--control-iconColor-rest", + "--control-transparent-bgColor-rest", + "--control-transparent-bgColor-hover", + "--control-transparent-bgColor-active", + "--control-transparent-bgColor-disabled", + "--control-transparent-bgColor-selected", + "--control-transparent-borderColor-rest", + "--control-transparent-borderColor-hover", + "--control-transparent-borderColor-active", + "--control-danger-fgColor-rest", + "--control-danger-fgColor-hover", + "--control-danger-bgColor-hover", + "--control-danger-bgColor-active", + "--control-checked-bgColor-rest", + "--control-checked-bgColor-hover", + "--control-checked-bgColor-active", + "--control-checked-bgColor-disabled", + "--control-checked-fgColor-rest", + "--control-checked-fgColor-disabled", + "--control-checked-borderColor-rest", + "--control-checked-borderColor-hover", + "--control-checked-borderColor-active", + "--control-checked-borderColor-disabled", + "--controlTrack-bgColor-rest", + "--controlTrack-bgColor-hover", + "--controlTrack-bgColor-active", + "--controlTrack-bgColor-disabled", + "--controlTrack-fgColor-rest", + "--controlTrack-fgColor-disabled", + "--controlTrack-borderColor-rest", + "--controlTrack-borderColor-disabled", + "--controlKnob-bgColor-rest", + "--controlKnob-bgColor-disabled", + "--controlKnob-bgColor-checked", + "--controlKnob-borderColor-rest", + "--controlKnob-borderColor-disabled", + "--controlKnob-borderColor-checked", + "--counter-borderColor", + "--button-default-fgColor-rest", + "--button-default-bgColor-rest", + "--button-default-bgColor-hover", + "--button-default-bgColor-active", + "--button-default-bgColor-selected", + "--button-default-bgColor-disabled", + "--button-default-borderColor-rest", + "--button-default-borderColor-hover", + "--button-default-borderColor-active", + "--button-default-borderColor-disabled", + "--button-default-shadow-resting", + "--button-primary-fgColor-rest", + "--button-primary-fgColor-disabled", + "--button-primary-iconColor-rest", + "--button-primary-bgColor-rest", + "--button-primary-bgColor-hover", + "--button-primary-bgColor-active", + "--button-primary-bgColor-disabled", + "--button-primary-borderColor-rest", + "--button-primary-borderColor-hover", + "--button-primary-borderColor-active", + "--button-primary-borderColor-disabled", + "--button-primary-shadow-selected", + "--button-invisible-fgColor-rest", + "--button-invisible-fgColor-hover", + "--button-invisible-fgColor-disabled", + "--button-invisible-iconColor-rest", + "--button-invisible-iconColor-hover", + "--button-invisible-iconColor-disabled", + "--button-invisible-bgColor-rest", + "--button-invisible-bgColor-hover", + "--button-invisible-bgColor-active", + "--button-invisible-bgColor-disabled", + "--button-invisible-borderColor-rest", + "--button-invisible-borderColor-hover", + "--button-invisible-borderColor-disabled", + "--button-outline-fgColor-rest", + "--button-outline-fgColor-hover", + "--button-outline-fgColor-active", + "--button-outline-fgColor-disabled", + "--button-outline-bgColor-rest", + "--button-outline-bgColor-hover", + "--button-outline-bgColor-active", + "--button-outline-bgColor-disabled", + "--button-outline-borderColor-hover", + "--button-outline-borderColor-selected", + "--button-outline-shadow-selected", + "--button-danger-fgColor-rest", + "--button-danger-fgColor-hover", + "--button-danger-fgColor-active", + "--button-danger-fgColor-disabled", + "--button-danger-iconColor-rest", + "--button-danger-iconColor-hover", + "--button-danger-bgColor-rest", + "--button-danger-bgColor-hover", + "--button-danger-bgColor-active", + "--button-danger-bgColor-disabled", + "--button-danger-borderColor-rest", + "--button-danger-borderColor-hover", + "--button-danger-borderColor-active", + "--button-danger-shadow-selected", + "--button-inactive-fgColor-rest", + "--button-inactive-bgColor-rest", + "--buttonCounter-default-bgColor-rest", + "--buttonCounter-invisible-bgColor-rest", + "--buttonCounter-primary-bgColor-rest", + "--buttonCounter-outline-bgColor-rest", + "--buttonCounter-outline-bgColor-hover", + "--buttonCounter-outline-bgColor-disabled", + "--buttonCounter-outline-fgColor-rest", + "--buttonCounter-outline-fgColor-hover", + "--buttonCounter-outline-fgColor-disabled", + "--buttonCounter-danger-bgColor-hover", + "--buttonCounter-danger-bgColor-disabled", + "--buttonCounter-danger-bgColor-rest", + "--buttonCounter-danger-fgColor-rest", + "--buttonCounter-danger-fgColor-hover", + "--buttonCounter-danger-fgColor-disabled", + "--focus-outlineColor", + "--menu-bgColor-active", + "--overlay-bgColor", + "--overlay-backdrop-bgColor", + "--selectMenu-borderColor", + "--selectMenu-bgColor-active", + "--sideNav-bgColor-selected", + "--skeletonLoader-bgColor", + "--timelineBadge-bgColor", + "--treeViewItem-leadingVisual-iconColor-rest", + "--underlineNav-borderColor-active", + "--underlineNav-borderColor-hover", + "--underlineNav-iconColor-rest", + "--fgColor-default", + "--fgColor-muted", + "--fgColor-onEmphasis", + "--fgColor-onInverse", + "--fgColor-disabled", + "--fgColor-link", + "--fgColor-link-onInverse", + "--fgColor-neutral", + "--fgColor-neutral-onInverse", + "--fgColor-accent", + "--fgColor-accent-onInverse", + "--fgColor-success", + "--fgColor-success-onInverse", + "--fgColor-attention", + "--fgColor-attention-onInverse", + "--fgColor-severe", + "--fgColor-severe-onInverse", + "--fgColor-danger", + "--fgColor-danger-onInverse", + "--fgColor-open", + "--fgColor-open-onInverse", + "--fgColor-closed", + "--fgColor-closed-onInverse", + "--fgColor-done", + "--fgColor-done-onInverse", + "--fgColor-sponsors", + "--fgColor-sponsors-onInverse", + "--bgColor-default", + "--bgColor-muted", + "--bgColor-inset", + "--bgColor-emphasis", + "--bgColor-inverse", + "--bgColor-disabled", + "--bgColor-transparent", + "--bgColor-neutral-muted", + "--bgColor-neutral-emphasis", + "--bgColor-accent-muted", + "--bgColor-accent-emphasis", + "--bgColor-success-muted", + "--bgColor-success-emphasis", + "--bgColor-attention-muted", + "--bgColor-attention-emphasis", + "--bgColor-severe-muted", + "--bgColor-severe-emphasis", + "--bgColor-danger-muted", + "--bgColor-danger-emphasis", + "--bgColor-open-muted", + "--bgColor-open-emphasis", + "--bgColor-closed-muted", + "--bgColor-closed-emphasis", + "--bgColor-done-muted", + "--bgColor-done-emphasis", + "--bgColor-sponsors-muted", + "--bgColor-sponsors-emphasis", + "--borderColor-default", + "--borderColor-muted", + "--borderColor-emphasis", + "--borderColor-disabled", + "--borderColor-transparent", + "--borderColor-neutral-muted", + "--borderColor-neutral-emphasis", + "--borderColor-accent-muted", + "--borderColor-accent-emphasis", + "--borderColor-success-muted", + "--borderColor-success-emphasis", + "--borderColor-attention-muted", + "--borderColor-attention-emphasis", + "--borderColor-severe-muted", + "--borderColor-severe-emphasis", + "--borderColor-danger-muted", + "--borderColor-danger-emphasis", + "--borderColor-open-muted", + "--borderColor-open-emphasis", + "--borderColor-closed-muted", + "--borderColor-closed-emphasis", + "--borderColor-done-muted", + "--borderColor-done-emphasis", + "--borderColor-sponsors-muted", + "--borderColor-sponsors-emphasis", + "--color-ansi-black", + "--color-ansi-black-bright", + "--color-ansi-white", + "--color-ansi-white-bright", + "--color-ansi-gray", + "--color-ansi-red", + "--color-ansi-red-bright", + "--color-ansi-green", + "--color-ansi-green-bright", + "--color-ansi-yellow", + "--color-ansi-yellow-bright", + "--color-ansi-blue", + "--color-ansi-blue-bright", + "--color-ansi-magenta", + "--color-ansi-magenta-bright", + "--color-ansi-cyan", + "--color-ansi-cyan-bright", + "--color-prettylights-syntax-comment", + "--color-prettylights-syntax-constant", + "--color-prettylights-syntax-constant-other-reference-link", + "--color-prettylights-syntax-entity", + "--color-prettylights-syntax-storage-modifier-import", + "--color-prettylights-syntax-entity-tag", + "--color-prettylights-syntax-keyword", + "--color-prettylights-syntax-string", + "--color-prettylights-syntax-variable", + "--color-prettylights-syntax-brackethighlighter-unmatched", + "--color-prettylights-syntax-brackethighlighter-angle", + "--color-prettylights-syntax-invalid-illegal-text", + "--color-prettylights-syntax-invalid-illegal-bg", + "--color-prettylights-syntax-carriage-return-text", + "--color-prettylights-syntax-carriage-return-bg", + "--color-prettylights-syntax-string-regexp", + "--color-prettylights-syntax-markup-list", + "--color-prettylights-syntax-markup-heading", + "--color-prettylights-syntax-markup-italic", + "--color-prettylights-syntax-markup-bold", + "--color-prettylights-syntax-markup-deleted-text", + "--color-prettylights-syntax-markup-deleted-bg", + "--color-prettylights-syntax-markup-inserted-text", + "--color-prettylights-syntax-markup-inserted-bg", + "--color-prettylights-syntax-markup-changed-text", + "--color-prettylights-syntax-markup-changed-bg", + "--color-prettylights-syntax-markup-ignored-text", + "--color-prettylights-syntax-markup-ignored-bg", + "--color-prettylights-syntax-meta-diff-range", + "--color-prettylights-syntax-sublimelinter-gutter-mark", + "--shadow-inset", + "--shadow-resting-xsmall", + "--shadow-resting-small", + "--shadow-resting-medium", + "--shadow-floating-small", + "--shadow-floating-medium", + "--shadow-floating-large", + "--shadow-floating-xlarge", + "--outline-focus" +]