mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 12:47:58 +00:00 
			
		
		
		
	 8483cf0b4b
			
		
	
	8483cf0b4b
	
	
	
		
			
			* chore: use Nx workspace lint rules Closes #3162 * Fix lint * Fix lint on BE * Fix tests --------- Co-authored-by: Charles Bochet <charles@twenty.com>
		
			
				
	
	
		
			283 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
 | |
| import { isIdentifier } from '@typescript-eslint/utils/ast-utils';
 | |
| import {
 | |
|   RuleFix,
 | |
|   RuleFixer,
 | |
|   SourceCode,
 | |
| } from '@typescript-eslint/utils/ts-eslint';
 | |
| 
 | |
| import postcss from 'postcss';
 | |
| 
 | |
| // NOTE: The rule will be available in ESLint configs as "@nx/workspace-sort-css-properties-alphabetically"
 | |
| export const RULE_NAME = 'sort-css-properties-alphabetically';
 | |
| 
 | |
| interface Loc {
 | |
|   start: {
 | |
|     line: number;
 | |
|     column: number;
 | |
|   };
 | |
|   end: {
 | |
|     line: number;
 | |
|     column: number;
 | |
|   };
 | |
| }
 | |
| 
 | |
| const isMemberExpression = (
 | |
|   node: TSESTree.Node,
 | |
| ): node is TSESTree.MemberExpression =>
 | |
|   node.type === TSESTree.AST_NODE_TYPES.MemberExpression;
 | |
| const isCallExpression = (
 | |
|   node: TSESTree.Node,
 | |
| ): node is TSESTree.CallExpression =>
 | |
|   node.type === TSESTree.AST_NODE_TYPES.CallExpression;
 | |
| 
 | |
