This article is probably a by-product of webpack learning. Code -> AST -> iterate over modify AST -> convert to target code.

In general, it is divided into two parts: the first part: file compilation, the second part: recursive search file.

This article is about the implementation of file compilation and some use of the encapsulation.

Micro channel small program converter (a) : the implementation of the converter.

Wechat applet converter (2) : recursive operation file.

Wechat applet converter (iii) : Loader design and implementation.

Wechat applet converter (four) : asynchronous Loader implementation.

To prepare

A few things to prepare before you start:

  • 1, learn some node API knowledge, used to operate files
  • 2, AST Explorer a site where you can see the AST tree structure of various plug-ins
  • 3, JS converter:@babel/parser(code -> AST),@babel/traverse(to traverse the AST),@babel/generator(AST -> code)
  • 4. HTML converterhtmlparser2(HTML -> AST), AST -> HTML by hand
  • 5, CSS convertercss-treeThis one does all the work

Tools to prepare

The configuration file

A configuration file is required to indicate the build entry

// analyze.config.js
const config = {
    entry: '/'.output: {
        name: 'dist'.src: '/'}}module.exports = config
Copy the code

Package tool method

// common.js
const path = require("path");
const fs = require("fs");
const config = require(path.resolve('./analyze.config.js'))

/ / read the file
function readFile(url) { 
    return fs.readFileSync(url, 'utf8')}/ / write file
function writeFile(filename, data) { 
    return fs.writeFileSync(filename, data, 'utf8')}// Delete folders recursively
