Setting Up Angular 2 with Webpack

Datetime:2016-08-23 00:23:48          Topic: Webpack  AngularJS           Share

Introduction

In this article, we'll be looking at the setup required to create an Angular 2 project with unit tests. We'll cover various required technologies and how to write each of their configurations.

Let's dive in.

Prerequisites and Assumptions

Before getting started, we should make sure that we have everything we need. It is assumed that you have:

  • An intermediate understanding of JavaScript, including concepts of CommonJS modules ,
  • At least a rough understanding of Angular 1,
  • An understanding of ES6/ES2015 concepts, such as arrow functions, modules, classes and block-scoped variables,
  • Comprehension of using command line or terminal, such as Git Bash, iTerm, or your operating system's built-in terminal, and
  • You have Node >= v5 and NPM >= v3 installed.

What is Angular 2?

Angular 2 is a modern framework for developing JavaScript applications. It is the second major version of the extremely popular Angular framework by Google. It was written from the ground up using TypeScript to provide a modern, robust development experience.

Note that, although TypeScript is the preferred language for developing with Angular 2, it is possible to develop through ES5 and regular ES2015. In these articles, we'll be using TypeScript.

Differences from Angular 1

Within the Angular community, there is a well-known video ( slides here ) where Angular team members Igor Minar and Tobias Bosch announced the death of many concepts Angular developers were familiar with. These concepts were:

Many of these changes have simplified the concepts which developers must keep track of in their head. That, in turn, simplifies development with Angular.

To see how Angular 1 concepts map to Angular 2 you can read this .

Using TypeScript

As mentioned above, Angular 2 can be developed without TypeScript, but the extra features that it provides on top of ES2015 make the development process richer. In this section, we'll run through a quick primer on TypeScript.

TypeScript is a superset of Javascript. What does that mean? In short, it means that the JavaScript you know today is understood by TypeScript. You can take any JavaScript file you have, change the extension to .ts , run it through the TypeScript parser, and it will all be understood. What the compiler outputs is JavaScript code, which, many times, is as good or even better written than the original code.

One of the most prominent TypeScript features is its optional typing system. Note that this is optional. Utilizing types, however, makes it easier to reason about code. Many editors have support for TypeScript, which allows for features such as code completion to be utilized.

Another feature is the ability to define interfaces, which can then be used as types throughout your code. This helps when you need to ensure that a data structure is consistent throughout your code.

// note this code is available in the repo
// at ./examples/introduction/types-and-interfaces.ts

// MyType is a custom interface
interface MyType {
    id: number;
    name: string;
    active?: boolean;       // the "?" makes this an optional field
}

// someFunction takes three parameters, the third of which is an
// optional callback
function someFunction(id: string, value: MyType, callback?: Function) {
    // ...
}

Now, if you were to code and call someFunction , your editor (with TypeScript support) would give you a detailed definition of the parameters. Similarly, if you had a variable defined as MyType , code completion would show you the attributes available on MyType and the types of those attributes.

TypeScript does require a build process of some sort to run code through its compiler. In later sections we'll set up the configuration for the TypeScript compiler.

What is Webpack?

From the Webpack website , "webpack is a module loader" that "takes modules with dependencies and generates static assets". Additionally, it provides a plugin system and methods for processing files.

Module Loading

So, what does module loading mean? Let's look at a very simple example. We have a project with 4 files: app.js , child1.js , child2.js , and grandchild.js .

  • app.js is dependent on child1.js and child2.js
  • child2.js depends on grandchild.js

We can then tell Webpack to run just on app.js , and it will compile a file that contains all files. It does this by finding all statements in app.js that indicate a dependency, such as an import or a require . In app.js , we have:

const child1 = require('./child1.js');
const child2 = require('./child2.js');

Webpack knows it needs to find those files, read them, find any of their dependencies, and repeat until it hits a file with no dependencies. The file child1.js is such a file. In child2.js , though, we have:

const grandchild = require('./grandchild.js');

So, Webpack finds grandchild.js , reads it, sees no dependencies, and stops its processing. What we end up with is all 4 files, compiled together and usable in the browser. In a nutshell, this is what module loading does.

