Upgrading an AngularJS 1.x app to use Webpack

I have an AngularJS 1.x app that I built a few years back, and as part of doing some updates, I thought it would be worthwhile to use Webpack to bundle and improve the deployment approach.

I first took a look at following this guide which was from a few years back

… but ran into this error:

Error: webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.

This was my starting point for my webpack.config.js:

var webpack = require('webpack');
module.exports = {
context: __dirname + '/app',
entry: {
app: './js/SpotVizApp.js',
vendor: ['angular']
},
output: {
path: __dirname + '/js',
filename: 'app.bundle.js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin(/* chunkName= /"vendor", / filename= */"vendor.bundle.js")
]
};

It seems like some plugins used in earlier versions of Webpack are now included in base features of Webpack 4, so following this guide here it seems we can narrow this down and address the error earlier to this config:

var webpack = require('webpack');
const path = require('path');
module.exports = {
entry: './src/app/js/SpotVizApp.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};

When webpack starts reading dependencies from my index.html entry point, I now need to tell it where the other source dependencies are, so at the bottom of my app.js I also added:

require('./MapVizControllers');
require('./MapVizServices');
require('./SpotDataControllers');

… and at the bottom of each of these 3 files, exported the module like this:

module.exports = SpotVizControllers;

Now running webpack, it picks up each of the source .js files and bundled together into a single bundle:

$ npm run bundle
spotviz@1.0.0 bundle /Users/kev/develop/AmateurRadioCallsignSpotHistory/spotviz-angularjs-webpacktest
webpack --mode production
Hash: 3e2905ee6f48f56e35b7
Version: webpack 4.41.2
Time: 141ms
Built at: 10/27/2019 2:25:31 PM
Asset Size Chunks Chunk Names
bundle.js 10.4 KiB 0 [emitted] main
Entrypoint main = bundle.js
[0] ./src/app/js/SpotVizApp.js 2.28 KiB {0} [built]
[1] ./src/app/js/MapVizControllers.js 20.6 KiB {0} [built]
[2] ./src/app/js/MapVizServices.js 794 bytes {0} [built]
[3] ./src/app/js/SpotDataControllers.js 2.2 KiB {0} [built]

Ok, now things are looking good, but we’re missing other 3rd party module dependencies, other static files like .css and images, and we also need to update the index.html to refer to the new bundle file.

Adding a loader for CSS files per steps here, using a webpack module:

module: {
rules: [
{
test: /.css$/,
use: 'css-loader'
}
]
}

… and then add an import to my css in app.js so webpack can find it and include it in the bundle:

import '../css/callsignviz.css';

… at this point, the css is bundled but when the index.html loads, I get 404s on the .css files, and for that we also need to add the style loader, so now the config looks like this:

{
test: /.(s*)css$/,
use: [
{
// Adds CSS to the DOM by injecting a <style> tag
loader: 'style-loader'
},
{
// Interprets @import and url() like import/require() and will resolve them
loader: 'css-loader'
}
]
}

We need to generate a new index.html that is updated with new references to the bundled, files. following the steps here, I added this plugin:

plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]

Ok, now I’ve got all my dependencies references as require(‘example.js’) in my main app.js, and now I’m moving on to resolving a 404 issue with AngularJS template files.

I added webpack config for the html templates:

{
test: /\.html$/, use: [
{
loader: 'html-loader'
}
],
}

Next, image files are getting 404:

ERROR in ./src/about.html
Module not found: Error: Can't resolve './images/wildfly_logo_200px.png' in '/Users/kev/develop/AmateurRadioCallsignSpotHistory/spotviz-angularjs-webpacktest/src'

So I add file-loader like this:

        {
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader'
}
],
},

This next error was unusual one, to do with Angular’s bundled jqlite, in place of a full version of jQuery:

Error: [jqLite:nosel] Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element
https://errors.angularjs.org/1.7.8/jqLite/nosel
at angular.js:138
at Object.JQLite as element

I think this is related to the need to load jQuery first before the AngularJS library (I’d see similar errors in an app prior to Webpack). This this post here, the solution was to force loading of jQuery with another Webpack plugin:

    new webpack.ProvidePlugin({'window.jQuery': 'jquery'}),

Although I didn’t know until later (see below re. the cal-heatmap library issues), I believe what ProvidePlugin is doing is allowing you to expose a library as a global, in this case by attaching jquery to window as jQuery.

This also showed up in at least 2 other areas in other libraries with a similar error for the same reason, saying jQuery is not defined but it’s just that it’s not visible in global scope. In this example a Bootstrap js library is expecting to find it defined:

