How to shim legacy dependencies in Webpack (like jQuery)
February 16, 2021 in JavaScriptLegacy code that was not designed for modules is a classic problem with migrating a codebase into Webpack. Case in point — the ActiveAdmin javascript package, which depends on the jquery-ujs package, which hasn’t been updated since 2016. jquery-ujs really wants to see a jQuery
global. So, what to do?
Just shim it
If you’re dealing with code that wants a global variable, just give it the global variable! Webpack does not break global resolution, so code that uses globals will work fine inside a Webpack build.
// jquery-shim.js
import jQuery from "jquery";
window.$ = window.jQuery = jQuery;
// now this code will continue to work
$(() => console.log("Document ready"));
It is tempting to put these shims into the root file of your Webpack entry. However, be aware that imported modules are always evaluated before the module itself. So if you need to shim something for an import, put the shim in an import too:
// root.js
console.log("root is evaluated");
import "shim";
import "legacy_module";
// shim.js
console.log("shim is evaluated");
// legacy_module.js
console.log("legacy_module is evaluated");
will output:
shim is evaluated
legacy_module is evaluated
root is evaluated
As you can see, a shim defined in root.js
will be evaluated too late to take effect. But a shim in a separate import evaluates in time.
This goes for any kind of order-dependent initialization, too — error reporting, etc — don’t put it directly into the root module.
ProvidePlugin
ProvidePlugin is an alternative to creating global variables. It will find free variable usage (like jQuery
), and substitute it with a module import.
Pros: no global variables! It does work with window.
properties too.
Cons: you cannot enable it for one entry only. In my example, the admin JS bundle uses jQuery, but the main app bundle doesn’t. But now any reference to jQuery
will create an dependency. Unfortunately, many legacy libraries will check if jQuery
is present in global scope, to inject their features — even if you don’t use jQuery and don’t need the features. So, the app bundle will inadvertently include a hefty jQuery module.
Also, ProvidePlugin requires code parsing, so it slows down the build — in our case, by about 5%.
My suggestion is to avoid ProvidePlugin unless you absolutely want to avoid exposing the global variables.
Sideload the shim + Externals
You might want to keep the shim outside of the Webpack entry. For example, you could load it from a CDN. Or, many legacy libraries fail to build within Webpack - for a number of reasons. You can load the library with a separate <script>
tag.
As long as the sideloaded script creates a global variable, and your code uses it, you’re good. But if the same bundle has other, modern modules that depend on the jQuery NPM package and do import 'jquery'
— you end up with two jQuery versions. Not only does this bloat the build, but also the jQueries don’t share the same namespace — so each will have a separate set of settings and plugins.
The feature to solve this is Webpack externals. Externals are, in a way, opposite to ProvidePlugin - they let you “reroute” a module import to a global variable. Note, Webpack does nothing to make sure the global is defined - that’s your job.
You would need externals if…
- …you are loading a library into a global variable — from CDN, or a sideloaded script,
- …but you still want to reference it with
import
statements (or have dependencies that do).
If you are shimming a global from a module (like window.$ = require('jquery')
), you don’t need externals.
To summarize
- Try solving the legacy dependency issue by assigning a module import to a global first.
- If you still get “xxx is undefined” errors, check the order of evaluation.
- If the dependency is so problematic that you cannot
import
it, load it in a separate<script>
tag, and use as a global.
And finally, use webpack-bundle-analyzer and the official analyzer to check that you avoided dependency bloat.
Liked the post? Treat me to a coffee