File Processing

In addition to just loading modules based on dependencies, Webpack also provides a processing mechanism called loaders.

To see what loaders do, let's use another example. Say we have a TypeScript file. As mentioned above, to compile a TypeScript file to JavaScript, it needs to be run through the TypeScript compiler.There are Webpack loaders that will do just that. We can tell Webpack that, as it encounters .ts files, it should run those files through the TypeScript compiler.

The same can be done for virtually any file type — SASS, LESS, HTML, Jade, not just JavaScript-like files. This concept is beneficial because it allows us to use Webpack as a sort of build system to do all the heavy lifting we need to get Angular 2 into the browser, or in our testing environment.

Why Should We Unit Test?

When we develop applications the most important thing we can do is to ship code as fast and bug-free as possible. Testing helps us achieve those goals. The number one concern for developers who do not unit test is that it takes too long. When you're first getting into utilizing unit testing in development, it may take longer than you're used to. However, the long-term benefits far outweigh that initial investment, especially if we test before we write our application code. This is known as Test-driven Development (TDD).

Test-driven Development

We need to write a JavaScript function called isPrimary . This function's main purpose is to return true if a number is a primary number and false if it's not.

In the past, we would just dive in head-first, probably hit Google to remember what a primary number is, or find a solid algorithm for it and code away, but let's use TDD. We know our ultimate goal, i.e. to output a boolean of whether a number is a primary number or not, but there are a few other concerns we need to address to try to achieve a bug-free function:

  • What parameters does the function take? What happens if they're not provided?
  • What happens if a user passes in a non-number value?
  • How do we handle non-integers?

When we approach a problem from a TDD perspective, our first step is to ask ourselves what could go wrong and figure out how to address it. Without this perspective, we may not think about these cases, and we might miss them. When these cases do arise, we may have to completely refactor our code to address them, potentially introducing new bugs.

One of the core tenets of TDD is writing just enough code to make a test pass. We use a process known as the red-green-refactor cycle to achieve this goal. The steps are:

  1. Think about the test you need to move towards completion,
  2. Write a test, execute it, watch it fail (red),
  3. Write just enough code to watch it pass (green),
  4. Take a moment to look at the code for any smells . If you find any, refactor the code. Run the tests with each change to the code to ensure you haven't broken anything, and
  5. Repeat.

Step number one is probably the hardest step for developers new to TDD and unit testing in general. Over time you'll become more and more comfortable and recognize patterns of how to test.

Advantages of Unit Testing and TDD

We've seen a couple of the advantages of unit testing already, but there are more. Here are a few examples:

  • Reduces the level of bugs in code,
  • Less application code because we write just enough code to achieve our goals,
  • Makes it easier to refactor code,
  • Provides sample code of how to use your functions,
  • You get a low-level regression test suite, and
  • Speeds up code-writing.

Disadvantages of Unit Testing and TDD

Unit testing isn't a silver bullet to writing perfect code. There are drawbacks to doing so. Here are a few of those drawbacks:

  • Could give a false sense of quality,
  • Can be time consuming,
  • Adds complexity to codebase,
  • Necessity to have mock objects and stubbed-out code, especially for things outside your control, i.e. third-party code, and
  • For a large codebase, tweaking one part of your application, such as a data structure, could result in large changes to tests.

While these disadvantages exist, if we are diligent and thoughtful in our testing approach, the benefits unit of testing and TDD outweigh these risks.

Using NPM as a Task Runner

In a prior section, we saw that we could use Webpack to perform many of our build process functions, but not how to invoke them. You may be familiar with the plethora of task runners out there today, e.g. Grunt, Gulp, Broccoli, so why not use one of them? In short, NPM, which we already use to install our project dependencies, provides a simple system for running tasks.

As you may know, each project that uses NPM needs a package.json file. One of the sections package.json offers is a scripts section. This section is just a JSON object where the keys are the name of our task, and the values are the script that will run once the task is approved.

So, if we have the following in our package.json :

