mirror of
				https://github.com/lingble/twenty.git
				synced 2025-11-03 22:27:57 +00:00 
			
		
		
		
	* 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;
 | 
						|
        }
 | 
						|
      },
 | 
						|
    };
 | 
						|
  },
 | 
						|
});
 |