// 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
           }
         }
       }
     }
   }
 }
The Angular CLI v8.0.0 release introduced a stable Builders API, giving developers the ability to customize, swap out, or design entirely new CLI commands. Among the most widely used builders for tweaking webpack configuration is @angular-builders/custom-webpack. Inspecting the source of every builder shipped with that package reveals remarkably concise implementations, none exceeding thirty lines of code. Ever thought about rolling your own? Consider that a challenge. To follow along, you should already be comfortable with Angular and the Angular CLI, have a working grasp of RxJS, and be prepared to read through roughly fifty lines of code.

What exactly is a builder?

A builder is essentially a function that the Angular CLI can invoke. It takes two arguments:
  1. An object-shaped configuration, similar to JSON
  2. A BuilderContext instance, which exposes utilities like a logger
The function can be synchronous or asynchronous; it may even return an Observable. Regardless of the approach, both Promise and Observable must ultimately yield a BuilderOutput. Packaged appropriately inside an npm module, such a function can drive CLI commands like build, test, lint, deploy, or any other entry listed under the architect section of angular.json.

So, this will just be another docs copy-paste?

Not quite. Naturally, I started with an example closely mirroring the official documentation. That builder came in handy while working with NX and deploying only changed applications. But soon, I hit a need for something else: a builder that could execute multiple angular.json commands in a specific sequence, with dependencies between them. Here’s a more relatable scenario: imagine needing your dev-server running while your tests execute. Plenty of console utilities and npm packages exist to boot up a server and wait for it, but wouldn’t it be cleaner to have a builder that starts the dev-server in watch mode, runs the tests, and then kills the server as soon as they finish?

Where do we start?

The first step is to scaffold a package that will house our builders. I set up the workspace using NX and generated a library scaffold for the builder. npx create-nx-workspace ng-builders
cd ./ng-builders
npx ng g @nrwl/node:library build

Build Configuration

Here’s the configuration I devised to address my problem:
ng run app:stepper
If you were to hand-code this in angular.json, it might look like:
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;
 }
After refining the specification, I settled on these interfaces:
// 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;
That’s the spec done. Naturally, the schema could be extended later—say, to include a choice of configuration like production or development—but for a v1.0, this suffices. I also wrote a JSON schema grounded in those interfaces, which will be used for validating configuration data.

Time to code

With the configuration interface in place, invoking the task via the Angular CLI should go off without a hitch. First, let’s write the runStepper function and define the StepperBuilder.
// index.ts
 function buildSteps(config: Schema, context: BuilderContext): Observable<any> {
   return concat(
      config.steps.map(step => buildStep(step, config.targets, context))
   );
 }
Notice that the first argument of runStepper is typed as Schema, matching the configuration spec above. The function returns an Observable<BuilderOutput>. Next up is the buildSteps function, which orchestrates the order of execution.
// index.ts
 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)
   );
 }
Nothing too complex here: each step only kicks off once the preceding one has wrapped up. One piece remains undefined, though—the buildStep function, responsible for running an individual step along with its dependencies.
{
   "$ schema": "../../@angular-devkit/architect/src/builders-schema.json",
   "builders": {
     "stepper": {
       "implementation": "./stepper",
       "schema": "./schema.json",
       "description": "Stepper"
     }
   }
 }
A few nuances stand out in this function:
  1. Dependencies run in parallel, and the step’s main task only starts after every dependency has emitted at least once. For instance, this ensures the dev-server (if listed as a dependency) is up before tests (the main task) begin.
  2. We use scheduleTargetAndForget from @angular-devkit/architect. It lets us trigger targets from angular.json while overriding their options. The returned Observable stops the ongoing task upon unsubscription.
  3. If the watch flag is truthy, the main task won’t conclude after a single emission. Instead, it keeps running until it finishes on its own, until the observable is unsubscribed, or the process exits.
And that covers the builder logic itself. The complete code is available here. It clocks in at 56 lines. Not bad, eh? The final crucial piece is the builders.json manifest.
{
  "name": "@ng-builders/build",
  "builders": "./builders.json",

}
As shown, this file enumerates the builders, each with an "implementation" (the entry point for importing), a "schema" (for validation), and a brief "description". After that, we locate the package.json and add a builders property pointing to the builders.json file with a relative path.
npm run build
Now all that’s left is to build the package:
npm i @ng-builders/build -D
Commit everything, then push the result up to Github.

Is that it?

Yes, that’s all it takes. Three straightforward functions, a dash of creativity, and compliance with the required configuration contracts—that’s everything needed to whip up custom Angular CLI builders. Though a vigilant reader might point out that our new builder lacks tests. Hopefully, that same reader feels a spark of motivation to write some, fork the repo, and give it a shot. One caveat: the builder is **not** yet suitable for production use (I’ll drop that not once tests are in place).

Wrapping up

The CLI Builders API is a robust avenue for extending and tailoring the Angular CLI. The builder we created isn’t solving the most common issues, but the entire package came together in about an hour. That alone demonstrates that crafting a bespoke builder for niche problems isn’t a daunting task. What else could you build? Maybe a builder for deployment, test automation, or codebase checks using your favorite tools. The possibilities hinge entirely on your requirements and imagination.

A final note:

Angular CLI Builders also integrate smoothly within NX Workspaces, even in projects devoid of Angular. I’ll show you that trick another time. Meanwhile, you can reach me on Twitter, message me on Telegram, or just say nice things about me out loud.