...
  "scripts": {
      "foo": "node ./scripts/foo.js",
      "bar": "node node_modules/bar",
      "baz": "baz some/config.file"
  }
...

To run these tasks, all we need to do is say npm run [task name] . To run foo , we'd just do:

npm run foo

and the command node ./scripts/foo.js would be run. If we ran npm run baz it would look for the baz node module through node_modules/.bin , and then use some/config.file .

Because we already have this task-runner capability, it will be used to perform tasks such as running unit tests. To read more about using the scripts section, take a look at the official NPM documentation .

Installing Dependencies

Now, we'll move on to actually setting up the project. The first step is to get all the dependencies we need. We'll be pulling in Angular, TypeScript, Webpack, and unit testing.

Creating the NPM Project

The first thing we need to do is create an NPM project. We'll take the following steps:

  1. Create a directory. The name doesn't matter, but it's useful to make it descriptive, e.g. ng2-webpack-test ,
  2. Change into that directory by doing cd ng2-webpack-test , or whatever you named your directory, and
  3. Run npm init -f . This will generate a package.json file for your project.

The following commands should all be run from the directory you created in step 1 above.

Angular Dependencies

Angular 2 is broken into a lot of packages under the @angular organization in NPM. We'll need to install them and pull in RxJS, Zone.js, and some shims.

This can be accomplished through a single install operation:

npm i -S @angular/common @angular/compiler @angular/core @angular/platform-browser @angular/platform-browser-dynamic es6-shim reflect-metadata rxjs@5.0.0-beta.6 zone.js

i is an alias for install , -S is an alias for --save .

To see what each of these projects is for, take a look at the Angular 2 documentation .

Although some of these packages are not immediately necessary for performing unit testing, they will allow us to run our application in the browser when the time comes.

Note that this is for Angular 2 RC4.

TypeScript Dependencies

Since TypeScript is going to be used in this project, we'll also need to pull it in as a dependency. To help our code have fewer mistakes and maintain a coding standard, we'll be using code linting through the TypeScript linter, tslint .

npm i -D typescript tslint typings

-D is an alias for --save-dev .

The dependency typings is a way to pull in TypeScript definition files so that TypeScript can understand third-party libraries and provide code completion suggestions for those libraries. We'll see how to use this later.

Webpack Dependencies

We'll also need to pull in all of the dependencies for using Webpack, too. This involves Webpack itself, as well as a list of loaders and plugins we'll need for Angular, TypeScript, and unit testing.

Here's the command we need to run:

npm i -D webpack webpack-dev-server html-webpack-plugin raw-loader ts-loader tslint-loader

The html-webpack-plugin and webpack-dev-server will benefit us when we run our application in a web browser. We'll see what the raw-loader does as we develop our application.

Unit Testing Dependencies

For unit testing, we'll be using Karma as our test runner with Jasmine as the testing framework. There are a multitude of testing libraries out there that could be used, like Mocha and Chai, but by default Angular 2 uses Jasmine, and Karma works well with Webpack.

npm i -D karma karma-jasmine jasmine-core karma-chrome-launcher karma-phantomjs-launcher phantomjs-prebuilt karma-sourcemap-loader karma-webpack

The Chrome and Phantom launchers provide an environment for Karma to run the tests. Phantom is a "headless" browser , which basically means it doesn't have a GUI. There are also launchers for Firefox, Internet Explorer, Safari, and others .

The karma-sourcemap-loader will take the sourcemaps that we produce in other steps and load them for use during testing. This will be useful when running tests in Chrome, so we can place breakpoints in the debugger to see where our code may have problems.

Configurations

The following sections will show how set up our project to run tests and how to run our application in a browser. We'll need to configure setups for:

  • TypeScript,
  • Unit Testing,
  • Webpack, and
  • NPM Scripts.

This may seem like a lot to undertake, but we'll see that the developers of these libraries have established configurations that are easy to understand.

You can follow along with the example files located in examples/introduction/ng2-webpack-test . You will need to run npm i if you have cloned this repository to get all the Node modules installed.

TypeScript Configuration

