diff --git a/README.md b/README.md index 0a32a16..3fbc920 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,26 @@ # @fullstory/eslint-plugin-annotate-react -An ESLint plugin for annotating React components. +An ESLint plugin for adding 'data-attribute' to React components. The purpose of this plugin is to automatically +make css selectors. Here is an example + +``` +const myDiv = () => ( +
+); `; +``` + +This plugin will autofix and add data-component to the div + +``` +const myDiv = () => ( +
+); `; +``` + +This plugin is intended to not be too opinionated. In general the approach is to suggest to the developer to add 'data-attribute' when there is an obvious approach, but in questionable cases, the plugin will tend towards being quiet. + +- When there is a [fragment](https://reactjs.org/docs/fragments.html) this plugin won't add data-attribute +- Where there are multiple return elements this plugin won't add data-attribute ## Installation @@ -36,6 +56,14 @@ Then configure the rules you want to use under the rules section. } ``` +## Maintaining this plugin + +Tests can be ran using + +``` +npm run test +``` + ## Supported Rules ### @fullstory/annotate-react/data-component diff --git a/index.js b/index.js index b3377b8..ca17399 100644 --- a/index.js +++ b/index.js @@ -138,49 +138,51 @@ const rules = { return flag; }); - const [componentNode] = componentNodes; + componentNodes.forEach((componentNode) => { + const componentName = + componentNode?.id?.name ?? + componentNode?.declarations?.map( + (declaration) => declaration?.id?.name, + ); + + let fixNode = null; + + traverseTree( + getReturnStatement(componentNode), + visitorKeys, + (current) => { + if (isSubtreeDone(current)) { + throw DONE_WITH_SUBTREE; + } else if (isTreeDone(current, excludeComponentNames)) { + fixNode = current.openingElement; - const componentName = - componentNode?.id?.name ?? - componentNode?.declarations?.map( - (declaration) => declaration?.id?.name, + throw DONE_WITH_TREE; + } + }, ); - let fixNode = null; - - traverseTree( - getReturnStatement(componentNode), - visitorKeys, - (current) => { - if (isSubtreeDone(current)) { - throw DONE_WITH_SUBTREE; - } else if (isTreeDone(current, excludeComponentNames)) { - fixNode = current.openingElement; - - throw DONE_WITH_TREE; - } - }, - ); - - if (Boolean(componentName)) { - context.report({ - node: fixNode, - message: `${ - Array.isArray(componentName) ? componentName[0] : componentName - } is missing the data-component attribute for the top-level element.`, - fix: (fixer) => - fixer.insertTextAfterRange( - Boolean(fixNode.typeParameters) - ? fixNode.typeParameters.range - : fixNode.name.range, - `\ndata-component="${ - Array.isArray(componentName) - ? componentName[0] - : componentName - }"`, - ), - }); - } + if (Boolean(componentName)) { + context.report({ + node: fixNode, + message: `${ + Array.isArray(componentName) + ? componentName[0] + : componentName + } is missing the data-component attribute for the top-level element.`, + fix: (fixer) => + fixer.insertTextAfterRange( + Boolean(fixNode.typeParameters) + ? fixNode.typeParameters.range + : fixNode.name.range, + ` data-component="${ + Array.isArray(componentName) + ? componentName[0] + : componentName + }"`, + ), + }); + } + }); }, }; }, diff --git a/test.js b/test.js index 3b3d148..326119a 100644 --- a/test.js +++ b/test.js @@ -6,8 +6,90 @@ const { join } = require('path'); // Test File Definitions //------------------------------------------------------------------------------ +const singleComponent = `const temp = () => { + ; +};`; +const singleComponentError = `const temp = () => { + ; +};`; + +const genericTest = ` +const yAxis = (xScale, xTicks) => ( + data-component="yAxis" width={1} height={1} xScale={xScale} xTicks={xTicks}> + 123 + +); `; +const genericTestError = ` +const yAxis = (xScale, xTicks) => ( + width={1} height={1} xScale={xScale} xTicks={xTicks}> + 123 + +); `; + +const renamingComponentDoesntError = ` +const myDiv = () => ( +
+); `; + +const nestedComponentsError = /* tsx */ ` +export const FooChart: React.FC = props => { + const [spacing, setSpacing] = useState({ top: 8, right: 38, bottom: 50, left: 50 }); + + return ( + + {({ height, width, overlay }) => ( + + )} + + ); +};`; + +const nestedComponents = /* tsx */ ` +export const FooChart: React.FC = props => { + const [spacing, setSpacing] = useState({ top: 8, right: 38, bottom: 50, left: 50 }); + + return ( + + {({ height, width, overlay }) => ( + + )} + + ); +};`; + +const multipleComponentsErrors = ` + const Component1 = () =>
; + const Component2 = () => ; + `; +const multipleComponents = ` + const Component1 = () =>
; + const Component2 = () => ; + `; + +const fragmentsWontUpdate = `const Component = () => { + <> + + + + +};`; + const tests = { - 'data-component-tsx': { + 'data-component': { // Require the actual rule definition rule: require('./index').rules['data-component'], @@ -31,20 +113,58 @@ const tests = { // Define the test cases testCases: { - valid: [], + valid: [ + { + code: singleComponent, + }, + { + code: genericTest, + }, + { + code: renamingComponentDoesntError, + }, + { + code: nestedComponents, + }, + { + code: multipleComponents, + }, + { + // Multiple return paths should not trigger the eslint warning + code: fragmentsWontUpdate, + }, + ], invalid: [ { - code: /* tsx */ `const temp = () => { - ; - };`, - output: /* tsx */ `const temp = () => { - ; - };`, + code: singleComponentError, + output: singleComponent, errors: [ 'temp is missing the data-component attribute for the top-level element.', ], }, + { + code: genericTestError, + output: genericTest, + errors: [ + 'yAxis is missing the data-component attribute for the top-level element.', + ], + }, + { + code: nestedComponentsError, + output: nestedComponents, + errors: [ + 'FooChart is missing the data-component attribute for the top-level element.', + ], + }, + { + // Multiple components with errors + code: multipleComponentsErrors, + output: multipleComponents, + errors: [ + 'Component1 is missing the data-component attribute for the top-level element.', + 'Component2 is missing the data-component attribute for the top-level element.', + ], + }, ], }, },