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