The pieces needed for utilizing TypeScript are type definitions, linting, and the actual configuration for the TypeScript compiler. Let's look at the type definitions first.

Type Definitions

First, we'll need to create the typings.json file by running the following command from the root of our project:

./node_modules/.bin/typings init

This will run Typings out of its node_modules directory and use its init command.

The typings.json file will be placed in the root of the project. It will contain the name of the project and an empty dependencies object. We'll use the install command to fill that object.

There are three files to install, but we need two commands:

./node_modules/.bin/typings install dt~jasmine env~node --save --global

Again, we are using Typings to install type definitions for jasmine and node .

The second flag, --global , tells Typings that the definitions being installed are for libraries placed in the global scope, i.e. window.<var> . You'll notice that each of the libraries is preceded by a ~ with some letters before it. Those letters correspond to different repositories to look for the type definition files. For information on those repositories, look at the "Sources" section of the Typings Github page .

We'll run a second install command for the es6-promise shim, as it is not a window.<var> library. Notice that there is no prefix required.

./node_modules/.bin/typings install es6-promise --save

Your type definitions are now installed.

Linting

We'll also be instituting code linting for our project. This will help our code stay as error-free as possible, but be aware that it won't completely prevent errors from happening.

As mentioned above, we'll use the tslint library to achieve this goal. It uses the file tslint.json to describe the rules for how code linting should behave. Let's take it one section at a time:

{
    "class-name": true,

This will ensure that all of our class names are in Pascal-case ( LikeThis ).

"comment-format": [
        true,
        "check-space"
    ],

Comments are required to have a space between the slashes and the comment itself ( // like this ).

"indent": [
        true,
        "spaces"
    ],

In the great war of tabs versus spaces, we'll take up in the spaces camp. If you're a fan of tabs, you can always change "spaces" to "tabs" .

"no-duplicate-variable": true,

This will help prevent us from redeclaring variables in the same scope.

"no-eval": true,

This disables the use of eval.

"no-internal-module": true,

TypeScript's module keyword has been known to cause confusion in the past, so we'll prevent its usage in favor of namespace .

"no-trailing-whitespace": true,

This will ensure we're not leaving spaces or tabs at the end of our lines.

"no-var-keyword": true,

ES2015 allows variables to be block-scoped by using const and let . Since TypeScript is a superset of ES2015, it also supports block-scoped variables. These new variable-declaration keywords provide clarity in our code which var does not, namely because let and const variables are not hoisted . To help achieve this clarity, this attribute tells tslint to raise a flag when it sees that we've used the var keyword.

"one-line": [
        true,
        "check-open-brace",
        "check-whitespace"
    ],

This rule says that an opening brace must be on the same line as the statement it is for and it needs to be preceded by a space.

"quotemark": [
        true,
        "single"
    ],

This states that all strings be surrounded by single quotemarks. To use double, change "single" to "double" .

"semicolon": true,

This ensures that our lines will end with a semicolon.

"triple-equals": [
        true,
        "allow-null-check"
    ],

This tells us to use triple equals. The "allow-null-check" lets == and != for doing null -checks.

"typedef-whitespace": [
        true,
        {
            "call-signature": "nospace",
            "index-signature": "nospace",
            "parameter": "nospace",
            "property-declaration": "nospace",
            "variable-declaration": "nospace"
        }
    ],

These rules say that when defining types there should not be any spaces on the left side of the colon. This rule holds for return type of a function, index types, function parameters, properties, or variables.

"variable-name": [
        true,
        "ban-keywords",
        "check-format"
    ],

We need to make sure we don't accidentally use any TypeScript keywords and that variable names are only in camelCase ( likeThis ) or, for constants, all uppercase ( LIKE_THIS ).

"whitespace": [
        true,
        "check-branch",
        "check-decl",
        "check-operator",
        "check-separator",
        "check-type"
    ]

We'll do a little more whitespace checking for the last rule. This checks branching statements, the equals sign of variable declarations, operators, separators ( , / ; ), and type definitions to see that there is proper spacing all around them.

Configuring TypeScript

The TypeScript compiler requires a configuration file, tsconfig.json . This file is broken into two sections: compilerOptions and exclude . There are other attributes which you can see in this schema , but we will focus on these two. The compiler options section is composed of more rules:

"compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,

Angular 2 relies heavily on decorators, e.g. @Component , and the above rules let TypeScript know that it can use them. The reflect-metadata library we pulled in above is used in conjunction with these rules to utilize decorators properly.

"module": "commonjs",
    "moduleResolution": "node",

With these two rules, the compiler knows we'll be using CommonJS modules and that they should be resolved the way Node resolves its modules. It does so by looking at the node_modules directory for modules included with non-relative paths. We could have selected "es2015" for moduleResolution , but since we will be compiling to ES5, we cannot use it.

"noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,

With the typing system, you can specify any as a type, the first attribute above prevents us from not specifying a type. If you don't know what the type is, then use any . The one spot where we want to avoid errors for not specifying a type is with indexing objects, such as arrays, since it should be understood.

"removeComments": false,

When TypeScript compiles our code it will preserve any comments we write.

"sourceMap": true,
    "target": "es5"
},

As mentioned above we'll be compiling to ES5. We're going to have TypeScript create sourcemaps for us so that the code we write can be seen in browser debugging tools.

The exclude section will tell the compiler which sections to ignore during compilation. There is a files section, but since it does not support globbing, we would end up entering every file we need TypeScript to compile, which becomes a serious problem only after a few files.

"exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
]

