Journey to webpack, part 3 - finish the vendors

Welcome to part 3 of our journey, catch up with part 1 and part 2.

quick recap

We’ve injected webpack into our build process, it’s run before ‘uglify:vendor’ and is bundling 2 of our 27 3rd party libraries (detectizr & modernizr). We did this by paring down our vendor.ts file, which we use as an entry point, then massaging the libraries to work with webpack, using various loaders. Currently we have 2 “vendor” files that are mutually exclusive.

{
test: require.resolve(‘modernizr’),
use: [
‘expose-loader?Modernizr’,
‘imports-loader?this=>window’,
‘exports-loader?window.Modernizr’
]
},
{
test: require.resolve(‘detectizr’),
use: [
‘expose-loader?Detectizr’,
‘imports-loader?this=>window’,
‘exports-loader?window.Detectizr’
]
},

The next step is to get the rest of the vendor files converted over to use webpack, and remove the ‘uglify:vendor’ task completely.

let’s get to it

Let’s take it from the top. Here is what our vendor.ts file looks like:

import ‘../node_modules/modernizr/modernizr’;
import ‘../node_modules/detectizr/dist/detectizr’;
// import ‘../node_modules/jquery/dist/jquery’;
// import ‘../node_modules/jquery.easing/jquery.easing’;
// import ‘../node_modules/lodash/index’;
// import ‘../node_modules/moment/min/moment-with-locales.min’;
// import ‘../node_modules/moment-timezone/builds/moment-timezone-with-data’;
// import ‘../node_modules/d3/d3’;
// import ‘../node_modules/bootstrap/dist/js/bootstrap’;
// import ‘../node_modules/fastclick/lib/fastclick’;
// import ‘../node_modules/angular/angular’;
// import ‘../node_modules/angular-cookie/angular-cookie’;
// import ‘../node_modules/angular-cookies/angular-cookies’;
// import ‘../node_modules/angular-ui-bootstrap/ui-bootstrap-tpls’;
// import ‘../node_modules/angular-moment/angular-moment’;
// import ‘../node_modules/angular-resource/angular-resource’;
// import ‘../node_modules/angular-sanitize/angular-sanitize’;
// import ‘../node_modules/angular-animate/angular-animate’;
// import ‘../node_modules/angular-touch/angular-touch’;
// import ‘../node_modules/angular-ui-router/release/angular-ui-router’;
// import ‘../node_modules/angulartics/src/angulartics’;
// import ‘../node_modules/angulartics/src/angulartics-ga’;
// import ‘../node_modules/log4javascript/log4javascript’;
// import ‘../node_modules/isotope/dist/isotope.pkgd’;
// import ‘../node_modules/phoneformat/phoneformat.min’;
// import ‘../node_modules/jquery-form/jquery.form’;
// import ‘../node_modules/intl-tel-input/build/js/intlTelInput’;
// import ‘../node_modules/intl-tel-input/lib/libphonenumber/build/utils’;
// import ‘../node_modules/bootstrap-colorpickersliders/dist/tinycolor’;
// import ‘../node_modules/bootstrap-colorpickersliders/dist/bootstrap.colorpickersliders’;
// import ‘../node_modules/ng-sortable/dist/ng-sortable’;
// import ‘../node_modules/highcharts/lib/highcharts’;
// import ‘../node_modules/highcharts/lib/modules/drilldown’;
// import ‘../node_modules/highcharts/lib/modules/exporting’;
// import ‘../node_modules/highcharts/lib/modules/heatmap’;
// import ‘../node_modules/oclazyload/dist/ocLazyLoad’;
// import ‘../node_modules/tslib/tslib’;

We’ll go down the list one-by-one, uncommenting from here, and commenting out from our ‘uglify:vendor’ task then running the server to make sure there are no console errors in the browser. First up: jQuery [caption id=”attachment_745” align=”aligncenter” width=”1232”] Uncaught ReferenceError: jQuery is not defined[/caption] Looks like something in vendor.min.js (the non-webpack’d file) expects jQuery to already be loaded and to be defined on window. Luckily, we know how to fix this via the expose-loader. Since jQuery is also often referred to as $, we need to expose it as $ too.

{
test: require.resolve(‘jquery’),
use: [
‘expose-loader?jQuery’,
‘expose-loader?$’
]
},

