How to start moving legacy codebase to webpack

Let's say we inherit a legacy JavaScript project, and it's our job to keep it alive. Often in those situations:

  • you have an old and outdated codebase that is far from current best practices
  • application works and is bringing money to the organization or solving some problem
  • it's too big to even hope for rewriting everything from scratch

In this article, I'll show you how you can start migrating a codebase like this to webpack - so we can replace the old build infrastructure based on half-abandoned projects like Grunt; maybe achieve smaller files to be transferred to the user & keep ourselves up to date with the industry standards.

Legacy codebase

To simplify, our legacy application is index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>webpack</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link
      rel="stylesheet"
      href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"
    />
    <link rel="stylesheet" href="/resources/demos/style.css" />
    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <script>
      $(function () {
        $("#datepicker").datepicker();
      });
    </script>
  </head>
  <body>
    <p>Date: <input type="text" id="datepicker" /></p>
  </body>
</html>

It is an input that uses jQuery UI Datepicker, and it looks like:

legacy-app-start.png

The way it's written has few features that JavaScript bundlers let us move away from:

  • all the dependencies are specified in index.html - so we need to keep it up to date to our JS files
  • we need to know what files are needed by our code, and all the 3rd party dependencies
  • it's our job to load the files in the correct order. In my example, importing the files as:
      <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>                 
      <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
    
    would not work
  • everything is imported to the global namespace - each dependency sees and can use (or mess up with) other dependencies

Migration plan

We could most likely migrate my simple example to webpack in one go. For bigger projects, it's not an option - too many things are interconnected, and it could take a really long time to do it all. It's unlikely we would get approval for spending a week or two on code setup only. Another problem is that this big-bang approach offers very little feedback along the way. We could learn very late that one of the 3rd party libraries we've been using has some problems when built with wepback.

Let's take the smallest step possible to avoid those issues - add webpack, and move jQuery import there.

Adding webpack

First, let's turn the folder we keep our index.html into npm package:

$ npm init -y
Wrote to /home/marcin/workspace/github/tmp/webpack-expose-loader/package.json:

{
  "name": "webpack-expose-loader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

add webpack as a dependency

npm install --save-dev webpack webpack-cli
+ webpack-cli@4.8.0
+ webpack@5.52.1
updated 2 packages and audited 121 packages in 10.088s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Default configuration

To avoid creating wepback.config.js, we make sure we keep on using default locations for the source & output files. So we will have src/index.js:

console.log("Hello!");

and we add to index.html:

    <script src="dist/main.js"></script>

Before all the other JS imports. Then, we add a build script to package.json:

{
  ...
  "scripts": {
    ...
    "build": "webpack --mode=production"
...

and we can build with:

$ npm run build

> webpack-starter@1.0.0 build /home/marcin/workspace/github/webpack-expose-loader
> webpack --mode=production

asset main.js 22 bytes [compared for emit] [minimized] (name: main)
./src/index.js 23 bytes [built] [code generated]
webpack 5.52.1 compiled successfully in 163 ms

The application should work as before, but with a 'test' log in a console.

Adding jQuery as a dependency

To start our migration, let's install jQuery in version 1.12.4 as a dependency:

$ npm install --save jquery@1.12.4
+ jquery@1.12.4
added 1 package from 1 contributor and audited 122 packages in 1.399s

Now, we can import jquery from our src/index.js:

import jQuery from "jquery";

console.log(jQuery);

When we rebuild:

npm run build                       

> webpack-starter@1.0.0 build /home/marcin/workspace/github/webpack-expose-loader
> webpack --mode=production

asset main.js 95.3 KiB [compared for emit] [minimized] (name: main) 1 related asset
runtime modules 663 bytes 3 modules
cacheable modules 287 KiB
  ./src/index.js 51 bytes [built] [code generated]
  ./node_modules/jquery/dist/jquery.js 287 KiB [built] [code generated]
webpack 5.52.1 compiled successfully in 2557 ms

We can see the output main.js is much bigger 95.3 KiB, so we clearly include jQuery in our code. But if we remove from index.html:

    <script src="https://code.jquery.com/jquery-1.12.4.js"></script>

our date picker will be broken, and we will see in the console log:

import-errors.png

That is because webpack is isolating modules. The imports are not polluting the global scope, and each module can access only things it explicitly imported. It's a good thing in the long term - it helps us avoid invisible coupling between modules, which can be very confusing. But in baby-step refactoring, we need to work around it.

expose-loader

expose-loader is a webpack loader that allows us to pollute a global scope with the import from a given file. To use it first, we need to install it as a dev dependency:

$ npm install --save-dev expose-loader
+ expose-loader@3.0.0
added 1 package from 1 contributor and audited 123 packages in 1.926s

Then we should change the import line in our src/index.js:

import jQuery from "expose-loader?exposes=$,jQuery!jquery";

console.log(jQuery);

The code means as follow:

  • import jQuery from is a part of the import that makes it usable in the current file
  • "expose-loader! is a special import syntax understood by wepback. It picks the loader to be used for the import that is specified after !
  • ?exposes=$,jQuery option provided to the loader. In this case, we want the default export of the file after ! to be put on the global scope as $ (for the script in index.html) and as jQuery - for the plugin
  • !jquery what we are importing

With those changes in place, after building, the application should work as before.

Links

Summary

We have walked through an example of how to start using a webpack in a legacy project. Let me know in the comment what experience with migration to webpack you have had so far.