Disclaimer
This tutorial originated before Angular provided a good way to implement libraries out of the box. Now they do. So please rather consider following this guide: https://angular.io/guide/creating-libraries. It's easier to set up and the way forward.
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 use case for this method could be a company with a MonoRepo, that still wants to split everything into multiple npm modules and libraries.
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:
- A complicated setup, that inlines CSS and HTML, compiles the sources, runs Rollup.js to build UMD and ES5 modules and does some other magic.
- A comparatively simple setup that just uses TypeScript source files. This is not encouraged by Angular. They encourage you to use "Angular Package Format".
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.
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:
- It uses
fs-extra
, so you'll need to runnpm install fs-extra --save-dev
in your project root. - It copies your source files to a new directory
dist-lib
. - (optional) It compiles the TypeScript in order to generate
index.d.ts
. This helps with type completion in IDEs. - It generates a
package.json
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!