Build Your Own Mini Webpack: A Step‑by‑Step Guide to Module Bundling
This article walks you through the fundamentals of abstract syntax trees, Babel, and Webpack's build process, then shows how to implement a simple mini‑webpack from scratch, install dependencies, configure the project, run the build, and verify the bundled output.
Introduction
While learning Webpack, I found its module‑dependency‑graph approach fascinating and built a simple mini‑webpack that bundles entry files and their dependencies into a single runnable script.
Background Knowledge
Abstract Syntax Tree (AST)
An AST is a tree‑structured representation of source code that enables tools to understand and transform code. It powers IDE features, linting, bundlers like Webpack and Rollup, and transpilers such as Babel.
Online tools like AST Explorer let you visualise the AST of any JavaScript snippet.
Babel
Babel is a toolchain that converts modern ECMAScript (ES2015+) code into backward‑compatible JavaScript. It can:
Transform syntax
Inject missing features via polyfills (e.g.,
core‑js)
Apply codemods to rewrite source code
In a typical Webpack setup,
babel‑loaderparses files into an AST, transforms them, and emits ES5 code.
<code>// Babel input: ES2015 arrow function
[1, 2, 3].map(n => n + 1);
// Babel output: ES5 equivalent
[1, 2, 3].map(function (n) {
return n + 1;
});</code>Webpack Bundling Principle
Webpack builds a project in several steps:
Read the basic configuration (
webpack.config.js)
Analyze the entry file and its dependencies
Parse each module into an AST
Transform the AST (e.g., via Babel)
Generate a dependency graph object
Wrap the graph in a self‑executing function that provides a
requireimplementation
Emit the final bundle file
<code>// mini‑webpack.config.js
const path = require('path');
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "bundle.js"
}
};</code> <code>// Core of the generated bundle (simplified)
(function (graph) {
function require(filename) {
function localRequire(relativePath) {
return require(graph[filename].dependencies[relativePath]);
}
const exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[filename].code);
return exports;
}
require('${entry}');
})(${graph});</code>Implementation Details
1. Install Dependencies
Run:
<code>npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D</code>2. Read Configuration
<code>const path = require('path');
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "bundle.js"
}
};</code>3. Core MiniWebpack Class
<code>class MiniWebpack {
constructor(options) {
this.options = options;
}
// Parse a file into AST, collect dependencies, and transform to code
parse = filename => {
const fileBuffer = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(fileBuffer, { sourceType: 'module' });
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newDirname = './' + path.join(dirname, node.source.value).replace('\\', '/');
dependencies[node.source.value] = newDirname;
}
});
const { code } = babel.transformFromAst(ast, null, { presets: ['@babel/preset-env'] });
return { filename, dependencies, code };
};
// Build the dependency graph starting from the entry
analyse = entry => {
const entryModule = this.parse(entry);
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; ++i) {
const { dependencies } = graphArray[i];
Object.keys(dependencies).forEach(filename => {
graphArray.push(this.parse(dependencies[filename]));
});
}
const graph = {};
graphArray.forEach(({ filename, dependencies, code }) => {
graph[filename] = { dependencies, code };
});
return graph;
};
// Generate bundle code from the graph
generate = (graph, entry) => {
return `
(function(graph){
function require(filename){
function localRequire(relativePath){
return require(graph[filename].dependencies[relativePath]);
}
const exports = {};
(function(require, exports, code){
eval(code);
})(localRequire, exports, graph[filename].code);
return exports;
}
require('${entry}');
})(${graph})
`;
};
// Write the bundle to disk
fileOutput = (output, code) => {
const { path: dirPath, filename } = output;
const outputPath = path.join(dirPath, filename);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
fs.writeFileSync(outputPath, code, 'utf-8');
};
// Run the whole process
run = () => {
const { entry, output } = this.options;
const graph = this.analyse(entry);
const graphStr = JSON.stringify(graph);
const code = this.generate(graphStr, entry);
this.fileOutput(output, code);
};
}
</code>4. Demonstration
Create
src/a.js,
src/b.js, and
src/index.js:
<code>// a.js
export default 1;
// b.js
export default function () {
console.log('I am b');
};
// index.js
import a from './a.js';
import b from './b.js';
console.log(a);
console.log(b);
</code>Add the configuration above to
mini-webpack.config.jsand update
package.jsonwith a
buildscript that runs
node main.js. Then execute:
<code>npm run build</code>The command creates
dist/bundle.js. Running the bundle prints the values from
a.jsand
b.js, confirming the mini‑webpack works.
Project Repository
The source code is available at: mini-webpack
References
Implement a Simple Webpack
Babel Documentation (Chinese)
Understanding AST
Webpack Build Principles and Simple Implementation
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.