Deploy Angular apps with ES2015, today

Every decent browser now supports ES2015.

ES2015 browser support

A few months ago, we ran into this fact and wondered why we were still publishing ES5-only production apps. We noticed we were not the first. We noticed it was worth the energy. We noticed our customers deserved it. So we went onto the challenge of publishing an ES2015 Angular app.

Not until recently, the biggest problem was that Angular CLI was not able to bundle ES2015 for production targets (and community was involved). But this finally arrived with Angular 5 and Angular CLI 1.5.

The easy part, bundling ES2015

Compiling the app to ES2015 was as easy as telling Typescript to do so. If your app supports only modern browsers, then you are done with this:

tsconfig.app.json (extract)
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "es2015",
"target": "es2015"

}
}

Support for IE

Sadly, our app must run on IE11 as well, so we cannot delete every trace of ES5 just yet. Our approach is to generate “two apps” from the same source code, given the Angular CLI kindly supports it. Have a look:

.angular-cli.json (extract)
{
"project": {
"name": "angular-es2015-prod-app"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"tsconfig": "tsconfig.app.json"
},
{
"root": "src",
"outDir": "dist",
"index": "index.legacy.html",
"main": "main.ts",
"polyfills": "polyfills.legacy.ts",
"tsconfig": "tsconfig.app.legacy.json"
}
]
}

To summarize, we have a default app with ES2015 and a “legacy” app with ES5. We use separate tsconfig, polyfills and index.html for each app.

The building step generates both apps within the same directory:

package.json (extract)
{
"name": "angular-es2015-prod-app",
"scripts": {
"start": "ng serve --app=0",
"start:legacy": "ng serve --app=1 --port=4300",
"build:prod:modern": "ng build --prod --app=0",
"build:prod:legacy": "ng build --prod --app=1",
"build:prod": "npm run build:prod:modern && npm run build:prod:legacy -- --no-delete-output-path",
},
}

Deployment

How do we load each on the correct browser? The ideal case would be to have only one index.html file and make use of ECMAScript modules on the browser. That would require no more tricks. We couldn’t find a way to get there yet with the CLI.

As we couldn’t wait for it, we decided to have two separate index.html files, one for each app. We serve the browser the correct one depending on the User-Agent string. Say, this is a config for an Apache web server:

.htaccess (Based on Angular docs)
RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{REQUEST_URI} !^/index.html$
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
# If the requested resource doesn't exist, use index.html
RewriteRule ^ /index.html

# If IE is detected, serve legacy version
RewriteCond %{HTTP_USER_AGENT} Trident
RewriteRule ^ /index.legacy.html

And… that’s it!

Results

We tested an app with on ~80 components and ~20K LOC. On the main.js file, we got ~10% downsize (parsed) and ~6% downsize (gzipped), almost for free.

One caveat… the unit testing

There is only one issue right now. The default CLI + Karma setup cannot run ES2015 testing.

For now, the workaround is to run unit tests only over the ES5 app:

package.json (extract)
{
// ...
"scripts": {
"test": "ng test --app=1"
}
}

Let’s make it happen!

Get the sources for an Angular ES2015 prod app here (coming soon… sorry!).

So! Give it a try and tell us…

What are your results?

What would you improve on this approach?