Company site to do front-end architecture transformation, need to put the history of the code, all the color values used to replace variables, easy to do the theme and style iteration. The first phase of the project uses nodeJS scripts to scan the code and manually replace it. With the new code in mind and other subsequent modifications, it was decided to write Lint and integrate it into the project’s CI script. Because react is used, the color values involved are partly in JSX code and partly in custom CSS, so you need to develop esLint and stylelint plugins respectively.

Develop the ESLint plugin

Before developing the ESLint plugin, let’s briefly clarify the following concepts:

  • Eslint rules
  • Eslint parser
  • Eslint plug-in

Eslint rules

Rules are one of the basic configurations of ESLint, and each rule is used to detect code that matches certain characteristics. A rule can be enabled or not and the error level can be configured. 0 or “off” indicates that the rule is disabled, 1 or warn indicates warning, and 2 or Error indicates error. Such as:

    rules: {
        "no-unused-vars": 1 // If there is a variable that is not used but declared, give warning}Copy the code

Some rules also have options, which can be configured using arrays. For example:

    rules: {
        "no-unused-vars": ["warn", { "ignoreRestSiblings": true}}}type. coords } = data;typeIgnore} if not usedCopy the code

Eslint parser

Eslint works by parsing javascript code into an AST (abstract syntax tree) using a parser and traversing the AST from top to bottom and from bottom to top. At the same time, the rule in effect listens on the selectors of some nodes in the AST and triggers a callback. An AST is essentially a tree-like data structure with a selector for each node. There are many selectors, and you can view AST selectors of different JS versions through MDN or ESTree. Here is a recommended online tool: astexplorer.net/ can parse JS code fragments online, which will bring great help to the later development of plug-ins. The official default parser for ESLint is espree, while babel-esLint supports more recent syntax features than the official parser.

Eslint plug-in

The officially provided configurable rules are built into the ESLint package. If you want to customize rules, such as special requirements like finding color values mentioned at the beginning, you must develop an ESLint plug-in. An ESLint plugin, usually a collection of rules and handlers, such as eslint-plugin-react, used to write react projects. Here’s a formal introduction to the main process of developing an ESLint plug-in.

Create a project

Install the officially recommended scaffolding tool Yeoman and the corresponding generator-ESLint:

npm install -g yo generator-eslint

Create project directory
mkdir eslint-plugin-console
cd eslint-plugin-console

# Build project
yo eslint:plugin
Copy the code

The project directory structure is as follows:

- - Eslint-plugin-console │ ├─ Changelo.md │ ├─ readme.md │ ├─ lib │ ├─ index.js // import │ │ ├─ processors // │ │ └ ─ ─ rules / / deposit rules │ ├ ─ ─ package. The json │ └ ─ ─ yarn. The lockCopy the code

Two points to note here:

  • Eslint plugins have a fixed naming form, starting with eslint-plugin-, which can be omitted during configuration
  • Note that the default VERSION of ESLint created by the scaffolding tool may be older, so you need to be consistent with the ESLint version of the project being applied to avoid mismatches

Open entry file:

// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");

// import processors
module.exports.processors = {

    // add your processors here
};
Copy the code

As you can see, the most basic ESLint plugin is just an object that contains rules and Processors. For details about other configurations, see the official documents

Create rules

Rules can be created by executing commands from the scaffold tool:

yo eslint:rule
Copy the code

You can also create it manually: Since the entry file references the entire rules directory with requireIndex, you can create a rule file directly under the rules directory with the rule name filename: No-css-hard-code-color.js There is no official restriction on the naming of rules, but for ease of understanding and maintenance, it is commonly used to disable some form of rule. It can start with no-, followed by the prohibited content, and the word is preceded by a short horizontal -.

The development rule

module.exports = {
    meta: {
      type: "problem",
    },
    create: function(context) {
        return{// return AST selector hook}; }};Copy the code

Each rule exports an object, and the core functionality of the object, mainly in create, is used to listen for AST selectors. In addition, the create callback also returns a context object, most commonly used by its report method, which is used to report errors. Please refer to the official documentation for details of the API

Choose AST selectors wisely

Next analyze the requirements, need to “detect all JS written dead CSS color value”, so first to summarize all forms of CSS color value. According to MDN, there are roughly four categories: built-in named color, HEX color value, RGB color value and HSL color value. So, for these four kinds of matching, respectively: built-in color value using enumeration check, the last three use regular check:

/ ^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/ //hex/^rgba? \(.+\)$/ //rgb /^hsla? \(.+\)$/ //hslCopy the code

The next step is to call the AST selector hook. The first thing that comes to mind is the simplest and most crude way to check all literals:

TemplateElement(node) {
  const { value } = node;

  if(value) { checkAndReport(value.raw, node); }},Copy the code

However, after testing, this method will have a lot of false detection, such as:

<a className="blue" href="/" />
Copy the code

Blue here is not the color value, but can also be detected by error. To adjust the policy, perform a layer of whitelist filtering for the key of the object. At the same time, the analysis shows that the color value may exist in the following situations:

  • The value of the object
  • The declared value of a variable
  • The value assigned to the variable
  • The value of a ternary expression
  • Template string

Change the code as follows:

// Property(node) {const whiteList = ["className"."type"."warn"];
  if (whiteList.indexOf(node.key.name) >= 0) return;

  if (node.value.type === "Literal") { checkAndReport(node.value.value, node.value); }}, // VariableDeclarator(node) {if(! node.init)return;

  if (node.init.type === "Literal") { checkAndReport(node.init.value, node.init); }}, // AssignmentExpression(node) {if (node.right.type === "Literal") { checkAndReport(node.right.value, node.right); ConditionalExpression(node) {// ConditionalExpression(node) {if (node.consequent.type === "Literal") {
    checkAndReport(node.consequent.value, node.consequent);
  }

  if (node.alternate.type === "Literal") { checkAndReport(node.alternate.value, node.alternate); }}, // TemplateElement(node) {const {value} = node; checkAndReport(value.raw, node); },Copy the code

