使用TypeScript强制React组件命名

时间:2018-03-09 15:16:08

标签: javascript reactjs typescript

有React + TypeScript应用程序,所有组件类都应该是大写的,后缀为Component,例如:

export class FooBarComponent extends React.Component {...}

应用程序弹出create-react-application应用程序,即使用Webpack构建。

如何强制组件命名与样式指南一致,至少对于组件类,如果存在不一致,则会在构建时抛出错误?

我认为单凭TSLint / ESLint无法实现这一点。如果TypeScript和JavaScript应该使用不同的方法,那么两种语言的解决方案都会有所帮助。

2 个答案:

答案 0 :(得分:10)

我只能为你提供打字稿的解决方案。

  

我认为单凭TSLint / ESLint无法实现这一目标。

有一个所谓的规则class-name可以部分解决您的问题,但似乎您需要为此类情况编写自定义规则。

因此,让我们尝试编写这样的custom tslint rule。为此,我们需要在tslint配置中使用rulesDirectory选项来指定自定义规则的路径

"rulesDirectory": [
    "./tools/tslint-rules/"
],

由于我要在打字稿中编写自定义规则,我将使用tslint@5.7.0中添加的一项功能

  

[增强]自定义lint规则将使用节点的路径解析   分辨率允许加载器如ts-node(#3108)

我们需要安装ts-node

npm i -D ts-node

然后在tslint.json中添加假规则

"ts-loader": true,

并在rulesDirectory中创建文件tsLoaderRule.js

const path = require('path');
const Lint = require('tslint');

// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node.
// This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file.
require('ts-node').register({
    project: path.join(__dirname, '../tsconfig.json')
});

// Add a noop rule so tslint doesn't complain.
exports.Rule = class Rule extends Lint.Rules.AbstractRule {
    apply() {}
};

这基本上是广泛用于角度包装,如角度材料,通用等的方法

现在我们可以创建将使用打字稿编写的自定义规则(class-name规则的扩展版本)。

<强> myReactComponentRule.ts

import * as ts from 'typescript';
import * as Lint from 'tslint';

export class Rule extends Lint.Rules.AbstractRule {
  /* tslint:disable:object-literal-sort-keys */
  static metadata: Lint.IRuleMetadata = {
    ruleName: 'my-react-component',
    description: 'Enforces PascalCased React component class.',
    rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
    optionsDescription: 'Not configurable.',
    options: null,
    optionExamples: [true],
    type: 'style',
    typescriptOnly: false,
  };
  /* tslint:enable:object-literal-sort-keys */

  static FAILURE_STRING = (className: string) => `React component ${className} must be PascalCased and prefixed by Component`;

  static validate(name: string): boolean {
    return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
  }

  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk);
  }
}

function walk(ctx: Lint.WalkContext<void>) {
  return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
    if (isClassLikeDeclaration(node) && node.name !== undefined && isReactComponent(node)) {
      if (!Rule.validate(node.name!.text)) {
        ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
      }
    }
    return ts.forEachChild(node, cb);
  });
}

function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
  return node.kind === ts.SyntaxKind.ClassDeclaration ||
    node.kind === ts.SyntaxKind.ClassExpression;
}

function isReactComponent(node: ts.Node): boolean {
  let result = false;
  const classDeclaration = <ts.ClassDeclaration> node;
  if (classDeclaration.heritageClauses) {
    classDeclaration.heritageClauses.forEach((hc) => {
      if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) {

        hc.types.forEach(type => {
          if (type.getText() === 'React.Component') {
            result = true;
          }
        });
      }
    });
  }

  return result;
}

function isUpperCase(str: string): boolean {
  return str === str.toUpperCase();
}

最后我们应该将新规则放到tsling.json

// Custom rules
"ts-loader": true,
"my-react-component": true

所以像

这样的代码
App extends React.Component

将导致:

enter image description here

我还创建了 ejected react-ts 应用程序,您可以尝试使用它。

更新

  

我想在爷爷奶奶中追踪班级名字不是一项微不足道的任务

确实我们可以处理继承。为此,我们需要从类Lint.Rules.TypedRule扩展的创建规则才能访问TypeChecker

<强> myReactComponentRule.ts

import * as ts from 'typescript';
import * as Lint from 'tslint';

export class Rule extends Lint.Rules.TypedRule {
  /* tslint:disable:object-literal-sort-keys */
  static metadata: Lint.IRuleMetadata = {
    ruleName: 'my-react-component',
    description: 'Enforces PascalCased React component class.',
    rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
    optionsDescription: 'Not configurable.',
    options: null,
    optionExamples: [true],
    type: 'style',
    typescriptOnly: false,
  };
  /* tslint:enable:object-literal-sort-keys */