| const isStyledTagname = (node: TSESTree.TaggedTemplateExpression): boolean => {
 | |
|   if (isIdentifier(node.tag)) {
 | |
|     return node.tag.name === 'css';
 | |
|   }
 | |
| 
 | |
|   if (isMemberExpression(node.tag) && isIdentifier(node.tag.object)) {
 | |
|     return node.tag.object.name === 'styled';
 | |
|   }
 | |
| 
 | |
|   if (isCallExpression(node.tag) && isIdentifier(node.tag.callee)) {
 | |
|     return node.tag.callee.name === 'styled';
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     isCallExpression(node.tag) &&
 | |
|     isMemberExpression(node.tag.callee) &&
 | |
|     isIdentifier(node.tag.callee.object)
 | |
|   ) {
 | |
|     return node.tag.callee.object.name === 'styled';
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     isCallExpression(node.tag) &&
 | |
|     isMemberExpression(node.tag.callee) &&
 | |
|     isMemberExpression(node.tag.callee.object) &&
 | |
|     isIdentifier(node.tag.callee.object.object)
 | |
|   ) {
 | |
|     return node.tag.callee.object.object.name === 'styled';
 | |
|   }
 | |
| 
 | |
|   return false;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * An atomic rule is a rule without nested rules.
 | |
|  */
 | |
| const isValidAtomicRule = (
 | |
|   rule: postcss.Rule,
 | |
| ): { isValid: boolean; loc?: Loc } => {
 | |
|   const decls = rule.nodes.filter(
 | |
|     (node) => node.type === 'decl',
 | |
|   ) as unknown as postcss.Declaration[];
 | |
| 
 | |
|   const invalidDeclIndex = decls.findIndex((decl, index) => {
 | |
|     if (index === 0) return false;
 | |
| 
 | |
|     const current = decl.prop;
 | |
|     const prev = decls[index - 1].prop;
 | |
| 
 | |
|     return current < prev;
 | |
|   });
 | |
| 
 | |
|   return invalidDeclIndex > 0
 | |
|     ? {
 | |
|         isValid: false,
 | |
|         loc: {
 | |
|           start: {
 | |
|             line: decls[invalidDeclIndex - 1].source!.start!.line,
 | |
|             column: decls[invalidDeclIndex - 1].source!.start!.column - 1,
 | |
|           },
 | |
|           end: {
 | |
|             line: decls[invalidDeclIndex].source!.end!.line,
 | |
|             column: decls[invalidDeclIndex].source!.end!.column - 1,
 | |
|           },
 | |
|         },
 | |
|       }
 | |
|     : { isValid: true };
 | |
| };
 | |
| 
 | |
| const isValidRule = (rule: postcss.Rule): { isValid: boolean; loc?: Loc } => {
 | |
|   // check each rule recursively
 | |
|   const { isValid, loc } = rule.nodes.reduce<{ isValid: boolean; loc?: Loc }>(
 | |
|     (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: 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;
 | |
|   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 fix = ({
 | |
|   rule,
 | |
|   fixer,
 | |
|   src,
 | |
| }: {
 | |
|   rule: postcss.Rule;
 | |
|   fixer: RuleFixer;
 | |
|   src: SourceCode;
 | |
| }): RuleFix[] => {
 | |
|   // concat fixings recursively
 | |
|   const fixings = rule.nodes
 | |
|     .filter((node): node is postcss.Rule => node.type === 'rule')
 | |
|     .flatMap((node) => fix({ rule: node, fixer, src }));
 | |
| 
 | |
|   const declarations = rule.nodes.filter(
 | |
|     (node): node is postcss.Declaration => node.type === 'decl',
 | |
|   );
 | |
|   const sortedDeclarations = sortDeclarations(declarations);
 | |
| 
 | |
|   return [
 | |
|     ...fixings,
 | |
|     ...declarations.flatMap((decl, index) => {
 | |
|       if (!areSameDeclarations(decl, sortedDeclarations[index])) {
 | |
|         try {
 | |
|           const range = getDeclRange({ decl, src });
 | |
|           const sortedDeclText = getDeclText({
 | |
|             decl: sortedDeclarations[index],
 | |
|             src,
 | |
|           });
 | |
| 
 | |
|           return [
 | |
|             fixer.removeRange([range.startIdx, range.endIdx + 1]),
 | |
|             fixer.insertTextAfterRange(
 | |
|               [range.startIdx, range.startIdx],
 | |
|               sortedDeclText,
 | |
|             ),
 | |
|           ];
 | |
|         } catch (e) {
 | |
|           console.log(e);
 | |
|         }
 | |
|       }
 | |
|     }),
 | |
|   ];
 | |
| };
 | |
| 
 | |
| const areSameDeclarations = (
 | |
|   a: postcss.ChildNode,
 | |
|   b: postcss.ChildNode,
 | |
| ): boolean =>
 | |
|   a.source!.start!.line === b.source!.start!.line &&
 | |
|   a.source!.start!.column === b.source!.start!.column;
 | |
| 
 | |
| const getDeclRange = ({
 | |
|   decl,
 | |
|   src,
 | |
| }: {
 | |
|   decl: postcss.ChildNode;
 | |
|   src: SourceCode;
 | |
| }): { startIdx: number; endIdx: number } => {
 | |
|   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,
 | |
| }: {
 | |
|   decl: postcss.ChildNode;
 | |
|   src: SourceCode;
 | |
| }) => {
 | |
|   const { startIdx, endIdx } = getDeclRange({ decl, src });
 | |
|   return src.getText().substring(startIdx, endIdx + 1);
 | |
| };
 | |
| 
 | |
| const sortDeclarations = (declarations: postcss.Declaration[]) =>
 | |
|   declarations
 | |
|     .slice()
 | |
|     .sort((declA, declB) => (declA.prop > declB.prop ? 1 : -1));
 | |
| 
 | |
| export const rule = ESLintUtils.RuleCreator(() => __filename)({
 | |
|   name: RULE_NAME,
 | |
|   meta: {
 | |
|     docs: {
 | |
|       description: 'Styles are sorted alphabetically.',
 | |
|       recommended: 'recommended',
 | |
|     },
 | |
|     messages: {
 | |
|       sortCssPropertiesAlphabetically:
 | |
|         'Declarations should be sorted alphabetically.',
 | |
|     },
 | |
|     type: 'suggestion',
 | |
|     schema: [],
 | |
|     fixable: 'code',
 | |
|   },
 | |
|   defaultOptions: [],
 | |
|   create: (context) => {
 | |
|     return {
 | |
|       TaggedTemplateExpression: (node) => {
 | |
|         if (!isStyledTagname(node)) return;
 | |
| 
 | |
|         try {
 | |
|           const root = postcss.parse(
 | |
|             getNodeStyles(node),
 | |
|           ) as unknown as postcss.Rule;
 | |
| 
 | |
|           const { isValid } = isValidRule(root);
 | |
| 
 | |
|           if (!isValid) {
 | |
|             return context.report({
 | |
|               node,
 | |
|               messageId: 'sortCssPropertiesAlphabetically',
 | |
|               fix: (fixer) =>
 | |
|                 fix({
 | |
|                   rule: root,
 | |
|                   fixer,
 | |
|                   src: context.sourceCode,
 | |
|                 }),
 | |
|             });
 | |
|           }
 | |
|         } catch (e) {
 | |
|           return true;
 | |
|         }
 | |
|       },
 | |
|     };
 | |
|   },
 | |
| });
 |