This will exclude the node_modules directory and the use of the type definitions found in the main directory, as well as the main.d.ts file of the typings directory.

Configuring Karma

Next, we will set up Karma to work with Webpack. If you've ever used Karma before, you're familiar with the fact that its configuration can easily become unwieldy.

Karma relies on its configuration file in which we specify which files should be tested and how. The file, typically named karma.conf.js , is usually the only file needed. In our setup, we'll have a second file, named karma.entry.js that will contain extra setup to work with Angular 2 and Webpack.

We're going to start developing our folder structure a little more here, to keep things clean as we proceed. Create a directory named karma in the root of your project. Save the files described in the following two sections inside this directory.

Setting Up karma.conf.js

'use strict';

module.exports = (config) => {

All Karma configuration files export a single function which takes, the Karma configuration object as a parameter. We'll see some of the properties this object provides below.

config.set({
        autoWatch: true,
        browsers: ['Chrome', 'PhantomJS'],

The first property config gives us is the .set method. This is how Karma requires us to set the configuration, which takes a JSON object as a parameter.

Our first two attributes of that configuration object are autoWatch and browsers . The autoWatch attribute is a boolean that tells Karma whether it should watch the list of files we'll later provide and reload the tests when any of those files change. This is a great feature for when we're running our red-green-refactor loops.

The second attribute, browsers , tells Karma in which browsers it should run the tests. This utilizes the karma-chrome-launcher and karma-phantomjs-launcher dependencies we installed earlier to launch the tests in those browsers.

files: [
            '../node_modules/es6-shim/es6-shim.min.js',
            'karma.entry.js'
        ],

Here, we describe the files we'll be asking Karma to track. If you've used Karma in the past, this list may seem very small. You'll see that we're going to leverage TypeScript and Webpack to really track those files.

The first file is the ES2015/ES6 shim that we installed earlier which adds in some functionality that hasn't quite hit in PhantomJS yet. Then, we require the file karma.entry.js , which will be developed in the next section.

frameworks: ['jasmine'],
        logLevel: config.LOG_INFO,

Here, we tell Karma we'll be using Jasmine and that the output messages should be at the console.info level or higher. The priority of messages are:

  • LOG_DISABLE — this will, display no messages,
  • LOG_ERROR ,
  • LOG_WARN ,
  • LOG_INFO , and
  • LOG_DEBUG .

When we do LOG_INFO , we'll see the output from console.info , console.warn , and console.error , but the console.debug message will not appear.

phantomJsLauncher: {
            exitOnResourceError: true
        },

This is a configuration item specific to PhantomJS which tells it to shut down if Karma throws a ResourceError . If we didn't, PhantomJS might not shut down, and this would eat away at our system resources.

preprocessors: {
            'karma.entry.js': ['webpack', 'sourcemap']
        },

We tell Karma to run a list of preprocessors on our karma.entry.js file. Those preprocessors are the Webpack preprocessor we installed earlier with karma-webpack and the sourcemap preprocessor installed with karma-sourcemap-loader . Karma and Webpack work in conjunction to look for the chain of dependencies starting with karma.conf.js and load sourcemaps as they run.

reporters: ['dots'],
        singleRun: false,

The first line tells Karma to use the dots reporter, which, instead of outputting a narrative descriptor for each test, just outputs a single dot, unless the test fails, in which case we get a descriptive message.

The second line tells it that we'll be rerunning the tests, so Karma can keep running after it completes running all the tests.

webpack: require('../webpack/webpack.test'),
        webpackServer: {
            noInfo: true
        }
    });
};

