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:
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:
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 inindex.html
) and asjQuery
- 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)
Hello Marcin, tried replicating this for a react app and it doesn't work. How does one go about that?
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.