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:
-
What is your name?
author name, just fill in your own name -
Where will this rule be published? (Use arrow keys)
The generated files are the same, so just press Enter -
What is the rule ID?
The name of the rule, such as:no-inner-style
-
Type a short description of this rule:
Just fill in whatever you want, or just press Enter -
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 projectsh: 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"
or0
- close the rule -
"warn"
or1
- enable rule, use warning level error:warn
(does not cause program to exit) -
"error"
or2
- 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。