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.

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(',
')}}
    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()

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

webpackdependency 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

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.