Frontend Development 12 min read

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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Demystifying Webpack: Build Your Own Simple JavaScript Bundler

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

require

statements 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

require

call, 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

JavaScriptWebpackmodule bundlerdependency graphcustom bundler
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.