From f0db77acc7a0a107296476d16b41dcc060225fd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 Aug 2025 03:14:41 +0000 Subject: [PATCH 1/4] Initial plan From 67bed0676bdf00d047f4ae90edc896e2e0d8dcdf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 Aug 2025 03:28:36 +0000 Subject: [PATCH 2/4] Add no-deprecated-flash rule to discourage Flash component usage Co-authored-by: pksjce <417268+pksjce@users.noreply.github.com> --- docs/rules/no-deprecated-flash.md | 68 ++++++ src/index.js | 1 + .../__tests__/no-deprecated-flash.test.js | 201 ++++++++++++++++++ src/rules/no-deprecated-flash.js | 132 ++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 docs/rules/no-deprecated-flash.md create mode 100644 src/rules/__tests__/no-deprecated-flash.test.js create mode 100644 src/rules/no-deprecated-flash.js diff --git a/docs/rules/no-deprecated-flash.md b/docs/rules/no-deprecated-flash.md new file mode 100644 index 0000000..f091fe8 --- /dev/null +++ b/docs/rules/no-deprecated-flash.md @@ -0,0 +1,68 @@ +# 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 \ No newline at end of file diff --git a/src/index.js b/src/index.js index 68de6f2..aded91f 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'), }, configs: { recommended: require('./configs/recommended'), 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..23da63b --- /dev/null +++ b/src/rules/__tests__/no-deprecated-flash.test.js @@ -0,0 +1,201 @@ +'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 +}`, + output: `import {Banner} from '@primer/react/experimental' + +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 + + ) +}`, + output: `import {Banner} from '@primer/react/experimental' + +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 +
+ ) +}`, + output: `import {Button, Text} from '@primer/react' +import {Banner} from '@primer/react/experimental' + +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 +}`, + output: `import {Banner} from '@primer/react/experimental' + +function Component() { + return Just Flash +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Self-closing Flash + { + code: `import {Flash} from '@primer/react' + +function Component() { + return +}`, + output: `import {Banner} from '@primer/react/experimental' + +function Component() { + return +}`, + errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], + }, + + // Multiple Flash components + { + code: `import {Flash} from '@primer/react' + +function Component() { + return ( +
+ Warning + Error +
+ ) +}`, + output: `import {Banner} from '@primer/react/experimental' + +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 +
+ ) +}`, + output: `import {Banner} from '@primer/react/experimental' + +function Component() { + return ( +
+ Flash message + Banner message +
+ ) +}`, + 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..21a13d6 --- /dev/null +++ b/src/rules/no-deprecated-flash.js @@ -0,0 +1,132 @@ +'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), + }, + fixable: 'code', + 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', + *fix(fixer) { + // Check if there's already a Banner import from @primer/react/experimental + const program = node.parent + const existingBannerImport = program.body.find( + stmt => + stmt.type === 'ImportDeclaration' && + stmt.source.value === '@primer/react/experimental' && + stmt.specifiers.some(spec => spec.imported?.name === 'Banner'), + ) + + // Remove Flash from current import + const otherSpecifiers = node.specifiers.filter(spec => spec !== flashSpecifier) + + if (otherSpecifiers.length === 0) { + // If Flash was the only import, replace entire import with Banner + if (!existingBannerImport) { + yield fixer.replaceText(node, "import {Banner} from '@primer/react/experimental'") + } else { + // Banner import already exists, remove this import entirely including newline + const nextToken = sourceCode.getTokenAfter(node) + if (nextToken && sourceCode.getText().substring(node.range[1], nextToken.range[0]).includes('\n')) { + // Remove including the newline after + yield fixer.removeRange([node.range[0], nextToken.range[0]]) + } else { + yield fixer.remove(node) + } + } + } else { + // Remove Flash specifier and handle commas properly + const indexOfFlash = node.specifiers.indexOf(flashSpecifier) + + if (indexOfFlash === 0) { + // Flash is first, remove Flash and the trailing comma + const tokenAfter = sourceCode.getTokenAfter(flashSpecifier) + if (tokenAfter && tokenAfter.value === ',') { + yield fixer.removeRange([flashSpecifier.range[0], tokenAfter.range[1]]) + } else { + yield fixer.remove(flashSpecifier) + } + } else { + // Flash is not first, remove the preceding comma and Flash + const tokenBefore = sourceCode.getTokenBefore(flashSpecifier) + if (tokenBefore && tokenBefore.value === ',') { + yield fixer.removeRange([tokenBefore.range[0], flashSpecifier.range[1]]) + } else { + yield fixer.remove(flashSpecifier) + } + } + + // Add Banner import if it doesn't exist + if (!existingBannerImport) { + yield fixer.insertTextAfter(node, "\nimport {Banner} from '@primer/react/experimental'") + } + } + }, + }) + }, + + 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', + *fix(fixer) { + // Replace opening tag + yield fixer.replaceText(node.openingElement.name, 'Banner') + + // Replace closing tag if it exists + if (node.closingElement) { + yield fixer.replaceText(node.closingElement.name, 'Banner') + } + }, + }) + }, + } + }, +} From 427e5b24e31045f1dc5dbb0036be03f9b81ca2cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:54:36 +0000 Subject: [PATCH 3/4] Fix review comments: remove auto-fixing, remove sx prop, add experimental import test Co-authored-by: pksjce <417268+pksjce@users.noreply.github.com> --- docs/rules/no-deprecated-flash.md | 15 ++--- .../__tests__/no-deprecated-flash.test.js | 63 +++--------------- src/rules/no-deprecated-flash.js | 65 ------------------- 3 files changed, 13 insertions(+), 130 deletions(-) diff --git a/docs/rules/no-deprecated-flash.md b/docs/rules/no-deprecated-flash.md index f091fe8..1bee09e 100644 --- a/docs/rules/no-deprecated-flash.md +++ b/docs/rules/no-deprecated-flash.md @@ -21,11 +21,7 @@ import {Flash} from '@primer/react' function ExampleComponent() { return ( - + Banner content ) @@ -47,11 +43,7 @@ import {Banner} from '@primer/react/experimental' function ExampleComponent() { return ( - + Banner content ) @@ -61,8 +53,9 @@ function ExampleComponent() { ## 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 \ No newline at end of file +- Avoid duplicate Banner imports when they already exist diff --git a/src/rules/__tests__/no-deprecated-flash.test.js b/src/rules/__tests__/no-deprecated-flash.test.js index 23da63b..2a41b2b 100644 --- a/src/rules/__tests__/no-deprecated-flash.test.js +++ b/src/rules/__tests__/no-deprecated-flash.test.js @@ -49,11 +49,6 @@ function Component() { function Component() { return Banner content -}`, - output: `import {Banner} from '@primer/react/experimental' - -function Component() { - return Banner content }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -66,25 +61,11 @@ function Component() { return ( Banner content ) -}`, - output: `import {Banner} from '@primer/react/experimental' - -function Component() { - return ( - - Banner content - - ) }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -101,18 +82,6 @@ function Component() { Some text ) -}`, - output: `import {Button, Text} from '@primer/react' -import {Banner} from '@primer/react/experimental' - -function Component() { - return ( -
- - Error message - Some text -
- ) }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -123,11 +92,6 @@ function Component() { function Component() { return Just Flash -}`, - output: `import {Banner} from '@primer/react/experimental' - -function Component() { - return Just Flash }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -138,11 +102,6 @@ function Component() { function Component() { return -}`, - output: `import {Banner} from '@primer/react/experimental' - -function Component() { - return }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -158,16 +117,6 @@ function Component() { Error ) -}`, - output: `import {Banner} from '@primer/react/experimental' - -function Component() { - return ( -
- Warning - Error -
- ) }`, errors: [{messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}, {messageId: 'flashDeprecated'}], }, @@ -185,13 +134,19 @@ function Component() { ) }`, - output: `import {Banner} from '@primer/react/experimental' + 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 - Banner message + Flash message + Tooltip content
) }`, diff --git a/src/rules/no-deprecated-flash.js b/src/rules/no-deprecated-flash.js index 21a13d6..61d3b2f 100644 --- a/src/rules/no-deprecated-flash.js +++ b/src/rules/no-deprecated-flash.js @@ -15,7 +15,6 @@ module.exports = { recommended: true, url: url(module), }, - fixable: 'code', schema: [], messages: { flashDeprecated: 'Flash component is deprecated. Use Banner from @primer/react/experimental instead.', @@ -42,61 +41,6 @@ module.exports = { context.report({ node: flashSpecifier, messageId: 'flashDeprecated', - *fix(fixer) { - // Check if there's already a Banner import from @primer/react/experimental - const program = node.parent - const existingBannerImport = program.body.find( - stmt => - stmt.type === 'ImportDeclaration' && - stmt.source.value === '@primer/react/experimental' && - stmt.specifiers.some(spec => spec.imported?.name === 'Banner'), - ) - - // Remove Flash from current import - const otherSpecifiers = node.specifiers.filter(spec => spec !== flashSpecifier) - - if (otherSpecifiers.length === 0) { - // If Flash was the only import, replace entire import with Banner - if (!existingBannerImport) { - yield fixer.replaceText(node, "import {Banner} from '@primer/react/experimental'") - } else { - // Banner import already exists, remove this import entirely including newline - const nextToken = sourceCode.getTokenAfter(node) - if (nextToken && sourceCode.getText().substring(node.range[1], nextToken.range[0]).includes('\n')) { - // Remove including the newline after - yield fixer.removeRange([node.range[0], nextToken.range[0]]) - } else { - yield fixer.remove(node) - } - } - } else { - // Remove Flash specifier and handle commas properly - const indexOfFlash = node.specifiers.indexOf(flashSpecifier) - - if (indexOfFlash === 0) { - // Flash is first, remove Flash and the trailing comma - const tokenAfter = sourceCode.getTokenAfter(flashSpecifier) - if (tokenAfter && tokenAfter.value === ',') { - yield fixer.removeRange([flashSpecifier.range[0], tokenAfter.range[1]]) - } else { - yield fixer.remove(flashSpecifier) - } - } else { - // Flash is not first, remove the preceding comma and Flash - const tokenBefore = sourceCode.getTokenBefore(flashSpecifier) - if (tokenBefore && tokenBefore.value === ',') { - yield fixer.removeRange([tokenBefore.range[0], flashSpecifier.range[1]]) - } else { - yield fixer.remove(flashSpecifier) - } - } - - // Add Banner import if it doesn't exist - if (!existingBannerImport) { - yield fixer.insertTextAfter(node, "\nimport {Banner} from '@primer/react/experimental'") - } - } - }, }) }, @@ -116,15 +60,6 @@ module.exports = { context.report({ node: node.openingElement.name, messageId: 'flashDeprecated', - *fix(fixer) { - // Replace opening tag - yield fixer.replaceText(node.openingElement.name, 'Banner') - - // Replace closing tag if it exists - if (node.closingElement) { - yield fixer.replaceText(node.closingElement.name, 'Banner') - } - }, }) }, } From bd991912b4b7344995191a526384651aa7ae7ffd Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Tue, 12 Aug 2025 12:54:05 +1000 Subject: [PATCH 4/4] Create chilled-masks-lay.md --- .changeset/chilled-masks-lay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-masks-lay.md 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