Check out myAngular article series with live demos and Facebook group Angular - Advanced Topics

Angular Bazel Build with Dev Server

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Sat Nov 11 2017

In this post I will show how to wire up a simple implementation of a Bazel build with a “live refresh” dev server.

This post is a continuation of my previous Bazel article, so you might want to read that article first.

The main thing the previous implementation lacked was a dev server with support for live refreshing after saving source files.

There are many ways to approach a dev server for Bazel builds. This is just one example, so I would love some feedback about improvements.

Build

In the previous article I showed how to set up Bazel (iBazel for watch mode) to build the project. This did not include on-the-fly bundling of the javascript files though.

Bundling is technically optional. There is nothing stopping you from serving the app from the individual files using SystemJS.

However, after trying this approach it became clear that it doesn’t scale for large projects. Serving thousands of files just isn’t scalable, even from localhost.

Instead the files should be bundled as part of the Bazel build.

The Angular team is currently figuring out the best approach for bundling Angular with Bazel. One simple idea they are experimenting with is file concatenation on-the-fly.

My poc uses a slight variation on the approach in Alex’s great demo repo.

The bundling is as simple as just combining all the application code into a single file.

Bazel gives us a manifest (.MF file) of all the source files, which tells me which files to bundle.

Here is the concatenation script:

const fs = require('fs') const start = new Date(); let content = fs.readFileSync('angular-demo/devsources.MF', {encoding: 'utf-8'}) let files = content.split('\n'); let src = files.filter(file => file.trim().length > 0 && !file.endsWith('.ngsummary.js')); let code = ''; src.forEach(file => { let content = fs.readFileSync(`../${file}`, {encoding: 'utf-8'}) code += '\n\n' + content; }); fs.writeFileSync('bundle.js', code, 'utf8'); let end = new Date(); console.log(`bundled JS in ${end - start}ms. Last bundled ${end}`);

The ngsummary.js files are not part of the application, so I am omitting them.

Lite-Server

The output of the concat.js script is a single bundle.js file.

In order to serve this file from lite-server I create a symlink from my project folder to the bazel-bin folder, where the Bazel output is found.

From there on it’s just regular lite-server configurations to serve the site.

Every time you make a code change, iBazel will cause the build to re-trigger and refresh bundle.js.

Note: I had to disable Git integration in Visual Studio Code since it was thrashing the iBazel process by triggering rebuilds every few seconds. I suspect there is some polling that causes constant rebuilding.

SystemJS

As I mentioned earlier, it’s not practical to serve the individual application files using SystemJS. However, there are a few advantages to using SystemJS to serve the generated application bundle + external dependencies like Angular umds and rxjs.

Bazel outputs the files as named AMD modules. You can load the AMD modules using something like require.js, but this creates a slight challenge with RxJs.

RxJs does not ship as AMD. To make RxjS compatible with require.js, operators etc will have to be wrapped as AMD modules.

Back to SystemJS…

Instead of wrapping RxJs I decided to go with SystemJS since it can understand most module formats, which means we can serve the application with mixed module formats.

This does not have the performance penalty of the previous implementation since the number of files is greatly reduced.

Here is my index.html file:

<!doctype html> <html> <head> <script src="/node_modules/zone.js/dist/zone.min.js"></script> <script src="/node_modules/systemjs/dist/system.src.js"></script> <script src="/system-config.js"></script> <script> System.import('out-dir/bundle.js').then(() => { return System.import("angular-demo/main"); }) .catch((e) => { console.log(e); }) </script> <link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <app></app> </body> </html>

Notice the nested System.import calls.

The first import call loads the application bundle. The second loads a named AMD module defined in the bundle.js file.

One clear advantage of this is that there is no special handling of RxJs operators since SystemJS is able to normalize different module formats (commonJS vs AMD).

Demo Components

To make this a bit more realistic I added a few semi-complex Angular components to the project.

As part of this I wired up Sass compilation to one of the components.

You can see the component code below:

import { Component, Input } from '@angular/core'; import { Directory } from './directory'; @Component({ selector: 'treeview', styleUrls: ['treeview-styles.css'], template: ` <ul> <li *ngFor="let dir of directories"> <span class="iconButton" (click)="dir.toggle()">{{dir.getIcon()}}</span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()" /> {{ dir.name }} <div *ngIf="dir.expanded"> <ul> <li *ngFor="let file of dir.files">{{file}}</li> </ul> <treeview [directories]="dir.directories"></treeview> </div> </li> </ul> ` }) export class Treeview { @Input() directories: Array<Directory>; }

Notice the style url treeview-styles.css. This file was generated by a Bazel Sass rule defined in the BUILD.bazel file for this component’s NgModule (seen below).

package(default_visibility = ["//visibility:public"]) load("@angular//:index.bzl", "ng_module") load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary") sass_binary( name = "treeview-styles", src = "treeview.scss" ) ng_module( name = "treeview", srcs = glob(["**/*.ts"]), tsconfig = "//:tsconfig.json", assets = [":treeview-styles"] )

The name of the generated css file corresponds to the name of the Sass Bazel rule, not the name of the original .scss file.

External Html Templates

If your components have external templates you have to list them as assets in the Bazel rule for the component's ngModule. This will ensure that the application will rebuild the component if the template changes. Look at my grid component for an example.

I have put the code on Github.

Look in the dev-envrionment branch for the code.

To run the project run: bazel run @yarn//:yarn and npm run start

If you liked this article, share it with your friends on social media:

We also have a new Facebook group about advanced Angular topics.

I invite you to follow me on twitter