Understanding Angular CLI Builders
Angular CLI Builders have become a significant topic of discussion since Angular 8 introduced them. In this piece, we'll explore how the Builders API works and how developers can leverage it.
I'll be presenting at Ng Sri Lanka this year. We're currently seeking sponsors — if your organization is interested in supporting a community-driven tech conference, please reach out to the event organizers.
Introduction to Builders
The Builders API shipped with Angular 8, providing a mechanism to customize built-in CLI operations such as ng build, ng test, and ng lint. This is distinct from Angular Schematics, which enable custom commands for ng generate or support for ng add.
How We Got Here
Before Angular 8, modifying the build pipeline meant extracting the default webpack configuration and layering in your own code. For many developers, this was a cumbersome process — after ejecting, standard CLI commands were replaced and no longer worked as intended.
The snippet below comes from .angular-cli.json, the configuration file used prior to Angular 6. Notice that it simply accepts a config file; the serve and build sections aren't even defined.
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
}
Now compare that with an Angular 8 project. I've trimmed unrelated sections for readability, but you can see that every command now has a builder option. Angular provides default builders, yet you can plug in your own implementations with minimal friction.
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
},
"styles": [
],
"scripts": []
},
"configurations": {
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ecommerceapp:build"
},
"configurations": {
"production": {
"browserTarget": "ecommerceapp:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ecommerceapp:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
],
"styles": [
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "ecommerceapp:serve"
},
"configurations": {
"production": {
"devServerTarget": "ecommerceapp:serve:production"
}
}
}
}
}
The CLI Builders API
When Angular 8 went public with the CLI builders API, it opened a wide range of opportunities for the community. Creating custom builders no longer requires ejecting the webpack configuration — in fact, the ng eject command has been removed entirely.
Let's build our own builder from scratch to understand the process. Before doing so, it's worth checking Angular Builders — a curated collection of custom builders that might already address your needs.
Setting Up a Builders API Project
The Angular documentation provides a starter project you can clone to begin building your own custom builder.
Let's review the key components of the API:
- BuilderContext
- BuilderOutput
- createBuilder
BuilderContext
Here are the most essential methods and properties:
- reportStatus: This method updates the command-line interface with the current status of your running task.
- logger: Access logging capabilities including log, debug, info, warn, error, and fatal.
- scheduleTarget: Schedule additional tasks with specified configuration. For instance, you could trigger a build as part of your custom operation.
createBuilder
This serves as the builder's entry point. It must return either a Promise or Observable of type BuilderOutput.
BuilderOutput
The output can include these fields:
- error: An optional error message to communicate back to the application.
- info: An optional
[key, value]pair with supplementary information. - success: A required boolean value indicating whether the task succeeded.
- target: An optional object describing the
configuration,project, andtarget.
To illustrate, let's adjust the starter project to execute both a build and a test using our custom builder.
- Replace the code in the downloaded repository's
index.tswith the following:
import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';
interface Options extends JsonObject {
command: string;
args: string[];
configuration: string;
}
export default createBuilder<Options>(
async (options: Options, context: BuilderContext): Promise<BuilderOutput> => {
context.reportStatus(`Executing "${options.command}"...`);
const configuration = options.configuration ? options.configuration : 'production';
const build = await context.scheduleTarget({
target: 'build',
project: context.target !== undefined ? context.target.project : '',
configuration
});
const test = await context.scheduleTarget({
target: 'test',
project: context.target !== undefined ? context.target.project : ''
});
let buildResult = await build.result && await test.result;
return { success: buildResult.success };
});
2. Build the project locally using npm run build
3. Run npm link to make the package available elsewhere on your machine
4. Generate a new Angular application using the CLI
5. Run npm link @example/command-runner — adjust the package name to match what's specified in your builder's package.json
6. Add this configuration to your Angular app's angular.json
"[your-command]": {
"builder": "@example/command-runner:command",
"options": {
"command": "[your-command]",
"args": [
"src/main.ts"
]
}
}
7. Execute the builder locally with ng run [project-name]: [your-command] or, if your command is named deploy, simply use ng deploy — this has been supported since Angular CLI 8.3.0.
You can also publish your builder to npm, enabling reuse across different applications.
Final Thoughts
The builders API offers extensive possibilities, and a growing number of community-driven builders are already available. The custom deploy builder proved so useful that the CLI team incorporated ng deploy into the default command set.
If you're a Netlify user, there's a convenient custom builder for deploying from the CLI.
Both the builder core and the example projects are open source — contributions are always appreciated.
