Migrating a project to Angular CLI build system

I recently migrated an Angular app from a gulp build system to an Angular CLI (version 1.2.6). I am enjoying the new build system and the improved Angular performance. I wanted to share the process, some pain points and lessons learned.

A good starting point for migrating is the angular-cli migration guide and moving guide. Also check out the list of general stories

Our migration plan was a little more involved since our project consists of an Angular client app and a backend node typescript server with Express.js to serve our client app and proxy an external API.

Migration Outline

  • Create a new angular-cli project
  • Copy over (move) your existing application into the new project
  • Merge package.json files and resolve package versions
  • Iterate over the app, build and tests
  • Switch over build and run scripts

Goals

  • Replace current gulp commands and code with angular-cli and npm scripts
  • Work on both windows and mac development machines
  • Be able to run a local server to host an API and client ap
  • Create a shell script for build and packaging for the build server

Migration Preparation

  • Move all static resources into the assets folder and ensure relative paths
  • Ensure node and npm prereqs for angular-cli. I prefer nvm.
  • Find and fix any private variables being referenced in components. Make those vars public.

Lessons Learned and Pain Points

  • I only had one global tsconfig.json file before but angular-cli makes use of multiple tsconfig.json files and for a new tsconfig.app.json file. Also, as of right now some editors ( VS Code #24362) don't support custom tsconfig filenames yet. In some cases I had to create a tsconfig.json for VS Code alongside the tsconfig.app.json file in order for the editor to recognize specific paths. Not an ideal solution.

  • I used to be to run tsc and compile both server and client apps at the same time. Unfortunately, with the cli I now have to compile the client and server apps separately and with different commands and config.

  • If you are going to ditch gulp entirely you will need to embrace npm scripts to augment angular-cli. I found this resource "Why I Left Gulp and Grunt for npm Scripts" by Cory House a must read before the migration.

  • It is important to realize that ng serve and ng test compile your typescript into memory and NOT physical files.

  • If you are clueless as to why ng test doesn't work with cryptic errors look into the pollyfills.ts file and start uncommenting lines.

  • I would recommend locking package.json versions with exact version numbers, shrinkwrap, lockfile or even yarn. There is no joy in debugging @types, tsc versions and incompatible modules.

  • Building with ng build --prod which uses AOT compilation is a lot more strict than the default typescript rules with ng serve.

  • You may run into libraries that don't support AOT compilation yet. For example, we had to switch from Alberplz/angular2-color-picker to zefoy/ngx-color-picker

  • In cases where you want to (not really necessary to remove just for the sake) but you can use plain old js node processes and wrap them in npm scripts.

  • Using sass for css is straightforward by setting "styleExt": "scss" in .angular-cli.json. See Angular2 - Angular-CLI SASS options

  • Using font awesome was straightforward by simply adding "../node_modules/font-awesome/css/font-awesome.css" to the styles array in .angular-cli.json file.

Serve and Build Scripts

Serving for development work

Just use npm start.

This compiles and serves the client app from memory.

Underneath the hood, serving our client and server requires some extra steps since we also need to compile a typescript server application. This gets tricky since ng serve is running its own lite http server. We can actually proxy some routes in the ng serve to our backend server running on a different port. This is available with a --proxy-config switch.

When we compile our client app with ng build we can actually just serve our node server and the compiled client since our app handles all the routing by design.

This is why there are some port number and proxy switching between the serve and build tasks.

Below are the baisc npm scripts we run in parallel in order to reduce the command into a simple npm start

"compile-server": "tsc -p ./src/server",

"server": "npm run compile-server && cross-env NODE_CONFIG_DIR=./src/server/config node dist/server/index.js",

"client": "ng serve --base-href=/ --proxy-config dev.proxy.conf.json -p 3000 --sourcemap=true --watch ",

"start": "cross-env NODE_PORT=1337 npm-run-all --parallel client server"

I make use of the cross platform cross-env and npm-run-all libraries to support macs and window machines.

Buliding for prod (Bundling and AOT)

The below table explains the difference between ng build --prod and ng serve (aka ng build)

Flag --dev --prod
--aot false true
--environment dev prod
--output-hashing media all
--sourcemaps true false
--extract-css false true

Run npm run start-build in order to compile the client and server apps into the dist directory and run a node server for that directoy. This is actually how we will in a deploy (production) mode.

Below are the package.json scripts:

"build": "ng build --prod --base-href=/app/ --watch",
"start-build": "cross-env NODE_ENV=dev NODE_PORT=3000 npm-run-all --parallel build server",

Running Tests

Use ng test or ng test --single-run --browsers PhantomJS for the build server and PhantomJS.

Build Script and Deployment Server

I won't go into great depth regarding our build script but I will include it for the big picture. We use this on our Bamboo build server. It works but is rather inefficient.

In the end we want a my-client-x-x-x.zip with the following:

  1. compiled client bundles
  2. compiled server code
  3. node_modules required for server app
#!/bin/sh

# get build number and branch provided in bamboo task
build=$1
branch=$2

echo "build $build and branch $branch"

. ~/.nvm/nvm.sh
nvm use 7.2.0

echo 'node -v'
node  -v

npm install -g npm@5.2.0 --no-progress

echo 'npm -v'
npm -v

echo 'npm install'
npm install --no-progress
echo 'npm install finished'

echo 'showing versions with npm ls'
npm ls

# Dump versions for logs
ng version

npm run clean

npm run compile-server
npm run compile-lib

npm run test-build-server

# remove current dist folder files
echo 'Removing current ./dist folder'
rm -rf ./dist
mkdir ./dist

rm -rf artifacts
mkdir -p artifacts

# Creates dist/app files
echo 'Running ng build --prod'
ng build --prod --progress=false
echo 'ng build finished'

echo 'optomize build images'
node ./devops/scripts/min-images.js

# Creates dist/server files. we have to compile server tsc separately
cd src/server

# run tsc from bin modules to control our tsc version with package.json
../../node_modules/.bin/tsc

# back to root proj directory
cd ../../

echo 'copy server config directory'
cp -rf ./src/server/config ./dist/server

echo 'Run devops/version.it.js to get version and bamboo build'
# Create version.json and bamboo vars text file
version=$(node devops/version-it.js $build $branch)

echo 'npm prune --production'
npm prune --production

# copy over to dist
cp -rf ./node_modules ./dist/node_modules

zipFileName=my-client-$version.zip
zip -r "./artifacts/$zipFileName" dist devops
echo "Created ZIP file for dist at ${zipFileName}"

Questions and Next Steps