cause

In a multi-person cooperative project, eslint and prettier are not indispensable. They can help you to standardize a certain thing and the use of a certain method.

But sometimes it is not satisfactory. Some official packages of specifications are not provided, but we also need to carry out certain specifications. At this time, it is unreliable to rely on manual code review.

So what we need is a custom eslint to standardize our development

principle

ast syntax tree

In fact, the principle of eslint is based on the ast syntax tree, you can refer to my previous article about him: babel and ast

initialization

First we need to create a repository to store our code, I named it: @grewer/eslint-plugin-rn

Use the directive to initialize the project:

 npx -p yo -p generator-eslint yo eslint:plugin

Here we can modify our project name and configuration in package.json , for example, I will change the project name to this:
eslint-plugin-rn => @grewer/eslint-plugin-rn

Then execute the yarn command (or npm i ) to download the dependencies we need

At this time, we can put generator-eslint into the project's devDependencies for our convenience,

yarn add -D generator-eslint

So far the initialization of the project is officially completed

create

Put the instruction npx yo eslint:plugin into scripts :

 {
  "scripts": {
    "test": "mocha tests --recursive",
+    "create:rule": "npx yo eslint:rule"
  }
}

Start executing the command: yarn create:rule :

The effect is as shown:

  1. What is your name? author name, just fill in your own name
  2. Where will this rule be published? (Use arrow keys) The generated files are the same, so just press Enter
  3. What is the rule ID? The name of the rule, such as: no-inner-style
  4. Type a short description of this rule: Just fill in whatever you want, or just press Enter
  5. Type a short example of the code that will fail: Failed code situation, because it is troublesome to type in the terminal, just press Enter to skip, and add later

Created file directory:

 ├── README.md
├── docs
│   └── rules
│       └── no-inner-style.md
├── lib
│   ├── index.js
│   └── rules
│       └── no-inner-style.js
├── package.json
├── tests
│   └── lib
│       └── rules
│           └── no-inner-style.js
└── yarn.lock

example

We need to fill in our logic in the lib/rules/no-inner-style.js file

 module.exports = {
    meta: {
        docs: {
            description: "jsx render cannot write style",
            category: "Fill me in",
            recommended: false
        },
        fixable: null,  // 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 {

            // give me methods

        };
    }
};

First of all, we must have an idea. My purpose is that jsx in render cannot write inline styles. What we have to do is to detect in the attributes of style whether this attribute exists, whether he is a object format

Of course, there are also special cases, such as style in width will change according to variables {width:flag?100:0} , or some changes in animation, so you need to pay attention when judging

The question is, how do we find the style attribute in jsx?

Here we need some tools, below I recommend two URLs (their functions are the same)

Use of tools

First we need two pieces of code that fit the scene:

 // 这就是特殊的情况, 这种情况下我们不会禁止行内样式
class TestView extends Component {
    render() {
        const mode = 'dark';
        return (
            <View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
            </View>
        )
    }
}

// 这是需要禁止的情况
class TestView extends Component {
    render() {
        const mode = 'dark';
        return (
            <View style={{flex: 1, width: 200}}>
            </View>
        )
    }
}

Put the situations that need to be prohibited into the above website (if an error is reported, the configuration needs to be modified so that it supports jsx)

Place the mouse cursor on the style label, as shown in the figure:

Through the analysis of the website, we can see that the attribute of style in ast is called JSXAttribute

So we use it as the key and add the method:

 module.exports = {
    meta: {
        // 省略, 不改变
    },
    create: function (context) {
        // 省略这里的注释
        return {
            // 将 JSXAttribute 作为键
            JSXAttribute: node => {
                // 注意 如果这里写不下去了, 可以直接跳到下一步(调试)中

                const propName = node.name && node.name.name;
                // console.log(propName)

                // 先判断 style 是否存在
                if (propName === 'style') {

                    // node.value.expression.type 这个路径, 在 ast 网站中可以快速找到, ObjectExpression也一样
                    // 意思是 当 style 的值是一个 Object 的时候, 抛出信息
                    if (node.value.expression.type === 'ObjectExpression') {

                        // context.report 的作用就是抛出信息, node参数 就是抛出信息的节点在那个位置
                        context.report({
                            node,
                            message: "不要使用行内样式",
                        });
                        // TODO 注意!  这里我们还没考虑特殊情况
                    }
                }
            }
        }
    }
};

At this point we have captured the essence of ast

Debug & Test

Here we need a test file to debug our rules, open the file tests/lib/rules/no-inner-style.js :

 "use strict";

var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("no-inner-style", rule, {

    valid: [
        // give me some code that won't trigger a warning
    ],

    // 先将我们刚刚的例子代码上到这里的 code 中
    invalid: [
        {
            code: `
            class TestView extends Component{
                render() {
                    const mode = 'dark';
                    return (
                        <View style={{flex:1, width: 200}} >
                        </View>
                    )
                }
            }
            `,
            errors: [{
                message: "不要使用行内样式",
            }]
        }
    ]
});

Fill code and errors message

Run the node command in the terminal: node tests/lib/rules/no-inner-style.js

