How to build and publish an Angular module

09/05/2017 By emehany

How to build and publish an Angular module

 
This post is about Angular 2 & 4.

 

Creating your Angular module: pitfalls

This part is quite the same as creating a module in your app : import the modules you need, declare components, directives or pipes, or provide some services. There is just a few points to be aware of.

First, never import BrowserModule. Your module is a feature module, only the final user should import BrowserModule, in the app root module. If you need the common directives (*ngIf, *ngFor…), import CommonModule.



import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ imports: [CommonModule] }) export class AmazingModule {}

If your module is about creating new components, directives or pipes, do not forget to export them. Declared ones are only accessible inside your module.

 
import { NgModule } from '@angular/core';

import { PrivateComponent } from './private.component';
import { PublicComponent }  from './public.component';

@NgModule({
  declarations: [
    PrivateComponent,
    PublicComponent
  ],
  exports: [PublicComponent]
})
export class AmazingModule {}


Most importantly, do not mix components/directives/pipes and services in the same module. Why?

  • A service provided in a module will be available everywhere in the app, so your module should be imported only once, in the user app root module (like the Http module).
  • An exported component/directive/pipe will only be available in the module importing yours, so your module should be imported in every user module (root and/or feature modules) that need them (like the CommonModule).

If this is not clear for you, you should my other post “Understanding Angular modules (NgModule) and their scopes”, as it’s an important (and confusing) point in Angular.

Finally, respect the Angular rule: never use browser-specific APIs (like the DOM) directly. If you do so, your module won’t be compatible with Universal server rendering and other Angular advanced options. If you really need to use browser-specific APIs (localStorage…), you should try/catch errors.

Exporting the public API

When you use an official Angular module, you just have one entry point to import all what you need (like '@angular/http').

So you’ll need to create an index.ts file, exporting all the public API of your module. It should at least contain your NgModule, and your components or services (the user will need to import them to inject them where they are needed).

 
export { AmazingModule }    from './amazing.module';
export { AmazingComponent } from './amazing.component';
export { AmazingService }   from './amazing.service';

Components/directives/pipes won’t be imported directly by the user, but you need to export them to be AoT compatible (thanks to Isaac Mann for this info).

Build tools

It’s where I started to struggle. So I managed to copy how official Angular modules work, like the HttpModule. They use:

npm install @angular/compiler @angular/compiler-cli typescript rollup uglify-js --save-dev

TypeScript configuration

Here’s the tsconfig.json of my module:

 
{
  "compilerOptions": {
    "baseUrl": ".",
    "declaration": true,
    "stripInternal": true,
    "experimentalDecorators": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "module": "es2015",
    "moduleResolution": "node",
    "paths": {
      "@angular/core": ["node_modules/@angular/core"],
      "rxjs/*": ["node_modules/rxjs/*"]
    },
    "rootDir": ".",
    "outDir": "dist",
    "sourceMap": true,
    "inlineSources": true,
    "target": "es5",
    "skipLibCheck": true,
    "lib": [
      "es2015", 
      "dom"
    ]
  },
  "files": [
    "index.ts"
  ],
  "angularCompilerOptions": {
    "strictMetadataEmit": true
  }
}

There are some important differences with your classic tsconfig.json:

  • explicit "paths" to other modules you use are needed, as the final bundle won’t include them directly (more on that later).
  • "angularCompilerOptions": { "strictMetadataEmit": true } is needed to be AoT compatible.
  • "declaration": true is important to generate type definitions files, so the user will have Intellisense for your module.
  • "noImplicitAny": true and "strictNullChecks": true are recommended to avoid errors, and to be compatible with all user configurations. "noImplicitAny": true must be respected since Angular 4.0, and "strictNullChecks": true starting from Angular 4.1.
  • "module": "es2015" is important for performance, and "sourceMap": true for debugging, but nothing specific here.
  • "stripInternal": true avoid useless declarations for internal APIs and "skipLibCheck": true avoid being blocked by (harmless) errors in the librairies you use.

