Build Your Own Mini Webpack: A Step‑by‑Step Guide to Single‑Entry Bundling

This article walks through creating a minimal webpack implementation that bundles a single entry file, covering prerequisite knowledge, initialization parameters, compilation steps, loader handling, dependency resolution, chunk creation, and file generation, complete with code examples and explanations.

ELab Team
ELab Team
ELab Team
Build Your Own Mini Webpack: A Step‑by‑Step Guide to Single‑Entry Bundling

Introduction

Webpack is a widely used bundling tool that packages many files so they can run in a browser. This guide implements a simple version of webpack that supports bundling a single entry file, without plugins or code splitting.

Prerequisite Knowledge

Consider the following demo: an index.js that requires a.js, which in turn requires b.js. Using the simplest webpack configuration, index.js is set as the entry point.

// index.js
require('./a.js');
console.log('entry load');

// a.js
require("./b.js");
const a = 1;
console.log("a load");
module.exports = a;

// b.js
console.log("b load");
const b = 1;
module.exports = b;

The bundled output is an immediately‑executed function that defines each module and implements a custom __webpack_require__ to replace the original require calls, enabling synchronous module loading in the browser.

Initialization Parameters

According to the Node API documentation, webpack exposes a webpack function that accepts a configuration object and returns a compiler instance. The compiler provides a run method to start the compilation.

const webpack = require('webpack');
const compiler = webpack(options);
compiler.run((err, stats) => {
  // compilation callback
});

In our mini‑webpack we expose a similar function that receives user options (entry, output, etc.).

// mini-webpack/core/index.js
function webpack(options) {
  const compiler = new Compiler(options);
  return compiler;
}
module.exports = webpack;

Compilation Process

Read the entry file and pass it through matching loaders to obtain transformed code.

Compile the transformed code.

Replace each require with the custom __webpack_require__, record dependencies, and recursively process them.

After all modules are processed, organize the results starting from the entry file.

Entry File Loader Handling

// mini-webpack/compiler.js
const fs = require('fs');
class Compiler {
  constructor(options) {
    this.options = options || {};
    this.modules = new Set();
  }
  run(callback) {
    const entryChunk = this.build(path.join(process.cwd(), this.options.entry));
  }
  build(modulePath) {
    let originCode = fs.readFileSync(modulePath);
    originCode = this.dealWidthLoader(modulePath, originCode.toString());
    return this.dealDependencies(originCode, modulePath);
  }
  // Pass source code through matching loaders
  dealWidthLoader(modulePath, originCode) {
    [...this.options.module.rules].reverse().forEach(item => {
      if (item.test(modulePath)) {
        const loaders = [...item.use].reverse();
        loaders.forEach(loader => originCode = loader(originCode));
      }
    });
    return originCode;
  }
}
module.exports = Compiler;

Entry File Processing

The entry file’s dependencies are collected, and each require is replaced with __webpack_require__ using Babel’s AST traversal.

// Inside Compiler class
dealDependencies(code, modulePath) {
  const fullPath = path.relative(process.cwd(), modulePath);
  const module = { id: fullPath, dependencies: [] };
  const ast = parser.parse(code, { sourceType: "module", ast: true });
  traverse(ast, {
    CallExpression: (nodePath) => {
      const node = nodePath.node;
      if (node.callee.name === "require") {
        const requirePath = node.arguments[0].value;
        const moduleDirName = path.dirname(modulePath);
        const fullPath = path.relative(path.join(moduleDirName, requirePath), requirePath);
        node.callee = t.identifier("__webpack_require__");
        node.arguments = [t.stringLiteral(fullPath)];
        const exitModule = [...this.modules].find(item => item.id === fullPath);
        if (!exitModule) {
          module.dependencies.push(fullPath);
        }
      }
    },
  });
  const { code: compilerCode } = generator(ast);
  module._source = compilerCode;
  return module;
}

Dependency Processing

After handling the entry file, its recorded dependencies are recursively built and added to the module set.

// Continuation of dealDependencies
module._source = compilerCode;
module.dependencies.forEach(dependency => {
  const depModule = this.build(dependency);
  this.modules.add(depModule);
});
return module;

Chunk Creation

// mini-webpack/compiler.js (continued)
buildChunk(entryName, entryModule) {
  return {
    name: entryName,
    entryModule: entryModule,
    modules: this.modules,
  };
}

File Generation

With all modules compiled, the final step writes the bundled code to the output path, preserving the basic structure of a real webpack bundle.

// mini-webpack/compiler.js (continued)
run(callback) {
  const entryModule = this.build(path.join(process.cwd(), this.options.entry));
  const entryChunk = this.buildChunk("entry", entryModule);
  this.generateFile(entryChunk);
}

generateFile(entryChunk) {
  const code = this.getCode(entryChunk);
  if (!fs.existsSync(this.options.output.path)) {
    fs.mkdirSync(this.options.output.path);
  }
  fs.writeFileSync(
    path.join(this.options.output.path, this.options.output.filename.replace("[name]", entryChunk.name)),
    code
  );
}

getCode(entryChunk) {
  return `
    (() => {
      // webpackBootstrap
      var __webpack_modules__ = {
        ${Array.from(entryChunk.modules).map(module => `"${module.id}": (module, __unused_webpack_exports, __webpack_require__) => {${module._source}}`).join(',')}
      };
      var __webpack_module_cache__ = {};
      function __webpack_require__(moduleId) {
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = (__webpack_module_cache__[moduleId] = { exports: {} });
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        return module.exports;
      }
      var __webpack_exports__ = {};
      (() => {
        ${entryChunk.entryModule._source}
      })();
    })()
  `;
}

Running the generated bundle in a browser produces the expected output, demonstrating a functional minimal webpack that handles a single entry file. Real webpack is far more complex, but this implementation captures its core bundling idea.

JavaScriptcompilerBuild ToolWebpackBundlernode
ELab Team
Written by

ELab Team

Sharing fresh technical insights

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.