diff --git a/.changeset/shiny-kiwis-hammer.md b/.changeset/shiny-kiwis-hammer.md new file mode 100644 index 00000000..80805217 --- /dev/null +++ b/.changeset/shiny-kiwis-hammer.md @@ -0,0 +1,6 @@ +--- +"eslint-plugin-primer-react": major +--- + +- Remove `no-deprecated-colors` plugin +- Remove dependency on `primer/primitives` diff --git a/README.md b/README.md index 5ab44c86..9163120c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ ESLint rules for Primer React ## Rules - [direct-slot-children](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/direct-slot-children.md) -- [no-deprecated-colors](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-colors.md) - [no-system-props](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-system-props.md) - [a11y-tooltip-interactive-trigger](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-tooltip-interactive-trigger.md) - [a11y-explicit-heading](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-explicit-heading.md) diff --git a/docs/rules/no-deprecated-colors.md b/docs/rules/no-deprecated-colors.md deleted file mode 100644 index 151bc716..00000000 --- a/docs/rules/no-deprecated-colors.md +++ /dev/null @@ -1,91 +0,0 @@ -# Disallow references to deprecated color variables (no-deprecated-colors) - -🔧 The `--fix` option on the [ESLint CLI](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can -automatically fix some of the problems reported by this rule. - -[Theming](https://primer.style/react/theming) in Primer React is made possible by a theme object that defines your -application's colors, spacing, fonts, and more. The color variables in Primer React's -[default theme object](https://primer.style/react/theme-reference) are pulled from -[Primer Primitives](https://github.com/primer/primitives). When a color variable is deprecated in Primer Primitives, -it's important to remove references to that color variable in your application before it's removed from the library. - -## Rule details - -This rule disallows references to color variables that are deprecated in -[Primer Primitives](https://github.com/primer/primitives). - -👎 Examples of **incorrect** code for this rule: - -```jsx -/* eslint primer-react/no-deprecated-colors: "error" */ -import {Box, themeGet} from '@primer/react' -import styled from 'styled-components' - -const SystemPropExample() = () => Incorrect - -const SxPropExample() = () => Incorrect - -const SxPropExample2() = () => `0 1px 2px ${theme.colors.some.deprecated.color}`}}>Incorrect - -const ThemeGetExample = styled.div` - color: ${themeGet('colors.some.deprecated.color')}; -` -``` - -👍 Examples of **correct** code for this rule: - -```jsx -/* eslint primer-react/no-deprecated-colors: "error" */ -import {Box, themeGet} from '@primer/react' -import styled from 'styled-components' - -const SystemPropExample() = () => Correct - -const SxPropExample() = () => Correct - -const SxPropExample2() = () => `0 1px 2px ${theme.colors.some.color}`}}>Correct - -const ThemeGetExample = styled.div` - color: ${themeGet('colors.some.color')}; -` -``` - -## Options - -- `skipImportCheck` (default: `false`) - - By default, the `no-deprecated-colors` rule will only check for deprecated colors used in functions and components - that are imported from `@primer/react`. You can disable this behavior by setting `skipImportCheck` to `true`. This is - useful for linting custom components that pass color-related props down to Primer React components. - - ```js - /* eslint primer-react/no-deprecated-colors: ["warn", {"skipImportCheck": true}] */ - import {Box} from '@primer/react' - - function MyBox({color, children}) { - return {children} - } - - function App() { - // Enabling `skipImportCheck` will find deprecated colors used like this: - return Hello - } - ``` - -- `checkAllStrings` (default: `false`) - - If `checkAllStrings` is set to `true`, the `no-deprecated-colors` rule will check for deprecated colors in all - strings. This is useful for catching uses of deprecated colors outside system props and the `sx` prop. - - ```js - /* eslint primer-react/no-deprecated-colors: ["warn", {"checkAllStrings": true}] */ - import {Box} from '@primer/react' - - function ExampleComponent() { - const styles = { - // Enabling `checkAllStrings` will find deprecated colors used like this: - color: 'text.primary', - } - return Hello - } - ``` diff --git a/package-lock.json b/package-lock.json index 62196bb1..365bfc06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@changesets/cli": "^2.16.0", "@github/markdownlint-github": "^0.6.0", "@github/prettier-config": "0.0.6", - "@primer/primitives": "^7.14.0", "eslint": "^8.42.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.7.0", @@ -29,7 +28,6 @@ "markdownlint-cli2-formatter-pretty": "^0.0.5" }, "peerDependencies": { - "@primer/primitives": ">=4.6.2", "eslint": "^8.42.0" } }, @@ -1897,12 +1895,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@primer/primitives": { - "version": "7.15.15", - "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.15.tgz", - "integrity": "sha512-K9fPgKVBtdmBMl+lHKUK4ctsRf+DUUKdJYEHnFhkua71m9FORK16ycJ4gHDpc4g/0xgNvEwQ9/ArsV1i8w3ZmQ==", - "dev": true - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9972,12 +9964,6 @@ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==" }, - "@primer/primitives": { - "version": "7.15.15", - "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.15.tgz", - "integrity": "sha512-K9fPgKVBtdmBMl+lHKUK4ctsRf+DUUKdJYEHnFhkua71m9FORK16ycJ4gHDpc4g/0xgNvEwQ9/ArsV1i8w3ZmQ==", - "dev": true - }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", diff --git a/package.json b/package.json index 66e3e9bc..bff095d9 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ }, "homepage": "https://github.com/primer/eslint-plugin-primer-react#readme", "peerDependencies": { - "@primer/primitives": ">=4.6.2", "eslint": "^8.42.0" }, "dependencies": { @@ -41,7 +40,6 @@ "@changesets/cli": "^2.16.0", "@github/markdownlint-github": "^0.6.0", "@github/prettier-config": "0.0.6", - "@primer/primitives": "^7.14.0", "eslint": "^8.42.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.7.0", diff --git a/src/configs/recommended.js b/src/configs/recommended.js index a1204151..7e4676fc 100644 --- a/src/configs/recommended.js +++ b/src/configs/recommended.js @@ -11,7 +11,6 @@ module.exports = { extends: ['plugin:github/react'], rules: { 'primer-react/direct-slot-children': 'error', - 'primer-react/no-deprecated-colors': 'warn', 'primer-react/no-system-props': 'warn', 'primer-react/a11y-tooltip-interactive-trigger': 'error', 'primer-react/new-color-css-vars': 'error', diff --git a/src/index.js b/src/index.js index 134eb161..2b070e60 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,6 @@ module.exports = { rules: { 'direct-slot-children': require('./rules/direct-slot-children'), - 'no-deprecated-colors': require('./rules/no-deprecated-colors'), 'no-deprecated-entrypoints': require('./rules/no-deprecated-entrypoints'), 'no-system-props': require('./rules/no-system-props'), 'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger'), diff --git a/src/rules/__tests__/no-deprecated-colors.test.js b/src/rules/__tests__/no-deprecated-colors.test.js deleted file mode 100644 index eeccfbfc..00000000 --- a/src/rules/__tests__/no-deprecated-colors.test.js +++ /dev/null @@ -1,221 +0,0 @@ -const rule = require('../no-deprecated-colors') -const {RuleTester} = require('eslint') - -const deprecatedVars = { - 'text.primary': 'fg.default', - 'bg.primary': 'canvas.default', - 'auto.green.5': ['success.fg', 'success.emphasis'], -} - -const removedVars = { - 'fade.fg10': null, - 'autocomplete.shadow': 'shadow.medium', -} - -jest.mock('@primer/primitives/dist/deprecated/colors', () => deprecatedVars) -jest.mock('@primer/primitives/dist/removed/colors', () => removedVars) - -const ruleTester = new RuleTester({ - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, -}) - -ruleTester.run('no-deprecated-colors', rule, { - valid: [ - `import {Box} from '@other/design-system'; Hello`, - `import {Box} from "@primer/react"; Hello`, - `import {hello} from "@primer/react"; hello("colors.text.primary")`, - `import {themeGet} from "@primer/react"; themeGet("space.text.primary")`, - `import {themeGet} from "@primer/react"; themeGet(props.backgroundColorThemeValue)`, - `import {themeGet} from "@primer/react"; themeGet(2)`, - `import {themeGet} from "@other/design-system"; themeGet("colors.text.primary")`, - `import {get} from "@other/constants"; get("space.text.primary")`, - `import {Box} from '@primer/react'; Hello`, - `import {Box} from '@primer/react'; Hello`, - `import {Box} from '@primer/react'; Hello`, - `{color: 'text.primary'}`, - ], - invalid: [ - { - code: `{color: 'text.primary'}`, - output: `{color: "fg.default"}`, - options: [{checkAllStrings: true}], - errors: [ - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; function Example() { return Hello }`, - output: `import {Box} from "@primer/react"; function Example() { return Hello }`, - errors: [ - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "../components"; function Example() { return Hello }`, - output: `import {Box} from "../components"; function Example() { return Hello }`, - options: [{skipImportCheck: true}], - errors: [ - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import Box from '@primer/react/lib-esm/Box'; function Example() { return Hello }`, - output: `import Box from '@primer/react/lib-esm/Box'; function Example() { return Hello }`, - errors: [ - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; const Example = () => Hello`, - output: `import {Box} from "@primer/react"; const Example = () => Hello`, - errors: [ - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; `, - output: `import {Box} from "@primer/react"; `, - errors: [ - { - message: '"bg.primary" is deprecated. Use "canvas.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; `, - output: `import {Box} from "@primer/react"; `, - errors: [ - { - message: '"bg.primary" is deprecated. Use "canvas.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; theme.shadows.autocomplete.shadow}} />`, - output: `import {Box} from "@primer/react"; theme.shadows.shadow.medium}} />`, - errors: [ - { - message: '"theme.shadows.autocomplete.shadow" is deprecated. Use "theme.shadows.shadow.medium" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; \`0 1px 2px \${theme.colors.text.primary}\`}} />`, - output: `import {Box} from "@primer/react"; \`0 1px 2px \${theme.colors.fg.default}\`}} />`, - errors: [ - { - message: '"theme.colors.text.primary" is deprecated. Use "theme.colors.fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; \`0 1px 2px \${t.colors.text.primary}\`}} />`, - output: `import {Box} from "@primer/react"; \`0 1px 2px \${t.colors.fg.default}\`}} />`, - errors: [ - { - message: '"t.colors.text.primary" is deprecated. Use "t.colors.fg.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; `, - output: `import {Box} from "@primer/react"; `, - errors: [ - { - message: '"bg.primary" is deprecated. Use "canvas.default" instead.', - }, - ], - }, - { - code: `import {Box} from "@primer/react"; `, - errors: [ - { - message: '"auto.green.5" is deprecated.', - suggestions: [ - { - desc: 'Use "success.fg" instead.', - output: `import {Box} from "@primer/react"; `, - }, - { - desc: 'Use "success.emphasis" instead.', - output: `import {Box} from "@primer/react"; `, - }, - ], - }, - ], - }, - { - code: `import {Box} from "@primer/react"; `, - errors: [ - { - message: - '"fade.fg10" is deprecated. Go to https://primer.style/primitives or reach out in the #primer channel on Slack to find a suitable replacement.', - }, - ], - }, - { - code: `import {Box, Text} from "@primer/react"; Hello`, - output: `import {Box, Text} from "@primer/react"; Hello`, - errors: [ - { - message: '"bg.primary" is deprecated. Use "canvas.default" instead.', - }, - { - message: '"text.primary" is deprecated. Use "fg.default" instead.', - }, - ], - }, - { - code: `import {themeGet} from "@primer/react"; themeGet("colors.text.primary")`, - output: `import {themeGet} from "@primer/react"; themeGet("colors.fg.default")`, - errors: [ - { - message: '"colors.text.primary" is deprecated. Use "colors.fg.default" instead.', - }, - ], - }, - { - code: `import {themeGet} from "@primer/react"; themeGet("shadows.autocomplete.shadow")`, - output: `import {themeGet} from "@primer/react"; themeGet("shadows.shadow.medium")`, - errors: [ - { - message: '"shadows.autocomplete.shadow" is deprecated. Use "shadows.shadow.medium" instead.', - }, - ], - }, - { - code: `import {get} from "./constants"; get("colors.text.primary")`, - output: `import {get} from "./constants"; get("colors.fg.default")`, - errors: [ - { - message: '"colors.text.primary" is deprecated. Use "colors.fg.default" instead.', - }, - ], - }, - { - code: `import {get} from "../constants"; get("colors.text.primary")`, - output: `import {get} from "../constants"; get("colors.fg.default")`, - errors: [ - { - message: '"colors.text.primary" is deprecated. Use "colors.fg.default" instead.', - }, - ], - }, - ], -}) diff --git a/src/rules/no-deprecated-colors.js b/src/rules/no-deprecated-colors.js deleted file mode 100644 index a1e2eb0e..00000000 --- a/src/rules/no-deprecated-colors.js +++ /dev/null @@ -1,193 +0,0 @@ -const deprecatedVars = require('@primer/primitives/dist/deprecated/colors') -const removedVars = require('@primer/primitives/dist/removed/colors') -const traverse = require('eslint-traverse') -const {isImportedFrom} = require('../utils/is-imported-from') -const {isPrimerComponent} = require('../utils/is-primer-component') - -const styledSystemColorProps = ['color', 'bg', 'backgroundColor', 'borderColor', 'textShadow', 'boxShadow'] -const deprecations = {...deprecatedVars, ...removedVars} - -module.exports = { - meta: { - type: 'suggestion', - hasSuggestions: true, - fixable: 'code', - schema: [ - { - type: 'object', - properties: { - skipImportCheck: { - type: 'boolean', - }, - checkAllStrings: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - create(context) { - // If `skipImportCheck` is true, this rule will check for deprecated colors - // used in any components (not just ones that are imported from `@primer/react`). - const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false - - const checkAllStrings = context.options[0] ? context.options[0].checkAllStrings : false - - // Track visited string literals to avoid reporting the same string multiple times - const visitedStrings = new Set() - - return { - Literal(node) { - if (checkAllStrings && Object.keys(deprecations).includes(node.value) && !visitedStrings.has(node)) { - replaceDeprecatedColor(context, node, node.value) - } - }, - JSXOpeningElement(node) { - // Skip if component was not imported from @primer/react - if (!skipImportCheck && !isPrimerComponent(node.name, context.getScope(node))) { - return - } - - for (const attribute of node.attributes) { - if (!attribute.name || !attribute.value) { - continue - } - - const propName = attribute.name.name - const propValue = attribute.value.value - - // Check for the sx prop - if (propName === 'sx' && attribute.value.expression.type === 'ObjectExpression') { - // Search all properties of the sx object (even nested properties) - traverse(context, attribute.value, path => { - if (path.node.type === 'Property' && path.node.value.type === 'Literal') { - const prop = path.node - const propName = prop.key.name - const propValue = prop.value.value - - if (styledSystemColorProps.includes(propName) && Object.keys(deprecations).includes(propValue)) { - replaceDeprecatedColor(context, prop.value, propValue) - visitedStrings.add(prop.value) - } - } - - // Check functions passed to sx object properties - // (e.g. boxShadow: theme => `0 1px 2px ${theme.colors.text.primary}` ) - if (path.node.type === 'Property' && path.node.value.type === 'ArrowFunctionExpression') { - traverse(context, path.node.value.body, path => { - if (path.node.type === 'MemberExpression') { - // Convert MemberExpression AST to string - const code = context.getSourceCode().getText(path.node) - - const [param, key, ...rest] = code.split('.') - const name = rest.join('.') - - if (['colors', 'shadows'].includes(key) && Object.keys(deprecations).includes(name)) { - replaceDeprecatedColor( - context, - path.node, - name, - str => [param, key, str].join('.'), - str => str, - ) - } - - // Don't traverse any nested member expressions. - // The root-level member expression gives us all the data we need. - return traverse.SKIP - } - }) - } - }) - } - - // Check if styled-system color prop is using a deprecated color - if (styledSystemColorProps.includes(propName) && Object.keys(deprecations).includes(propValue)) { - replaceDeprecatedColor(context, attribute.value, propValue) - visitedStrings.add(attribute.value) - } - } - }, - CallExpression(node) { - // Skip if not calling the `themeGet` or `get` function - // `get` is the internal version of `themeGet` that's used in the primer/react repository - if ( - !isThemeGet(node.callee, context.getScope(node), skipImportCheck) && - !isGet(node.callee, context.getScope(node)) - ) { - return - } - - const argument = node.arguments[0] - // Skip if the argument is not a Literal (themeGet(props.backgroundColor)) - // or a string themeGet(2) - if (argument.type !== 'Literal' || typeof argument.value !== 'string') { - return - } - - const [key, ...path] = argument.value.split('.') - const name = path.join('.') - - if (['colors', 'shadows'].includes(key) && Object.keys(deprecations).includes(name)) { - replaceDeprecatedColor(context, argument, name, str => [key, str].join('.')) - } - }, - } - }, -} - -function isThemeGet(identifier, scope, skipImportCheck = false) { - if (!skipImportCheck) { - return isImportedFrom(/^@primer\/react/, identifier, scope) && identifier.name === 'themeGet' - } - - return identifier.name === 'themeGet' -} - -// `get` is the internal version of `themeGet` that's used in the primer/react repository. -function isGet(identifier, scope) { - // This is a flaky way to check for the `get` function and should probably be improved. - return isImportedFrom(/^\.\.?\/constants$/, identifier, scope) && identifier.name === 'get' -} - -function replaceDeprecatedColor( - context, - node, - deprecatedName, - transformName = str => str, - transformReplacementValue = str => JSON.stringify(str), -) { - const replacement = deprecations[deprecatedName] - - if (replacement === null) { - // No replacement - context.report({ - node, - message: `"${transformName( - deprecatedName, - )}" is deprecated. Go to https://primer.style/primitives or reach out in the #primer channel on Slack to find a suitable replacement.`, - }) - } else if (Array.isArray(replacement)) { - // Multiple possible replacements - context.report({ - node, - message: `"${transformName(deprecatedName)}" is deprecated.`, - suggest: replacement.map(replacementValue => ({ - desc: `Use "${transformName(replacementValue)}" instead.`, - fix(fixer) { - return fixer.replaceText(node, transformReplacementValue(transformName(replacementValue))) - }, - })), - }) - } else { - // One replacement - context.report({ - node, - message: `"${transformName(deprecatedName)}" is deprecated. Use "${transformName(replacement)}" instead.`, - fix(fixer) { - return fixer.replaceText(node, transformReplacementValue(transformName(replacement))) - }, - }) - } -}