This basically detects all hard-coded color values. But there may still be some problems with the rules. On the one hand, some special cases may be mischecked, in which case some code snippets can be filtered through ESLint comments. On the other hand, there are some loopholes in the rule, such as if the built-in color names are split by template strings and assigned to new variables, which cannot be detected. However, this case is generally ignored, if to bypass the detection, you can simply ignore it with the aforementioned ESLint comments.

The test rules

There are three ways I can test plugin rules:

  • Eslint integrated test automation tool
  • Install to run in a real project
  • Online AST analysis tool AstExplorer

The esLint test tool relies on Mocha, so you’ll need to install Mocha first (skip this step for scaffolding) :

npm install mocha --dev
Copy the code

Then write the test case in the Tests directory:

var rule = require(".. /.. /.. /lib/rules/no-css-hard-code-color"),
  RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("no-css-hard-code-color", rule, {
  valid: [{ code: "var designToken = T_COLOR_DEFAULT" }],

  invalid: [
    {
      code: "var designToken = '#ffffff'",
      errors: [
        {
          message: "Please replace '#ffffff' with DesignToken. You can find in http://ui.components.frontend.ucloudadmin.com/#/Design%20Tokens?id=color",},],},],});Copy the code

Add NPM script:

"scripts": {
  "test": "mocha tests --recursive",},Copy the code

Run NPM run test to display the result:

If writing test cases is too much hassle, you can simply install tests in a real project:

"dependencies": {
  "eslint-plugin-console": ".. /eslint-plugin-console",}Copy the code

Then add the.eslintrc configuration:

{
  "parser": "babel-eslint"."env": {
    "browser": true."es6": true."node": true
  },
  "rules": {
    "console/no-css-hard-code-color": 2}."plugins": [
    "eslint-plugin-console"]}Copy the code

Or use an online tool:

release

Eslint plugins are typically distributed and referenced as NPM packages, so you can add a publishing script to package.json:

"scripts": {
  "_publish": "npm publish"."publish:patch": "standard-version --release-as patch --no-verify && npm run _publish"."publish:minor": "standard-version --release-as minor --no-verify && npm run _publish"
},
Copy the code

The introduction of standard-version enables the automatic generation of CHANGELOG files.

Develop the Stylelint plug-in

Eslint is used to parse javascript, but there are also some hard-coded color values in.css files in the project, so is there a way to detect those files? The answer is to use stylelint.

Differences from ESLint

Stylelint’s design is generally very similar to ESLint’s, so I’ll focus on their differences here. The main differences are as follows:

  • The parser
  • Plug-in entry
  • Naming rules

Stylelint parser

The core difference with ESLint, of course, is the parser. Stylelint is the parser used by the better known PostCSS. If you’ve developed a PostCSS plug-in, the processing logic of stylelint is similar to that of the PostCSS plug-in.

To implement stylelInt, the stylelint. CreatePlugin method receives a rule callback and returns a function. Function can get the postCSS object of the detected CSS code, which can call the POSTCSS API to parse, traverse, modify the code and other operations:

function rule(actual) {
  return(root, result) => {// root is postCSS object}; }Copy the code

CSS has much fewer node types than ESLint, mainly rule, such as #main {border: 1px solid black; }, decl, such as color: red, atrule, such as @media, comment, etc.

To check whether CSS attribute values contain color values, we can call root.walkDecls to traverse all CSS rules:

root.walkDecls((decl) => {
  if(decl) { ... }});Copy the code

Then, use postCSs-value-parser to parse the value part of the rule, and check whether it is color value through enumeration or regular:

const parsed = valueParser(decl.value);
parsed.walk((node) => {
  const { type, value, sourceIndex } = node;

  if (type= = ="word") {
    if (
      /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(value) ||colorKeywords.includes(value) ) { ... }}if (type= = ="function"&& /^(rgba? |hsla?) $/.test(value)) { ... }});Copy the code

Finally, when a color value is detected, the report method provided by the stylelint is called to give an error message:

const messages = ruleMessages(ruleName, {
  rejected: (color) =>
    `Unexpected hard code color "${color}", please replace it with DesignToken.`,
});
report({
  message: messages.rejected(valueParser.stringify(node)),
  node: decl,
  result,
  ruleName,
});
Copy the code

Plug-in entry

Unlike ESLint, the StylelInt plug-in is created through the stylelint.createPlugin. If a plug-in contains multiple rules, an array can be returned:

const requireIndex = require("requireindex");
const { createPlugin } = require("stylelint");
const namespace = require("./lib/utils/namespace");
const rules = requireIndex(__dirname + "/lib/rules");

const rulesPlugins = Object.keys(rules).map((ruleName) => {
  return createPlugin(namespace(ruleName), rules[ruleName]);
});

module.exports = rulesPlugins;
Copy the code

Here, a directory structure similar to the ESLint plugin is used to dump the entry port file together with requireIndex.

Naming rules

Stylelint makes suggestions for naming rules in contrast to ESLint. The stylelint has two parts: the object to detect + the object to detect. For example, if we detect a hard-coded color value, we can name it color-no-hard-code. The specific rules can be seen: stylelint. IO /user-guide/…

conclusion

Eslint and Stylelint can help teams style their code consistently and reduce bugs, while custom plug-ins and rules allow you to customize features based on business and framework situations, which can be helpful in architectural iterations, such as going offline for a component or component’s API. But lint is ultimately an aid tool, and testing is essential in real development, and automated unit tests can be used if possible.