Building an Angular Library?

May 2018

How to write an Angular library

This tutorial was updated in May 2018 and works for Angular 2, Angular 4, Angular 5 and Angular 6.

Setting up an Angular-compatible library isn't straight forward at all, which you might have noticed by now. Many steps are involved, like inlining html and css, building UMD and ES5 modules and much more magic. To put it in Angular's lingo, you'll have to adhere to the "Angular Package Format". Historically (Angular 2-5), Angular doesn't provide a straight forward way to build a library that has the correct format. Community solutions emerged, most notably ng-packagr. In Angular 6, an option ng generate library my-lib was added to AngularCLI, that uses ng-packagr under the hood.

In this article we're going to talk about a different approach, that involves less magic. At the core of this approach is not compiling the TypeScript at all, but use the source files directly. This approach has shortcomings, but it might suit your needs.

So to summarize up until now, you have two ways to build your library:

When should you use which method? If you're writing a module for the community, then you should stick with the complicated setup. This is not what is covered in this tutorial, but instead you can use "ng generate library my-lib" starting with AngularCLI 6. This sets up the complicated build pipeline for you and you don't have to understand exactly what it does. One drawback of this method is that when there's a problem with the magic, it's a hard-to-debug black box. Another drawback is that you'll have to compile the library every single time you'll make a change. Not exactly the hot reloading and rapid development we're used to. But it's quite certainly the right choice for building a community package. If you do so, you can stop reading this article now and head over to https://github.com/angular/angular-cli/wiki/stories-create-library.

What this tutorial describes is how you can also use TypeScript source files to build your library. That's only recommendable if you want to use the libraries for company internal purposes. You'll have to keep in mind, that the consumer of your library will have to use a compatible TypeScript version! So if you use a TypeScript 2.8 feature in the library, but the consumer is only at TypeScript 2.7, the code won't compile. That's why it's not an ideal solution for a community library. A good usecase for this method could be a company with a MonoRepo, that still wants to split everything into multiple npm modules and libraries.

Step 1: Create a new project with the AngularCLI

Create a new project. This will be a wrapper and consumer for your library module. I am going to call my library libex (for "library-example", and it was still free on npm) so I call the new project libex-project.

ng new libex-project --prefix libex

Use your library title instead of libex. Prefix is what you'll write in front of your components, for example if I have a HelloComponent it will be used by libex-hello now.

Step 2: Create a new module

Your library will reside in it's own module. But first we've got to create that module.

ng g module libex

Then we cd into that folder.

cd src/app/libex/

Step 3: Build your library module

Create components, services etc., e.g.

ng g component hello

When you're done, you'll have to export the desired components:

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [HelloComponent],
  exports: [HelloComponent]
})

You can use your AppModule to test the library:

...
imports: [
  BrowserModule,
  LibexModule
],

If you need singleton services, you should modify your library module like so:

@NgModule({
  providers: [ /* Don't add the services here */ ]
})
export class LibexModule {
  static forRoot() {
    return {
      ngModule: LibexModule,
      providers: [ SomeService ]
    }
  }
}

and change the imports in AppModule to:

...
imports: [
  BrowserModule,
  LibexModule.forRoot()
],

Step 4: Setup index.ts

To declare the public API for your consumers, you should setup an index.ts where you export all modules, components etc. your consumers should have access to. Here for example it would look like this:

export {LibexModule} from './libex.module';
export {HelloService} from './hello.service';
export {HelloComponent} from './hello/hello.component';

That way, it's also easier to import for the consumers of the library, as they can just use something like.

import {HelloComponent} from 'libex'

instead of

import {HelloComponent} from 'libex/hello/hello.component'

This will also help you to generate an index.d.ts in the next step, which in turn helps your IDE with autocompletion. Having a so called "barrel" file (a file, that re-exports some classes) can lead to some problems. If you run into an Angular DI Exception, see this stackoverflow thread.

Step 5: Publish

So far, we have pretty much set up a regular Angular application. Now we want to get it to npm! How can we achieve this? We need to have a package.json and bundle all the files we want to distribute together. Personally, I'm using a small node script to do this build task. It looks like this:

const fsextra = require('fs-extra');
const { exec } = require('child_process');

fsextra.copy('./src/app/libex', './dist-lib', err => {
  if (err) return console.error(err);
  console.log('Copied files');
  createDeclarations();
});

function createDeclarations() {
  exec('cd dist-lib && tsc index.ts --declaration', () => {
    console.log('Generated declarations (and some JS files...)');
    createPackageJson();
  });
}

function createPackageJson() {
  const packageJSON =  {
    "name": "libex",
    "version": "2.0.0",
    "description": "How to build libraries with Angular (2, 4, 5...)",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "repository": {
      "type": "git",
      "url": "git+https://github.com/bersling/angular-library-example"
    },
    "keywords": [
      "Angular",
      "Angular2",
      "Library",
      "Example"
    ],
    "author": "bersling@gmail.com",
    "license": "MIT",
    "bugs": {
      "url": "https://github.com/bersling/angular-library-example/issues"
    },
    "homepage": "https://github.com/bersling/angular-library-example#readme",
    "types": "index.d.ts"
  };
  fsextra.writeJson('./dist-lib/package.json', packageJSON, {spaces: 2}, err => {
    if (err) return console.error(err);
    console.log('Created package.json');
  });

}

It looks more complicated than it is, the most part is just the specification for the package.json. What happens in this script:

The most important part is the generation of the package.json. Now since we're just publishing the typescript sources (note: that means your library will only work for consumers that also use typescript), we're ready to publish. Just run npm publish from the dist-lib folder! You can also add a .npmignore so you publish only exactly what's needed, it works like .gitignore, just for npm.

In case your library requires other libraries, you'll need to add them to your package.json and also to your local installation.

For subsequent releases, adjust the version number in the build file according to semver (semantic versioning). In the format x.y.z, x is the major version, y is the minor version and z is a patch version. The most important thing here is to understand, that you shouldn't publish any breaking changes to minor versions or patches. Minor versions serve the purpose of adding features without touching the existing API. Patches serve - well, to patch, to bugfix. And if you have a breaking change, then you'll need to publish a new major version or some people might get angry at you. It's also good to first publish with a tag, for example npm publish --tag next, such that you can test your new version with npm install mylib@next and other people don't automatically install it.

Step 6: Consuming your library

You can either install your library by downloading it from npm with npm i your-library or you can use npm link.

To install with npm link, you'll need to run Angular with the --preserve-symlinks option. You can also specify this in your .angular-cli.json:

"defaults": {
  ...
  "build": {
    "preserveSymlinks": true
  }
}

Important: Another thing you'll have to do, starting with Angular 5, is to include your files. So in the tsconfig.json you'll need:

"include": [
  "src/**/*",
  “node_modules/your-library/index.ts",
]

Well and that's it, now you have your library, which you can develop & test locally and publish to npm!


Check out the full source of the demo library on github: https://github.com/bersling/angular-library-example

Interested in TypeScript?