Journey to webpack, part 6 – moment.js

webpack + Moment.js

Moment.js is a powerful library that lets you “parse, validate, manipulate, and display dates and times in JavaScript”. Despite the recent emergence of other libraries like fecha and date-fns, Moment.js has reigned for a long time as the go to library for manipulating dates in JavaScript. Please do check out the features of these other lightweight alternatives if you have the opportunity. Unfortunately we are fairly entrenched with Moment because we’re working with a 3 year old code base. “It is, what it is”, so we just have to deal with it.

onto the problem

Our initial problem was that we couldn’t even get webpack to build moment into the bundle: from webpack-config/vendor.ts:

1
import '../node_modules/moment/min/moment-with-locales';

grunt shell:webpackDev resulted in:

1
2
3
4
WARNING in ./node_modules/moment/min/moment-with-locales.js
Module not found: Error: Can't resolve './locale' in '/Users/oloreb/dev/redbox-pay/redbox\_angular/node\_modules/moment/min'
@ ./node_modules/moment/min/moment-with-locales.js 263:16-43
@ ./webpack-config/vendor.ts

The quick fix was to ignore the locale resolution, because moment-with-locales.js already had them:

1
new webpack.IgnorePlugin(/\\.\\/locale$/, /moment\\/min/)

But after looking at the BundleAnalyzer, we realized we were bringing in way too many locales. Much has been written about how to do this, so we stood on the shoulders of the giants and made the following changes: We tweaked vendor.ts to include moment instead of moment-with-locale and replaced the IgnorePlugin with this

1
new webpack.ContextReplacementPlugin(/moment\[\\/\\\\]locale$/, /en|fr|es|zh|pt|nl|de|it/),

and now our analyzer output looks like this:

140k to 29k !

And that’s the gzip comparison! SHIP IT! A quick update to our TODO list:

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

Journey to webpack, part 5 - misc

New to the series? Start at the beginning

As we continue on our journey to convert Grunt to webpack, I realized I missed a few things that are probably useful.

  1. uglification
  2. source maps
  3. split dev & prod
  4. helper files
  5. ng-upgrade
  6. CommonChunksPlugin
  7. BundleAnalyzerPlugin

ugilfication

Uglification was one of the most straightforward config options we’ve added to webpack. Here’s what we use:

plugins: [
new webpack.optimize.UglifyJsPlugin({
/*eslint-disable camelcase*/
beautify: false,
output: {
comments: false
},
mangle: {
screw_ie8: true
},
compress: {
drop_console: true,
screw_ie8: true,
warnings: false,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true,
negate_iife: false
},
sourceMap: true
/*eslint-enable camelcase*/
}))
]

Of note - the uglification of vendor adds 15 seconds to the build time (from 5sec to 20sec!). We also noticed that sourceMap: true adds ~2 seconds itself, but more importantly we couldn’t find the map files.

sourcemaps

Source maps are a must have. A little digging and we found the proper way to enable sourcemaps within webpack is via:

devtool: ‘source-map’,

Ah, much better :)

split dev & prod

Given how much time uglification adds to the build, we don’t want to run it during development, we really just need it in our production build. We have 2 generally accepted methods of doing this: switching on NODE_ENV or 2 configs. We opted for the 2 configs because our config files were already getting big, and we didn’t want to add lots of if logic. It seemed like it would make it easier to debug. So, off we go:

  1. Copy webpack.config.js to webpack-dev.config.js and webpack-prod.config.js
  2. Remove webpack.config.js
  3. Remove UglifyJsPlugin from the dev config
  4. Update the ‘webpack’ Grunt task
  5. Update references to the ‘webpack’ Grunt task

