Angular - Building with Webpack

We document the main steps required when using Webpack below, but please refer to ag-grid-angular-example on GitHub for a full working example of this.

Refer to the Angular Site on this topic for more information - it was used as the basis for our offering below.

We have 3 Webpack Configurations in the example project - a common configuration, and then a dev and prod configuration that both use the common one.

Webpack Common (Shared) Configuration

// webpack.common.js
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');

module.exports = {
    entry: {
        'polyfills': './app/polyfills.ts',
        'vendor': './app/vendor.ts',
        'app': './app/boot.ts'
    },

    resolve: {
        extensions: ['', '.ts', '.js']
    },

    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html'
            },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'file?name=[path]/[name].[ext]'
            },
            {
                test: /\.css$/,
                exclude: helpers.root('src', 'app'),
                loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
            },
            {
                test: /\.css$/,
                include: helpers.root('src', 'app'),
                loader: 'raw'
            }
        ]
    },

    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),

        new HtmlWebpackPlugin({
            template: 'config/index.html'
        })
    ]
};

entry

We could generate one large bundle, but it's better to break the bundle up into the fairly "static" dependencies and the more fluid application code. Using the entry property we can specify the entry points we want to use - we have specified 3 here:

  • polyfills: polyfills we require to run Angular / ES6 applications in current browsers.
  • vendor: the vendor (or 3rd party) libraries we need - ag-Grid, Angular etc.
  • app: our application code.

resolve

As our imports done specify what file extension to use, we need to specify what file types we want to match on - in this case we're looking at TypeScript and JavaScript files, but you could also add CSS & HTML files too.

module.loaders

Loaders tell Webpack how & what to do with certain types of file - we have specified a few here to deal with Typescript, HTML, CSS and Images:

  • awesome-typescript-loader: transpile Typescript to ES5
  • angular2-template-loader: processes Angular components' template/styles
  • html
  • images & fonts
  • css: the first phe pattern matches application-wide styles, and the second handles component-scoped styles (ie with styleUrls)

plugins

  • CommonsChunkPlugin: separates our entry points into distinct files (one each for polyfills, vendor and application)
  • HtmlWebpackPlugin: takes our supplied template index.html and inserts the generates JS & CSS files for us

Webpack Development Configuration

// webpack.dev.js
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

module.exports = webpackMerge(commonConfig, {
    devtool: 'cheap-module-eval-source-map',

    output: {
        path: helpers.root('dist'),
        publicPath: 'http://localhost:8080/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },

    plugins: [
        new ExtractTextPlugin('[name].css')
    ],

    devServer: {
        historyApiFallback: true,
        stats: 'minimal'
    }
});

The dev configuration doesn't generate any files - it keeps all bundles in memory, so you won't find any artifacts in the dist directory (from this configuration).

Webpack Production Configuration

// webpack.prod.js
var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

const ENV = process.env.NODE_ENV = process.env.ENV = 'production';

module.exports = webpackMerge(commonConfig, {
    devtool: 'source-map',

    output: {
        path: helpers.root('dist'),
        publicPath: '/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },

    htmlLoader: {
        minimize: false // workaround for ng2
    },

    plugins: [
        new webpack.NoErrorsPlugin(),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
            mangle: {
                keep_fnames: true
            }
        }),
        new ExtractTextPlugin('[name].[hash].css'),
        new webpack.DefinePlugin({
            'process.env': {
                'ENV': JSON.stringify(ENV)
            }
        })
    ]
});

We don't use a development server with this configuration - we generate the final artifacts in the dist/ folder and expect this to be deploy to a server.

We use the plugins to stop the build on an error, remove duplicates and minify and extract the CSS into cache busting hash named files.

Finally, we use the DefinePlugin to provide an environment variable that we can use in our application code to enableProdMode()

if (process.env.ENV === 'production') {
    enableProdMode();
}