Success!

fast-forward a bit

We can clearly see where this is headed… let’s use expose-loader for all the libraries that create global variables: lodash, moment, fastclick, angular, d3, log4javascript, and highcharts

{
test: require.resolve(‘lodash’),
use: [ ‘expose-loader?_’ ]
},
{
test: require.resolve(‘moment’),
use: [ ‘expose-loader?moment’ ]
},
{
test: require.resolve(‘fastclick’),
use: [ ‘expose-loader?FastClick’ ]
},
{
test: require.resolve(‘angular’),
use: [ ‘expose-loader?angular’ ]
},
{
test: require.resolve(‘d3’),
use: [ ‘expose-loader?d3’ ]
},
{
test: require.resolve(‘log4javascript’),
use: [ ‘expose-loader?log4javascript’ ]
},
{
test: require.resolve(‘highcharts’),
use: [ ‘expose-loader?Highcharts’ ]
},

Seems simple enough. But whooops, something is wrong with moment.

WARNING in .//moment/min/moment-with-locales.min.js
Module not found: Error: Can’t resolve ‘./locale’ in ‘/Users/oloreb/dev/app/node_modules/moment/min’
@ ./
/moment/min/moment-with-locales.min.js 1:2798-2820
@ ./webpack-config/vendor.ts

A quick Google search and people suggesting the IgnorePlugin. Apparently the pathing for moment’s locale files is funky, so we will just ignore them for now. We’ll queue it up to deal with later.

new webpack.IgnorePlugin(/\.\/locale$/)

Success with no warnings this time! OK, what’s left now?

// import ‘../node_modules/bootstrap/dist/js/bootstrap’;
// import ‘../node_modules/angular-cookie/angular-cookie’;
// import ‘../node_modules/angular-cookies/angular-cookies’;
// import ‘../node_modules/angular-ui-bootstrap/ui-bootstrap-tpls’;
// import ‘../node_modules/angular-moment/angular-moment’;
// import ‘../node_modules/angular-resource/angular-resource’;
// import ‘../node_modules/angular-sanitize/angular-sanitize’;
// import ‘../node_modules/angular-animate/angular-animate’;
// import ‘../node_modules/angular-touch/angular-touch’;
// import ‘../node_modules/angular-ui-router/release/angular-ui-router’;
// import ‘../node_modules/angulartics/src/angulartics’;
// import ‘../node_modules/angulartics/src/angulartics-ga’;
// import ‘../node_modules/isotope/dist/isotope.pkgd’;
// import ‘../node_modules/phoneformat/phoneformat.min’;
// import ‘../node_modules/jquery-form/jquery.form’;
// import ‘../node_modules/intl-tel-input/build/js/intlTelInput’;
// import ‘../node_modules/intl-tel-input/lib/libphonenumber/build/utils’;
// import ‘../node_modules/bootstrap-colorpickersliders/dist/tinycolor’;
// import ‘../node_modules/bootstrap-colorpickersliders/dist/bootstrap.colorpickersliders’;
// import ‘../node_modules/ng-sortable/dist/ng-sortable’;
// import ‘../node_modules/oclazyload/dist/ocLazyLoad’;
// import ‘../node_modules/tslib/tslib’;

Not much! Next we knock out all the angular stuff, bootstrap, ng-sortable, ocLazyLoad and tslib. This is going smooth! Leaving us with a few odd balls:

// import ‘../node_modules/isotope/dist/isotope.pkgd’;
// import ‘../node_modules/phoneformat/phoneformat.min’;
// import ‘../node_modules/intl-tel-input/build/js/intlTelInput’;
// import ‘../node_modules/intl-tel-input/lib/libphonenumber/build/utils’;

Uncommenting isotope, leaves us with an error which we can resolve with our old friend imports-loader

{
test: require.resolve(‘isotope/dist/isotope.pkgd’),
use: [
‘imports-loader?this=>window’
]
}

Note: this is only a problem with isotope v2. If you are using isotope v3, use this method.

the final three

