Force React naming with TypeScript

There is a React + TypeScript application, and all component classes must be uppercase and have the Component suffix, for example:

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

The application is unloaded by the create-react-application , i.e. created using webpack.

How can you assign component names to match the style guide, at least for component classes, with an error that occurs during assembly if there are inconsistencies?

I believe that this cannot be achieved only with TSLint / ESLint. If you should use different methods for TypeScript and JavaScript, solutions for both languages ​​will be useful.

+6
javascript reactjs typescript
source share
2 answers

I can only offer you a solution for typescript.

I believe that this cannot be achieved only with TSLint / ESLint.

There is a so-called class-name rule that can partially solve the problem, but it seems you need to write your own rule for this case.

So, try to write such a tslint rule . To do this, we need to use the rulesDirectory option in the tslint configuration to specify the path to user rules

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

Since I'm going to write my own rule in typescript, I will use one function added in tslint@5.7.0

[improvement] custom lint rules will be allowed using the node permission path for loaders like ts-node (# 3108)

We need to install the ts-node package

 npm i -D ts-node 

Then add the fake rule to tslint.json

 "ts-loader": true, 

and create the tsLoaderRule.js file in our tsLoaderRule.js rules:

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

This is basically an approach that is widely used in angular packages such as angular, universal, etc.

Now we can create our custom rule (an extended version of the class-name rule), which will be written in 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(); } 

and finally, we have to put our new rule in tsling.json :

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

So code like

 App extends React.Component 

will result in:

enter image description here

I also created an exempt ts response application where you can try it.

Update

I think grandparents tracking class names won't be a trivial task

Indeed, we can handle inheritance. To do this, we need to create a rule extended from the Lint.Rules.TypedRule class in order to have access to 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(); } 

See also commit:

+10
source share

This is much easier to do in eslint . A custom plugin is much less complex. So I created a plugin demonstrating the same thing. To test the plugin, I created the file below

 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 { } 

And then launched the plugin on the same

Plug-in Results

I followed the guides below when writing the plugin

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

The last code I came up with was below for the 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) } }); } } }; } }; 

The key is to understand the AST tree that I used with astexplorer . The break code is quite explanatory.

I placed the plugin below the repo if you want to give it a short

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

Install the plugin using the following command

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

Then add it to your .eslintrc

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

Then add the rules to .eslintrc

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

All Articles