Creating Nx Executors or Angular Devkit Builders in Your Nx Workspace
Creating Nx Executors/Angular Devkit Builders for your workspace standardizes scripts that are run during your development/building/deploying tasks in order to enable Nx's affected
command and caching capabilities.
This guide will show you how to create, run, and customize executors/builders within your Nx workspace. In the examples, we'll use the trivial use-case of an echo
command.
Creating a Builder with @angular-devkit
Note: In this article, we'll refer to executors that use the
@angular-devkit
as Angular Devkit Builders.
Your executor should be created within the tools
directory of your Nx workspace like so:
happynrwl/
├── apps/
├── libs/
├── tools/
│ └── executors/
│ └── echo/
│ ├── builder.json
│ ├── impl.ts
│ ├── package.json
│ └── schema.json
├── nx.json
├── package.json
└── tsconfig.json
schema.json
This file will describe the options being sent to the builder (very similar to the schema.json
file of generators).
1{
2 "$schema": "http://json-schema.org/schema",
3 "type": "object",
4 "properties": {
5 "textToEcho": {
6 "type": "string",
7 "description": "Text To Echo"
8 }
9 }
10}
This example describes a single option for the builder that is a string
called 'textToEcho'. When using this builder, we'll specify a 'textToEcho' property inside the options.
In our impl.ts
file, we're creating an Options
interface that matches the json object being described here.
impl.ts
The impl.ts
contains the actual code for your builder. Your builder should use the createBuilder
function of the @angular-devkit/architect
package to create a builder that can be run via the Nx CLI tools.
1import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
2import * as childProcess from 'child_process';
3import { Observable } from 'rxjs';
4import { json } from '@angular-devkit/core';
5
6interface Options extends json.JsonObject {
7 textToEcho: string;
8}
9
10export default createBuilder((_options: Options, context) => {
11 context.logger.info(`Executing "echo"...`);
12 context.logger.info(`Options: ${JSON.stringify(_options, null, 2)}`);
13 const child = childProcess.spawn('echo', [_options.textToEcho]);
14 return new Observable<BuilderOutput>((observer) => {
15 child.stdout.on('data', (data) => {
16 context.logger.info(data.toString());
17 });
18 child.stderr.on('data', (data) => {
19 context.logger.error(data.toString());
20 });
21 child.on('close', (code) => {
22 context.logger.info(`Done.`);
23 observer.next({ success: code === 0 });
24 observer.complete();
25 });
26 });
27});
See the official Angular documentation on builders for more clarification on creating builders.
Also note that Node’s childProcess
is likely to be used in most cases.
Part of the power of the architect API is the ability to compose builders via existing build targets. This way you can combine other builders from your workspace into one which could be helpful when the process you’re scripting is a combination of other existing builders provided by the CLI or other custom-builders in your workspace.
Here's an example of this (from a hypothetical project), that will serve an api (project name: "api") in watch mode, then serve a frontend app (project name: "web-client") in watch mode:
1import {
2 BuilderContext,
3 BuilderOutput,
4 createBuilder,
5 scheduleTargetAndForget,
6 targetFromTargetString,
7} from '@angular-devkit/architect';
8import { concat } from 'rxjs';
9import { concatMap, map } from 'rxjs/operators';
10interface Options extends json.JsonObject {}
11
12export default createBuilder((_options: Options, context: BuilderContext) => {
13 return concat(
14 scheduleTargetAndForget(
15 context,
16 targetFromTargetString('api:serve'),
17 { watch: true }
18 ),
19 scheduleTargetAndForget(
20 context,
21 targetFromTargetString('web-client:serve'),
22 { watch: true }
23 )
24 ).pipe(
25 map(([apiBuilderContext, webClientBuilderContext]) =>
26 ({ success: apiBuilderContext.success && webClientBuilderContext.success})
27 )
28 );
For other ideas on how to create your own builders, you can always check out Nx's own open-source builders as well!
(e.g. our cypress builder)
builder.json
The builder.json
file provides the description of your builder to the CLI.
1{
2 "builders": {
3 "echo": {
4 "implementation": "./impl",
5 "schema": "./schema.json",
6 "description": "Runs `echo` (to test builders out)."
7 }
8 }
9}
Note that this builder.json
file is naming our builder 'echo' for the CLI's purposes, and maping that name to the given implemetation file and schema.
package.json
This is all that’s required from the package.json
file:
1{
2 "builders": "./builder.json"
3}
Creating an Nx Executor
Creating an Nx Executor is in principle nearly identical to the Angular Devkit Builder example in the section above, we'll explain in this section the few differences involved.
Marking the Executor as an Nx Executor
The first difference to adjust is to mark the executor as an Nx Executor in the schema. To do this, we'll need to add the cli
property to the builder's schema, and give it the value "nx"
:
1{
2 "$schema": "http://json-schema.org/schema",
3 "type": "object",
4 "cli": "nx",
5 "properties": {
6 "textToEcho": {
7 "type": "string",
8 "description": "Text To Echo"
9 }
10 }
11}
Implementing an Executor Without the Angular Devkit
Your executor's implementation must consist of a function that takes an options object and returns a Promise<{ success: boolean }>
. Given the echo implementation provided in the Angular Devkit Builder section above, our Nx executor would look like this:
1import * as childProcess from 'child_process';
2
3interface Options {
4 textToEcho: string;
5}
6
7export default async function (
8 _options: Options
9): Promise<{ success: boolean }> {
10 const child = childProcess.spawn('echo', [_options.textToEcho]);
11 return new Promise<{ success: boolean }>((res) => {
12 child.on('close', (code) => {
13 res({ success: code === 0 });
14 });
15 });
16}
Compiling and Running your Builder
After your files are created, you can compile your builder with tsc
(which should be available as long as you've installed Typescript globally: npm i -g typescript
):
1tsc tools/builders/echo/impl
This will create the impl.js
file in your file directory, which will serve as the artifact used by the CLI.
Our last step is to add this builder to a given project’s architect
object in your project's workspace.json
or angular.json
file. The example below adds this builder to a project named 'platform':
1{
2 //...
3 "projects": {
4 "platform": {
5 //...
6 "architect": {
7 "build": {
8 // ...
9 },
10 "serve": {
11 // ...
12 },
13 "lint": {
14 // ,,,
15 },
16 "echo": {
17 "executor": "./tools/builders/echo:echo",
18 "options": {
19 "textToEcho": "Hello World"
20 }
21 }
22 }
23 }
24 }
25}
Note that the format of the executor
string here is: ${Path to directory containing the builder's package.json}:${builder name}
.
Finally, we may run our builder via the CLI as follows:
1nx run platform:echo
To which we'll see the console output:
1> ng run platform:echo
2Executing "echo"...
3Hello World
4
5Done.
Debugging Builders
As part of Nx's computation cache process, Nx forks the node process, which can make it difficult to debug a builder command. Follow these steps to debug an executor:
- Make sure VSCode's
debug.node.autoAttach
setting is set toOn
. - Find the executor code and set a breakpoint.
- Use node in debug to execute your executor command, replacing
nx
with the internaltao
script.
node --inspect-brk node_modules/.bin/tao build best-app