DEV Community

Marcin Wosinek
Marcin Wosinek

Posted on • Updated on • Originally published at how-to.dev

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!");
Enter fullscreen mode Exit fullscreen mode

and we add to index.html:

    <script src="dist/main.js"></script>
Enter fullscreen mode Exit fullscreen mode

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

{
  ...
  "scripts": {
    ...
    "build": "webpack --mode=production"
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

import jQuery from "jquery";

console.log(jQuery);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

console.log(jQuery);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
akpanowo profile image
akpanowo

Hello Marcin, tried replicating this for a react app and it doesn't work. How does one go about that?

Collapse
 
marcinwosinek profile image
Marcin Wosinek

Was the app generated by create react app (CRA)? If so, there is webpack hidden behind the CRA scripts - if you want to modify the configuration, one of the approaches would be eject the application.

If it's some other React app, try sharing some minimal error example - I could try taking a look on it then.