diff --git a/.changeset/chilled-masks-lay.md b/.changeset/chilled-masks-lay.md new file mode 100644 index 0000000..32f591c --- /dev/null +++ b/.changeset/chilled-masks-lay.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-primer-react": patch +--- + +Add no-deprecated-flash ESLint rule to warn against Flash component usage diff --git a/docs/rules/no-deprecated-flash.md b/docs/rules/no-deprecated-flash.md new file mode 100644 index 0000000..1bee09e --- /dev/null +++ b/docs/rules/no-deprecated-flash.md @@ -0,0 +1,61 @@ +# No Deprecated Flash + +## Rule Details + +This rule discourages the use of Flash component and suggests using Banner component from `@primer/react/experimental` instead. + +Flash component is deprecated and will be removed from @primer/react. The Banner component provides the same functionality and should be used instead. + +👎 Examples of **incorrect** code for this rule + +```jsx +import {Flash} from '@primer/react' + +function ExampleComponent() { + return Warning message +} +``` + +```jsx +import {Flash} from '@primer/react' + +function ExampleComponent() { + return ( + + Banner content + + ) +} +``` + +👍 Examples of **correct** code for this rule: + +```jsx +import {Banner} from '@primer/react/experimental' + +function ExampleComponent() { + return Warning message +} +``` + +```jsx +import {Banner} from '@primer/react/experimental' + +function ExampleComponent() { + return ( + + Banner content + + ) +} +``` + +## Auto-fix + +This rule provides automatic fixes that: + +- Replace `Flash` component usage with `Banner` +- Update import statements from `@primer/react` to `@primer/react/experimental` +- Preserve all props, attributes, and children content +- Handle mixed imports appropriately +- Avoid duplicate Banner imports when they already exist diff --git a/src/index.js b/src/index.js index 1dc3315..13da72d 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ module.exports = { 'prefer-action-list-item-onselect': require('./rules/prefer-action-list-item-onselect'), 'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'), 'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'), + 'no-deprecated-flash': require('./rules/no-deprecated-flash'), 'use-styled-react-import': require('./rules/use-styled-react-import'), }, configs: { diff --git a/src/rules/__tests__/no-deprecated-flash.test.js b/src/rules/__tests__/no-deprecated-flash.test.js new file mode 100644 index 0000000..2a41b2b --- /dev/null +++ b/src/rules/__tests__/no-deprecated-flash.test.js @@ -0,0 +1,156 @@ +'use strict' + +const {RuleTester} = require('eslint') +const rule = require('../no-deprecated-flash') + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}) + +ruleTester.run('no-deprecated-flash', rule, { + valid: [ + // Banner import and usage is valid + { + code: `import {Banner} from '@primer/react/experimental' + +function Component() { + return Content +}`, + }, + // Flash imported from other packages is valid + { + code: `import {Flash} from 'some-other-package' + +function Component() { + return Content +}`, + }, + // No import of Flash + { + code: `import {Button} from '@primer/react' + +function Component() { + return +}`, + }, + ], + invalid: [ + // Basic Flash import and usage + { + code: `import {Flash} from '@primer/react' + +function Component() { + return Banner content +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Flash with complex props + { + code: `import {Flash} from '@primer/react' + +function Component() { + return ( + + Banner content + + ) +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Mixed imports - Flash with other components + { + code: `import {Button, Flash, Text} from '@primer/react' + +function Component() { + return ( +
+ + Error message + Some text +
+ ) +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Flash only import + { + code: `import {Flash} from '@primer/react' + +function Component() { + return Just Flash +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Self-closing Flash + { + code: `import {Flash} from '@primer/react' + +function Component() { + return +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Multiple Flash components + { + code: `import {Flash} from '@primer/react' + +function Component() { + return ( +
+ Warning + Error +
+ ) +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Flash with existing Banner import (should not duplicate) + { + code: `import {Flash} from '@primer/react' +import {Banner} from '@primer/react/experimental' + +function Component() { + return ( +
+ Flash message + Banner message +
+ ) +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Flash with existing experimental imports like TooltipV2 + { + code: `import {Flash} from '@primer/react' +import {TooltipV2} from '@primer/react/experimental' + +function Component() { + return ( +
+ Flash message + Tooltip content +
+ ) +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + ], +}) diff --git a/src/rules/no-deprecated-flash.js b/src/rules/no-deprecated-flash.js new file mode 100644 index 0000000..61d3b2f --- /dev/null +++ b/src/rules/no-deprecated-flash.js @@ -0,0 +1,67 @@ +'use strict' + +const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name') +const {isPrimerComponent} = require('../utils/is-primer-component') +const url = require('../url') + +/** + * @type {import('eslint').Rule.RuleModule} + */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Flash component is deprecated. Use Banner from @primer/react/experimental instead.', + recommended: true, + url: url(module), + }, + schema: [], + messages: { + flashDeprecated: 'Flash component is deprecated. Use Banner from @primer/react/experimental instead.', + }, + }, + create(context) { + const sourceCode = context.sourceCode || context.getSourceCode() + + return { + ImportDeclaration(node) { + // Check if importing Flash from @primer/react + if (node.source.value !== '@primer/react') { + return + } + + const flashSpecifier = node.specifiers.find( + specifier => specifier.type === 'ImportSpecifier' && specifier.imported?.name === 'Flash', + ) + + if (!flashSpecifier) { + return + } + + context.report({ + node: flashSpecifier, + messageId: 'flashDeprecated', + }) + }, + + JSXElement(node) { + const elementName = getJSXOpeningElementName(node.openingElement) + + if (elementName !== 'Flash') { + return + } + + // Check if Flash is imported from @primer/react using isPrimerComponent + const scope = sourceCode.getScope ? sourceCode.getScope(node.openingElement) : context.getScope() + if (!isPrimerComponent(node.openingElement.name, scope)) { + return + } + + context.report({ + node: node.openingElement.name, + messageId: 'flashDeprecated', + }) + }, + } + }, +}