The last 3 items all deal with our telephone number input boxes. Uncommenting phoneformat leaves us with: [caption id=”attachment_747” align=”aligncenter” width=”722”] i18n is not defined[/caption] Uncommenting intl-tel-input leaves us with with the same thing. Neither just work on their own, so let’s dig into phoneformat phoneformat uses a Google module loading system, so first we need to use imports-loader to convert “this=>window” It’s also creating a global variable called goog, so we need to expose it via the expose-loader. Then, similar to modernizr, we also need the exports-loader to export the goog variable.

{
test: require.resolve(‘phoneformat’),
use: [
‘expose-loader?goog’,
‘imports-loader?this=>window’,
‘exports-loader?goog’
]
},

This isn’t so bad, but we’re still not working. Same error, but at least window.goog is available. We have libraries that expecting window.i18n to be available, but it’s different than window.goog, and we can only use expose-loader to expose a single variable (or, as with jQuery, expose a single variable with different names: jQuery & $). Looking into the source of phoneformat, we find a a line declaring the i18n variable. Not to be confused with window.goog.i18n, which is completely different (but threw me off the track for a solid hour!) So, I thought to myself, hey if that said “window.i18n =” instead of “var i18n =”, we’d be in business. Luckily there is a plugin that lets you do string replacement:

{
test: require.resolve(‘phoneformat’),
use: [
‘expose-loader?goog’,
‘imports-loader?this=>window’,
‘exports-loader?goog’,
{
loader: StringReplacePlugin.replace({
replacements: [
{
pattern: /var i18n/ig,
replacement: function (match, p1, offset, string) {
return ‘window.i18n’;
}
}
]})
}
]
},

Bingo! Nailed it! While this feels incredibly hacky, it’s 100% functional and the logic checks out. The string replacement is confined to the phoneformat JS file, and won’t affect others. We’ll have to revisit if we ever upgrade the library, but we can worry about that later. * For the record, even though it sounds like this took minutes, it took a few hours over the course of a few days to come up with this solution. In hindsight, it seems obvious, but reality is that this one had us stumped for a while. It’s good to know that webpack has your back for situations like these. Now we switch over intl-tel-input, and it works without further changes! Now we can go back through and remove references to files and tasks that build the old vendor.min.js (remember we’re using vendor-webpack.min.js). This includes the the grunt tasks and index.html. In fact, we’ll leave the references to vendor.min.js in the index.html file, and switch our webpack config to create vendor.min.js. That’s it! We have now replaced ‘uglify:vendor’ with ‘shell:webpack’ to generate vendor.min.js! Looking ahead, we’ve still got lots more for webpack to do:

  1. Create app-components.min.js
  2. Create templates.app.min.js
  3. Create app.min.js
  4. Handle CSS/LESS/SASS
  5. Try out webpack 3.0!
  6. Use the webpack’d files in karma for unit tests
  7. Use the dev server instead of grunt-contrib-connect
  8. Try the CommonChunksPlugin
  9. Figure out what’s up with moment.js

for reference

Here’s our webpack.config.js Here are the versions of these libraries that we are currently using. Things change, and maybe newer releases of these libraries will play more nicely with webpack. So, here are the relevant entries from package.json:

“angular”: “1.5.5”,
“angular-cookie”: “4.0.10”,
“angular-cookies”: “1.5.5”,
“angular-moment”: “0.10.3”,
“angular-resource”: “1.5.5”,
“angular-sanitize”: “1.5.5”,
“angular-touch”: “1.5.5”,
“angular-resource”: “1.5.5”,
“angular-sanitize”: “1.5.5”,
“angular-touch”: “1.5.5”,
“bootstrap-colorpickersliders”: “3.0.2”,
“bootstrap”: “3.3.5”,
“d3”: “3.5.6”,
“detectizr”: “2.0.0”,
“fastclick”: “1.0.6”,
“highcharts”: “4.1.10”,
“intl-tel-input”: “3.6.1”,
“isotope”: “2.0.0”,
“jquery”: “2.1.4”,
“jquery-form”: “3.46.0”,
“jquery.easing”: “1.3.2”,
“lodash”: “3.10.1”,
“log4javascript”: “1.4.15”,
“modernizr”: “2.8.3”,
“moment”: “2.10.6”,
“moment-timezone”: “0.5.1”,
“ng-sortable”: “1.3.0”,
“oclazyload”: “1.0.6”,
“phoneformat”: “0.0.7”,
“tslib”: “1.5.0”,