The last two lines of our configuration set up Webpack for use with Karma. The first tells the karma-webpack plugin that the Webpack configuration file is located in our root directory's webpack directory under the filename webpack.test.js .

Webpack outputs a lot of messages, which can become cumbersome when we run tests in the console. To combat this, we'll set up the Webpack server to keep its output to a minimum by setting noInfo to true .

That's the entire karma.conf.js . Let's take a look at its sibling file, karma.entry.js .

Setting Up karma.entry.js

As mentioned in the previous section, the file karma.entry.js acts as the starting point for pulling in our test and application files when using Karma. Webpack provides a file as an entry point, and then it looks for dependencies and loads them file-by-file. By using TypeScript's module capabilities, we can tell Webpack to look just for our test files, which will all be suffixed .spec.js . Since we're going to test from those test files, we'll load all the files we need.

Additionally, we'll perform minor Angular 2 and Jasmine setup. Remember, this file should be placed in the karma directory under the root project directory.

require('es6-shim');
require('reflect-metadata');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');

The first thing we'll do is to pull in some dependencies. The ones you may not notice are those from zone.js . Zone is a library for doing change detection. It's a library owned by the Angular team, but shipped separately from Angular. If you'd like to learn more about it, here's a nice talk given by former Angular team member Brian Ford at ng-conf 2014 .

const browserTesting = require('@angular/platform-browser-dynamic/testing');
const coreTesting = require('@angular/core/testing');

coreTesting.setBaseTestProviders(
    browserTesting.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS,
    browserTesting.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS
);

Next, we'll pull in and store more dependencies. The first two are libraries we'll need for testing provided by Angular. They will let us set the base Angular providers we'll need to run our application. Then, we'll use those imported libraries to set up the base test providers.

const context = require.context('../src/', true, /\.spec\.ts$/);

context.keys.forEach(context);

These two lines are the ones that start pulling in our .spec.ts files from our src directory. The .context method comes from Webpack . The second parameter of the first line tells Webpack to look in subdirectories for more files.

After that, we'll use the context we created just like we'd use a regular require statement. This context also has a map of all the files it found where each key is the name of a file found. Hence, by running .forEach over the array of keys and calling function for each, we read in each of those .spec.ts files and, as a result, any code those tests require to run.

Error.stackTraceLimit = Infinity;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;

These lines are the Jasmine setup mentioned above. We'll make sure that we get full stack traces when we have a problem and that Jasmine uses two seconds as its default timeout. The timeout is used when we test asynchronous processes. If we don't set this properly, some of our tests could hang forever.

With these two files, we've configured Karma to run. There's a good chance we'll never need to touch these files again.

Configuring Webpack

Now, we'll set up Webpack to perform its role. If we have a webpack.test.js , a webpack.dev.js , and a webpack.prod.js there is bound to be an overlap in functionality. Some projects will use the webpack-merge from SurviveJS which keeps us from duplicating parts of our configurations.

We won't be using this approach in order to have a complete understanding of what the configuration files are providing us. For our purposes, we will have just a webpack.dev.js and a webpack.test.js . The .dev configuration will be used when spinning up the Webpack development server so that we can see our application in the browser.

