Demystifying Webpack: Build Your Own Simple JavaScript Bundler
This article explains webpack’s core concept as a static module bundler, describes why bundling is needed for browser execution, details how webpack builds a dependency graph and loads modules, and walks through building a minimal custom bundler using Node and Babel to illustrate the underlying principles.
Webpack is a powerful and flexible static module bundler for modern JavaScript applications. It builds a dependency graph of all modules required by the project and generates one or more bundles, which greatly simplifies front‑end engineering.
At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
What Is It
Webpack’s official definition (as shown above) emphasizes three key aspects: a static module bundler, a dependency graph, and bundle generation. In essence, webpack packages JavaScript modules into one or more JavaScript files that can run in the browser.
Why Do We Need It
Most Node.js code follows the CommonJS module system, which cannot be directly executed in a browser. Webpack rewrites
requirestatements and resolves file paths so that the resulting bundles can be loaded with
<script>tags, enabling code reuse across environments.
How It Works
Webpack starts from the entry file(s) defined in the configuration, recursively traverses every
requirecall, and builds a complete dependency graph. Each module is stored in the
__webpack_modules__object, keyed by its resolved path, and wrapped in a function that receives
module,
exports, and
__webpack_require__as arguments.
The
__webpack_require__function loads a module, caches its exports in
__webpack_module_cache__, and returns the cached value on subsequent requests, which prevents re‑execution and handles circular dependencies efficiently.
Simple Implementation
Below is a minimal bundler built with Node.js, Babel parser, and Babel traverse. It reads the entry file, collects dependencies, transforms the AST, and outputs a self‑executing bundle that mimics CommonJS behavior.
<code>const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
// read a module, collect its dependencies and transformed code
function readModuleInfo(filePath) {
filePath = './' + path.relative(process.cwd(), path.resolve(filePath)).replace(/\+/g, '/')
const content = fs.readFileSync(filePath, 'utf-8')
const ast = parser.parse(content)
const deps = []
traverse(ast, {
CallExpression({ node }) {
if (node.callee.name === 'require') {
node.callee.name = '_require_'
let moduleName = node.arguments[0].value
moduleName += path.extname(moduleName) ? '' : '.js'
moduleName = path.join(path.dirname(filePath), moduleName)
moduleName = './' + path.relative(process.cwd(), moduleName).replace(/\+/g, '/')
deps.push(moduleName)
node.arguments[0].value = moduleName
}
},
})
const { code } = babel.transformFromAstSync(ast)
return { filePath, deps, code }
}
// build the dependency graph starting from the entry
function buildDependencyGraph(entry) {
const entryInfo = readModuleInfo(entry)
const graph = [entryInfo]
for (const mod of graph) {
mod.deps.forEach(dep => {
graph.push(readModuleInfo(path.resolve(dep)))
})
}
return graph
}
// generate the final bundle
function pack(graph, entry) {
const modules = graph.map(
module => `"${module.filePath}": function(module, exports, _require_) { eval(\`${module.code}\`) }`
)
return `(() => {
var modules = {${modules.join(',\n')}}
var modules_cache = {}
function _require_(moduleId) {
if (modules_cache[moduleId]) return modules_cache[moduleId].exports
var module = modules_cache[moduleId] = { exports: {} }
modules[moduleId](module, module.exports, _require_)
return module.exports
}
_require_('${entry}')
})()`
}
// entry point
function main(entry = './src/index.js', output = './dist.js') {
fs.writeFileSync(output, pack(buildDependencyGraph(entry), entry))
}
main()
</code>Conclusion
Webpack’s design boils down to starting from an entry point, recursively discovering all dependent modules, and emitting a JavaScript file that implements a CommonJS‑style module loader. Understanding this core workflow makes it easier to grasp advanced features or even build a custom bundler.
References
Concepts – https://webpack.js.org/concepts/
Modules – https://github.com/webpack/webpack/blob/2e1460036c5349951da86c582006c7787c56c543/README.md
Dependency Graph – https://webpack.js.org/concepts/dependency-graph/
Build Your Own Webpack – https://www.youtube.com/watch?v=Gc9-7PBqOC8
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
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.