However , she will report the error:
AssertionError [ERR_ASSERTION]: A fatal parsing error occurred: Parsing error: The keyword 'class' is reserved
The reason is due to environmental issues of execution

test configuration

Add the file ---ce796128449135cdd3fd08d6bb79fe06 tests/lib in config.js , the path is ( tests/lib/config.js )

 const testConfig = {
    env: {
        es6: true
    },
    parserOptions: {
        ecmaFeatures: {
            jsx: true,

        },
        ecmaVersion: 2021,
        sourceType: 'module',
    },
}

module.exports = {
    testConfig
}

Introduced and used in tests/lib/rules/no-inner-style.js :

 var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

// 引入
const {testConfig} = require("../config.js");

// 使用
var ruleTester = new RuleTester(testConfig);
ruleTester.run("no-inner-style", rule, {

    valid: [],
    invalid: [
        {
            // 因为没变化所以此处省略
        }
    ]
});

At this point, we execute the command again: node tests/lib/rules/no-inner-style.js

If the verification is successful (i.e. context.report thrown message and test case message are equal), there will be no return information

If validation fails, the reason is printed

Add special cases

We talked about the special case of style before. In this case of variables, we will not throw an error message again.

Now copy the code into valid in the test case file:

 var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

// 引入
const {testConfig} = require("../config.js");

// 使用
var ruleTester = new RuleTester(testConfig);
ruleTester.run("no-inner-style", rule, {

    valid: [
        `
        class TestView extends Component {
            render() {
                const mode = 'dark';
                return (
                    <View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
                    </View>
                )
            }
        }
        `
    ],
    invalid: [
        {
            // 因为没变化所以此处省略
        }
    ]
});

At this time, we execute the command: node tests/lib/rules/no-inner-style.js and we will find that an error is reported. Of course, this is a special case that we have expected:

 AssertionError [ERR_ASSERTION]: Should have no errors but had 1: [
  {
    ruleId: 'no-inner-style',
    severity: 1,
    message: '不要使用行内样式',
    line: 6,
    column: 27,
    nodeType: 'JSXAttribute',
    endLine: 6,
    endColumn: 98
  }
]

At this time, we will modify our rule file lib/rules/no-inner-style.js :

 module.exports = {
    meta: {
        // 省略没有变化的内容
    },
    create: function (context) {
        return {
            JSXAttribute: node => {
                const propName = node.name && node.name.name;
                if (propName === 'style') {
                    // 可以通过执行  `node tests/lib/rules/no-inner-style.js` 和 console 来调试当前程序
                    // console.log(node.value.expression)
                    if (node.value.expression.type === 'ObjectExpression') {
                        // const arr = node.value.expression.properties
                        // 如果 style 中有表达式, 则不判断
                        for (let i = 0, len = arr.length; i < len; i++) {
                            // ConditionalExpression当然是可以在 ast 网站中找到, 也可以通过 console 慢慢调试出来
                            if (arr[i].value.type === 'ConditionalExpression') {
                                // 如果有表达式则直接返回, 不抛出任何信息
                                return
                            }
                        }
                        context.report({
                            node,
                            message: "不要使用行内样式",
                        });
                    }
                }
            }
        }

    }
};

Execute the command again, you can find that it passed normally

Add some functional components to expand the test scope and ensure that our rules are foolproof. This part can be seen in the warehouse at the end of me, and I won't go into details in this article.

test tools

When initialized in our project, it has built-in test tool mocha , we can run it directly:

 yarn test
// 或者
npm run test
If an error is reported in the project sh: mocha: command not found
You can use this command: "test": "node_modules/.bin/mocha tests --recursive", replace the previous old command

It can test tests all the cases in the file, of course debugging can also be, as long as you don't bother him

Documentation

When we use the instruction to create a new rule, he also has a new file which is the document file: docs/rules/no-inner-style.md

In this file, we can write down the matters needing attention in detail, and some areas to be filled can be deleted

 # jsx render cannot write style (no-inner-style)

## Rule Details

Examples of **incorrect** code for this rule:

```js

function TestView(){
    return (
        <View style={{flex:1, width: 200}} >
        </View>
    )
}
```

Examples of **correct** code for this rule:

```js

function TestView() {
    const mode = 'dark';
    return (
        <View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
        </View>
    )
}
```

## When Not To Use It

规范项目的行内样式, 如果不需要可以关闭

And also need to update the README.md document

Project integration

The current project can be published directly

After the release of my project, you can see his full name is: @grewer/eslint-plugin-rn

After adding it in the main project, package.json add it like this:

 "eslintConfig": {
    "extends": [
      // 省略
    ],
+    "plugins": ["@grewer/rn"], // 将我们的插件插入这边
    "rules": {
        // 编写规则的危险等级
+      "@grewer/rn/no-inner-style": 1
    },
    "env": {
      "react-native/react-native": true
    }
  },

In eslint, the value of a rule can be one of the following:

  • "off" or 0 - close the rule
  • "warn" or 1 - enable rule, use warning level error: warn (does not cause program to exit)
  • "error" or 2 - enable rule, use error level error: error (when triggered, the program will exit)

Of course, the above configuration is also configured in the .eslintrc file