Uncaught ReferenceError: jQuery is not defined
at Object../node_modules/bootstrap/js/transition.js (bundle.js:formatted:75970)
at webpack_require (bundle.js:formatted:37)

I ended up with 3 different configs to resolve the jQuery global related issues, like this:

new webpack.ProvidePlugin({
'window.jQuery': 'jquery',
$: "jquery",
jQuery: "jquery"
}),

Next, an issue with the moment library that I hadn’t seen before that required including the angular-moment library to provide the ‘angularMoment’ module, and then injecting it into each controller where needed.

Next problem at runtime, one of my pages uses ng-include directives, and on this one page I get 404s trying to load these templates in this file:

<div ng-include src="./historyPlaybackControls.html">
</div>


<div ng-include src="./visualizationPlayback.html">
</div>

Similar to how all other templates and static resource need to be referenced with an import or require and have their own Webpack loader to ensure they can be loaded at runtime from the bundle, these templates needed to be loaded by the ngtemplate-loader, configured like this:

        {
test: /historyPlaybackControls\.html$/,
use: [
{
loader: 'ngtemplate-loader'
}
],
},

And then changed in template to reference the 2 files like this:

<div ng-include src="'/src/app/mapviz/historyPlaybackControls.html'">
</div>

<div ng-include src="'/src/app/mapviz/visualizationPlayback.html'">
</div>

This working solution I pieced together from multiple articles, none completely gave me a working solution, but I got a working approach by combining the tips in each of these references, here, here, here, and here.

Next, the single remaining that I probably spent the most time looking into, before I found a description of what the problem actually was, and how it’s solved with Webpack’s ProvidePlugin (which I already used to solve the jQuery issues above, not understanding at the time what that plugin was doing on what issue it was solving).

The issue was with the cal-heatmap library, used by the angular-cal-heatmap-directive library, and looks like this:

Uncaught ReferenceError: CalHeatMap is not defined
at Module../src/app/js/SpotVizApp.js (bundle.js:formatted:135650)
at webpack_require (bundle.js:formatted:37)

Without understanding the problem, I went off on wild goose chase with loading angular-cal-heatmap-directive as a module using bower (because it doesn’t exist in npm), but the other 2 dependencies this library needs, d3 and cal-heatmap are available in npm. Initially I thought that including the libraries that had been installed locally with the different package managers was something to do with the issue (it’s not, the same .js exists as a file regardless of whether it was downloaded by bower or npm), so I tried to get the 3 of these all loaded by bower. This meant I needed to add this config:

resolve: {
modules: ["node_modules", "bower_components"],
descriptionFiles: ['package.json', 'bower.json'],
alias: {
'd3': path.resolve(__dirname, 'bower_components/d3/d3.js'),
'cal-heatmap': path.resolve(__dirname, 'bower_components/cal-heatmap/cal-heatmap.js'),
'angular-cal-heatmap': path.resolve(__dirname, 'bower_components/angular-cal-heatmap-directive/dist/1.3.0/calHeatmap.min.js')
}
},

… adding bower_components and it’s config file bower.json so Webpack knows to look there for modules, and the declaring aliases for d3, cal-heatmap and angular-cal-heatmap that each point to where their .js file needs to get loaded from.

This part is all good, but I’ve still got the “ReferenceError: CalHeatMap is not defined” error. At this point with some debugging I realized I could reference ‘new CalHeatMap()’ in the very top of my app.js, but it was not visible in any of my controllers., giving me the hint that this was a scope issue. After trying many (too many) random variations importing it with ‘import * as CalHeatMap from ‘cal-heatmap’ and other syntax combinations, some googling about weback and libraries with global scope vars led me to some tips about Webpack’s ProvidePlugin (here, and docs here). Adding one more config line to my ProvidePlugin like this:

CalHeatMap: "CalHeatMap"

… did the trick. It exposes CalHeatMap as a global var, so where the angular-cal-heatmap-directive library is looking to call ‘new CalHeatMap()’ it finds it in global scope as expected and resolves this issue.

One last error – after building my production minified build with webpack, at app startup I got this error:

Uncaught Error: [$injector:modulerr] Failed to instantiate module SpotVizApp due to:
Error: [$injector:unpr] Unknown provider: e

It seems this is to do with injected server names in AngularJS controllers getting minified and loosing reference, as described here. This also applied to how I configured my router, so I needed to use the same passing dependencies in an array approach, changing from this:

spotVizApp.config(function($stateProvider, $urlRouterProvider) {
...
});

to this:

spotVizApp.config([ '$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
...
]]);

I’ve still got a couple of issues with the ng-include templates that look like they’re not getting bundled in my prod build, but at this point I think I’m pretty close.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.