diff --git a/.changeset/wicked-areas-jog.md b/.changeset/wicked-areas-jog.md new file mode 100644 index 00000000..5582f9e1 --- /dev/null +++ b/.changeset/wicked-areas-jog.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-primer-react': minor +--- + +Add `no-deprecated-experimental-components` rule diff --git a/README.md b/README.md index 03a4c86b..e0bf39c5 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,4 @@ ESLint rules for Primer React - [a11y-link-in-text-block](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-link-in-text-block.md) - [a11y-remove-disable-tooltip](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-remove-disable-tooltip.md) - [a11y-use-accessible-tooltip](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/a11y-use-accessible-tooltip.md) +- [no-deprecated-experimental-components](https://github.com/primer/eslint-plugin-primer-react/blob/main/docs/rules/no-deprecated-experimental-components.md) diff --git a/docs/rules/no-deprecated-experimental-components.md b/docs/rules/no-deprecated-experimental-components.md new file mode 100644 index 00000000..f57130de --- /dev/null +++ b/docs/rules/no-deprecated-experimental-components.md @@ -0,0 +1,29 @@ +# No deprecated experimental components + +## Rule Details + +This rule discourages the usage of specific imports from `@primer/react/experimental`. + +👎 Examples of **incorrect** code for this rule + +```jsx +import {SelectPanel} from '@primer/react/experimental' + +function ExampleComponent() { + return +} +``` + +👍 Examples of **correct** code for this rule: + +You can satisfy the rule by either converting to the non-experimental version: + +```jsx +import {SelectPanel} from '@primer/react' + +function ExampleComponent() { + return +} +``` + +Or by removing usage of the component. diff --git a/src/configs/recommended.js b/src/configs/recommended.js index e29c3c00..1d4a9f33 100644 --- a/src/configs/recommended.js +++ b/src/configs/recommended.js @@ -12,6 +12,7 @@ module.exports = { rules: { 'primer-react/direct-slot-children': 'error', 'primer-react/no-system-props': 'warn', + 'primer-react/no-deprecated-experimental-components': 'warn', 'primer-react/a11y-tooltip-interactive-trigger': 'error', 'primer-react/new-color-css-vars': 'error', 'primer-react/a11y-explicit-heading': 'error', diff --git a/src/index.js b/src/index.js index 9ba22767..1cb3a431 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ module.exports = { 'direct-slot-children': require('./rules/direct-slot-children'), 'no-deprecated-entrypoints': require('./rules/no-deprecated-entrypoints'), 'no-system-props': require('./rules/no-system-props'), + 'no-deprecated-experimental-components': require('./rules/no-deprecated-experimental-components'), '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'), diff --git a/src/rules/__tests__/no-deprecated-experimental-components.test.js b/src/rules/__tests__/no-deprecated-experimental-components.test.js new file mode 100644 index 00000000..3488328a --- /dev/null +++ b/src/rules/__tests__/no-deprecated-experimental-components.test.js @@ -0,0 +1,44 @@ +'use strict' + +const {RuleTester} = require('eslint') +const rule = require('../no-deprecated-experimental-components') + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, +}) + +ruleTester.run('no-deprecated-experimental-components', rule, { + valid: [ + { + code: `import {SelectPanel} from '@primer/react'`, + }, + { + code: `import {DataTable} from '@primer/react/experimental'`, + }, + { + code: `import {DataTable, ActionBar} from '@primer/react/experimental'`, + }, + ], + invalid: [ + // Single experimental import + { + code: `import {SelectPanel} from '@primer/react/experimental'`, + errors: [ + 'SelectPanel is deprecated. Please import from the stable entrypoint (@primer/react) if available, or check https://primer.style/product/components/ for alternative components.', + ], + }, + // Multiple experimental import + { + code: `import {SelectPanel, DataTable, ActionBar} from '@primer/react/experimental'`, + errors: [ + 'SelectPanel is deprecated. Please import from the stable entrypoint (@primer/react) if available, or check https://primer.style/product/components/ for alternative components.', + ], + }, + ], +}) diff --git a/src/rules/no-deprecated-experimental-components.js b/src/rules/no-deprecated-experimental-components.js new file mode 100644 index 00000000..3746c9fa --- /dev/null +++ b/src/rules/no-deprecated-experimental-components.js @@ -0,0 +1,67 @@ +'use strict' + +const url = require('../url') + +const components = [ + { + identifier: 'SelectPanel', + entrypoint: '@primer/react/experimental', + }, +] + +const entrypoints = new Map() + +for (const component of components) { + if (!entrypoints.has(component.entrypoint)) { + entrypoints.set(component.entrypoint, new Set()) + } + entrypoints.get(component.entrypoint).add(component.identifier) +} + +/** + * @type {import('eslint').Rule.RuleModule} + */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Use a stable component from the `@primer/react` entrypoint, or check the docs for alternatives', + recommended: true, + url: url(module), + }, + fixable: true, + schema: [], + }, + create(context) { + return { + ImportDeclaration(node) { + if (!entrypoints.has(node.source.value)) { + return + } + + const entrypoint = entrypoints.get(node.source.value) + + const experimental = node.specifiers.filter(specifier => { + return entrypoint.has(specifier.imported.name) + }) + + const components = experimental.map(specifier => specifier.imported.name) + + if (experimental.length === 0) { + return + } + + if (experimental.length > 0) { + const message = `${components.join(', ')} ${ + components.length > 1 ? 'are' : 'is' + } deprecated. Please import from the stable entrypoint (@primer/react) if available, or check https://primer.style/product/components/ for alternative components.` + + context.report({ + node, + message, + }) + } + }, + } + }, +}