Build Your Own JS Code Bundler

 4 months ago
source link: https://blog.bitsrc.io/building-your-first-bundler-99e4fdf502b2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Build Your Own JS Code Bundler

Our bundler will: read all JavaScript files inside our project, calculate dependency graph, and grab code from all relevant files.

Photo by Joanna Kosinska on Unsplash

If you’re a front-end developer, you’re using bundlers whether you know it or not.

It’s a tool that is embedded into every development workflow, there are plenty of options out there, but pretty much they all do the same thing, they scan your files, and try to bundle them up into a single file. That way the browser downloads a single file with all the code and is able to cache it, so next time it needs it, it’ll load even faster.

I personally believe developers should understand the tools they use, whether they might or might not end up building their own, it’s important if they ever need to attempt any type of advanced operation with them. And a perfect way of learning about these tools, is to reverse-engineer how they work and build them, even if it’s a simpler, plainer version of the tool they use every day.

So today, I’m going to show you how to build a basic bundler that takes your multiple JavaScript files and bundles them all together into a single one that you can serve to the browser.

What is our bundler going to do?

The key operations that we’ll want our bundler to take care of, are:

  1. Reading all JavaScript files inside our project.
  2. Calculating the dependency graph of our project.
  3. Grab the code of all the relevant files, bundle them up together and make sure the code is not duplicated.

The end result will be a single file, but the source project for this example, is going to be a set of individual JavaScript files:

Our entry point will be the first file from the list, the index.js file. We’ll use it to start the dependency calculation.

Reading all JS files inside our project

Reading all files inside a folder can be a complex task, especially if your folder structure is not straightforward.

This is why we’re going to take advantage of the jest-haste-map package:

This package is going to let us read all files, and calculate their full paths (which is not trivial) and later, when we need to calculate the dependency graph, it’ll also come in handy.

The above code is setting the folder src inside our current project as the root folder where our code should reside. The output for that code in my case, is as follows:

Now that we have the list of files, we also need to calculate how they’re related to each other.

Calculating the dependency graph

For this task, we’ll add two more Jest-based packages: jest-resolve-dependencies and jest-resolve .

I know we’re not building a testing library here, but these packages really come in handy for what we’re trying to build, so why not use them?!

So now, let’s add the resolver logic

I’m using a Set to calculate the files that have been already processed, mostly to avoid circular dependencies. In fact, if you pay attention to the import statements from the code, you’ll notice that if we’re not careful, we can accidentally fall into an infinite loop if we’re not careful.

During this process, I’m also creating a dependencyMap where I map the name used to import each file, with a set of properties, like the full path of the file and its code. And for the code, you’ll also notice that when I read it, I’m also removing all export statements (lines 37– 44), since our code will not export anything.

The output of this section will give us the code for every file we need. The only thing missing is the bundle it all together, and somehow make sure that the import statements get replaced by the corresponding code, without adding duplicated code when the same import statement is found.

Bundling everything together

The output of this section should be the final code, and the aim here is to:

  1. Iterate through all of our files in reverse order (so the entry point is the last one to process).
  2. For every import statement, we’ll replace it with the actual code it’s trying to import (as long as we haven’t done so for the same import before).
  3. Put the final output together and print it out.

We’ll start by iterating over all our files in reverse order and “clean” the code by replacing the undesired import statements:

For every file we’re reading, we’re accessing the dependency map to get their code (which we read earlier), we’ll clean it by calling the cleanCode function.

And we’ll add the code into an array, which we’ll later join together.

The cleanCode function will take care of finding all import statements and replacing them with the proper code using the dependencyMap we created before:

This function is long, but it’s quite simple, it’s essentially looking for every import statement inside a piece of code and when it finds it, it’ll try to check if that module was already inserted into the code (if it’s inside the insertedModules Set or not). If it is, then it’ll just remove the line because it’s not needed. And if it hasn’t, then it’ll get the code through the dependencyMap and mark the module as inserted.

At this point, we’ll have an array of code snippets with no import nor export statements, except in the case where the first code snippet inserted has, itself, imports (like in our example). This is why we need to perform one final cleaning of the code with a single line:

This line takes care of doing one final review of the code, and any pending import will be cleaned and the code will be returned and saved into the fullCode variable.

We can then print it out. In the case of this example, the produced output is the following:

You’ll notice the comments added, where the import statements weren’t needed anymore. And the comments with the full path to a file are an indication of a file that wasn’t needed anymore because it had already been inserted through a previous import replacement.

You can serve this output as a JS file and in the console of your browser, you’ll see the following output:


The bundler is still far from done, since you can keep adding more improvements and optimizations. This code doesn’t care for name collision, which is a problem that could arise if your files declare variables or functions with the same names. You’d have to change a bit the way the final code is generated to ensure that doesn’t happen.

As an added improvement, you could also add tree-shaking logic, where you don’t really add imports or functions that you don’t use. If you pay attention, the utils.js file is importing 2 files that it’s not actually using. Granted, those files are used somewhere else, so they end up inside the final bundle, but if they weren’t, they would be added anyway, because there is no logic checking for their usage. That could be improved to decrease the final bundle size.

While this code can be greatly improved, I hope you got a basic idea of what your favorite bundlers are doing behind the scene and how you could, potentially, create your own if you had needs that weren’t met by any of the current alternatives.

If you’d like to get the full source code of this project, you can do so in this GitHub repo.

Go composable: Build apps faster like Lego


Bit is an open-source tool for building apps in a modular and collaborative way. Go composable to ship faster, more consistently, and easily scale.

Learn more

Build apps, pages, user-experiences and UIs as standalone components. Use them to compose new apps and experiences faster. Bring any framework and tool into your workflow. Share, reuse, and collaborate to build together.

Help your team with:


Design Systems

Code-Sharing and reuse


About Joyk

Aggregate valuable and interesting links.
Joyk means Joy of geeK