Angular CLI v8.0.0 brought us a stable CLI Builders API, which allows you to customize, replace, or even create new CLI commands
Now the most popular builder for customizing the webpack
configuration is @angular-builders/custom-webpack. If you look at the source code of all the builders supplied with the package, you will see very compact solutions that do not exceed 30 lines of code.
Maybe make your own?
Challenge Accepted!
This material assumes that you are already familiar with the Angular and Angular CLI, you know what rxjs is and how to live with it, and you are also ready to read 50 lines of code.
So what exactly are these builders?
Bu*ilder* is just a function that can be called using the Angular CLI. It is called with two parameters:
- JSON-like configuration object
-
BuilderContext
— an object that provides access, for example, to the logger.
A function can be either synchronous or asynchronous. As a bonus, you can return Observable
. But in any case, both Promise
and Observable
should emit BuilderOutput
.
This function, packaged in a certain way in the npm package, can be used to configure such CLI commands as build
, test
, lint
, deploy
, and any other from the architect
section of the angular.json
configuration file.
OK. Will there be just an example from the documentation?
Not. Of course, at first, I made an example, most similar to the example from the documentation. I used such a builder when working with NX and deploying only modified applications. But I suddenly needed a builder who can run commands from angular.json
in a certain order and depending on each other.
A more real-world example: you suddenly needed your application running dev-server in tests. There are various console utilities and npm packages for starting and waiting for the server to start, but why not create a builder that can run dev-server in watch mode before running the tests and kill dev-server as soon as the tests are complete?
Where to begin?
You need to start by creating a blank for the package in which we will pack the builders. I generated the workspace using NX, as well as the library blank for the builder.
npx create-nx-workspace ng-builders
cd ./ng-builders
npx ng g @nrwl/node:library build
Build Configuration
This is how I saw the configuration that solved my problem:
// angular.json
{
"version": 1,
"projects": {
"app": {
"architect": {
"stepper": {
"builder": "@ ng-builders / build: stepper",
"options": {
"targets": { // description of targets
"jest": { // target name and configuration
"target": "app: jest", // existing task in angular.json
"deps": ["server"] // dependent targets that need to be run before the main
},
"server": {
"target": "app: serve",
"watch": true // watch mode
}
},
"steps": ["jest"] // list of goals to complete
}
}
}
}
}
}
In theory, such a task in angular.json
can be done using the command:
ng run app:stepper
Having worked a little on the specification, I came to the following interfaces:
export interface Target {
/ **
* A list of target ids that must be completed before starting the task
*
* Differs from Schema#steps in that the task does not wait for the full
* performing dependent tasks
* /
deps?: string[];
/ **
* Purpose to fulfill
* /
target: string;
/ **
* Turn on watch mode
* /
watch?: boolean;
/ **
* Overriding target configuration parameters
* /
overrides?: {[key: string]: any};
}
export interface Targets {
// targetId - task name
[targetId: string]: Target;
}
export interface Schema {
/ **
* Strict sequence of tasks in the array
* indicate targetId of Targets
*
* The next task is launched only after the previous
* /
steps: string[];
targets: Targets;
}
The specification is completed. Of course, the scheme can be further expanded and, for example, add a configuration choice (production, dev, etc.), but for v1.0
this will be enough.
JSON schema was also written on these interfaces, which will be used when validating configuration data.
Let’s code
So, we have a configuration interface. When starting a task through the Angular CLI, everything should work wonderfully.
To begin with, write the runStepper
function and create the StepperBuilder
.
// index.ts
export function runStepper(
input: Schema,
context: BuilderContext
): BuilderOutputLike {
return buildSteps(input, context).pipe (
map(() => ({
success: true
})),
catchError(error => {
return of({error: error.toString(), success: false});
})
);
}
export const StepperBuilder = createBuilder(runStepper);
export default StepperBuilder;
Note that the type of the first argument of the runStepper
function is the same Schema
from the configuration specification above. The function returns Observable<BuilderOutput>
.
Next, we will implement the buildSteps
function, which will be responsible for the steps
function buildSteps(config: Schema, context: BuilderContext): Observable<any> {
return concat(
config.steps.map(step => buildStep(step, config.targets, context))
);
}
It seems nothing complicated. Each next step is performed after the previous one is completed.
In fact, we have one thing that remains unknown — the buildStep
function, which will run each individual step with its dependencies:
function buildStep(
stepName: string,
targets: Targets,
context: BuilderContext
): Observable<any> {
const {deps = [], overrides, target, watch}: Target = targets[stepName];
const deps$ = deps.length
? combineLatest(deps.map(depName => buildStep(depName, targets, context)))
: of(null);
return deps$.pipe (
concatMap(() => {
return scheduleTargetAndForget(context, targetFromTargetString(target), {
watch
...overrides
});
}),
watch ? tap(noop) : take(1)
);
}
There are several interesting points in this function:
- Step dependencies run in parallel, and the main task of the step is only after each of the dependencies emits at least one event. For example, this gives us a guarantee that the dev-server (if the task to start it in the list of dependencies of the current step) is launched before running the tests (if this is the main task of the step)
- The function
scheduleTargetAndForget
, imported from@angular-devkit/architect
. This function allows you to run targets fromangular.json
and override their settings. This function returnsObservable
, unsubscribing from which stops the task in progress. - If the
watch
parameter has a positive value, then the main task of the step will not end after the first emitted event, therefore the current task will live forever until it completes itself, or until the unsubscribe from the returnedObservable
, or the process is completed.
Actually, this is all about the builder itself. The full version can be viewed here. Stayed in 56 lines of code. Not bad, right?
The last point that is interesting and important for us is the builders.json
file
{
"$ schema": "../../@angular-devkit/architect/src/builders-schema.json",
"builders": {
"stepper": {
"implementation": "./stepper",
"schema": "./schema.json",
"description": "Stepper"
}
}
}
As you can see, this file lists the builders with the parameters “implementation” (entry point for importing the builder), “schema” (validation scheme) and “description”
Then we search for package.json and add the builders
property with a relative path to the builders.json
file
{
"name": "@ng-builders/build",
"builders": "./builders.json",
…
}
It remains only to collect the package:
npm run build
Add everything into a commit and push all this beauty to Github.
And that’s all?
Yes, that’s all. Three simple functions, a little imagination, and the fulfillment of mandatory configuration contracts are all that is needed to quickly create custom builders for the Angular CLI. But an attentive reader, of course, will point out the lack of tests for a new builder. And I hope that this very reader unexpectedly ignites himself with the desire to close this gap, forks the project, and will try to write tests himself.
The builder can NOT be used (as soon as tests appear, I will definitely remove NOT) in your projects
npm i @ng-builders/build -D
Summary
The CLI Builders API is a powerful tool for extending and customizing the Angular CLI. The builder created by us does not solve the most popular problems, but it took only 1 hour to create the whole package. This means that creating a custom builder to solve particular problems is not such a difficult task.
What other builders can you create? It can be a builder for deploying, testing, and codebase checking for your application using special tools. It all depends on your needs and imagination.
P.S.:
Angular CLI Builders are perfectly used and work in NX Workspace even without Angular. An example of this miracle I will definitely show you in the future. In the meantime, you can read me on Twitter, write to me on Telegram, and speak only good words.
Top comments (0)