Dec 05, 2016

Configure Angular 2 and Webpack

In the documentation for Angular 2, the main focus is on working with System JS. An example of a quick start application from the Angular team is also based on SystemJS. I want to talk about setting up Angular 2 with Webpack.

There are few of different starters for working Angular with Webpack. We'll talk at the end of the article about them. It is useful for us, at least in general terms, to know how to connect the Webpack itself, as well as its plugins.

Why Webpack? It's a matter of personal choice. SystemJS is not declared by the Angular team as a standard, it is just a recommendation. I found Webpack more convenient to set up and work with.

What is Webpack? It is a powerful tool for building project packages (modules). We can connect various libraries, generate CSS based on SASS or LESS, use prefixes, minimizers, etc.

So, for the work we need first Node.js and npm.

Project configuration

At the root of the project create a package.json. Here are all the necessary modules:

{
  "name": "angular2-webpack",
  "version": "1.0.0",
  "description": "A webpack starter for Angular",
  "scripts": {
    "start": "webpack-dev-server --inline --progress --port 8080",
    "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  },
  "license": "MIT",
  "dependencies": {
    "@angular/common": "~2.2.0",
    "@angular/compiler": "~2.2.0",
    "@angular/core": "~2.2.0",
    "@angular/forms": "~2.2.0",
    "@angular/http": "~2.2.0",
    "@angular/platform-browser": "~2.2.0",
    "@angular/platform-browser-dynamic": "~2.2.0",
    "@angular/router": "~3.2.0",
    "core-js": "^2.4.1",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.25"
  },
  "devDependencies": {
    "@types/node": "^6.0.45",
    "angular2-template-loader": "^0.4.0",
    "awesome-typescript-loader": "^2.2.4",
    "css-loader": "^0.23.1",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "html-loader": "^0.4.3",
    "html-webpack-plugin": "^2.15.0",
    "null-loader": "^0.1.1",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.2",
    "style-loader": "^0.13.1",
    "typescript": "^2.0.3",
    "webpack": "^1.13.0",
    "webpack-dev-server": "^1.14.1",
    "webpack-merge": "^0.14.0"
  }
}

Pay attention to the line"start": "webpack-dev-server --inline --progress --port 8080", here you can see the port where the project will be opened. You can change this value if for some reason you are not satisfied with 8080.

This file describes the commands to run and build the project (scripts section), as well as dependencies — packages that need to be installed to develop and run the project. The dependencies section describes the packages required for the project. In the section devDependencies — packages that are needed only during the development phase.

To install everything you need, run the command from the console in the project folder:

npm install

Configuration for Typescript

At the root of the project create a file tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

Configuration for Webpack

Consider a develop-only configuration. Create a config folder, there we will store files for configuration.

config/helpers.js:

var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
    args = Array.prototype.slice.call(arguments, 0);
    return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

config/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': './src/polyfills.ts',
        'vendor': './src/vendor.ts',
        'app': './src/main.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=assets/[name].[hash].[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: 'src/index.html'
        })
    ]
};

The application is allocated 3 entry points:

  1. polyfills — a set of scripts that allows Angular to work in most modern browsers
  2. vendor — here we import various libraries, styles, etc.
  3. app — our application

The resolve section is responsible for ensuring that the extension can not be specified during import, that is, instead of:

import { AppComponent } from './app.component.ts';

We can write:

import { AppComponent } from './app.component';

You can include both CSS and HTML in this section.

In the loaders section, we "teach" webpack to convert typescript to ES5 code, as well as load HTML, images, and styles specified in the components. 

As you can see, there are 2 patterns for CSS. The first one loads all styles that do not in src/app folders, the second pattern loads styles specified in the styleUrls property of the component.

The last section of plugins allows you to manage library import dependencies in the application, vendor and polyfills, as well as automatically include scripts and styles to the index.html, so we don't have to write them by hand.

config/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'
    }
});

This is a developer configuration for webpack. It starts the local server (if you changed the value of the port above, then it also needs to be changed).

Let's create a simple application.

SRC directory

vendor.ts

import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
import 'rxjs';

polyfills.ts

import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
Error['stackTraceLimit'] = Infinity;
require('zone.js/dist/long-stack-trace-zone');

main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

index.html

<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <title>Angular 2 and Webpack</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

SRC/APP directory

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
    imports: [
        BrowserModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [ AppComponent ]
})
export class AppModule { }

app.component.ts

import { Component } from '@angular/core’;

@Component({
    selector: 'my-app',
    templateUrl: './app.component.html'
})
export class AppComponent { }

app.component.html

<main>
    <h1>Hi, Angular 2!</h1>
</main>

Now to run the application, you need to type in the console:

npm start

If everything went well, there are no errors in the console, then at localhost:8080 we will see: Hi, angular 2!

As a rule, it is long and unproductive to set up the configuration for each project from scratch. As I mentioned above, you can use starters — special configuration packages for quick start.

One of the most popular is the Angular CLI (although it is not a starter, but rather a command interface for Webpack and Angular 2). Perhaps I come across most often during the search for particular information on the Angular and Webpack. However, I declined to use it because it does not currently support changing the Webpack configuration.

The most common https://github.com/AngularClass/angular2-webpack-starter and https://github.com/preboot/angular2-webpack (I prefer this).