Chore(front): Create a custom eslint rule for Props naming (#1904)
Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
@ -8,5 +8,6 @@ module.exports = {
|
||||
"matching-state-variable": require("./src/rules/matching-state-variable"),
|
||||
"sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"),
|
||||
"styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"),
|
||||
"component-props-naming": require("./src/rules/component-props-naming"),
|
||||
},
|
||||
};
|
||||
|
||||
66
packages/eslint-plugin-twenty-ts/dist/src/rules/component-props-naming.js
vendored
Normal file
66
packages/eslint-plugin-twenty-ts/dist/src/rules/component-props-naming.js
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const utils_1 = require("@typescript-eslint/utils");
|
||||
const createRule = utils_1.ESLintUtils.RuleCreator(() => "https://docs.twenty.com/developer/frontend/style-guide#props");
|
||||
const checkPropsTypeName = (node, context, functionName) => {
|
||||
const expectedPropTypeName = `${functionName}Props`;
|
||||
if (/^[A-Z]/.test(functionName)) {
|
||||
node.params.forEach((param) => {
|
||||
var _a, _b;
|
||||
if ((param.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
||||
param.type === utils_1.AST_NODE_TYPES.Identifier) &&
|
||||
((_b = (_a = param.typeAnnotation) === null || _a === void 0 ? void 0 : _a.typeAnnotation) === null || _b === void 0 ? void 0 : _b.type) ===
|
||||
utils_1.AST_NODE_TYPES.TSTypeReference &&
|
||||
param.typeAnnotation.typeAnnotation.typeName.type ===
|
||||
utils_1.AST_NODE_TYPES.Identifier) {
|
||||
const { typeName } = param.typeAnnotation.typeAnnotation;
|
||||
const actualPropTypeName = typeName.name;
|
||||
if (actualPropTypeName !== expectedPropTypeName) {
|
||||
context.report({
|
||||
node: param,
|
||||
messageId: "invalidPropsTypeName",
|
||||
data: { expectedPropTypeName, actualPropTypeName },
|
||||
fix: (fixer) => fixer.replaceText(typeName, expectedPropTypeName),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const componentPropsNamingRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
ArrowFunctionExpression: (node) => {
|
||||
var _a, _b;
|
||||
if (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
||||
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
||||
const functionName = (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.id) === null || _b === void 0 ? void 0 : _b.name;
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
FunctionDeclaration: (node) => {
|
||||
var _a;
|
||||
if ((_a = node.id) === null || _a === void 0 ? void 0 : _a.name) {
|
||||
const functionName = node.id.name;
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "component-props-naming",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Ensure component props follow naming convention",
|
||||
recommended: "recommended",
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
invalidPropsTypeName: "Expected prop type to be '{{ expectedPropTypeName }}' but found '{{ actualPropTypeName }}'",
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
module.exports = componentPropsNamingRule;
|
||||
exports.default = componentPropsNamingRule;
|
||||
54
packages/eslint-plugin-twenty-ts/dist/src/tests/component-props-naming.spec.js
vendored
Normal file
54
packages/eslint-plugin-twenty-ts/dist/src/tests/component-props-naming.spec.js
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import componentPropsNamingRule from "../rules/component-props.naming";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("component-props-naming", componentPropsNamingRule, {
|
||||
valid: [
|
||||
{
|
||||
code: `
|
||||
type MyComponentProps = { id: number; };
|
||||
const MyComponent: React.FC<MyComponentProps> = (props) => <div>{props.id}</div>;
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: `
|
||||
type AnotherComponentProps = { message: string; };
|
||||
export const AnotherComponent: React.FC<AnotherComponentProps> = (props) => <div>{props.message}</div>;
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `
|
||||
type UnmatchedComponentProps = { id: number; };
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `
|
||||
type UnmatchedComponentProps = { message: string; };
|
||||
const DifferentComponent: React.FC<UnmatchedComponentProps> = (props) => <div>{props.message}</div>;
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -6,5 +6,6 @@ module.exports = {
|
||||
"matching-state-variable": require("./src/rules/matching-state-variable"),
|
||||
"sort-css-properties-alphabetically": require("./src/rules/sort-css-properties-alphabetically"),
|
||||
"styled-components-prefixed-with-styled": require("./src/rules/styled-components-prefixed-with-styled"),
|
||||
"component-props-naming": require("./src/rules/component-props-naming"),
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from "@typescript-eslint/utils";
|
||||
import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(
|
||||
() => "https://docs.twenty.com/developer/frontend/style-guide#props",
|
||||
);
|
||||
|
||||
const checkPropsTypeName = (
|
||||
node: TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression,
|
||||
context: Readonly<RuleContext<"invalidPropsTypeName", never[]>>,
|
||||
functionName: string,
|
||||
) => {
|
||||
const expectedPropTypeName = `${functionName}Props`;
|
||||
|
||||
if (/^[A-Z]/.test(functionName)) {
|
||||
node.params.forEach((param) => {
|
||||
if (
|
||||
(param.type === AST_NODE_TYPES.ObjectPattern ||
|
||||
param.type === AST_NODE_TYPES.Identifier) &&
|
||||
param.typeAnnotation?.typeAnnotation?.type ===
|
||||
AST_NODE_TYPES.TSTypeReference &&
|
||||
param.typeAnnotation.typeAnnotation.typeName.type ===
|
||||
AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
const { typeName } = param.typeAnnotation.typeAnnotation;
|
||||
const actualPropTypeName = typeName.name;
|
||||
if (actualPropTypeName !== expectedPropTypeName) {
|
||||
context.report({
|
||||
node: param,
|
||||
messageId: "invalidPropsTypeName",
|
||||
data: { expectedPropTypeName, actualPropTypeName },
|
||||
fix: (fixer) => fixer.replaceText(typeName, expectedPropTypeName),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const componentPropsNamingRule = createRule({
|
||||
create: (context) => {
|
||||
return {
|
||||
ArrowFunctionExpression: (node) => {
|
||||
if (
|
||||
node.parent.type === AST_NODE_TYPES.VariableDeclarator &&
|
||||
node.parent.id.type === AST_NODE_TYPES.Identifier
|
||||
) {
|
||||
const functionName = node.parent?.id?.name;
|
||||
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
FunctionDeclaration: (node) => {
|
||||
if (node.id?.name) {
|
||||
const functionName = node.id.name;
|
||||
|
||||
checkPropsTypeName(node, context, functionName);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
name: "component-props-naming",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Ensure component props follow naming convention",
|
||||
recommended: "recommended",
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
invalidPropsTypeName:
|
||||
"Expected prop type to be '{{ expectedPropTypeName }}' but found '{{ actualPropTypeName }}'",
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
});
|
||||
|
||||
module.exports = componentPropsNamingRule;
|
||||
export default componentPropsNamingRule;
|
||||
@ -0,0 +1,47 @@
|
||||
import { RuleTester } from "@typescript-eslint/rule-tester";
|
||||
|
||||
import componentPropsNamingRule from "../rules/component-props-naming";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run("component-props-naming", componentPropsNamingRule, {
|
||||
valid: [
|
||||
{
|
||||
code: "export const MyComponent= (props: MyComponentProps) => <div>{props.message}</div>;",
|
||||
},
|
||||
{
|
||||
code: "export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "export const MyComponent = (props: OwnProps) => <div>{props.message}</div>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"export const MyComponent = (props: MyComponentProps) => <div>{props.message}</div>;",
|
||||
},
|
||||
{
|
||||
code: "export const MyComponent = ({ message }: OwnProps) => <div>{message}</div>;",
|
||||
errors: [
|
||||
{
|
||||
messageId: "invalidPropsTypeName",
|
||||
},
|
||||
],
|
||||
output:
|
||||
"export const MyComponent = ({ message }: MyComponentProps) => <div>{message}</div>;",
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user