How to build your own module bundler
What is a Module Bundler?
Module bundlers help us to bundle our code they can do more than bundling a code example like webpack it can even do code splitting so that users only download required files instead of over fetched the resources.
How module bundler works under the hood ?
Let’s learn it by building a simple module bundler. first, we need to install some dependencies.
npm init -y
npm install @babel/parser babel-core babel-traverse babel-preset-env
we are using babel parser to parse our code into Abstract syntax tree(AST).
checkout astexplorer.net it is a good resource to check various Ast’s.
Folder structure
- Create two new modules which are add.js, answer.js.
Create a packup.js file in your folder and add below code.
const fs = require("fs");
const path = require("path");
const babelParser = require("@babel/parser");
const babelTraverse = require("babel-traverse");
const { transformFromAst } = require("babel-core");
First, we need to read the file and find it’s dependencies we are using babel parser to parse our code and transform it into ast(abstract syntax tree) by using babelTraverse
we are finding its dependencies.
function createAsset(filename) {
let getData = fs.readFileSync(filename, "utf-8");
let ast = babelParser.parse(getData, {
sourceType: "module"
});
const dependencies = [];
let genrateDependencies = babelTraverse.default(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
}
});
let { code } = transformFromAst(ast, null, {
presets: ["env"] //applying the presets it means converting to es5 code
});
return {
filename,
dependencies,
code
};
}
Next is the dependency graph by using this we are finding which module is depending upon which modules.
function dependencyGraph(entry) {
const initialAsset = createAsset(entry);
//collecting all assets
const assets = [initialAsset];
for (const asset of assets) {
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach(relativePath => {
// getting the extension name of file example .js
const extname = path.extname(asset.filename);
//generating the absolute path
const absolutePath = path.join(dirname, relativePath + extname);
const childAsset = createAsset(absolutePath);
childAsset.filename = relativePath + extname;
assets.push(childAsset);
});
}
return assets;
}
The final thing is bundling we need to bundle all of our modules by using the dependency graph.
function bundle(graph) {
let modules = '';
// for each module we are creating key-value pairs where
// the key is the filename of that module and value is its code.
graph.forEach(mod => {
modules += `${JSON.stringify(mod.filename.replace(/.js$/gi, ""))}: [
function ( module, exports,require) {
${mod.code}
}
],`;
});
// final thing is we are creating an Immediately invoking function expression
or IIFE where that function accepts the modules in its parameter.
var result = `(function (modules) {
function require(name) {
const [fn] = modules[name];
const module={},exports={};
fn(module, exports,(name)=>require(name));
return exports;
}
require("./getSum");
})({${modules}})`;
return result; //finally we are returning the IIFE with modules
}
Let me explain you more detailed by showing our module bundler generated code.
const graph = dependencyGraph("./getSum.js");
//creating the bundle.js file and adding generated code.
fs.appendFile("bundle.js", bundle(graph), err => {
if (err) throw err;
console.log("bundle.js created");
});
It is the generated code from our module bundler you can check it by copy and paste it in your browser’s console.
What above code does is we are passing the module’s object as its argument. By default, we need to pass our initial filename to the require function so that require function start accepting the modules. let me show you by adding the debugger.
This is just an example of how it works behind the scenes for highly optimized builds use webpack.