Writing an AOT Compliant Angular Library
As the use of Angular expands in my workplace, I find myself delving deeper into the corners of the Angular framework. Recently, this happened to be library development. To my surprise, there seemed to be a lack of relevant material on the subject, which led to a lot of digging and experimentation. The following write up details my experience, which I hope will prove useful to others as they attempt to tackle library development.
Background
Let's take a step back. Why did I find the need to create my own libraries, and why must it be AOT compliant? What is "AOT compliance", and why should anyone care?
Standardize with libraries
In our workplace, we have 4 developers working in the Angular realm, with varying levels of experience. Up until this point, shared services and components have been passed around from person to person in crude file sharing methods. As changes are made to different versions of the library, we end up with vastly different variations of what was originally the same library. This also makes it difficult to find the "latest" version of the library.
To solve this, I created a private, IP secure NPM repository using Verdaccio. This repository would serve as the home of our company's libraries. As long as all developers agree to use this NPM repository as the source of truth, we can successfully control what changes to the library are published and used in production applications. This has the added benefit of making it very simple to install and use these libraries through NPM.
AOT Compliance
AOT stands for Ahead Of Time compilation. Without going into too much detail, AOT compilation has the following benefits:
-
Less code generated in the production bundle. A compiler is not included in the output, and tree-shaking reduces unneeded dependencies.
-
Faster rendering. This means a faster application for the end user.
-
Compile time syntax checking. Various template errors will be caught at compile time instead of run time.
-
Improved Security. Since templates are compiled ahead of time, there is less room for HTML injection.
Due to all of these benefits, the majority of our newer applications utilize AOT compilation. When an application compiles ahead of time, all libraries used by the app must also be AOT compliant. Thus, this became a requirement for any library I was going to make.
Library Development
Now, with a better understanding of the topic, we will transition into the instructional portion of this post. I will walk you step by step through creating a library that will successfully compile with an AOT compliant application. Let's get started.
Example Code
The instructions to follow have been replicated and uploaded to Github repositories for those that learn better by having a complete example (myself included). Refer to the commit history if you'd like to follow along.
Angular Library Example:
https://github.com/dylanb124/angular2-library-example
Angular Client App Example:
https://github.com/dylanb124/angular2-client-app-example
Scaffolding with Yeoman
To avoid having to self-maintain my own library template, I use the Yeoman Angular Library Generator.
[
Follow the excellent instructions provided on the github page for the generator in order to get it installed and generate a source controlled library. While this gets us 90% of the way there, I have made some of my own modifications to the generated library in support of AOT compilation, which I will detail below.
File Cleanup
Depending on the type of library you are making, you will most likely want to delete some of the example files. In this tutorial, I am going to be creating a service library. I will be keeping the sample.service.ts
file, and deleting sample.component.ts
, sample.directive.ts
, and sample.pipe.ts
.
Once you have removed the example files you won't be using, you will have to remove the imports from index.ts
.
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
// import { SampleComponent } from './src/sample.component';
// import { SampleDirective } from './src/sample.directive';
// import { SamplePipe } from './src/sample.pipe';
import { SampleService } from './src/sample.service';
// export * from './src/sample.component';
// export * from './src/sample.directive';
// export * from './src/sample.pipe';
export * from './src/sample.service';
@NgModule({
imports: [
CommonModule
],
// declarations: [
// SampleComponent,
// SampleDirective,
// SamplePipe
// ],
// exports: [
// SampleComponent,
// SampleDirective,
// SamplePipe
// ]
})
export class SampleModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: SampleModule,
providers: [SampleService]
};
}
}
Configuration Changes
Since we are making the library AOT compliant, there are a few changes we can implement to make development go smoother.
First, add the following to the bottom of tsconfig.json
:
"angularCompilerOptions": {
"strictMetadataEmit": true,
"genDir": "compiled"
}
This will output AOT specific files to a compiled
folder, which you will want to create in the project root directory. The files generated in this folder should end with a ngsummary.json
extension.
However, there's no need to track these files in our Git repo, so we will want to add the following to .gitignore
.
# Ignore AOT generated files
*.ngsummary.json
In addition to this, we will want to add the same to .npmignore
to ensure summary files are not published to the NPM repository either. While you are here, you may find it useful to exclude lint files as well.
# Ignore AOT generated files
*.ngsummary.json
# Ignore linting
tslint.json
Feel free to make changes to package.json
and README.MD
at this point to reflect application specific information. However, this is entirely optional.
Test Updates
At this point, you'll want to make sure the changes you have made are working as expected.
-
Build the application with the typescript compiler. Run the following command in the command line:
npm run tsc
. You should now have new files in thedist
folder. -
Build the application with the AOT compiler. Run the following command in the command line:
npm run build
. You should now have new files in thecompiled
folder.
If you had any compilation errors, you will want to review the instructions up to this point and ensure you have followed the steps properly. If you are unable to resolve your issue, please feel free to leave a comment and I will do my best to assist you.
Service Parameters
This section will prove useful to libraries that provide a service and require arguments from the calling application.
Library
To start with, we will have to expose a parameter for the parent application to use. If we want our library to be AOT compliant, we will need to expose this as a function in index.ts
:
export class SampleModule {
static forRoot(getConfig: Function): ModuleWithProviders {
return {
ngModule: SampleModule,
providers: [
{ provide: 'config', useFactory: getConfig },
SampleService
]
};
}
}
This config
value may now be accessed through dependency injection anywhere in the library. For example, in sample.service.ts
:
import { Inject, Injectable } from '@angular/core';
@Injectable()
export class SampleService {
constructor(@Inject('config') private config: string) {
console.log(this.config);
}
}
In real usage, the config variable would be typed as a custom interface or class containing the configurable properties. However, I am using a string for the sake of simplicity.
Client Application
Now that we have exposed the parameters, we need to figure out how to pass configuration values from the client application. For my example, I will be generating an app using the popular CLI tool, Angular CLI.
First, we need to package up our library so we can use it in other applications. I would suggest locally packaging the file, as opposed to publishing it to an NPM repository.
npm pack
This should generate a file called [LIBRARY_NAME]-[LIBRARY_VERSION].tgz
. I suggest moving this file to the project directory of your client application.
Now that we have the library packaged up, we can install it from the project directory of our client application using the following command:
npm install [LIBRARY_NAME]-[LIBRARY_VERSION].tgz
Once installed, the library can be imported into app.module.ts
, the same place where we will pass our configuration arguments to the library service.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { SampleModule } from 'angular2-library-example';
export function createConfig(): string {
return 'Config injection successful!';
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
SampleModule.forRoot(createConfig)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We can then inject the service in any component we like, such as app.component.ts
.
import { Component } from '@angular/core';
import { SampleService } from 'angular2-library-example';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
// Inject sample serivce
constructor(private sampleService: SampleService) {
}
}
If all works as planned, you should be able to AOT build and run the application, receiving the following message in the browser console:
Config injection successful!
Note: If you are using Angular CLI, you may build and run your application by issuing the ng serve --aot
command from your project root directory.
Publish Your Library
There's a high probability that you'll want to eventually publish this library to either the public NPM repository or to a private NPM repository. Luckily for you, NPM makes this very easy to do.
Public
Ensure your package.json
file of your library is configured appropriately, and run the following in the command line:
npm publish
Private
Append to the bottom of your package.json
file the private repository which you wish to publish to:
"publishConfig": {
"registry": "http://MY-REPO-URL/"
}
And then perform the following in the command line:
npm config set registry <registry url>
npm login
npm publish
Summary
After reading this blog post, you should be able to accomplish the following:
- Create an Angular library using a template generator
- Implement an AOT compatible service in the library that accepts arguments
- Successfully install and use your library in any Angular application
- Publish the library to an NPM repository
Thank you for reading. If you'd like to leave feedback or have any questions, please feel free to leave a comment and I'll get back to you promptly.