javascript - Tvinga React-komponentnamngivning med TypeScript


Det finns en React+TypeScript-applikation, och alla komponentklasser ska skrivas med stor bokstav och ha suffixet Component, t.ex:

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

Applikationen är en utsprungen create-react-application-applikation, det vill säga är byggd med Webpack.

Hur kan komponentnamngivning tvingas att vara konsekvent med stilguiden, åtminstone för komponentklasser, med ett fel som genereras vid byggande när det finns inkonsekvenser?

Jag tror att detta inte kan uppnås med enbart TSLint/ESLint. Om olika metoder ska användas för TypeScript och JavaScript skulle lösningar för båda språken vara hjälpsamma.

Svar #1

Jag kan bara erbjuda dig en lösning för TypeScript.

Jag tror att detta inte kan uppnås med enbart TSLint/ESLint.

Det finns en så kallad regel class-name som kan lösa ditt problem delvis men det verkar som att du behöver skriva en anpassad regel för ett sådant fall.

Så låt oss försöka skriva en sådan anpassad tslint-regel. För det behöver vi använda rulesDirectory-alternativet i tslint-konfigurationen för att specificera sökvägen till anpassade regler

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

Eftersom jag ska skriva en anpassad regel i TypeScript kommer jag att använda en funktion som lades till i [email 

[förbättring] anpassade lintregler kommer att lösas med nodes sökvägsupplösning för att tillåta loader som ts-node (#3108)

Vi behöver installera ts-node-paketet

npm i -D ts-node

Lägg sedan till en fejkregel i tslint.json

"ts-loader": true,

och skapa filen tsLoaderRule.js i vår rulesDirectory:

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() {}
};

Detta är i princip ett tillvägagångssätt som används i stor utsträckning i angular-paket som angular material, universal osv.

Nu kan vi skapa vår egna regel (utökad version av class-name regeln) som kommer att skrivas i TypeScript.

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();
}

och slutligen bör vi lägga till vår nya regel i tsling.json:

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

Sådan kod som

App extends React.Component

kommer att resultera i:

Jag skapade också en ejected react-ts-applikation där du kan prova det.

Uppdatera

Jag antar att spåra klassnamn hos mor- och farföräldrar inte kommer att vara en trivial uppgift

Faktiskt kan vi hantera arv. För att göra det behöver vi skapa en regel som är utökad från klassen Lint.Rules.TypedRule för att få tillgång till 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();
}

Se även commit:

  • https://github.com/alexzuza/react-ts-test/commit/98ea77fa5a1f4fc73c53a055deb6960a3e7f3afe

Svar #2

Detta är mycket lättare att göra i eslint. Det anpassade tillägget är mycket mindre komplext. Så jag skapade ett tillägg som visar samma sak. För att testa tillägget skapade jag filen nedan

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 {

}

Och sedan körde pluginet på samma

Jag följde de nedanstående guiderna när jag skrev pluginet

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

Den slutgiltiga koden jag kom fram till var nedan för reglerna

/**
 * @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)
                            }
                        });

                }
            }

        };
    }
};

Nyckeln är att förstå AST-trädet, vilket jag gjorde med astexplorer. Resten av koden är ganska självförklarande.

Jag har hostat pluginet på nedanstående repo ifall du vill ge det en snabb titt direkt

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

Installera pluginet med kommandot nedan

npm i tarunlalwani/eslint-plugin-react-class-naming#master

Lägg sedan till det i din .eslintrc

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

Lägg sedan till reglerna i .eslintrc

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

Relaterade artiklar