function deleteall(path) { 
	var files = [];
	if(fs.existsSync(path)) {
		files = fs.readdirSync(path);
		files.forEach(function(file, index) {
			var curPath = path + "/" + file;
			if(fs.statSync(curPath).isDirectory()) { // recurse
				deleteall(curPath);
			} else { // delete filefs.unlinkSync(curPath); }}); fs.rmdirSync(path); }}// Copy the file
function copyFile(src, dist) { 
    fs.writeFileSync(dist, fs.readFileSync(src));
}

// Replace attributes
function replaceAtrr(option, key, aimsKey) {
    const value = option[key]
    option[aimsKey] = value
    delete option[key]
}

// Get the input path
function inputAppPath(url) {
    return url ? path.resolve(config.entry, url) : path.resolve(config.entry)
}

// Get the output path
function outputAppPath(url) {
    return url ? path.resolve(config.output.src, config.output.name, url) : path.resolve(config.output.src, config.output.name)

}

Copy the code

Encapsulate JSON to replace the mapping table

The window and tabbar properties in app.json need to be replaced. The window property is also used in the json of the page.

// compares.js
const WINDOWCONVERTERCONFIG = {
    'navigationBarTitleText': {target: 'defaultTitle' },
    'enablePullDownRefresh': {target: 'pullRefresh'.handler: (config) = > { 
            const enablePullDownRefresh = config['enablePullDownRefresh']
            if (enablePullDownRefresh) config['allowsBounceVertical'] = 'YES'}},'navigationBarTitleText': {target: 'defaultTitle' },
    'navigationStyle': {
        handler: (config) = > {
            if (config['navigationStyle'] = ='custom') {
                config['transparentTitle'] = ='always'
                delete config['navigationStyle']}}},'navigationBarBackgroundColor': {target: 'titleBarColor' },
    'onReachBottomDistance': {target: 'onReachBottomDistance'}},const TABBARCONVERTERCONFIG = [
    { originalKey: 'color'.key: 'textColor' },
    { originalKey: 'list'.key: 'items' , list: [{originalKey: 'text'.key: 'name' },
        { originalKey: 'iconPath'.key: 'icon' },
        { originalKey: 'selectedIconPath'.key: 'activeIcon'}},],]module.exports = {
    WINDOWCONVERTERCONFIG,
    TABBARCONVERTERCONFIG
}

Copy the code

Compile the file

Compile the entrance

According to different types of files, select different types of entry, and separate the entry and source code processing to facilitate the expansion of similar loader processing later. After the compilation part only care about the input source code and output code can be.

// analyze.js

// Js compiler entry
function buildJs(inputPath, outputPath) {
    let source = readFile(inputPath)
    source = parseJS(source)
    writeFile(outputAppPath(outputPath), source)
    return source
}

// Json compiler entry
function buildJson(inputPath, outputPath) {
    let source = readFile(inputPath)
    
    const jonParser = new JsonParser()
    source = jonParser.parser(JSON.parse(source))
    source = JSON.stringify(source)
    
    writeFile(outputAppPath(outputPath), source)
    return source
}

// Html compiler entry
function buildXml(inputPath, outputPath) {
    let source = readFile(inputPath)

    parseXML(source).then(code= > {
        writeFile(outputAppPath(outputPath), code)
    })
}

// Wxss compiler entry
function buildWxss(inputPath, outputPath) {
    let source = readFile(inputPath)
    
    const code = parseCSS(source)
    
    writeFile(outputAppPath(outputPath), code)
}

Copy the code

Use converter


function parseJS(source) {
    const jsParser = new JsParser()
    let ast = jsParser.parse(source)
    ast = jsParser.astConverter(ast)
    return jsParser.astToCode(ast)
}

async function parseXML(source) {
    const templateParser = new TemplateParser()
    let ast = await templateParser.parse(source)
    ast = templateParser.templateConverter(ast)
    return templateParser.astToString(ast)
}

function parseCSS(source) {
    const cssParser = new CssParser()
    let ast = cssParser.parse(source)
    ast = cssParser.astConverter(ast)
    return cssParser.astToCss(ast)
}
Copy the code

Implementation converter

Js converter package

// jsparser.js js converter
const parser = require('@babel/parser')
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default

class JsParser{
    constructor() {}

    // code -> ast
    parse(source) {
        let ast = parser.parse(source, {
            sourceType: 'module'
        })
        return ast
    }
    
    // ast syntax tree editing
    astConverter(ast) {
        traverse(ast, {
            MemberExpression(p) {
                let node = p.node
                 // Iterate over the node of the WX method call and replace it with my call
                if (node.object.name == 'wx') {
                    node.object.name = 'my'}}})return ast
    }

    // ast -> code
    astToCode(ast) {
        return generate(ast).code
    }
}
Copy the code

CSS Converter Encapsulation

// cssparser.js CSS converter
const csstree = require('css-tree')

class CssParser {
    constructor() {}

    // code -> ast
    parse(source) {
        const ast = csstree.parse(source)
        return ast
    }

    // ast syntax tree editing
    astConverter(ast) {
        csstree.walk(ast, function(node) {
            if (node.type == 'Atrule' && node.name == 'import') {
                node.prelude.children.forEach(item= > {
                    const value = item.value
                    item.value = value.replace('.wxss'.'.acss')}); }})return ast
    }
    
    // ast -> code
    astToCss(ast) {
        return csstree.generate(ast)
    }
}
Copy the code

Json converter package

// jsonParser.js json converter
const { WINDOWCONVERTERCONFIG } = require('./compares')

class JsonParser{
    constructor() {}
    
    // Replace the attribute key
    parser(source) {
        function replaceAtrr(orginKey, key) {
            const value = source[orginKey]
            source[key] = value
            delete source[orginKey]
        }

        Object.keys(source).forEach(key= > {
            const item = WINDOWCONVERTERCONFIG[key]
            if (item) {
                if (item.target) replaceAtrr(key, item.target)
                item.handler && item.handler(source)
            }  
        })

        return source
    }

}

Copy the code

HTML converter encapsulation

The HTML compiler is more complicated, because its transformation library does not provide the AST to convert HTML, you need to implement it yourself. The reference tables that need to be replaced are also complex. Refer to this article for instructions

// htmltemplateParser.js HTML converter
const htmlparser = require('htmlparser2')   // HTML AST class library

const ATTRCONVERTERCONFIG = {
    'wx:for': {target:'a:for',},'wx:if': {target: 'a:if' },
    'wx:elif': {target: 'a:elif' },
    'else': {target: 'a:else' },
    'wx:else': {target: 'a:else' },
    'wx:for-index': {target: 'a:for-index' },
    'wx:for-item': {target: 'a:for-item' },
    'wx:key': {target: 'a:key' },
    'bindtap': {target: 'onTap' },
    'bindtouchstart': {target: 'onTouchstart' },
    'bindtouchmove': {target: 'onTouchMove' },
    'bindtouchend': {target: 'onTouchEnd' },
    'bindtouchcancel': {target: 'onTouchCancel' },
    'bindlongtap': {target: 'onLongTap' },
    'bindlongpress': {target: 'onLongTap' },
    'catchtap': {target: 'catchTap' },
    'catchtouchstart': {target: 'catchTouchstart' },
    'catchtouchmove': {target: 'catchTouchMove' },
    'catchtouchend': {target: 'catchTouchEnd' },
    'catchtouchcancel': {target: 'catchTouchCancel' },
    'catchlongtap': {target: 'catchLongTap' },
    'catchlongpress': {target: 'catchLongTap'}},function comparesAtrr(attr, key) {
    function replaceAtrr(orginKey, key) {
        const value = attr[orginKey]
        attr[key] = value
        delete attr[orginKey]
    }

    if (ATTRCONVERTERCONFIG[key]) replaceAtrr(key, ATTRCONVERTERCONFIG[key].target)
}

class TemplateParser{
    constructor() {}
	
    // code -> ast
    parse(source){
        return new Promise((resolve, reject) = > {
            const handler = new htmlparser.DomHandler((error, dom) = >{
                if (error) reject(error);
                else resolve(dom);
            });
            let parser = new htmlparser.Parser(handler)
            parser.write(source)
            parser.end()
        })
    }
        
    // ast -> code
    astToString (ast) {
        let str = ' ';
            ast.forEach(item= > {
                if (item.type === 'text') {
                str += item.data;
                } else if (item.type === 'tag') {
                str += '<' + item.name;
                if (item.attribs) {
                    Object.keys(item.attribs).forEach(attr= > {
                    str += ` ${attr}="${item.attribs[attr]}"`;
                    });
                }
                str += '>';
                if (item.children && item.children.length) {
                    str += this.astToString(item.children);
                }
                str += ` < /${item.name}> `; }});return str;
    }
   
    // ast syntax tree editing
    templateConverter(ast){
        for(let i = 0; i<ast.length; i++){let node = ast[i]

          // An HTML node is detected
          if(node.type === 'tag') {// Walk through the node attributes and compare the reference table to see if there are any parts that need to be replaced
            Object.keys(node.attribs).forEach(key= > {
                comparesAtrr(node.attribs, key)
            })
          }
          // Because it is a tree structure, we need to recurse
          if(node.children) this.templateConverter(node.children)
        }
        return ast
    }

}

Copy the code

What I want to say about cross-applet

First of all, let me tell you my opinion on this matter. There are antMove on the market that have done this matter, but in general, the difference can not be completely smoothed, only that the cost of changing the same small program platform will be relatively low. Then I felt that the runtime might have to adapt to n sets of promiscuous rules at the same time by writing each set of methods to distinguish between different platforms. When there was a problem, I did not know which set of rules to follow, and the development experience might not be particularly good. Therefore, I prefer to roughly smooth out the differences by compilation, or I can choose to compile only the updated parts of the content in subsequent iterations. The above is my humble opinion. Please don’t take it too seriously.