Handwritten simple webpack

Posted by michealholding on Wed, 05 Jan 2022 03:57:36 +0100

Compile product analysis

 (() => {
   // Module dependency
 var __webpack_modules__ = ({

     "./src/index.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
       // Execute the module code, where it is executed at the same time__ webpack_require__  Reference code
             eval(`const str = __webpack_require__("./src/a.js");

console.log(str);`);
         }),

     "./src/a.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
             eval(`const b = __webpack_require__("./src/base/b.js");

module.exports = 'a' + b;`);
         }),

     "./src/base/b.js":
         ((module, __unused_webpack_exports, __webpack_require__) => {
             eval(`module.exports = 'b';`);
         }),

 });
 var __webpack_module_cache__ = {};
 function __webpack_require__(moduleId) {
   // Get_ webpack_module_cache__  Is there an exports value 
     var cachedModule = __webpack_module_cache__[moduleId];
   // If you already have it, you don't have to execute the module code
     if (cachedModule !== undefined) {
         return cachedModule.exports;
     }
     var module = __webpack_module_cache__[moduleId] = {
         exports: {}
     };
   // According to the moduleId module file path, find the module code and execute the incoming module, module exports, __ webpack_ require__
     __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

     return module.exports;
 }
   // Execute entry file code
 var __webpack_exports__ = __webpack_require__("./src/index.js");
 })()

The above code is simplified. You can see the following tool functions

  • __ webpack_modules__: Is an object, its value is the code of all modules, and the key value corresponds to the module file path
  • __ webpack_module_cache__: Cache the value of exports
  • __ webpack_require__: Load the module code according to the module file path
  • __ webpack_exports__: Module external exposure method

Through the above tools and methods, you can run in the browser; From the source code es6 and es7, new features and new writing methods need to be transformed into code recognized by the browser;

For example:

// es6
import 

// es5 
__webpack_require__

Webpack through customization__ webpack_require__,__ webpack_exports__ ..., Implement multiple module code packaging.

Next, we will build a simple version of webpack according to the above logic through the following stages

  1. configuration information
  2. Dependency build
  3. Generate template code
  4. Generate file

configuration information

class Compiler {
  constructor(config) {
    // Get configuration information
    this.config = config;
    // Save entry path
    this.entryId;
    // Module dependencies
    this.modules = {};
    // Entry path
    this.entry = config.entry;
    // Working path
    this.root = process.cwd();
  }

Build dependency

getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    return content;
  }
buildModule(modulePath, isEntry) {
    // Get module content
    const source = this.getSource(modulePath);
    // Module id
    const moduleName = './' + path.relative(this.root, modulePath);
    if (isEntry) {
      this.entryId = moduleName;
    }
    // To parse the source code, you need to transform the source code and return a dependency list
    const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
    // Match the relative path with the content in the module
    this.modules[moduleName] = sourceCode;
    dependencies.forEach((dep) => { // Recursive loading module
      this.buildModule(path.join(this.root, dep), false)
    })
  }

Analyze the source code through buildModule to form a module dependent on this modules[moduleName;

  • Find the module source code this getSource(modulePath);
  • Parse the source code, convert the ast, return the source code and module dependency path this parse(source, path.dirname(moduleName))
  • Generate path and module code object: this modules[moduleName] = sourceCode
  • For the dependent files in the module, form an iterative call this Buildmodule (path. Join (this. Root, DEP), false) re executes the above method

Parsing source code

  parse(source, parentPatch) { // AST parsing syntax tree
    const ast = babylon.parse(source);
    let dependencies = []; // Dependent array
    traverse(ast, {
      CallExpression(p) {
        const node = p.node;
        if (node.callee.name == 'require') {
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value; // Module name
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js
          moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    });
    const sourceCode = generator(ast).code;
    return {
      sourceCode, dependencies
    }

  }

Parse the module source code and replace the require method with__ webpack_require__, At the same time, the file path is also converted

Code generation template

// ejs template code
(() => {
var __webpack_modules__ = ({
<%for(let key in modules){%>
    "<%-key%>":
    ((module, __unused_webpack_exports, __webpack_require__) => {
      eval(`<%-modules[key]%>`);
     }),
<%}%>
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};

__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

return module.exports;
}
var __webpack_exports__ = __webpack_require__("<%-entryId%>");
})()
;

Will put this modules,this.entryId data is passed into this template to generate executable code

Generate file

  emitFile() {
    const {output} = this.config;
    const main = path.join(output.path, output.filename);
    // Module string
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    // Generate code
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
    this.assets = {};
    this.assets[main] = code;
    // Write code to output folder / file
    fs.writeFileSync(main, this.assets[main])
  }

loader

Convert referenced resources into modules
 getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
        const rule = rules[i];
        const {test,use} = rule;
        let len = use.length -1
        if(test.test(modulePath)) {
          function normalLoader() {
            const loader = require(use[len--]);
            content = loader(content);
            if(len >= 0) {
              normalLoader();
            }
          }
          normalLoader();
        }
    }
    return content;
  }

Obtain the source code according to the path and judge whether the current path can match the loader file test test(modulePath),

If it can be matched, pass the module source code into the loader method, and then make other transformations. content = loader(content); And form recursive call;

// Custom loader

// less-loader
const {render} = require('less')
function loader(source) {
  let css = '';

  render(source,(err,c) => {
    css = c;
  })
  css = css.replace(/\n/g,'\\n')
  return css;
}

module.exports = loader;

// style-loader

function loader(source) {
 let style = `
  let style = document.createElement('style')
  style.innerHTML = ${JSON.stringify(source)}
  document.head.appendChild(style);
 `;

 return style;
}
module.exports = loader;

configuration file

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle2.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.less$/,
        use:[
          path.resolve(__dirname,'loader','style-loader'), // Post execution
          path.resolve(__dirname,'loader','less-loader') // Execute first
        ]
      }
    ]
  },
}

