Using Executors / Builders
Executors perform actions on your code. This can include building, linting, testing, serving and many other actions.
Executors are written using @nrwl/devkit
or @angular-devkit
. Executors written with the @angular-devkit
are called Builders.
There are two main differences between an executor and a shell script or an npm script:
- Executors encourage a consistent methodology for performing similar actions on unrelated projects. i.e. A developer switching between teams can be confident that
nx build project2
will buildproject2
with the default settings, just likenx build project1
builtproject1
. - Nx can leverage this consistency to perform the same executor across multiple projects. i.e.
nx affected --target=test
will run thetest
executor on every project that is affected by the current code change.
Executor definitions
The executors that are available for each project are defined and configured in the /workspace.json
file.
1{
2 "projects": {
3 "cart": {
4 "root": "apps/cart",
5 "sourceRoot": "apps/cart/src",
6 "projectType": "application",
7 "generators": {},
8 "targets": {
9 "build": {
10 "executor": "@nrwl/web:build",
11 "options": {
12 "outputPath": "dist/apps/cart",
13 ...
14 },
15 "configurations": {
16 "production": {
17 "sourceMap": false,
18 ...
19 }
20 }
21 },
22 "test": {
23 "executor": "@nrwl/jest:jest",
24 "options": {
25 ...
26 }
27 }
28 }
29 }
30 }
31}
Note: There are a few property keys in
workspace.json
that have interchangeable aliases. You can replacegenerators
withschematics
,targets
witharchitect
orexecutor
withbuilder
.
Each project has its executors defined in the targets
property. In this snippet, cart
has two executors defined - build
and test
.
Note:
build
andtest
can be any strings you choose. For the sake of consistency, we maketest
run unit tests for every project andbuild
produce compiled code for the projects which can be built.
Each executor definition has an executor
property and, optionally, an options
and a configurations
property.
executor
is a string of the from[package name]:[executor name]
. For thebuild
executor, the package name is@nrwl/web
and the executor name isbuild
.options
is an object that contains any configuration defaults for the executor. These options vary from executor to executor.configurations
allows you to create presets of options for different scenarios. All the configurations start with the properties defined inoptions
as a baseline and then overwrite those options. In the example, there is aproduction
configuration that overrides the default options to setsourceMap
tofalse
.
Running executors
The nx run
cli command (or the shorthand versions) can be used to run executors.
nx run [project]:[command]
nx run cart:build
As long as your command name doesn't conflict with an existing nx cli command, you can use this short hand:
nx [command] [project]
nx build cart
You can also use a specific configuration preset like this:
nx [command] [project] --configuration=[configuration]
nx build cart --configuration=production
Or you can overwrite individual executor options like this:
nx [command] [project] --[optionNameInCamelCase]=[value]
nx build cart --outputPath=some/other/path
Simplest executor
1{
2 "cli": "nx",
3 "id": "CustomExecutor",
4 "type": "object",
5 "properties": {},
6 "additionalProperties": true
7}
1export default async function (opts) {
2 console.log('options', opts);
3}
Defining an executor schema
An executor's schema describes the inputs--what you can pass into it. The schema is used to validate inputs, to parse args (e.g., covert strings into numbers), to set defaults, and to power the VSCode plugin. It is written with JSON Schema.
1{
2 "cli": "nx",
3 "id": "Echo",
4 "description": "echo given string",
5 "type": "object",
6 "properties": {
7 "message": {
8 "type": "string",
9 "description": "Message to echo"
10 },
11 "upperCase": {
12 "type": "boolean",
13 "description": "Covert to all upper case",
14 "default": false
15 }
16 },
17 "required": ["message"]
18}
The schema above defines two fields: message
and upperCase
. The message
field is a string, upperCase
is a boolean. The schema support for executors and generators is identical. See the section on generators above for more information.
Implementing an executor
The implementation function takes two arguments (the options and the executor context) and returns a promise (or an async iterable) with the success property. The context params contains information about the workspace and the invoked target.
Most of the time executors return a promise.
1interface Schema {
2 message: string;
3 upperCase: boolean;
4}
5
6export default async function printAllCaps(
7 options: Schema,
8 context: ExecutorContext
9): Promise<{ success: true }> {
10 if (options.upperCase) {
11 console.log(options.message.toUpperCase());
12 } else {
13 console.log(options.message);
14 }
15 return { success: true };
16}
But you can also return an async iterable that can yield several values.
1async function wait() {
2 return new Promise((res) => {
3 setTimeout(() => res(), 1000);
4 });
5}
6
7export default async function* counter(opts: { to: number; result: boolean }) {
8 for (let i = 0; i < opts.to; ++i) {
9 console.log(i);
10 yield { success: false };
11 await wait();
12 }
13 yield { success: opts.result };
14}
Composing executors
An executor is just a function, so you can import and invoke it directly, as follows:
1import printAllCaps from 'print-all-caps';
2
3export default async function (
4 options: Schema,
5 context: ExecutorContext
6): Promise<{ success: true }> {
7 // do something before
8 await printAllCaps({ message: 'All caps' });
9 // do something after
10}
This only works when you know what executor you want to invoke. Sometimes, however, you need to invoke a target. For instance, the e2e target is often configured like this:
1{
2 "e2e": {
3 "builder": "@nrwl/cypress:cypress",
4 "options": {
5 "cypressConfig": "apps/myapp-e2e/cypress.json",
6 "tsConfig": "apps/myapp-e2e/tsconfig.e2e.json",
7 "devServerTarget": "myapp:serve"
8 }
9 }
10}
In this case we need to invoke the target configured in devSeverTarget. We can do it as follows:
1async function* startDevServer(
2 opts: CypressExecutorOptions,
3 context: ExecutorContext
4) {
5 const { project, target, configuration } = parseTargetString(
6 opts.devServerTarget
7 );
8 for await (const output of await runExecutor<{
9 success: boolean;
10 baseUrl?: string;
11 }>(
12 { project, target, configuration },
13 {
14 watch: opts.watch,
15 },
16 context
17 )) {
18 if (!output.success && !opts.watch)
19 throw new Error('Could not compile application files');
20 yield opts.baseUrl || (output.baseUrl as string);
21 }
22}
The runExecutor
utility will find the target in the configuration, find the executor, construct the options (as if you invoked it in the terminal) and invoke the executor. Note that runExecutor
always returns an iterable instead of a promise.
Devkit helper functions
logger
-- Wrapsconsole
to add some formatting.getPackageManagerCommand
-- Returns commands for the package manager used in the workspace.parseTargetString
-- Parses a target string into {project, target, configuration}.readTargetOptions
-- Reads and combines options for a given target.runExecutor
-- Constructs options and invokes an executor.
See more helper functions in the Devkit API Docs
Using RxJS observables
The Nx devkit only uses language primitives (promises and async iterables). It doesn't use RxJS observables, but you can use them and convert them to a Promise
or an async iterable.
You can convert Observables
to a Promise
with toPromise
.
1import { of } from 'rxjs';
2
3export default async function (opts) {
4 return of({ success: true }).toPromise();
5}
You can use the rxjs-for-await
library to convert an Observable
into an async iterable.
1import { of } from 'rxjs';
2import { eachValueFrom } from 'rxjs-for-await-async';
3
4export default async function (opts) {
5 return eachValueFrom(of({ success: true }));
6}