grunt.config(‘shell’, {
webpackDev: {
command: ‘node node_modules/webpack/bin/webpack –config webpack-dev.config.js –progress’,
options: {
execOptions: { cwd: ‘.’},
async: false, stdout: true, stderr: true, failOnError: true
}
},
webpackProd: {
command: ‘node node_modules/webpack/bin/webpack –config webpack-prod.config.js –progress’,
options: {
execOptions: { cwd: ‘.’},
async: false, stdout: true, stderr: true, failOnError: true
}
},

This keeps our development build blazing fast!

Helper files

I’m not fan of duplication. Early on with all the work we did on the vendor libraries made our config file huge and it looked like we were doing the same things over & over. So we created vendor-helper.js. The immediate purpose of this file was to extract the repetitive expose-loader configurations, making our main config file more readable.

module.exports = {

exposeLoaderConfig: [
{
name: ‘lodash’,
global: ‘_’
},
{
name: ‘moment’,
global: ‘moment’
},
{
name: ‘fastclick’,
global: ‘FastClick’
},
{
name: ‘angular’,
global: ‘angular’
},
{
name: ‘d3’,
global: ‘d3’
},
{
name: ‘log4javascript’,
global: ‘log4javascript’
},
{
name: ‘highcharts’,
global: ‘Highcharts’
}
],

getSimpleGlobals: function() {
return this.exposeLoaderConfig.map((config) => {
return {
test: require.resolve(config.name),
use: [
`expose-loader?${config.global}`
]
};
});
}
};

Then in the main config file:

const VendorHelper = require(‘./webpack-config/vendor-helper’);

module: {
rules: {
…VendorHelper.getSimpleGlobals(),

Boom! We just made the config file that much smaller and more readable, getting those pesky libraries that need special handling out of our way. We don’t need to see that every day, so shoving them off to the side has brought a sense of calmness and clarity that we were missing… or something like that :) While we’re in the business of extracting stuff, the verbosity of the config for the UglifyJsPlugin was bothersome, so we move it to a plugin-helper.ts. Again, this is something you essentially “set and forget”, and it takes up a lot of real estate in the editor. Because of our pre-existing Grunt infrastructure, we have an environment variable called USE_MINIFIED, so we’ll piggy back on that to tell us whether or not we should uglify.

const webpack = require(‘webpack’);

module.exports = {

getUgilifyConfiguration: function() {
let uglifyConfiguration = [];

if (process.env.USE_MINIFIED === 'true') {

  uglifyConfiguration.push(
    new webpack.optimize.UglifyJsPlugin({
      /\*eslint-disable camelcase\*/
      beautify: false,
      output: {
        comments: false
      },
      mangle: {
        screw_ie8: true
      },
      compress: {
        drop_console: true,
        screw_ie8: true,
        warnings: false,
        conditionals: true,
        unused: true,
        comparisons: true,
        sequences: true,
        dead_code: true,
        evaluate: true,
        if_return: true,
        join_vars: true,
        negate_iife: false
      },
      sourceMap: true
      /\*eslint-enable camelcase\*/
    }))
}

return uglifyConfiguration;

}
};

Then in our config file:

plugins: [
…PluginHelper.getUgilifyConfiguration(),

Can you tell we’re into the spread operator over here? :) You might be thinking… Hey I thought you moved to separate config files so you shouldn’t need to extract the uglify config AND have an ENV check, because it’s only in the prod config. Yeah, you are probably right. But we got the feeling that there were going to be more differences in the future…

ng-upgrade

I mentioned before that we have an Angular app we are trying to move our legacy AngularJS code base towards. Thus far we are using ng-upgrade to downgrade the Angular components to AngularJS successfully. To that end, we need to add these libraries to our webpack config. We have a Grunt task already in place to generate the ng-upgrade(able) bundle, we we’ll add that as an entry point.

entry: {
‘ng-upgrade/dist/bundle.js’: ‘./ng-upgrade/bootstrap-aot.ts’,

We also need to tell the resolver where to look for these files, so we’ll add the following:

resolve: {
modules: [‘ng-upgrade’, ‘node_modules’],
},

The resolve.modules key requires us to also include “node_modules”, which is there by default, but if you specify another path for modules, you have to include it. THen, we need to include the AotPlugin.

plugins: [
new ngToolsWebpack.AotPlugin({
tsConfigPath: ‘./ng-upgrade/tsconfig-aot.json’
}),

Finally, we add the module.rules necessary to use the AotPlugin against our .ts files. Also, because our downgraded components contain style and templates, we need to tell webpack how to handle them. For this, we will simply use the raw-loader.

module: {
rules: [
{
test: /\.ts$/,
loader: ‘@ngtools/webpack’
},
{ test: /\.css$/, loader: ‘raw-loader’ },
{ test: /\.html$/, loader: ‘raw-loader’ },

CommonChunksPlugin

The CommonChunksPlugin is one of those magical parts of webpack that “just works”. We’ll configure it to look for at least 2 common includes before it does the extraction into a separate JS file.

plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ‘bundle’,
minChunks: 2,
filename: ‘common-chunks/[name].min.js’
})

BundleAnalyzerPlugin

The BundleAnalyzerPlugin says that it generates a representation of “bundle content as convenient interactive zoomable treemap”. Simply put - it’s an amazing, interactive visual of the contents of the files webpack outputs. Setup is really easy:

// eslint-disable-next-line no-unused-vars
const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin;

plugins: [
new BundleAnalyzerPlugin(),

We put the eslint-disable line in there, because most of the time we don’t want to take the extra time to run it, but it’s nice to have the analyzer only a commented line away. We then modify package.json to give us a quick shortcut we can execute with yarn or npm run

“inspect-common-chunk”: “source-map-explorer build/static/app/common-chunks/bundle.min.js”
“inspect-ng-upgrade-bundle”: “source-map-explorer build/static/app/ng-upgrade/dist/bundle.js”,

Words cannot do this plugin justice… see for yourself: [caption id=”” align=”aligncenter” width=”908”] BundleAnalyzer plugin[/caption] This is a great way to see how your config alters your output. And it’s fun to play with!

todo list

A quick checkin our our todo list shows that we did a bunch of stuff that wasn’t even on the list! This is the nature of working on something unknown, you never quite know what path you will take. It’s still important to have a list - so we all have a target.

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