  static FAILURE_STRING = (className: string) =>
    `React component ${className} must be PascalCased and prefixed by Component`;

  static validate(name: string): boolean {
    return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
  }

  applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker());
  }
}

function walk(ctx: Lint.WalkContext<void>, tc: ts.TypeChecker) {
  return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
    if (
        isClassLikeDeclaration(node) && node.name !== undefined &&
        containsType(tc.getTypeAtLocation(node), isReactComponentType) &&
        !Rule.validate(node.name!.text)) {
      ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
    }

    return ts.forEachChild(node, cb);
  });
}
/* tslint:disable:no-any */
function containsType(type: ts.Type, predicate: (symbol: any) => boolean): boolean {
  if (type.symbol !== undefined && predicate(type.symbol)) {
    return true;
  }

  const bases = type.getBaseTypes();
  return bases && bases.some((t) => containsType(t, predicate));
}

function isReactComponentType(symbol: any) {
  return symbol.name === 'Component' && symbol.parent && symbol.parent.name === 'React';
}
/* tslint:enable:no-any */

function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
  return node.kind === ts.SyntaxKind.ClassDeclaration ||
    node.kind === ts.SyntaxKind.ClassExpression;
}

function isUpperCase(str: string): boolean {
  return str === str.toUpperCase();
}

另见commit:

答案 1 :(得分:6)

eslint更容易做到这一点。自定义插件不那么复杂。所以我创建了一个展示相同的插件。为了测试插件,我创建了以下文件

import React from "react"

class ABCComponent extends React.Component {

}

class ABC2component extends React.Component {

}

class TestComponent {

}


class FooBarComponent extends React.Component {

}

class fooBazComponent extends React.Component {

}

class FooBazing extends React.Component {

}

然后在同一个

上运行插件

Plugin results

我在编写插件时遵循以下指南

https://flexport.engineering/writing-custom-lint-rules-for-your-picky-developers-67732afa1803

https://www.kenneth-truyers.net/2016/05/27/writing-custom-eslint-rules/

https://eslint.org/docs/developer-guide/working-with-rules

我提出的最终代码是下面的规则

/**
 * @fileoverview Check that proper naming convention is followed for React components
 * @author Tarun Lalwani
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
var toPascalCase = require('to-pascal-case');

module.exports = {
    meta: {
        docs: {
            description: "Check that proper naming convention is followed for React components",
            category: "Fill me in",
            recommended: false
        },
        fixable: "code",  // or "code" or "whitespace"
        schema: [
            // fill in your schema
        ]
    },

    create: function(context) {

        // variables should be defined here

        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------

        // any helper functions should go here or else delete this section

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {

            ClassDeclaration: function(node) {
                var isReactComponent = false;
                if (node.superClass && node.superClass && node.superClass)
                {
                    if (node.superClass.object && node.superClass.object.name == 'React' && node.superClass.property.name === 'Component')
                        {
                            isReactComponent = true;
                        }
                    else if (node.superClass && node.superClass.name === 'Component') {
                        // if you want to suppot extends Component instead of just React.Component
                        isReactComponent = true;
                    }
                }

                if (isReactComponent) {
                    var className = node.id.name;
                    if (className[0] !== className[0].toUpperCase() || !className.endsWith("Component"))
                         context.report({
                            node: node, 
                            message: "Please use Proper case for the React Component class - {{identifier}}",
                            data: {
                                identifier: className
                            }, fix: (fixer) => {
                                var newClassName = className.toLowerCase().replace('component', '') + 'Component';
                                newClassName = toPascalCase(newClassName);
                                return fixer.replaceTextRange(node.id.range, newClassName)
                            }
                        });

                }
            }

        };
    }
};

关键是要了解AST树,我使用astexplorer做了。 Rest代码非常自我解释。

我已经在下面的repo上托管了这个插件,以防你想直接给它一个简短的

https://github.com/tarunlalwani/eslint-plugin-react-class-naming

使用以下命令

安装插件
npm i tarunlalwani/eslint-plugin-react-class-naming#master

然后将其添加到.eslintrc

{
    "plugins": [
       "react-class-naming"
    ]
}

然后在.eslintrc中添加规则

"rules": {
   "react-class-naming/react-classnaming-convention": ["error"],
   ....
}