In your project directory, create a sub-directory named webpack , which will house both of these files.

Setting up webpack.test.js

This file has been mentioned a couple of times. Now, we'll finally see what it's all about.

'use strict';

const path = require('path');
const webpack = require('webpack');

Here, we're pulling in a couple of dependencies we'll need. The path library is a Node core library. We'll mainly use it for resolving full file paths. We'll also going to need to pull in Webpack to use it.

module.exports = {
    devtool: 'inline-source-map',

The Webpack configuration is a JSON object, provided to Webpack by using Node's module.exports mechanism.

The first attribute in the configuration defines that we'll be using inline source maps as our debugging helper. You can read more about the devtool options on the Webpack site's documentation for configuration .

modules: {
        preLoaders: [
            { exclude: /node_modules/, loader: 'tslint', test: /\.ts$/ }
        ],
        loaders: [
            { loader: 'raw', test: /\.(css|html)$/ },
            { exclude: /node_modules/, loader: 'ts', test: /\.ts$/ }
        ]
    },

We've discussed loaders before, and here can we see them in action. We can also specify preLoaders which run before our regular loaders. We could put this loader with the other "regular" loaders, but as our application grows, having this separation of concerns will help prevent compilation from getting sluggish.

Our first "real" loader will take .css and .html files and pull them in raw, whithout doing any processing, but will pull them in as JavaScript modules. We'll then load all .ts files with the ts-loader we installed before, which is going to run each file through the TypeScript compiler. The exclude attribute allows us to avoid compiling any third-party TypeScript files. In this case, it will avoid pulling in any TypeScript files from the node_modules directory.

If we wanted to use SASS on our CSS or Jade for our HTML, we could have installed the sass-loader or pug-loader respectively, and used them in a similar way to how we utilize the ts-loader .

resolve: {
        extensions: ['', '.js', '.ts'],
        modulesDirectories: ['node_modules'],
        root: path.resolve('.', 'src')
    },

This section lets Webpack know which types of file extensions it should be loading. The empty string is needed for pulling in Node modules which do not need to provide an extension — for instance, how we pulled in path before. We also inform Webpack that the root directory for our modules is our src directory and that any external modules can be found in the node_modules directory.

tslint: {
        emitErrors: true
    }
};

The final part of this configuration sets up the tslint-loader to display any errors it finds in the console. This file, in conjunction with the two Karma files created previously, will power all of our unit testing.

Setting Up webpack.dev.js

Note: if you are not interested in using the webpack-dev-server as you follow these tutorials, you can skip this section. Also, any portion of the configuration which is discussed in the webpack.test.js section will not be rehashed below.

'use strict';

const HtmlWebpack = require('html-webpack-plugin');
const path = require('path');
const webpack = require('webpack');
const ChunkWebpack = webpack.optimize.CommonsChunkPlugin;

const rootDir = path.resolve(__dirname, '..');

The two new Webpack dependencies here are the HtmlWebpack and ChunkWebpack plugins.

The last line of this snippet utilizes that path library so we can be sure that we'll always be referencing files using our project directory as the starting point.

