diff --git a/front/package.json b/front/package.json index 38a403e72..79a56e241 100644 --- a/front/package.json +++ b/front/package.json @@ -167,7 +167,6 @@ "eslint-plugin-react": "^7.31.11", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-storybook": "^0.6.12", - "eslint-plugin-twenty": "file:../packages/eslint-plugin-twenty", "eslint-plugin-twenty-ts": "file:../packages/eslint-plugin-twenty-ts", "eslint-plugin-unused-imports": "^3.0.0", "http-server": "^14.1.1", diff --git a/front/src/modules/ui/dropdown/components/DropdownCloseEffect.tsx b/front/src/modules/ui/dropdown/components/DropdownCloseEffect.tsx index 507771e50..d53ca16d5 100644 --- a/front/src/modules/ui/dropdown/components/DropdownCloseEffect.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownCloseEffect.tsx @@ -15,7 +15,7 @@ export const DropdownCloseEffect = ({ if (!isDropdownButtonOpen) { onDropdownClose(); } - }, [isDropdownButtonOpen]); + }, [isDropdownButtonOpen, onDropdownClose]); return null; }; diff --git a/front/yarn.lock b/front/yarn.lock index 5f58c1b99..9d3ddd9db 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -9830,12 +9830,7 @@ eslint-plugin-testing-library@^5.0.1: "@typescript-eslint/utils" "^5.58.0" "eslint-plugin-twenty-ts@file:../packages/eslint-plugin-twenty-ts": - version "1.0.1" - -"eslint-plugin-twenty@file:../packages/eslint-plugin-twenty": - version "0.0.2" - dependencies: - postcss "^8.4.24" + version "1.0.2" eslint-plugin-unused-imports@^3.0.0: version "3.0.0" @@ -15590,7 +15585,7 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.4: +postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4: version "8.4.28" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5" integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw== diff --git a/packages/eslint-plugin-twenty-ts/index.ts b/packages/eslint-plugin-twenty-ts/index.ts index bb96f7ffa..68b50740d 100644 --- a/packages/eslint-plugin-twenty-ts/index.ts +++ b/packages/eslint-plugin-twenty-ts/index.ts @@ -1,5 +1,9 @@ module.exports = { rules: { "effect-components": require("./src/rules/effect-components"), + "no-hardcoded-colors": require("./src/rules/no-hardcoded-colors"), + "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"), }, }; diff --git a/packages/eslint-plugin-twenty-ts/package.json b/packages/eslint-plugin-twenty-ts/package.json index 5bc91207b..9f6e4e489 100644 --- a/packages/eslint-plugin-twenty-ts/package.json +++ b/packages/eslint-plugin-twenty-ts/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-twenty-ts", - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "dist/index.js", "files": [ @@ -27,6 +27,7 @@ "eslint-plugin-import": "^2.28.1", "eslint-plugin-prettier": "^5.0.0", "jest": "^28.1.3", + "postcss": "^8.4.29", "prettier": "^3.0.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", 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 index 45e0ac3b1..26c290387 100644 --- 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 @@ -1,42 +1,58 @@ -import { TSESTree, ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils"; import postcss from "postcss"; +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils"; +import { ESLintUtils } from "@typescript-eslint/utils"; +import type { Identifier, TaggedTemplateExpression } from "@babel/types"; import { + RuleFix, 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"))) -); +interface loc { + start: { + line: number; + column: number; + }; + end: { + line: number; + column: number; + }; +} +const isStyledTagname = (node: TSESTree.TaggedTemplateExpression): boolean => { + return ( + (node.tag.type === "Identifier" && node.tag.name === "css") || + (node.tag.type === "MemberExpression" && + // @ts-ignore + node.tag.object.name === "styled") || + (node.tag.type === "CallExpression" && + // @ts-ignore + (node.tag.callee.name === "styled" || + // @ts-ignore + (node.tag.callee.object && + // @ts-ignore + + ((node.tag.callee.object.callee && + // @ts-ignore + node.tag.callee.object.callee.name === "styled") || + // @ts-ignore + (node.tag.callee.object.object && + // @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) { +const isValidAtomicRule = ( + rule: postcss.Root +): { isValid: boolean; loc?: loc } => { + const decls = rule.nodes.filter( + (node) => node.type === "decl" + ) as unknown as postcss.Declaration[]; + if (decls.length < 0) { return { isValid: true }; } @@ -46,12 +62,12 @@ const isValidAtomicRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcs if (current < prev) { const loc = { start: { - line: decls[i - 1].source?.start?.line || 0, - column: (decls[i - 1].source?.start?.column || 0) - 1, + line: decls[i - 1].source!.start!.line, + column: decls[i - 1].source!.start!.column - 1, }, end: { - line: decls[i].source?.end?.line || 0, - column: (decls[i].source?.end?.column || 0) - 1, + line: decls[i].source!.end!.line, + column: decls[i].source!.end!.column - 1, }, }; @@ -62,11 +78,13 @@ const isValidAtomicRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcs return { isValid: true }; }; -const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcss.NodeSource } => { +const isValidRule = (rule: postcss.Root): { isValid: boolean; loc?: loc } => { // check each rule recursively - const { isValid, loc } = rule.nodes.reduce( - (map:any, node: postcss.Node) => { - return node.type === "rule" ? isValidRule(node) : map; + const { isValid, loc } = rule.nodes.reduce<{ isValid: boolean; loc?: loc }>( + (map, node) => { + return node.type === "rule" + ? isValidRule(node as unknown as postcss.Root) + : map; }, { isValid: true } ); @@ -80,24 +98,22 @@ const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: postcss.Node return isValidAtomicRule(rule); }; -const getNodeStyles = ( - node: TSESTree.TaggedTemplateExpression -): string => { +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; + const lineBreakCount = node.quasi.loc.start.line - 1; let styles = `${"\n".repeat(lineBreakCount)}${" ".repeat( - (node.quasi.loc?.start?.column || 0) + 1 + 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 ? node.quasi.loc : quasis[idx - 1].loc; - const lineBreaksCount = (loc?.start?.line || 1) - (prevLoc?.end?.line || 0); + 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 || 0) - (prevLoc?.end?.column || 0) + 2 - : (loc?.start?.column || 0) + 1; + 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}`; @@ -114,20 +130,22 @@ const fix = ({ rule: postcss.Rule; fixer: RuleFixer; src: SourceCode; -}) => { - let fixings: ReturnType[] = []; +}): RuleFix[] => { + let fixings: RuleFix[] = []; // concat fixings recursively - rule.nodes.forEach((node: postcss.Node) => { + rule.nodes.forEach((node) => { if (node.type === "rule") { fixings = [...fixings, ...fix({ rule: node, fixer, src })]; } }); - const declarations = rule.nodes.filter((node: postcss.Node) => node.type === "decl"); + const declarations = rule.nodes.filter( + (node) => node.type === "decl" + ) as unknown as postcss.Declaration[]; const sortedDeclarations = sortDeclarations(declarations); - declarations.forEach((decl: postcss.Declaration, idx: number) => { + declarations.forEach((decl, idx) => { if (!areSameDeclarations(decl, sortedDeclarations[idx])) { try { const range = getDeclRange({ decl, src }); @@ -152,21 +170,27 @@ const fix = ({ }; const areSameDeclarations = ( - a: postcss.Declaration, - b: postcss.Declaration + a: postcss.ChildNode, + b: postcss.ChildNode ): boolean => - a.source?.start.line === b.source?.start.line && - a.source?.start.column === b.source?.start.column; + 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 getDeclRange = ({ + decl, + src, +}: { + decl: postcss.ChildNode; + src: SourceCode; +}): { startIdx: number; endIdx: number } => { const loc = { start: { - line: decl.source?.start?.line || 1, - column: (decl.source?.start?.column || 0) - 1, + line: decl.source!.start!.line, + column: decl.source!.start!.column - 1, }, end: { - line: decl.source?.end?.line || 1, - column: (decl.source?.end?.column || 0) - 1, + line: decl.source!.end!.line, + column: decl.source!.end!.column - 1, }, }; @@ -175,65 +199,70 @@ const getDeclRange = ({ decl, src }: { decl: postcss.Declaration; src: SourceCod return { startIdx, endIdx }; }; -const getDeclText = ({ decl, src }: { decl: postcss.Declaration; src: SourceCode }) => { +const getDeclText = ({ + decl, + src, +}: { + decl: postcss.ChildNode; + src: SourceCode; +}) => { const { startIdx, endIdx } = getDeclRange({ decl, src }); return src.getText().substring(startIdx, endIdx + 1); }; -const sortDeclarations = ( - declarations: postcss.Declaration[] -): postcss.Declaration[] => +const sortDeclarations = (declarations: 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; - } +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: [], - }); + }; + }, + 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/yarn.lock b/packages/eslint-plugin-twenty-ts/yarn.lock index f7b8f2b45..84cbc1249 100644 --- a/packages/eslint-plugin-twenty-ts/yarn.lock +++ b/packages/eslint-plugin-twenty-ts/yarn.lock @@ -2930,6 +2930,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +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== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3157,6 +3162,15 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +postcss@^8.4.29: + version "8.4.29" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd" + integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3363,6 +3377,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +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== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"