Babel Plugin development thinking

Babel is defined

Babel is an interpreter that translates ecma’s newer JS syntax into an interpreter that browsers can recognize, as detailed on the Babel website

babel plugin

  • About the design structure of the plugin

    Plugins are a very common design construct. Back in the days of jquery, jquery exposed a extend method that hung plugins under $.extend

    Plugins such as WebPack register the webPack lifecycle hooks and then trigger the lifecycle

  • About the design concept of plugin

    There is usually a tool that provides a core that does the core computing, and the rest of the functionality is open to third parties, like jquery webPack Babel.

Babel practice

Basic knowledge of

If you have enough time, you can read the official handbook first. There are many knowledge points, it may take a little time to digest, but I still suggest you read it.

handbook

Refine some basic, important knowledge points

Babel-core turns the code into an AST tree, where the AST is node by node information, for example

Export default class {} export default class {

{
	type: ExportDefaultDeclaration
	start: 501,
	end: 2128
	declaration: {....}
}
Copy the code

Direct manipulation of AST nodes is generally not recommended for the simple reason that the AST node information is relatively independent. Babel assembles these independent nodes into a program with some description information, the smallest unit of which is path. Path also exposes ways to add, remove, and move nodes, which is much more convenient than directly modifying the AST

Path and AST are called reactive. The AST changes, and the PATH changes, and the AST changes

practice

  • A basic example of a single file transform is as follows

      var babel = require('babel-core');
      var t = require('babel-types');
      const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
      const visitor = {
          Identifier(path){
              console.log(path.node.name)
          }
      }
      const result = babel.transform(code, {
          plugins: [{
              visitor: visitor
          }]
      })
    Copy the code

Parse to the Identifier to output the node name: uniq extend flatten cloneDeep

  • A slightly more complicated example

Status: There is a contamination problem with the styles in the project. Generally, the code is organized into one JSX for one SCSS file, but some SCSS files in the project are written this way

input{

}

textarea{

}

.form{

}
Copy the code

Corresponding JSX code

render () {
	return (<div> 	
		....
	</div>)
Copy the code

Objective: Parse JSX. If the outermost element does not have a classname set, manually add classname = {$__dirname}_container to the outer part of the SCSS file, the corresponding SCSS file

.$__dirname}_container{
	input{
	
	}
	
	textarea{
	
	}
	
	.form{
	
	}
}
Copy the code

Corresponding JSX code

render () {
	return (<div className =`{$__dirname}_container`> 
		....
	</div>)
Copy the code
To analyze problems

There are thousands of files in the project, so we can’t execute them one by one through manual Babel. We must combine them with Webpack, because Webpack will automatically help us collect dependencies, which has an advantage.

In basic form, able.config.js writes the following code

module.exports = { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "ignore": [ "node_modules/**", "dist" ], "env": { "test": { "plugins": ["@babel/plugin-transform-runtime"] } }, "plugins": [ ["founder-transform", {test: 'fz'}], // test plugin ["syntax-dynamic-import"], ["@babel/plugin-proposal-export-default-from"], ["@babel/plugin-proposal-export-namespace-from"], ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose" : true }], ["import", { "libraryName": "mtui-d", "libraryDirectory": "lib", "style":true } ] ] }Copy the code

There is a point to note here, our custom plug-in must be written in the first plugin, plugin execution order, refer to the official website

  • The plug-in runs before the Presets.
  • The plugins are ordered from front to back.
  • Preset is reversed, (from back to front).

Roughly. plugin: founder-transform …… import -> presets: @babel/preset-react @babel/preset-env

Preset is also a set of plug-ins, so you can imagine a row of plug-ins overwriting the AST

##### Start practice

  1. Extract the JSX file and capture import ‘XXXX. SCSS’ to record the SCSS file path
  2. Extract the outermost element of render in export default class, add classname if there is no classname, and then seal a layer of classname on the corresponding SCSS file
  3. If there is a classname in the outer layer, but the corresponding SCSS file does not have a classname, record the path and manually check whether there is pollution

Isn’t that easy?

The main process is two steps

  1. Bind a classname to render’s JSX outermost layer
  2. Then go and seal the classname to the outermost layer

Step two, two lines of code

let content = fs.readFileSync(path);

fs.writeFileSync(path, ‘utf-8’)

The first step is also simple. Write down the following code and cut the key code

  1. Extract JSX file and capture import ‘XXXX. SCSS’ record SCSS file path

    ImportDeclaration(path, _ref = {opts:{}}){ co(function *(){ const specifiers = path.node.specifiers; const {value} = path.node.source; if(specifiers.length === 0 && value.slice(-5) === ".scss" && _ref.filename.indexOf('/container/') > -1){ Const cssPath = paths.join(_ref.filename, '.. ' , value); let exist = yield statPromise(cssPath); let source = exist && fs.readFileSync(cssPath).toString() if(! containerJSCSS[_ref.filename]){ containerJSCSS[_ref.filename] = {} } if(! containerJSCSS[_ref.filename].cssPath){ containerJSCSS[_ref.filename].cssPath = [] } containerJSCSS[_ref.filename].filename = _ref.filename; containerJSCSS[_ref.filename].cssPath.push(cssPath); containerJSCSS[_ref.filename].cssSource = source; }})Copy the code

    },

  2. Bind a classname to render’s JSX outermost layer

    ExportDefaultDeclaration(path, _ref = {opts: {}}){ const declaration = path.node.declaration; let code = _ref.file.code; let ast = _ref.file.ast; //class if(_ref.filename.indexOf('/container/') > -1 && declaration.type === "ClassDeclaration"){ if(declaration && declaration.body){ // render return() let {body: declarationBody} = declaration.body; let render = declarationBody && declarationBody.find(_ => { return _.key.name === "render" && _.type === "ClassMethod" }) if(render && render.body){ let {body: renderBody} = render.body; let returnStatement = renderBody.find(_ => _.type === "ReturnStatement") || {}; let {argument} = returnStatement; if(argument && argument.type === "JSXElement"){ // render return (<> </>) let {openingElement: {attributes, name}} = argument; if(name.type === "JSXIdentifier"){ if(! containerJSCSS[_ref.filename]){ containerJSCSS[_ref.filename] = {} } containerJSCSS[_ref.filename]['wrapElement'] = name.name } let attributesClass = attributes.find(_ => { return _.name && _.name.name ! == "className" }) containerJSCSS[_ref.filename]['wrapElementClass'] = !! attributesClass containerJSCSS[_ref.filename]['filename'] = _ref.filename; attributes.push(t.jSXAttribute(t.JSXIdentifier('className'), t.stringLiteral(_ref.filename))); // bind classname const output = generate(ast, {}, code); output.code += `/** ${_ref.filename} **/\n`; fs.writeFile('./data.jsx', output.code, { flag: 'a' }, (err) => { if (err) throw err; })}}}}}Copy the code

The code is simple, just get the Render Return () A element of the Export class step by step

But when the code runs, there are two problems

  • const output = generate(ast, {}, code); Found that code is not the source file, es5 code appears in it
  • How does containerJSCSS we declared in the Babel Plugin pass out? I need the final result, and I need some batch operations at the end

The first question is easy, break it down

  • When is the Babel Plugin called?

    The design of Webpack itself is pipelined, one file is processed, the next file comes in, get the first JSX and run through babel-Loader, then run plugin to preset plugin once

  • What does babel-loader do?

    Result = yield transform(source, options);

    Source is the source file, option is the parameter, and babel-loader is the parameter

  • Since the plugin we wrote was executed first, why is code not source code, but es5 code?

    Guess: Babel treats each statement type, such as ImportDeclaration, as a hook. When an import is caught, it is delivered in the order of the plug-in. Es5 code appears. It is likely that other plug-ins have hooks that modify ast tree validation before ImportDeclaration:

    Program: { enter(path, _ref = {opts:{}}) { if( _ref.filename.indexOf('/container/') > -1 ){ let _ast = _ref.file.ast; const output = generate(_ast, {}, _ref.file.code); output.code += `/** ${_ref.filename} **/\n`; fs.writeFile('./data.jsx', output.code, { flag: 'a' }, (err) => { if (err) throw err; })}}}Copy the code

    We added an additional hook to the plugin. This hook is executed first, before the ImportDeclaration, and printed as the source code

Solution:

Cache the AST tree in the Program Enter and generate code based on the AST tree, but the cost is huge because the AST is a reference type and you need to make a deep copy or it will still be modified

Second question

  • How do variables in the Babel Plugin pass out after the WebPack task ends?

Since Babel itself is pipe-based, it does not know if the file entering the pipe is the last one, so we simply hang this variable under Global and do subsequent batch operations in the WebPack Compilation done hook, code example

const fs = require('fs'); class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.done.tap('done', compilation => { fs.writeFile('./data.txt', JSON.stringify(global.containerJSCSS), 'utf-8', (err) => { if (err) throw err; })}); } } module.exports.default = ConsoleLogOnBuildWebpackPluginCopy the code

conclusion

Don’t do state reversals in Babel unless you need to, like I did earlier

Other plugin hooks have rewritten the AST, and your hook logic relies on the old AST, which is bad