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.