diff --git a/front/.eslintrc.js b/front/.eslintrc.js index d72767752..c7a5c56fd 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -10,7 +10,6 @@ module.exports = { 'unused-imports', 'simple-import-sort', 'prefer-arrow', - 'twenty', 'twenty-ts', ], extends: [ @@ -61,11 +60,11 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'off', 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', - 'twenty/sort-css-properties-alphabetically': 'error', - 'twenty/no-hardcoded-colors': 'error', - 'twenty/styled-components-prefixed-with-styled': 'error', - 'twenty/matching-state-variable': 'error', 'twenty-ts/effect-components': 'error', + 'twenty-ts/no-hardcoded-colors': 'error', + 'twenty-ts/matching-state-variable': 'error', + 'twenty-ts/sort-css-properties-alphabetically': 'error', + 'twenty-ts/styled-components-prefixed-with-styled': 'error', 'func-style':['error', 'declaration', { 'allowArrowFunctions': true }], "@typescript-eslint/no-unused-vars": "off", "no-unused-vars": "off", diff --git a/packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts b/packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts new file mode 100644 index 000000000..0423b3ad9 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/rules/matching-state-variable.ts @@ -0,0 +1,141 @@ +import { + TSESTree, + ESLintUtils, + AST_NODE_TYPES, +} from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); + +const matchingStateVariableRule = createRule({ + create: (context) => { + return { + VariableDeclarator(node: TSESTree.VariableDeclarator) { + if ( + node?.init?.type === AST_NODE_TYPES.CallExpression && + node.init.callee.type === AST_NODE_TYPES.Identifier && + [ + "useRecoilState", + "useRecoilFamilyState", + "useRecoilSelector", + "useRecoilScopedState", + "useRecoilScopedFamilyState", + "useRecoilScopedSelector", + "useRecoilValue", + ].includes(node.init.callee.name) + ) { + const stateNameBase = + node.init.arguments?.[0]?.type === AST_NODE_TYPES.Identifier + ? node.init.arguments[0].name + : undefined; + + if (!stateNameBase) { + return; + } + + let expectedVariableNameBase = stateNameBase.replace( + /(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/, + "" + ); + + if (node.id.type === AST_NODE_TYPES.Identifier) { + const actualVariableName = node.id.name; + if (actualVariableName !== expectedVariableNameBase) { + context.report({ + node, + messageId: "invalidVariableName", + data: { + actual: actualVariableName, + expected: expectedVariableNameBase, + callee: node.init.callee.name, + }, + fix(fixer) { + return fixer.replaceText(node.id, expectedVariableNameBase); + }, + }); + } + return; + } + + if (node.id.type === AST_NODE_TYPES.ArrayPattern) { + const actualVariableName = + node.id.elements?.[0]?.type === AST_NODE_TYPES.Identifier + ? node.id.elements[0].name + : undefined; + if ( + actualVariableName && + actualVariableName !== expectedVariableNameBase + ) { + context.report({ + node, + messageId: "invalidVariableName", + data: { + actual: actualVariableName, + expected: expectedVariableNameBase, + callee: node.init.callee.name, + }, + fix(fixer) { + if (node.id.type === AST_NODE_TYPES.ArrayPattern) { + return fixer.replaceText( + node.id.elements[0] as TSESTree.Node, + expectedVariableNameBase + ); + } + return null; + }, + }); + return; + } + + if (node.id.elements?.[1]?.type === AST_NODE_TYPES.Identifier) { + const actualSetterName = node.id.elements[1].name; + const expectedSetterName = `set${expectedVariableNameBase + .charAt(0) + .toUpperCase()}${expectedVariableNameBase.slice(1)}`; + + if (actualSetterName !== expectedSetterName) { + context.report({ + node, + messageId: "invalidSetterName", + data: { + actual: actualSetterName, + expected: expectedSetterName, + }, + fix(fixer) { + if (node.id.type === AST_NODE_TYPES.ArrayPattern) { + return fixer.replaceText( + node.id.elements[1]!, + expectedSetterName + ); + } + return null; + }, + }); + } + } + } + } + }, + }; + }, + name: "recoil-hook-naming", + meta: { + type: "problem", + docs: { + description: "Ensure recoil value and setter are named after their atom name", + recommended: "recommended", + }, + fixable: "code", + schema: [], + messages: { + invalidVariableName: + "Invalid usage of {{hookName}}: the value should be named '{{expectedName}}' but found '{{actualName}}'.", + invalidSetterName: + "Invalid usage of {{hookName}}: Expected setter '{{expectedName}}' but found '{{actualName}}'.", + }, + }, + defaultOptions: [], +}); + +module.exports = matchingStateVariableRule; + +export default matchingStateVariableRule; \ No newline at end of file diff --git a/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts b/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts new file mode 100644 index 000000000..1a1366d00 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/rules/no-hardcoded-colors.ts @@ -0,0 +1,49 @@ +import { TSESTree, ESLintUtils } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); + +const noHardcodedColorsRule = createRule({ + create(context) { + return { + TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) { + if (context.getFilename().endsWith("themes.ts")) { + return; + } + + node.quasi.quasis.forEach((quasi) => { + const colorRegex = + /(?:rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(,\s*\d+\.?\d*)?\))|(?:#[0-9a-fA-F]{6})/i; + + if (colorRegex.test(quasi.value.raw)) { + context.report({ + node, + messageId: "avoidHardcodedColors", + data: { + color: quasi.value.raw, + }, + }); + } + }); + }, + }; + }, + name: "avoid-hardcoded-colors", + meta: { + type: "suggestion", + docs: { + description: "Avoid hardcoded RGBA or Hex colors, use colors from the theme file.", + recommended: "recommended", + }, + schema: [], + fixable: "code", + messages: { + avoidHardcodedColors: + "Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.", + }, + }, + defaultOptions: [] +}); + +module.exports = noHardcodedColorsRule; + +export default noHardcodedColorsRule; \ No newline at end of file diff --git a/packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts b/packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts new file mode 100644 index 000000000..45e0ac3b1 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/rules/sort-css-properties-alphabetically.ts @@ -0,0 +1,239 @@ +import { TSESTree, ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils"; +import postcss from "postcss"; +import { + RuleFixer, + SourceCode, +} from "@typescript-eslint/utils/ts-eslint"; +const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); + +const isStyledTagname = ( + node: TSESTree.TaggedTemplateExpression +): boolean => + (node.tag.type === "Identifier" && node.tag.name === "css") || + (node.tag.type === "MemberExpression" && + node.tag.object.type === "Identifier" && + node.tag.object.name === "styled") || + (node.tag.type === "CallExpression" && + (node.tag.callee.type === "Identifier" && node.tag.callee.name === "styled") || + // @ts-ignore + (node.tag.callee.object && + // @ts-ignore + ((node.tag.callee.object.type === "CallExpression" && + // @ts-ignore + node.tag.callee.object.callee.type === "Identifier" && + // @ts-ignore + node.tag.callee.object.callee.name === "styled") || + // @ts-ignore + (node.tag.callee.object.type === "MemberExpression" && + // @ts-ignore + node.tag.callee.object.object.type === "Identifier" && + // @ts-ignore + node.tag.callee.object.object.name === "styled"))) +); + +/** + * An atomic rule is a rule without nested rules. + */ +const isValidAtomicRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcss.NodeSource } => { + const decls = rule.nodes.filter((node: postcss.Node) => node.type === "decl"); + if (decls.length === 0) { + return { isValid: true }; + } + + for (let i = 1; i < decls.length; i++) { + const current = decls[i].prop; + const prev = decls[i - 1].prop; + if (current < prev) { + const loc = { + start: { + line: decls[i - 1].source?.start?.line || 0, + column: (decls[i - 1].source?.start?.column || 0) - 1, + }, + end: { + line: decls[i].source?.end?.line || 0, + column: (decls[i].source?.end?.column || 0) - 1, + }, + }; + + return { isValid: false, loc }; + } + } + + return { isValid: true }; +}; + +const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcss.NodeSource } => { + // check each rule recursively + const { isValid, loc } = rule.nodes.reduce( + (map:any, node: postcss.Node) => { + return node.type === "rule" ? isValidRule(node) : map; + }, + { isValid: true } + ); + + // if there is any invalid rule, return result + if (!isValid) { + return { isValid, loc }; + } + + // check declarations + return isValidAtomicRule(rule); +}; + +const getNodeStyles = ( + node: TSESTree.TaggedTemplateExpression +): string => { + const [firstQuasi, ...quasis] = node.quasi.quasis; + // remove line break added to the first quasi + const lineBreakCount = (node.quasi.loc?.start?.line || 1) - 1; + let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat( + (node.quasi.loc?.start?.column || 0) + 1 + )}${firstQuasi.value.raw}`; + + // replace expression by spaces and line breaks + quasis.forEach(({ value, loc }, idx) => { + const prevLoc = idx === 0 ? node.quasi.loc : quasis[idx - 1].loc; + const lineBreaksCount = (loc?.start?.line || 1) - (prevLoc?.end?.line || 0); + const spacesCount = + loc?.start?.line === prevLoc?.end?.line + ? (loc?.start?.column || 0) - (prevLoc?.end?.column || 0) + 2 + : (loc?.start?.column || 0) + 1; + styles = `${styles}${" "}${"\n".repeat(lineBreaksCount)}${" ".repeat( + spacesCount + )}${value.raw}`; + }); + + return styles; +}; + +const fix = ({ + rule, + fixer, + src, +}: { + rule: postcss.Rule; + fixer: RuleFixer; + src: SourceCode; +}) => { + let fixings: ReturnType[] = []; + + // concat fixings recursively + rule.nodes.forEach((node: postcss.Node) => { + if (node.type === "rule") { + fixings = [...fixings, ...fix({ rule: node, fixer, src })]; + } + }); + + const declarations = rule.nodes.filter((node: postcss.Node) => node.type === "decl"); + const sortedDeclarations = sortDeclarations(declarations); + + declarations.forEach((decl: postcss.Declaration, idx: number) => { + if (!areSameDeclarations(decl, sortedDeclarations[idx])) { + try { + const range = getDeclRange({ decl, src }); + const sortedDeclText = getDeclText({ + decl: sortedDeclarations[idx], + src, + }); + + fixings.push(fixer.removeRange([range.startIdx, range.endIdx + 1])); + fixings.push( + fixer.insertTextAfterRange( + [range.startIdx, range.startIdx], + sortedDeclText + ) + ); + } catch (e) { + console.log(e); + } + } + }); + return fixings; +}; + +const areSameDeclarations = ( + a: postcss.Declaration, + b: postcss.Declaration +): boolean => + a.source?.start.line === b.source?.start.line && + a.source?.start.column === b.source?.start.column; + +const getDeclRange = ({ decl, src }: { decl: postcss.Declaration; src: SourceCode }) => { + const loc = { + start: { + line: decl.source?.start?.line || 1, + column: (decl.source?.start?.column || 0) - 1, + }, + end: { + line: decl.source?.end?.line || 1, + column: (decl.source?.end?.column || 0) - 1, + }, + }; + + const startIdx = src.getIndexFromLoc(loc.start); + const endIdx = src.getIndexFromLoc(loc.end); + return { startIdx, endIdx }; +}; + +const getDeclText = ({ decl, src }: { decl: postcss.Declaration; src: SourceCode }) => { + const { startIdx, endIdx } = getDeclRange({ decl, src }); + return src.getText().substring(startIdx, endIdx + 1); +}; + +const sortDeclarations = ( + declarations: postcss.Declaration[] +): postcss.Declaration[] => + declarations + .slice() + .sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1)); + + const sortCssPropertiesAlphabeticallyRule = createRule({ + create(context) { + return { + TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression) { + if (isStyledTagname(node)) { + try { + const root = postcss.parse(getNodeStyles(node)); + + const { isValid, loc } = isValidRule(root); + + if (!isValid) { + return context.report({ + node, + messageId: "sort-css-properties-alphabetically", + loc, + fix: (fixer) => + fix({ + // @ts-ignore + rule: root, + fixer, + src: context.getSourceCode(), + }), + }); + } + } catch (e) { + return true; + } + } + }, + }; + }, + name: "sort-css-properties-alphabetically", + meta: { + docs: { + description: "Styles are sorted alphabetically.", + recommended: "recommended", + }, + messages: { + "sort-css-properties-alphabetically": + "Declarations should be sorted alphabetically.", + }, + type: "suggestion", + schema: [], + fixable: "code", + }, + defaultOptions: [], + }); + +module.exports = sortCssPropertiesAlphabeticallyRule; +export default sortCssPropertiesAlphabeticallyRule; diff --git a/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts b/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts new file mode 100644 index 000000000..60c2e43f2 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/rules/styled-components-prefixed-with-styled.ts @@ -0,0 +1,51 @@ +import { TSESTree, ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils"; + +const createRule = ESLintUtils.RuleCreator((name) => `https://docs.twenty.com`); + +const styledComponentsPrefixedWithStyledRule = createRule({ + create(context) { + return { + VariableDeclarator: (node: TSESTree.VariableDeclarator) => { + const templateExpr = node.init + if (templateExpr?.type !== AST_NODE_TYPES.TaggedTemplateExpression) { + return; + } + const tag = templateExpr.tag + const tagged = tag.type === AST_NODE_TYPES.MemberExpression ? tag.object + : tag.type === AST_NODE_TYPES.CallExpression ? tag.callee + : null + if (tagged?.type === AST_NODE_TYPES.Identifier && tagged.name === 'styled') { + const variable = node.id as TSESTree.Identifier; + if (variable.name.startsWith('Styled')) { + return; + } + context.report({ + node, + messageId: 'noStyledPrefix', + data: { + componentName: variable.name + } + }); + } + }, + } + }, + name: 'styled-components-prefixed-with-styled', + meta: { + type: 'suggestion', + docs: { + description: 'Warn when StyledComponents are not prefixed with Styled', + recommended: "recommended" + }, + messages: { + noStyledPrefix: '{{componentName}} is a StyledComponent and is not prefixed with Styled.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [] +}); + +module.exports = styledComponentsPrefixedWithStyledRule; + +export default styledComponentsPrefixedWithStyledRule; \ No newline at end of file diff --git a/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts new file mode 100644 index 000000000..5a07b6cec --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/tests/matching-state-variable.spec.ts @@ -0,0 +1,47 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import matchingStateVariableRule from "../rules/matching-state-variable"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('matching-state-variable', matchingStateVariableRule, { + valid: [ + { + code: 'const myState = useRecoilValue(someAtom);', + }, + { + code: 'const [myState, setMyState] = useRecoilState(someAtom);', + }, + ], + invalid: [ + { + code: 'const myValue = useRecoilValue(someAtom);', + errors: [ + { + messageId: 'invalidVariableName', + }, + ], + output: 'const some = useRecoilValue(someAtom);', + }, + { + code: 'const [myValue, setMyValue] = useRecoilState(someAtom);', + errors: [ + { + messageId: 'invalidVariableName', + }, + { + messageId: 'invalidSetterName', + }, + ], + output: 'const [some, setSome] = useRecoilState(someAtom);', + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts new file mode 100644 index 000000000..10013637e --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/tests/no-hardcoded-colors.spec.ts @@ -0,0 +1,56 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import noHardcodedColorsRule from "../rules/no-hardcoded-colors"; + + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run("no-hardcoded-colors", noHardcodedColorsRule, { + valid: [ + { + code: 'const color = theme.primaryColor;', + filename: 'example.ts', + }, + { + code: 'const color = "#FFFFFF";', + filename: 'example.ts', + }, + ], + invalid: [ + { + code: 'const color = "#FF0000";', + filename: 'example.ts', + errors: [ + { + messageId: 'avoidHardcodedColors', + }, + ], + }, + { + code: 'const color = "rgba(255, 0, 0, 0.5)";', + filename: 'example.ts', + errors: [ + { + messageId: 'avoidHardcodedColors', + }, + ], + }, + { + code: 'const color = "#123456";', + filename: 'themes.ts', + errors: [ + { + messageId: 'avoidHardcodedColors', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts new file mode 100644 index 000000000..3d5f43534 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/tests/sort-css-properties-alphabetically.spec.ts @@ -0,0 +1,60 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import sortCssPropertiesAlphabeticallyRule from "../rules/sort-css-properties-alphabetically"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run("sort-css-properties-alphabetically", sortCssPropertiesAlphabeticallyRule, { + valid: [ + { + code: 'const style = css`color: red;`;', + filename: 'example.ts', + }, + { + code: 'const style = styled.div`background-color: $bgColor;`;', + filename: 'example.ts', + }, + // Add more valid cases as needed + ], + invalid: [ + { + code: 'const style = css`color: #FF0000;`;', + filename: 'example.ts', + errors: [ + { + messageId: "sort-css-properties-alphabetically", + suggestions: [ + { + messageId: "sort-css-properties-alphabetically", + output: 'const style = css`color: red;`;', + }, + ], + }, + ], + }, + { + code: 'const style = styled.div`background-color: $bgColor; color: #FFFFFF;`;', + filename: 'example.ts', + errors: [ + { + messageId: "sort-css-properties-alphabetically", + suggestions: [ + { + messageId: "sort-css-properties-alphabetically", + output: 'const style = styled.div`background-color: $bgColor; color: white;`;', + }, + ], + }, + ], + }, + // Add more invalid cases as needed + ], +}); diff --git a/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts b/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts new file mode 100644 index 000000000..3f1fedd77 --- /dev/null +++ b/packages/eslint-plugin-twenty-ts/src/tests/styled-components-prefixed-with-styled.spec.ts @@ -0,0 +1,75 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import styledComponentsPrefixedWithStyledRule from "../rules/styled-components-prefixed-with-styled"; + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run("styled-components-prefixed-with-styled", styledComponentsPrefixedWithStyledRule, { + valid: [ + { + code: 'const StyledButton = styled.button``;', + filename: 'example.ts', + }, + { + code: 'const StyledComponent = styled.div``;', + filename: 'example.ts', + }, + + ], + invalid: [ + { + code: 'const Button = styled.button``;', + filename: 'example.ts', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + { + code: 'const Component = styled.div``;', + filename: 'example.ts', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + { + code: 'const styled = {}; const Button = styled.button``;', + filename: 'example.ts', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + { + code: 'const styled = {}; const Component = styled.div``;', + filename: 'example.ts', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + { + code: 'const StyledButton = styled.button``;', + filename: 'themes.ts', + errors: [ + { + messageId: 'noStyledPrefix', + }, + ], + }, + + ], +}); diff --git a/packages/eslint-plugin-twenty/index.js b/packages/eslint-plugin-twenty/index.js deleted file mode 100644 index 667956567..000000000 --- a/packages/eslint-plugin-twenty/index.js +++ /dev/null @@ -1,14 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const noHardcodedColors = require('./rules/no-hardcoded-colors'); -const cssAlphabetically = require('./rules/sort-css-properties-alphabetically'); -const styledComponentsPrefixedWithStyled = require('./rules/styled-components-prefixed-with-styled'); -const matchingStateVariable = require('./rules/matching-state-variable'); - -module.exports = { - rules: { - 'no-hardcoded-colors': noHardcodedColors, - 'sort-css-properties-alphabetically': cssAlphabetically, - 'styled-components-prefixed-with-styled': styledComponentsPrefixedWithStyled, - 'matching-state-variable': matchingStateVariable - }, -}; diff --git a/packages/eslint-plugin-twenty/package.json b/packages/eslint-plugin-twenty/package.json deleted file mode 100644 index 77ce68635..000000000 --- a/packages/eslint-plugin-twenty/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "eslint-plugin-twenty", - "version": "0.0.2", - "main": "index.js", - "dependencies": { - "postcss": "^8.4.24" - } -} diff --git a/packages/eslint-plugin-twenty/rules/matching-state-variable.js b/packages/eslint-plugin-twenty/rules/matching-state-variable.js deleted file mode 100644 index 0f28aaa39..000000000 --- a/packages/eslint-plugin-twenty/rules/matching-state-variable.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; - -module.exports = { - meta: { - type: "problem", - docs: { - description: - "Ensure recoil value and setter are named after their atom name", - category: "Possible Errors", - recommended: true, - }, - fixable: "code", - schema: [], - }, - create: (context) => ({ - VariableDeclarator(node) { - if ( - node?.init?.callee?.name && - typeof node.init.callee.name === "string" && - [ - "useRecoilState", - "useRecoilFamilyState", - "useRecoilSelector", - "useRecoilScopedState", - "useRecoilScopedFamilyState", - "useRecoilScopedSelector", - "useRecoilValue", - ].includes(node.init.callee.name) - ) { - const stateNameBase = node.init.arguments?.[0].name; - - if (!stateNameBase) { - return; - } - - let expectedVariableNameBase = stateNameBase.replace( - /(State|FamilyState|Selector|ScopedState|ScopedFamilyState|ScopedSelector)$/, - "" - ); - - if (node.id.type === "Identifier") { - const actualVariableName = node.id.name; - if (actualVariableName !== expectedVariableNameBase) { - context.report({ - node, - message: `Invalid usage of ${node.init.callee.name}: the value should be named '${expectedVariableNameBase}' but found '${actualVariableName}'.`, - fix(fixer) { - return fixer.replaceText(node.id, expectedVariableNameBase); - }, - }); - } - return; - } - - if (node.id.type === "ArrayPattern") { - const actualVariableName = node.id.elements?.[0]?.name; - - if ( - actualVariableName && - actualVariableName !== expectedVariableNameBase - ) { - context.report({ - node, - message: `Invalid usage of ${node.init.callee.name}: the value should be named '${expectedVariableNameBase}' but found '${actualVariableName}'.`, - fix(fixer) { - return fixer.replaceText( - node.id.elements[0], - expectedVariableNameBase - ); - }, - }); - return; - } - - if (node.id.elements?.[1]?.name) { - const actualSetterName = node.id.elements[1].name; - const expectedSetterName = `set${expectedVariableNameBase - .charAt(0) - .toUpperCase()}${expectedVariableNameBase.slice(1)}`; - - if (actualSetterName !== expectedSetterName) { - context.report({ - node, - message: `Invalid usage of ${node.init.callee.name}: Expected setter '${expectedSetterName}' but found '${actualSetterName}'.`, - fix(fixer) { - return fixer.replaceText( - node.id.elements[1], - expectedSetterName - ); - }, - }); - } - } - } - } - }, - }), -}; diff --git a/packages/eslint-plugin-twenty/rules/no-hardcoded-colors.js b/packages/eslint-plugin-twenty/rules/no-hardcoded-colors.js deleted file mode 100644 index c4b169b4d..000000000 --- a/packages/eslint-plugin-twenty/rules/no-hardcoded-colors.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - create: (context) => ({ - TaggedTemplateExpression(node) { - if (context.getFilename().endsWith("themes.ts")) { - return; - } - - node.quasi.quasis.forEach((quasi) => { - const colorRegex = - /(?:rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(,\s*\d+\.?\d*)?\))|(?:#[0-9a-fA-F]{6})/i; - - if (colorRegex.test(quasi.value.raw)) { - context.report({ - node, - message: - "Do not use hardcoded RGBA or Hex colors. Please use a color from the theme file.", - }); - } - }); - }, - }), -}; diff --git a/packages/eslint-plugin-twenty/rules/sort-css-properties-alphabetically.js b/packages/eslint-plugin-twenty/rules/sort-css-properties-alphabetically.js deleted file mode 100644 index 3c062cb20..000000000 --- a/packages/eslint-plugin-twenty/rules/sort-css-properties-alphabetically.js +++ /dev/null @@ -1,198 +0,0 @@ -"use strict"; -const postcss = require("postcss"); - -const isStyledTagname = (node) => - (node.tag.type === "Identifier" && node.tag.name === "css") || - (node.tag.type === "MemberExpression" && node.tag.object.name === "styled") || - (node.tag.type === "CallExpression" && - (node.tag.callee.name === "styled" || - (node.tag.callee.object && - ((node.tag.callee.object.callee && - node.tag.callee.object.callee.name === "styled") || - (node.tag.callee.object.object && - node.tag.callee.object.object.name === "styled"))))); - -/** - * An atomic rule is a rule without nested rules. - */ -const isValidAtomicRule = (rule) => { - const decls = rule.nodes.filter((node) => node.type === "decl"); - if (decls.length < 0) { - return { isValid: true }; - } - - for (let i = 1; i < decls.length; i++) { - const current = decls[i].prop; - const prev = decls[i - 1].prop; - if (current < prev) { - const loc = { - start: { - line: decls[i - 1].source.start.line, - column: decls[i - 1].source.start.column - 1, - }, - end: { - line: decls[i].source.end.line, - column: decls[i].source.end.column - 1, - }, - }; - - return { isValid: false, loc }; - } - } - - return { isValid: true }; -}; - -const isValidRule = (rule) => { - // check each rule recursively - const { isValid, loc } = rule.nodes.reduce( - (map, node) => { - return node.type === "rule" ? isValidRule(node) : map; - }, - { isValid: true } - ); - - // if there is any invalid rule, return result - if (!isValid) { - return { isValid, loc }; - } - - // check declarations - return isValidAtomicRule(rule); -}; - -const getNodeStyles = (node) => { - const [firstQuasi, ...quasis] = node.quasi.quasis; - // remove line break added to the first quasi - const lineBreakCount = node.quasi.loc.start.line - 1; - let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat( - node.quasi.loc.start.column + 1 - )}${firstQuasi.value.raw}`; - - // replace expression by spaces and line breaks - quasis.forEach(({ value, loc }, idx) => { - const prevLoc = idx === 0 ? firstQuasi.loc : quasis[idx - 1].loc; - const lineBreaksCount = loc.start.line - prevLoc.end.line; - const spacesCount = - loc.start.line === prevLoc.end.line - ? loc.start.column - prevLoc.end.column + 2 - : loc.start.column + 1; - styles = `${styles}${" "}${"\n".repeat(lineBreaksCount)}${" ".repeat( - spacesCount - )}${value.raw}`; - }); - - return styles; -}; - -const create = (context) => ({ - TaggedTemplateExpression(node) { - if (isStyledTagname(node)) { - try { - const root = postcss.parse(getNodeStyles(node)); - - const { isValid, loc } = isValidRule(root); - - if (!isValid) { - return context.report({ - node, - messageId: "sort-css-properties-alphabetically", - loc, - fix: (fixer) => - fix({ - rule: root, - fixer, - src: context.getSourceCode(), - }), - }); - } - } catch (e) { - return true; - } - } - }, -}); - -const fix = ({ rule, fixer, src }) => { - let fixings = []; - - // concat fixings recursively - rule.nodes.forEach((node) => { - if (node.type === "rule") { - fixings = [...fixings, ...fix({ rule: node, fixer, src })]; - } - }); - - const declarations = rule.nodes.filter((node) => node.type === "decl"); - const sortedDeclarations = sortDeclarations(declarations); - - declarations.forEach((decl, idx) => { - if (!areSameDeclarations(decl, sortedDeclarations[idx])) { - try { - const range = getDeclRange({ decl, src }); - const sortedDeclText = getDeclText({ - decl: sortedDeclarations[idx], - src, - }); - - fixings.push(fixer.removeRange([range.startIdx, range.endIdx + 1])); - fixings.push( - fixer.insertTextAfterRange( - [range.startIdx, range.startIdx], - sortedDeclText - ) - ); - } catch (e) { - console.log(e); - } - } - }); - return fixings; -}; - -const areSameDeclarations = (a, b) => - a.source.start.line === b.source.start.line && - a.source.start.column === b.source.start.column; - -const getDeclRange = ({ decl, src }) => { - const loc = { - start: { - line: decl.source.start.line, - column: decl.source.start.column - 1, - }, - end: { - line: decl.source.end.line, - column: decl.source.end.column - 1, - }, - }; - - const startIdx = src.getIndexFromLoc(loc.start); - const endIdx = src.getIndexFromLoc(loc.end); - return { startIdx, endIdx }; -}; - -const getDeclText = ({ decl, src }) => { - const { startIdx, endIdx } = getDeclRange({ decl, src }); - return src.getText().substring(startIdx, endIdx + 1); -}; - -const sortDeclarations = (declarations) => - declarations - .slice() - .sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1)); - -module.exports = { - meta: { - docs: { - description: "Styles are sorted alphabetically.", - category: "Fill me in", - recommended: false, - }, - messages: { - "sort-css-properties-alphabetically": - "Declarations should be sorted alphabetically.", - }, - fixable: "code", - }, - create, -}; diff --git a/packages/eslint-plugin-twenty/rules/styled-components-prefixed-with-styled.js b/packages/eslint-plugin-twenty/rules/styled-components-prefixed-with-styled.js deleted file mode 100644 index 3c32f0efe..000000000 --- a/packages/eslint-plugin-twenty/rules/styled-components-prefixed-with-styled.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - meta: { - type: 'suggestion', - docs: { - description: 'Warn when StyledComponents are not prefixed with Styled', - }, - recommended: true, - fixable: 'code', - schema: [], - }, - create: function(context) { - return { - VariableDeclarator: node => { - const templateExpr = node.init - if (templateExpr?.type !== 'TaggedTemplateExpression') { - return; - } - const tag = templateExpr.tag - const tagged = tag.type === 'MemberExpression' ? tag.object - : tag.type === 'CallExpression' ? tag.callee - : null - if (tagged?.name === 'styled') { - const variable = node.id; - if (variable?.name.startsWith('Styled')) { - return; - } - context.report({ node, message: `'${variable.name}' is a StyledComponent and is not prefixed with Styled.` }); - } - }, - } - } -}; diff --git a/packages/eslint-plugin-twenty/yarn.lock b/packages/eslint-plugin-twenty/yarn.lock deleted file mode 100644 index e5dbe13b1..000000000 --- a/packages/eslint-plugin-twenty/yarn.lock +++ /dev/null @@ -1,27 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -postcss@^8.4.24: - version "8.4.24" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df" - integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 6e43b5862..9a4dd43c3 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -5,7 +5,7 @@ module.exports = { tsconfigRootDir : __dirname, sourceType: 'module', }, - plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports'], + plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports','twenty-ts'], extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', @@ -17,6 +17,11 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', 'src/core/@generated/**'], rules: { + 'twenty-ts/effect-components': 'error', + 'twenty-ts/no-hardcoded-colors': 'error', + 'twenty-ts/matching-state-variable': 'error', + 'twenty-ts/sort-css-properties-alphabetically': 'error', + 'twenty-ts/styled-components-prefixed-with-styled': 'error', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/server/package.json b/server/package.json index c84166df4..dd8c5f17c 100644 --- a/server/package.json +++ b/server/package.json @@ -108,6 +108,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-twenty-ts": "file:../packages/eslint-plugin-twenty-ts", "eslint-plugin-unused-imports": "^3.0.0", "jest": "28.1.3", "prettier": "^2.3.2", diff --git a/server/yarn.lock b/server/yarn.lock index a14ce170f..2cd417784 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -4883,6 +4883,9 @@ eslint-plugin-prettier@^4.0.0: dependencies: prettier-linter-helpers "^1.0.0" +"eslint-plugin-twenty-ts@file:../packages/eslint-plugin-twenty-ts": + version "1.0.1" + eslint-plugin-unused-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz#d25175b0072ff16a91892c3aa72a09ca3a9e69e7"