plugin

In terms of form, a plug-in is usually a class with an apply function:

class SomePlugin {
    apply(compiler) {
    }
}

When the apply function runs, it will get the parameter compiler, which can be used as a starting point to call the hook object to register various hook callbacks,

For example: compiler hooks. make. tapAsync, where make is the hook name. tapAsync defines the call method of the hook,

The plug-in architecture of webpack is built based on this pattern. Plug-in developers can use this pattern to insert specific code into hook callbacks

configuration file

const path = require('path');

class P {
  constructor() {

  }
  apply(compiler) {
    // Get the method on the compiler and register callbacks at various stages
    compiler.hooks.emit.tap('emit',function () {
      console.log('emit')
    })
  }
}

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle2.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new P()
  ]
}

compiler.js

const {SyncHook} = require('tapable');
class Compiler {
  constructor(config) {
    this.config = config;
    // Save entry path
    this.entryId;
    // Module dependencies
    this.modules = {};
    // Entry path
    this.entry = config.entry;
    // Working path
    this.root = process.cwd();
    // Start registering synchronous publications and subscriptions
    this.hooks = {
      entryOption:new SyncHook(),
      compile:new  SyncHook(),
      afterCompile:new SyncHook(),
      afterPlugins:new SyncHook(),
      run:new SyncHook(),
      emit:new SyncHook(),
      done:new SyncHook()
    };

    const plugins = this.config.plugins;
    // Get the plugin in the configuration item 
    if(Array.isArray(plugins)) {
      plugins.forEach((plugin) => {
        // Call the instance method apply in the plugin and pass in the entire Compiler class
        plugin.apply(this);
      })
    }
    this.hooks.afterPlugins.call();
  }

The core of the plugin is that tapable adopts the publish / subscribe mode. First collect / subscribe the callbacks required in the plug-in and execute them in the webpack life cycle, so that the plug-in can obtain the desired context at the time of use, so as to intervene and other operations.

The above is the key core code of each stage

Complete code

const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
const {SyncHook} = require('tapable');
// babylon parsing js transform ast
// https://www.astexplorer.net/
// @babel/travers
// @babel/types
// @babel/generator
class Compiler {
  constructor(config) {
    this.config = config;
    // Save entry path
    this.entryId;
    // Module dependencies
    this.modules = {};
    // Entry path
    this.entry = config.entry;
    // Working path
    this.root = process.cwd();

    this.hooks = {
      entryOption:new SyncHook(),
      compile:new  SyncHook(),
      afterCompile:new SyncHook(),
      afterPlugins:new SyncHook(),
      run:new SyncHook(),
      emit:new SyncHook(),
      done:new SyncHook()
    };

    const plugins = this.config.plugins;
    if(Array.isArray(plugins)) {
      plugins.forEach((plugin) => {
        plugin.apply(this);
      })
    }
    this.hooks.afterPlugins.call();
  }

  getSource(modulePath) {
    const rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
        const rule = rules[i];
        const {test,use} = rule;
        let len = use.length -1
        if(test.test(modulePath)) {
          function normalLoader() {
            const loader = require(use[len--]);
            content = loader(content);
            if(len >= 0) {
              normalLoader();
            }
          }
          normalLoader();
        }
    }
    return content;
  }

  parse(source, parentPatch) { // AST parsing syntax tree
    const ast = babylon.parse(source);
    let dependencies = []; // Dependent array
    traverse(ast, {
      CallExpression(p) {
        const node = p.node;
        if (node.callee.name == 'require') {
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value; // Module name
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js
          moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    });
    const sourceCode = generator(ast).code;
    return {
      sourceCode, dependencies
    }

  }

  buildModule(modulePath, isEntry) {
    // Get module content
    const source = this.getSource(modulePath);
    // Module id
    const moduleName = './' + path.relative(this.root, modulePath);
    if (isEntry) {
      this.entryId = moduleName;
    }
    // To parse the source code, you need to transform the source code and return a dependency list
    const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
    // Match the relative path with the content in the module
    this.modules[moduleName] = sourceCode;
    dependencies.forEach((dep) => { // Recursive loading module
      this.buildModule(path.join(this.root, dep), false)
    })
  }

  emitFile() {
    const {output} = this.config;
    const main = path.join(output.path, output.filename);
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
    this.assets = {};
    this.assets[main] = code;
    fs.writeFileSync(main, this.assets[main])
  }

  run() {
    this.hooks.run.call();
    this.hooks.compile.call();
    // Execute and create dependencies on the module
    this.buildModule(path.resolve(this.root, this.entry), true);
    this.hooks.afterCompile.call();
    // Launch a file, packaged file
    this.emitFile();
    this.hooks.emit.call();
    this.hooks.done.call();
  }
}

module.exports = Compiler;

github link:
https://github.com/NoahsDante...
If it helps you, click start

Topics: node.js Front-end Webpack source code analysis