Chasing the Holy Grail of NG Development : Angular CLI, NPM Library, and Packaging

$URLMapContent.title

Chasing the Holy Grail of NG Development : Angular CLI, NPM Library, and Packaging Posted: 10.11.2017

Holy grail

It is amazing how far we have come on the JavaScript side of the house. Creating UI richness in the browser has always been the holy grail. From Flash to Applets to old school DHTML and outside solutions like Java Webstart, we have never been as close to that holy grail of rich desktop-like applications in the browser as we are today.

The combination of HTML 5 and Angular 2+ (NG2) has brought us closer to Avalon than ever before. Using these two recent technologies, front end web developers have created more rich desktop-like apps in recent years than any single library or technology has before. HTML 5 and NG2 development does not require a browser plugin or some other client-side binary to be installed like a Flash or Java solution and it works well across browsers and operating systems. While NG2 has a lot to offer, it also brought some complexity to the JavaScript side of the equation that was not previously as common when using good old JQuery. Not only does NG2 rely heavily on ASYNC programming but also carries with it a number of established OO Design Patterns. All of this is accomplished through TypeScript. While it is true, at the end of the day, that code in the browser is just plain old JavaScript, it is written as TypeScript and gets compiled. This provides us lots of benefits like compile time checking of code and even some interesting performance options using NG Ahead-of-Time Compilation.

NG2 also works well with Node.js and Node.js’s well known package management, NPM. This allows us to share libraries that we write in addition to using the amazing amount of NG2-NPM libraries that already exist. Everything from Logger libraries to File System libraries are available. If it is a common thing you are doing, chances are that someone already has a library you can use in NPM.

The Problem

Of course everything has a cost. While all the development tooling and functionality of the NG2 stack is amazing, building in NG2 can be difficult. There are lots of people who will die on the hill of their favorite build system. Often build systems focus on building your own web application. There is a difference between building a web application and building a library. By library, I am referring to a set of NG2 Components, Services, and Modules that are placed into NPM. At first you might think, (as I did) that other build systems have the most common libraries up so I assumed all the popular build systems out there offer more or less the same libraries. This is just not true. Most build systems for NG2 are focused on building and packaging web applications, not building libraries for NPM.

As the Director of Engineering at dotCMS, I can safely say we’ve used everything to build our NG2 code from SystemJS to WebPack to NG-CLI. We believe NG-CLI is the best and, considering their latest versions, have the best development tooling. We also find that it requires less code and files to maintain. Under the covers, NG-CLI uses a number of libraries including WebPack but it provides easy ways to interact with the build system. In addition, it has scaffolding tools akin to what Ruby on Rails did which speed initial build and help with maintaining coding conventions.

This is where the problem arises. We at dotCMS needed to write a library for both our clients to consume for use in their web application but also for us to consume and use in our administrator application. The library is essentially a collection of shared NG2 reusable pieces to aid in development and integration of dotCMS applications and functionality.

The Solution

If you are at all familiar with the packaging problem, you are also probably aware of a famous and polarizing Github issue for NG-CLI. It is unclear exactly how NG-CLI will solve the issue or even if the issue is 100% solved. In fact Google, doesn’t use NG-CLI to package NG itself. They have a custom build process. There are countless people online trying to solve the issue and a number of blogs which claim to have an easy way, none of which worked for us at dotCMS for one reason or another.

The most recent solution mentioned in the blogs is using ng-packagr. See https://medium.com/@ngl817/building-an-angular-4-component-library-with-the-angular-cli-and-ng-packagr-53b2ade0701e and https://medium.com/spektrakel-blog/angular-libraries-are-fun-fece73cceb05.

For us, ng-packagr had its own issues. We had issues using the components and modules across different NG apps using it. So we were left wondering what to do, but we knew we wanted:

  • Something easy to maintain. It is one thing to get something working but we were concerned with the maintenance and growth of our library. We need all our devs to be able to jump in and keep developing without spending all their time playing with the build
  • A system that was understandable
  • To use NG-CLI
  • Support for images, css, NG Modules and NG Templates within our NG Components

Let’s start taking a look at the scripts section of our package.json