Rollup configuration

Angular modules are delivered in UMD format, so your rollup.config.jsshould be set consequently. Here is an example:

 
export default {
  entry: 'dist/index.js',
  dest: 'dist/bundles/amazing.umd.js',
  sourceMap: false,
  format: 'umd',
  moduleName: 'ng.amazing',
  globals: {
    '@angular/core': 'ng.core',
    'rxjs/Observable': 'Rx',
    'rxjs/ReplaySubject': 'Rx',
    'rxjs/add/operator/map': 'Rx.Observable.prototype',
    'rxjs/add/operator/mergeMap': 'Rx.Observable.prototype',
    'rxjs/add/observable/fromEvent': 'Rx.Observable',
    'rxjs/add/observable/of': 'Rx.Observable'
  }
}

The entry script is your transpiled index.ts, so it should match your TypeScript configuration. bundles/modulename.umd.js is the conventional path and name used by Angular modules.

Rollup requires a moduleName for the UMD format. It will be a JavaScript object, so do not use special characters (no dashes).

Then, it’s where the important point takes place. Your module use Angular things (at least the NgModule decorator), but your bundle should not include Angular.

Why? Angular will already be included by the user app. If your module includes it too, it will be there twice, and there will be fatal (and incomprehensible) errors.

So you need to set Angular as a global. And you need to know the UMD module name for each module. It follows this convention: ng.modulename (ng.core, ng.common, ng.http...).

Same goes for RxJS, if your module uses it. And module names are quite a mess here. For classes (Observable…), it’s Rx. For operators (map, filter…), it’s Rx.Observable.prototype. For direct methods of classes (of, fromEvent…), it’s Rx.Observable.

Building, finally

You can now build your module bundle. You can save command lines in npm scripts :

 
{
  "scripts": {
    "transpile": "ngc",
    "package": "rollup -c",
    "minify": "uglifyjs dist/bundles/amazing.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/amazing.umd.min.js",
    "build": "npm run transpile && npm run package && npm run minify"
  }
}

Then:

npm run build

Note that transpiling is not done directly by TypeScript, you should use the Angular compiler (ngc) : it’s TypeScript with some additional Angular magic.

Publishing on npm

Do not publish everything on npm, only the dist directory.

You’ll need to create a new and specific dist/package.json. For example :

 
{
  "name": "angular-amazing",
  "version": "1.0.0",
  "description": "An amazing module for Angular.",
  "main": "bundles/amazing.umd.js",
  "module": "index.js",
  "typings": "index.d.ts",
  "keywords": [
    "angular",
    "angular2",
    "angular 2",
    "angular4"
  ],
  "author": "Your name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/youraccount/angular-amazing.git"
  },
  "homepage": "https://github.com/youraccount/angular-amazing",
  "bugs": {
    "url": "https://github.com/youraccount/angular-amazing/issues"
  },
  "peerDependencies": {
    "@angular/core": "^2.4.0 || ^4.0.0",
    "rxjs": "^5.0.1"
  }
}

Some specific points :

  • "version" must follow semantic versioning. Any breaking change means a major number increment (even if it’s a small change). And when you’ll modify your module to stay up to date with Angular, it’s a minor number increment.
  • "main" and "module" paths are needed for user imports. "typings"path is for Intellisense.
  • "licence": "MIT": an open-source licence is important, or your module is useless. Angular uses the MIT licence, and you should stick to it.
  • Angular modules you’ve used will be listed in the peerDependencies. Still follow semver, with the ^ sign, or your module will be obsolete each time Angular upgrades. For other libraries (RxJS, zone.js…), you can see the current requirements of Angular here.

Do not forget to write a README, with the documentation of your API. Otherwise your module is useless. You can use a library like copyfiles to copy your README from your root project directory (displayed on Github) to your dist directory (displayed on npm repository).

And with a configured npm account, you can now publish your module:

cd dist
npm publish

And anytime you need to update your module, just rebuild, change the version number, update the changelog and publish again.

Leave a comment

Login to Comment

Loading