Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 = () => (
<div/>
); `;
```

This plugin will autofix and add data-component to the div

```
const myDiv = () => (
<div data-component="temp"/>
); `;
```

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

Expand Down Expand Up @@ -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
Expand Down
82 changes: 42 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}"`,
),
});
}
});
},
};
},
Expand Down
138 changes: 129 additions & 9 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,90 @@ const { join } = require('path');
// Test File Definitions
//------------------------------------------------------------------------------

const singleComponent = `const temp = () => {
<Icon data-component="temp" name="metric" size={24} />;
};`;
const singleComponentError = `const temp = () => {
<Icon name="metric" size={24} />;
};`;

const genericTest = `
const yAxis = (xScale, xTicks) => (
<BottomAxis<Date> data-component="yAxis" width={1} height={1} xScale={xScale} xTicks={xTicks}>
123
</BottomAxis>
); `;
const genericTestError = `
const yAxis = (xScale, xTicks) => (
<BottomAxis<Date> width={1} height={1} xScale={xScale} xTicks={xTicks}>
123
</BottomAxis>
); `;

const renamingComponentDoesntError = `
const myDiv = () => (
<div data-component="temp"/>
); `;

const nestedComponentsError = /* tsx */ `
export const FooChart: React.FC<FooChartProps> = props => {
const [spacing, setSpacing] = useState({ top: 8, right: 38, bottom: 50, left: 50 });

return (
<Chart height={400} spacing={spacing}>
{({ height, width, overlay }) => (
<InnerFooChart
width={width}
height={height}
overlay={overlay}
spacing={spacing}
setSpacing={setSpacing}
{...props}
/>
)}
</Chart>
);
};`;

const nestedComponents = /* tsx */ `
export const FooChart: React.FC<FooChartProps> = props => {
const [spacing, setSpacing] = useState({ top: 8, right: 38, bottom: 50, left: 50 });

return (
<Chart data-component="FooChart" height={400} spacing={spacing}>
{({ height, width, overlay }) => (
<InnerFooChart
width={width}
height={height}
overlay={overlay}
spacing={spacing}
setSpacing={setSpacing}
{...props}
/>
)}
</Chart>
);
};`;

const multipleComponentsErrors = `
const Component1 = () => <div />;
const Component2 = () => <span />;
`;
const multipleComponents = `
const Component1 = () => <div data-component="Component1" />;
const Component2 = () => <span data-component="Component2" />;
`;

const fragmentsWontUpdate = `const Component = () => {
<>
<a/>
<a/>
<a/>
</>
};`;

const tests = {
'data-component-tsx': {
'data-component': {
// Require the actual rule definition
rule: require('./index').rules['data-component'],

Expand All @@ -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 = () => {
<Icon name="metrics-insights/insight-icon" size={24} />;
};`,
output: /* tsx */ `const temp = () => {
<Icon
data-component="temp" name="metrics-insights/insight-icon" size={24} />;
};`,
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.',
],
},
],
},
},
Expand Down