module.exports = {
    debug: true,
    devServer: {
        contentBase: path.resolve(rootDir, 'dist'),
        port: 9000
    },
    devtool: 'source-map',

The first attribute is debug which, when set to true , lets Webpack know it can switch all of our loaders into debug mode, which gives more information when things go wrong.

The devServer attribute describes how we want webpack-dev-server to be set up. This says that the location from which files are served will be the dist directory of our project and that we'll be using port 9000.

Don't worry about creating a dist directory, as the dev server is going to serve all of our files from memory. You will actually never see a physical dist directory be created, since it is served from memory. However, doing this tells the browser that the files are coming from another location.

entry: {
        app: [ path.resolve(rootDir, 'src', 'bootstrap') ],
        vendor: [ path.resolve(rootDir, 'src', 'vendor') ]
    },

Here, we're telling Webpack that there are two entry points for our code. One is going to be src/bootstrap.ts , and the other will be src/vendor.ts . The file vendor.ts will be our entry to load the third-party code, such as Angular, while bootstrap.ts is where our application code will begin.

You'll notice that we don't need to provide the .ts to the files. We'll explain this in a moment. Also, we didn't need this in webpack.test.js because the file karma.entry.js acted as a sort of faux entrypoint for that process.

module: {
        loaders: [
            { loader: 'raw', test: /\.(css|html)$/ },
            { exclude: /node_modules/, loader: 'ts', test: /\.ts$/ }
        ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(rootDir, 'dist')
    },

If you don't remember how we used our loaders, you can take a look at the webpack.test.js section for more information.

As mentioned before, files will be served from the dist directory, which we'll define here. The name of each file will be its key in the entry section with a .bundle.js suffix. So, we'll end up serving an app.bundle.js and a vendor.bundle.js .

plugins: [
        new ChunkWebpack({
            filename: 'vendor.bundle.js',
            minChunks: Infinity,
            name: 'vendor'
        }),
        new HtmlWebpack({
            filename: 'index.html',
            inject: 'body',
            template: path.resolve(rootDir, 'src', 'app', 'index.html')
        })
    ],

The two plugins we pulled in earlier — ChunkWebpack and HtmlWebpack — are utilized in the plugins section. The chunk plugin makes Webpack pull in the file which is referenced many times only once. The HTML plugin keeps us from having to add <script> tags to our index.html . It just takes the bundles we created in the output section and injects them into the <body> of index.html .

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

Earlier, when we didn't specify the .ts for our entry points, we did so because this section lets Webpack know that if there is a file with .ts or .js and it matches that file path, it should be read in.

We're finished setting up the development environment for Webpack.

Task Running Configuration (via NPM)

To execute any of the threads, we can run Node commands as follows:

node ./node_modules/.bin/webpack --config webpack/webpack.dev.js

Running that command — and keeping it in our minds can be quite cumbersome. To combat this, we'll leverage the aforementioned scripts section of package.json to act as our task runner. We'll be creating the following tasks:

  • Manual linting,
  • Running the dev server,
  • In-browser (Chrome) testing, and
  • Headless browser (PhantomJS) testing.

Doing this is very simple — we'll utilize our installed Node modules to perform any of the above actions. You can add the following to your scripts section of your package.json :

"lint": "tslint ./src/**/*.ts",
"start": "webpack-dev-server --config ./webpack/webpack.dev.js",
"test": "karma start ./karma/karma.conf.js",
"test:headless": "karma start ./karma/karma.conf.js --browsers PhantomJS"

Instead of saying node ./node_modules/tslint/bin/tslint.js , we can just use the name of a package, e.g. karma or tslint . This is because there is a symlink in ./node_modules/.bin to this file which NPM can utilize to run the package. The test task will run the unit tests in both Chrome and PhantomJS, while the test:headless task will run them just in PhantomJS, as specified by the browsers flag.

If you're unfamiliar with running NPM tasks, there are two ways to run them. The first one is by doing npm run [task name] which will run any task. If you used npm run lint , it would run the lint task.

NPM also has the concept of lifecycle events, each of which can be run through npm [event] . The events that we have in our list are start and test , but not test:headless . There are other events as well, which you can learn about through NPM's documentation in the scripts section .

We've now finished 99% of the configuration needed for running our tests. NPM will run Karma which will, in turn, leverage Webpack to load all of our test and application files. Once our test and application modules are all loaded, Karma will execute our tests and let us know what succeeded and what failed.

Right now if we try to execute npm test or npm run test:headless , we'll get an error from Webpack telling us we don't have an src directory. On top of that, we have no .spec.ts files, so Webpack has nothing to load.

Conclusion

We've covered a lot of ground here. Our application is completely configured for unit testing and running the red-green-refactor cycle. We were able to set up TypeScript, Karma, Webpack, and our test running with a very small amount of code, too.

In the second part of this series, we'll look at what to test before jumping into writing a sample Angular 2 application with unit tests. Feel free to leave any comments and questions in the section below.





About List