If your plugin full name is not scoped, add it like this:

(if the full name of the plugin is: eslint-plugin-rn)

 {
  "plugins": [
    "rn"
  ],
  "rules": {
    "rn/rule-name": 2
  }
}

It needs to be added like this, the gap is also a prefix

Notice

After changing the eslint configuration, to take effect, you need to restart eslint
For example, in webstorm, you need to open the eslint module in the configuration, display Disabled Eslint , after selecting ok to close

Open the module again to restore the original state, of course, restarting the editor can also be solved

rule default

Here comes the problem: when we have more and more rules, we need to add this attribute rules every time we connect the plugin to the project.
Here we need to optimize

In the project we need to write the default value, one solution is to write directly:
Modified in lib/index.js file module.exports

 -module.exports.rules = requireIndex(__dirname + '/rules')

+module.exports = {
+    rules: requireIndex(__dirname + '/rules'),
+    configs: {
+        recommended: {
+            plugins: ["@grewer/rn"],
+            rules: {
+                "@grewer/rn/no-inner-style": 1
+            },
+        }
+    }
+}

Of course, the configuration of eslint in the main project also needs to be modified:

 {
  "eslintConfig": {
    "extends": [
      "xxx 之前另外的 config",
+      "plugin:@grewer/rn/recommended"
    ],
-    "plugins": ["@grewer/rn"], // 删除 "@grewer/rn"
-    "rules": {
-        "@grewer/rn/no-inner-style": 1
-    },// 删除, 但是我们也可以加上, 来覆盖默认值
    "env": {
      "react-native/react-native": true
    }
  }
}

Of course, because of the increase of rules, it is more troublesome to write directly in the lib/index.js file. We can create a new script to automatically add the default value of the rule:

Create a file in the root directory create.js

 const requireIndex = require("requireindex");
const fs = require('fs')

const pluginName = '@grewer/rn'

const rules = requireIndex(__dirname + "/lib/rules")

const keys = Object.keys(rules)

const defaultLevel = keys.map(key => {
    // 这里可以进行更加详细的判断
    return `'${pluginName}/${key}': 1`
})


const data = `
const requireIndex = require("requireindex");

module.exports = {
    rules: requireIndex('./rules'),
    configs:{
        recommended: {
          plugins: ['${pluginName}'],
          rules: {
            ${defaultLevel.join(',')}
          },
        }
    }
}`


fs.writeFileSync('./lib/index.js', data, 'utf8')

Run the script: node create.js

This generated lib/index.js file looks like this:

 const requireIndex = require("requireindex");

module.exports = {
    rules: requireIndex('./rules'),
    configs:{
        recommended: {
          plugins: ['@grewer/rn'],
          rules: {
            '@grewer/rn/no-inner-style': 1
          },
        }
    }
}

further optimization

Now the project depends on requireindex this library, many plug-in libraries do not depend on this library, this time we also need to slightly optimize:

Modified package.json :

 {
  "dependencies": {
    // 原来 requireindex 的位置, 删除
-     "requireindex": "~1.1.0",
  },
  "devDependencies": {
+    "requireindex": "~1.1.0", // 现在的位置
    "eslint": "^7.1.0",
    "generator-eslint": "^2.0.0",
    "mocha": "^8.3.0"
  },
}

Modify the create.js script just now:

 const requireIndex = require("requireindex");
    const fs = require('fs')
    
    const pluginName = '@grewer/rn'
    
    const rules = requireIndex(__dirname + "/lib/rules")
    
    const keys = Object.keys(rules)
    
    const defaultLevel = keys.map(key => {
        // 这里可以进行更加详细的判断
        return `'${pluginName}/${key}': 1`
    })
    
    
+    const temp = keys.map(key => {
+        return `'${key}': require('./lib/rules/${key}.js')`
+    })
    
    
    const data = `
-    const requireIndex = require("requireindex");
    
    module.exports = {
        rules:{
+             ${temp.join(',')}
        },
        configs:{
            recommended: {
              plugins: ['${pluginName}'],
              rules: {
                ${defaultLevel.join(',')}
              },
            }
        }
    }`
    
    
    fs.writeFileSync('./lib/index.js', data, 'utf8')

File after running:

 module.exports = {
    rules:{
         'no-inner-style': require('./rules/no-inner-style.js')
    },
    configs:{
        recommended: {
          plugins: ['@grewer/rn'],
          rules: {
            '@grewer/rn/no-inner-style': 1
          },
        }
    }
}

Now the plugin is more pure , and only depends on node

Finally modify our package command:

 {
    "scripts": {
        "test": "node_modules/.bin/mocha tests --recursive",
        "create:rule": "npx yo eslint:rule",
-       "pub": "npm publish",
+       "pub": "node create.js && npm publish",          
    },
}

Epilogue

This article introduces the eslint plug-in, from project creation to plug-in creation, to distribution and finally optimization

In the team, for the decision of our meeting, the consensus can be implemented into the project, the eslint plugin is essential

The eslint plugin library created in this project: https://github.com/Grewer/eslint-plugin-rn

Citation Reference


Grewer
984 声望28 粉丝

Developer