How to Deploy an Angular CLI Webpack Project to Heroku

by Jason Swett,

Angular CLI recently switched from SystemJS to Webpack. This affects the Heroku deployment process.

Here I’ll show you how to initiate an Angular CLI/Webpack project and deploy it to Heroku. (By the way, this is an Angular-only post. I won’t be talking about Rails at all here. Even if you use the Angular/Rails combo I’d recommend deploying an Angular-only app first just so you understand how the process works. If you’re ready for the Rails version, that can be found here.)

First, make sure you have the following versions of the following things installed:

Angular CLI: 1.0.0-beta.11-webpack.8
NPM: 3.10.6
Node: 6.5.0

First let’s initiate the project, which I’ll give the random name of bananas:

$ ng new bananas

When we deploy this, we’ll want to invoke the ng build command after installation. When you run ng build it creates a dist directory with your TypeScript transpiled to JavaScript and all that stuff. It’s a “static” set of files that can be served up as-is.

Because the error-free operation of ng build is critical to our success, let’s make sure we can run it locally before we try to invoke it on Heroku:

$ cd bananas
$ ng build

In all likelihood this did not run error-free for you. You probably got Cannot find global type 'Array'. and a bunch of other similar stuff like these people did.

The way I fixed that was to first have the right Node and NPM versions installed. We’ve already taken care of that at the beginning. The second step is to change your typescript version in package.json from ^2.0.0 to 2.0.0.

Here’s what my package.json looks like after the change:

{
  "name": "bananas",
  "version": "0.0.0",
  "license": "MIT",
  "angular-cli": {},
  "scripts": {
    "start": "ng serve",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "2.0.0-rc.5",
    "@angular/compiler": "2.0.0-rc.5",
    "@angular/core": "2.0.0-rc.5",
    "@angular/forms": "0.3.0",
    "@angular/http": "2.0.0-rc.5",
    "@angular/platform-browser": "2.0.0-rc.5",
    "@angular/platform-browser-dynamic": "2.0.0-rc.5",
    "@angular/router": "3.0.0-rc.1",
    "core-js": "^2.4.0",
    "rxjs": "5.0.0-beta.11",
    "ts-helpers": "^1.1.1",
    "zone.js": "0.6.12"
  },
  "devDependencies": {
    "@types/jasmine": "^2.2.30",
    "angular-cli": "1.0.0-beta.11-webpack.8",
    "codelyzer": "~0.0.26",
    "jasmine-core": "2.4.1",
    "jasmine-spec-reporter": "2.5.0",
    "karma": "0.13.22",
    "karma-chrome-launcher": "0.2.3",
    "karma-jasmine": "0.3.8",
    "karma-remap-istanbul": "^0.2.1",
    "protractor": "4.0.3",
    "ts-node": "1.2.1",
    "tslint": "3.13.0",
    "typescript": "2.0.0"
  }
}

Now do an npm install and ng build again. You should be error-free this time.

$ npm install
$ ng build

Next you’ll need to change the scripts section of package.json as follows:

  "scripts": {
    "start": "http-server",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor",
    "preinstall": "npm install -g http-server",
    "postinstall": "ng build && mv dist/* ."
  },

We did three things: First, we told Heroku to install http-server globally, which we’ll use to serve our application. Second, we told Heroku to run ng build after installation as well as to move everything in the dist directory to the project root. Third, we told Heroku to serve the app by running http-server. (Heroku automatically runs npm start for Node projects.)

We also have to move our devDependencies into dependencies. Heroku won’t pick up anything in devDependencies but we need some of it. Here’s my final package.json:

{
  "name": "bananas",
  "version": "0.0.0",
  "license": "MIT",
  "angular-cli": {},
  "scripts": {
    "start": "http-server",
    "lint": "tslint \"src/**/*.ts\"",
    "test": "ng test",
    "pree2e": "webdriver-manager update",
    "e2e": "protractor",
    "preinstall": "npm install -g http-server",
    "postinstall": "ng build && mv dist/* ."
  },
  "private": true,
  "dependencies": {
    "@angular/common": "2.0.0-rc.5",
    "@angular/compiler": "2.0.0-rc.5",
    "@angular/core": "2.0.0-rc.5",
    "@angular/forms": "0.3.0",
    "@angular/http": "2.0.0-rc.5",
    "@angular/platform-browser": "2.0.0-rc.5",
    "@angular/platform-browser-dynamic": "2.0.0-rc.5",
    "@angular/router": "3.0.0-rc.1",
    "core-js": "^2.4.0",
    "rxjs": "5.0.0-beta.11",
    "ts-helpers": "^1.1.1",
    "zone.js": "0.6.12",
    "@types/jasmine": "^2.2.30",
    "angular-cli": "1.0.0-beta.11-webpack.8",
    "codelyzer": "~0.0.26",
    "jasmine-core": "2.4.1",
    "jasmine-spec-reporter": "2.5.0",
    "karma": "0.13.22",
    "karma-chrome-launcher": "0.2.3",
    "karma-jasmine": "0.3.8",
    "karma-remap-istanbul": "^0.2.1",
    "protractor": "4.0.3",
    "ts-node": "1.2.1",
    "tslint": "3.13.0",
    "typescript": "2.0.0"
  },
  "devDependencies": {
  }
}

Now let’s create the Heroku project. (I’m assuming you already have a Heroku account and Heroku toolbelt set up.)

$ heroku create

Make sure you have all your changes commit and push your code.

$ git push heroku master

When that has finished, run:

$ heroku open

You should see “app works!”.

Page reload problem

There is one problem, though. If your Angular/Rails application has any routes defined, you’ll notice that if you navigate to a route by clicking a link, then refresh the page, the page will no longer work. The reasons for this are nuanced but I explain them in depth in my HTML5 pushState post.

The fix to the problem is pretty easy.

First, add the rack-rewrite gem to your Gemfile.

# Gemfile

gem 'rack-rewrite'

Then bundle install, of course.

$ bundle install

Then you’ll add a redirect rule to your config.ru that looks like this:

# config.ru

# This file is used by Rack-based servers to start the application.

use Rack::Rewrite do
  rewrite %r{^(?!.*(api|\.)).*$}, '/index.html'
end

require ::File.expand_path('../config/environment',  __FILE__)
run Rails.application

After you commit these changes and deploy again you should be good.

Leave a Reply

Your email address will not be published. Required fields are marked *