"scripts": {
       "ng": "ng",
       "start": "ng serve",
       "build": "ng build",
       "test": "ng test",
       "lint": "ng lint",
       "e2e": "ng e2e",
       "transpile:dotcms-js": "ngc -p ./tsconfig.dotcms-js.json",
       "package:dotcms-js": "rollup -c",
       "minify:dotcms-js":
           "uglifyjs build-dotcms-js/bundles/amazing.umd.js --screw-ie8 --compress --mangle --comments --output build-dotcms-js/bundles/amazing.umd.min.js",
       "build:dotcms-js":
           "npm run transpile:dotcms-js && npm run package:dotcms-js && npm run minify:dotcms-js && npm run copy-files:dotcms-js",
       "copy-files:dotcms-js":
           "cd src && copyfiles './**/*.html' './**/*.css' './**/*.jpg' './**/*.gif' './**/*.png' ../build-dotcms-js/ && cd /"
   }

As you can see we have all the normal NG-CLI stuff. Our project is a NG-CLI project and we use it in our IDE including the usage of lint, tests and serving for development. When we build we run `npm run build:dotcms-js`

This builds our library for us into the build-dotcms-js directory. From that directory we copy a package.json into the directory with the proper version to publish and run `npm publish` which publishes our library to NPM so it can be used as a library in other NG apps.

The build: dotcms-js script makes use of ngc, uglifyjs, rollup and copy-files.

ngc is used to compile the TypeScript. We install ngc globally through npm. ngc reads our typescript config file. We maintain a specific Typescript config for packaging the library.

Our tsconfig.dotcms-js.json config file has the following options  

{
   "compilerOptions": {
       "baseUrl": ".",
       "target": "es5",
       "declaration": true,
       "module": "es2015",
       "outDir": "./build-dotcms-js",
       "moduleResolution": "node",
       "emitDecoratorMetadata": true,
       "experimentalDecorators": true,
       "sourceMap": true,
       "rootDir": "./src",
       "skipLibCheck": true,
       "inlineSources": true,
       "stripInternal": true,
       "paths": {
           "@angular/core": ["node_modules/@angular/core"],
           "rxjs/*": ["node_modules/rxjs/*"]
       },
       "declarationDir": "./build-dotcms-js",
       "lib": [
           "es2015",
           "dom",
           "dom.iterable"
       ]
   },
 "files": [
       "src/dotcms-js.ts"
   ],
 "exclude": [
   "showcase",
   "dist",
   "build-dotcms-js",
   "e2e/**/*.*",
   "src/**/*.it-spec.ts",
   "src/**/*.e2e.ts",
   "src/**/app.module.ts"
 ],
   "angularCompilerOptions": {
     "strictMetadataEmit": true,
     "genDir": "./ngc-out"
   }
}

After everything is compiled we use rollup to handle the bundling of the library. This is key. In your final package.json you need to include your main - i.e. "main": "bundles/amazing.umd.js". The file your main points at is a bundled up and minified version of all your NG components, modules and services. We are using UMD format which is the Universal Module Definition. We have rollup setup as a devDependency "rollup": "^0.47.4"

Here is our rollup.config.js

export default {
   entry: 'build-dotcms-js/dotcms-js.js',
   dest: 'build-dotcms-js/bundles/amazing.umd.js',
   sourceMap: true,
   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'
   },
   external: [ '@angular/core', 'rxjs', 'angular2-logger/core',
   '@angular/http', 'rxjs/add/operator/catch', 'rxjs/add/operator/toPromise', 'rxjs/add/operator/debounceTime',
   'rxjs/add/operator/map', 'primeng/components/breadcrumb/breadcrumb', '@angular/common', '@angular/forms']
}

After everything is bundled we use uglifyjs to minify the Javascript. We have uglifyjs set up in the package.json as a devDependency "@types/uglify-js": "^2.6.28"

Finally we use copyfiles to copy our html, css, jpg, and png files to our build directory for publishing. This library is also setup as a devDependency "copyfiles": "^1.2.0". This process simply copies all our assets to the build directory.

What works for us

While some might find the manual copy of the files and usage of rollup messy or hacky, we find it simple and easy to understand. We have control with a minimal amount of config files. We have extensive experience with SystemJS and WebPack which, once you get going with them, require lots of config files and other libraries to get what you want. We have found that our build system is able to handle all our needs.

We have all the NG-CLI tooling for development and with the usage of ngc, rollup, uglify and copyfiles, we have a system that allows us to easily build for packaging our library to NPM.