5 lines
34 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"id": "guide/cli-builder",
"title": "Angular CLI builders",
"contents": "\n\n\n<div class=\"github-links\">\n <a href=\"https://github.com/angular/angular/edit/master/aio/content/guide/cli-builder.md?message=docs%3A%20describe%20your%20change...\" aria-label=\"Suggest Edits\" title=\"Suggest Edits\"><i class=\"material-icons\" aria-hidden=\"true\" role=\"img\">mode_edit</i></a>\n</div>\n\n\n<div class=\"content\">\n <h1 id=\"angular-cli-builders\">Angular CLI builders<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#angular-cli-builders\"><i class=\"material-icons\">link</i></a></h1>\n<p>A number of Angular CLI commands run a complex process on your code, such as linting, building, or testing.\nThe commands use an internal tool called Architect to run <em>CLI builders</em>, which apply another tool to accomplish the desired task.</p>\n<p>With Angular version 8, the CLI Builder API is stable and available to developers who want to customize the Angular CLI by adding or modifying commands. For example, you could supply a builder to perform an entirely new task, or to change which third-party tool is used by an existing command.</p>\n<p>This document explains how CLI builders integrate with the workspace configuration file, and shows how you can create your own builder.</p>\n<div class=\"alert is-helpful\">\n<p> You can find the code from the examples used here in <a href=\"https://github.com/mgechev/cli-builders-demo\">this GitHub repository</a>.</p>\n</div>\n<h2 id=\"cli-builders\">CLI builders<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#cli-builders\"><i class=\"material-icons\">link</i></a></h2>\n<p>The internal Architect tool delegates work to handler functions called <a href=\"guide/glossary#builder\"><em>builders</em></a>.\nA builder handler function receives two arguments; a set of input <code>options</code> (a JSON object), and a <code>context</code> (a <code>BuilderContext</code> object).</p>\n<p>The separation of concerns here is the same as with <a href=\"guide/glossary#schematic\">schematics</a>, which are used for other CLI commands that touch your code (such as <code>ng generate</code>).</p>\n<ul>\n<li>\n<p>The <code>options</code> object is provided by the CLI user, while the <code>context</code> object is provided by the CLI Builder API.</p>\n</li>\n<li>\n<p>In addition to the contextual information, the <code>context</code> object, which is an instance of the <code>BuilderContext</code>, also provides access to a scheduling method, <code>BuilderContext.scheduleTarget()</code>. The scheduler executes the builder handler function with a given <a href=\"guide/glossary#target\">target configuration</a>.</p>\n</li>\n</ul>\n<p>The builder handler function can be synchronous (return a value) or asynchronous (return a Promise), or it can watch and return multiple values (return an Observable).\nThe return value or values must always be of type <code>BuilderOutput</code>.\nThis object contains a Boolean <code>success</code> field and an optional <code>error</code> field that can contain an error message.</p>\n<p>Angular provides some builders that are used by the CLI for commands such as <code>ng build</code>, <code>ng test</code>, and <code>ng lint</code>.\nDefault target configurations for these and other built-in CLI builders can be found (and customized) in the \"architect\" section of the <a href=\"guide/workspace-config\">workspace configuration file</a>, <code>angular.json</code>.\nYou can also extend and customize Angular by creating your own builders, which you can run using the <a href=\"cli/run\"><code>ng run</code> CLI command</a>.</p>\n<h3 id=\"builder-project-structure\">Builder project structure<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#builder-project-structure\"><i class=\"material-icons\">link</i></a></h3>\n<p>A builder resides in a \"project\" folder that is similar in structure to an Angular workspace, with global configuration files at the top level, and more specific configuration in a source folder with the code files that define the behavior.\nFor example, your <code>myBuilder</code> folder could contain the following files.</p>\n<table>\n<thead>\n<tr>\n<th align=\"left\">FILES</th>\n<th align=\"left\">PURPOSE</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td align=\"left\"><code>src/my-builder.ts</code></td>\n<td align=\"left\">Main source file for the builder definition.</td>\n</tr>\n<tr>\n<td align=\"left\"><code>src/my-builder.spec.ts</code></td>\n<td align=\"left\">Source file for tests.</td>\n</tr>\n<tr>\n<td align=\"left\"><code>src/schema.json</code></td>\n<td align=\"left\">Definition of builder input options.</td>\n</tr>\n<tr>\n<td align=\"left\"><code>builders.json</code></td>\n<td align=\"left\">Builders definition.</td>\n</tr>\n<tr>\n<td align=\"left\"><code>package.json</code></td>\n<td align=\"left\">Dependencies. See <a href=\"https://docs.npmjs.com/files/package.json\">https://docs.npmjs.com/files/package.json</a>.</td>\n</tr>\n<tr>\n<td align=\"left\"><code>tsconfig.json</code></td>\n<td align=\"left\"><a href=\"https://www.typescriptlang.org/docs/handbook/tsconfig-json.html\">TypeScript configuration</a>.</td>\n</tr>\n</tbody>\n</table>\n<p>You can publish the builder to <code>npm</code> (see <a href=\"guide/creating-libraries#publishing-your-library\">Publishing your Library</a>). If you publish it as <code>@example/my-builder</code>, you can install it using the following command.</p>\n<code-example language=\"sh\">\n\nnpm install @example/my-builder\n\n</code-example>\n<h2 id=\"creating-a-builder\">Creating a builder<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#creating-a-builder\"><i class=\"material-icons\">link</i></a></h2>\n<p>As an example, let's create a builder that executes a shell command.\nTo create a builder, use the <code>createBuilder()</code> CLI Builder function, and return a <code>Promise&#x3C;BuilderOutput></code> object.</p>\n<code-example path=\"cli-builder/src/my-builder.ts\" header=\"src/my-builder.ts (builder skeleton)\" region=\"builder-skeleton\">\nimport { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { JsonObject } from '@angular-devkit/core';\n\ninterface Options extends JsonObject {\n command: string;\n args: string[];\n}\n\nexport default createBuilder(commandBuilder);\n\nfunction commandBuilder(\n options: Options,\n context: BuilderContext,\n ): Promise&#x3C;BuilderOutput> {\n}\n\n\n</code-example>\n<p>Now lets add some logic to it.\nThe following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish.\nIf the process is successful (returns a code of 0), it resolves the return value.</p>\n<code-example path=\"cli-builder/src/my-builder.ts\" header=\"src/my-builder.ts (builder)\" region=\"builder\">\nimport { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { JsonObject } from '@angular-devkit/core';\nimport * as childProcess from 'child_process';\n\ninterface Options extends JsonObject {\n command: string;\n args: string[];\n}\n\nexport default createBuilder(commandBuilder);\n\nfunction commandBuilder(\n options: Options,\n context: BuilderContext,\n ): Promise&#x3C;BuilderOutput> {\n const child = childProcess.spawn(options.command, options.args);\n return new Promise(resolve => {\n child.on('close', code => {\n resolve({ success: code === 0 });\n });\n });\n}\n\n\n</code-example>\n<h3 id=\"handling-output\">Handling output<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#handling-output\"><i class=\"material-icons\">link</i></a></h3>\n<p>By default, the <code>spawn()</code> method outputs everything to the process standard output and error.\nTo make it easier to test and debug, we can forward the output to the CLI Builder logger instead.\nThis also allows the builder itself to be executed in a separate process, even if the standard output and error are deactivated (as in an <a href=\"https://electronjs.org/\">Electron app</a>).</p>\n<p>We can retrieve a Logger instance from the context.</p>\n<code-example path=\"cli-builder/src/my-builder.ts\" header=\"src/my-builder.ts (handling output)\" region=\"handling-output\">\nimport { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { JsonObject } from '@angular-devkit/core';\nimport * as childProcess from 'child_process';\n\ninterface Options extends JsonObject {\n command: string;\n args: string[];\n}\n\nexport default createBuilder(commandBuilder);\n\nfunction commandBuilder(\n options: Options,\n context: BuilderContext,\n ): Promise&#x3C;BuilderOutput> {\n const child = childProcess.spawn(options.command, options.args);\n\n child.stdout.on('data', data => {\n context.logger.info(data.toString());\n });\n child.stderr.on('data', data => {\n context.logger.error(data.toString());\n });\n\n return new Promise(resolve => {\n child.on('close', code => {\n resolve({ success: code === 0 });\n });\n });\n}\n\n\n</code-example>\n<h3 id=\"progress-and-status-reporting\">Progress and status reporting<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#progress-and-status-reporting\"><i class=\"material-icons\">link</i></a></h3>\n<p>The CLI Builder API includes progress and status reporting tools, which can provide hints for certain functions and interfaces.</p>\n<p>To report progress, use the <code>BuilderContext.reportProgress()</code> method, which takes a current value, (optional) total, and status string as arguments.\nThe total can be any number; for example, if you know how many files you have to process, the total could be the number of files, and current should be the number processed so far.\nThe status string is unmodified unless you pass in a new string value.</p>\n<p>You can see an <a href=\"https://github.com/angular/angular-cli/blob/ba21c855c0c8b778005df01d4851b5a2176edc6f/packages/angular_devkit/build_angular/src/tslint/index.ts#L107\">example</a> of how the <code>tslint</code> builder reports progress.</p>\n<p>In our example, the shell command either finishes or is still executing, so theres no need for a progress report, but we can report status so that a parent builder that called our builder would know whats going on.\nUse the <code>BuilderContext.reportStatus()</code> method to generate a status string of any length.\n(Note that theres no guarantee that a long string will be shown entirely; it could be cut to fit the UI that displays it.)\nPass an empty string to remove the status.</p>\n<code-example path=\"cli-builder/src/my-builder.ts\" header=\"src/my-builder.ts (progress reporting)\" region=\"progress-reporting\">\nimport { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { JsonObject } from '@angular-devkit/core';\nimport * as childProcess from 'child_process';\n\ninterface Options extends JsonObject {\n command: string;\n args: string[];\n}\n\nexport default createBuilder(commandBuilder);\n\nfunction commandBuilder(\n options: Options,\n context: BuilderContext,\n ): Promise&#x3C;BuilderOutput> {\n context.reportStatus(`Executing \"${options.command}\"...`);\n const child = childProcess.spawn(options.command, options.args);\n\n child.stdout.on('data', data => {\n context.logger.info(data.toString());\n });\n child.stderr.on('data', data => {\n context.logger.error(data.toString());\n });\n\n return new Promise(resolve => {\n context.reportStatus(`Done.`);\n child.on('close', code => {\n resolve({ success: code === 0 });\n });\n });\n}\n\n\n</code-example>\n<h2 id=\"builder-input\">Builder input<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#builder-input\"><i class=\"material-icons\">link</i></a></h2>\n<p>You can invoke a builder indirectly through a CLI command, or directly with the Angular CLI <code>ng run</code> command.\nIn either case, you must provide required inputs, but can allow other inputs to default to values that are pre-configured for a specific <a href=\"guide/glossary#target\"><em>target</em></a>, provide a pre-defined, named override configuration, and provide further override option values on the command line.</p>\n<h3 id=\"input-validation\">Input validation<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#input-validation\"><i class=\"material-icons\">link</i></a></h3>\n<p>You define builder inputs in a JSON schema associated with that builder.\nThe Architect tool collects the resolved input values into an <code>options</code> object, and validates their types against the schema before passing them to the builder function.\n(The Schematics library does the same kind of validation of user input).</p>\n<p>For our example builder, we expect the <code>options</code> value to be a <code>JsonObject</code> with two keys: a <code>command</code> that is a string, and an <code>args</code> array of string values.</p>\n<p>We can provide the following schema for type validation of these values.</p>\n<code-example language=\"json\" header=\"command/schema.json\">\n{\n \"$schema\": \"http://json-schema.org/schema\",\n \"type\": \"object\",\n \"properties\": {\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n}\n\n</code-example>\n<div class=\"alert is-helpful\">\n<p>This is a very simple example, but the use of a schema for validation can be very powerful.\nFor more information, see the <a href=\"http://json-schema.org/\">JSON schemas website</a>.</p>\n</div>\n<p>To link our builder implementation with its schema and name, we need to create a <em>builder definition</em> file, which we can point to in <code>package.json</code>.</p>\n<p>Create a file named <code>builders.json</code> file that looks like this.</p>\n<code-example language=\"json\" header=\"builders.json\">\n\n{\n \"builders\": {\n \"command\": {\n \"implementation\": \"./command\",\n \"schema\": \"./command/schema.json\",\n \"description\": \"Runs any command line in the operating system.\"\n }\n }\n}\n\n</code-example>\n<p>In the <code>package.json</code> file, add a <code>builders</code> key that tells the Architect tool where to find our builder definition file.</p>\n<code-example language=\"json\" header=\"package.json\">\n\n{\n \"name\": \"@example/command-runner\",\n \"version\": \"1.0.0\",\n \"description\": \"Builder for Command Runner\",\n \"builders\": \"builders.json\",\n \"devDependencies\": {\n \"@angular-devkit/architect\": \"^1.0.0\"\n }\n}\n\n</code-example>\n<p>The official name of our builder is now <code>@example/command-runner:command</code>.\nThe first part of this is the package name (resolved using node resolution), and the second part is the builder name (resolved using the <code>builders.json</code> file).</p>\n<p>Using one of our <code>options</code> is very straightforward, we did this in the previous section when we accessed <code>options.command</code>.</p>\n<code-example path=\"cli-builder/src/my-builder.ts\" header=\"src/my-builder.ts (report status)\" region=\"report-status\">\ncontext.reportStatus(`Executing \"${options.command}\"...`);\nconst child = childProcess.spawn(options.command, options.args);\n\n</code-example>\n<h3 id=\"target-configuration\">Target configuration<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#target-configuration\"><i class=\"material-icons\">link</i></a></h3>\n<p>A builder must have a defined target that associates it with a specific input configuration and <a href=\"guide/glossary#project\">project</a>.</p>\n<p>Targets are defined in the <code>angular.json</code> <a href=\"guide/workspace-config\">CLI configuration file</a>.\nA target specifies the builder to use, its default options configuration, and named alternative configurations.\nThe Architect tool uses the target definition to resolve input options for a given run.</p>\n<p>The <code>angular.json</code> file has a section for each project, and the \"architect\" section of each project configures targets for builders used by CLI commands such as 'build', 'test', and 'lint'.\nBy default, for example, the <code>build</code> command runs the builder <code>@angular-devkit/build-angular:<a href=\"api/animations/browser\" class=\"code-anchor\">browser</a></code> to perform the build task, and passes in default option values as specified for the <code>build</code> target in <code>angular.json</code>.</p>\n<code-example language=\"json\" header=\"angular.json\">\n{\n \"myApp\": {\n ...\n \"architect\": {\n \"build\": {\n \"builder\": \"@angular-devkit/build-angular:browser\",\n \"options\": {\n \"outputPath\": \"dist/myApp\",\n \"index\": \"src/index.html\",\n ...\n },\n \"configurations\": {\n \"production\": {\n \"fileReplacements\": [\n {\n \"replace\": \"src/environments/environment.ts\",\n \"with\": \"src/environments/environment.prod.ts\"\n }\n ],\n \"optimization\": true,\n \"outputHashing\": \"all\",\n ...\n }\n }\n },\n ...\n\n</code-example>\n<p>The command passes the builder the set of default options specified in the \"options\" section.\nIf you pass the <code>--configuration=production</code> flag, it uses the override values specified in the <code>production</code> alternative configuration.\nYou can specify further option overrides individually on the command line.\nYou might also add more alternative configurations to the <code>build</code> target, to define other environments such as <code>stage</code> or <code>qa</code>.</p>\n<h4 id=\"target-strings\">Target strings<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#target-strings\"><i class=\"material-icons\">link</i></a></h4>\n<p>The generic <code>ng run</code> CLI command takes as its first argument a target string of the form <em>project:target[:configuration]</em>.</p>\n<ul>\n<li>\n<p><em>project</em>: The name of the Angular CLI project that the target is associated with.</p>\n</li>\n<li>\n<p><em>target</em>: A named builder configuration from the <code>architect</code> section of the <code>angular.json</code> file.</p>\n</li>\n<li>\n<p><em>configuration</em>: (optional) The name of a specific configuration override for the given target, as defined in the <code>angular.json</code> file.</p>\n</li>\n</ul>\n<p>If your builder calls another builder, it may need to read a passed target string.\nYou can parse this string into an object by using the <code>targetFromTargetString()</code> utility function from <code>@angular-devkit/architect</code>.</p>\n<h2 id=\"schedule-and-run\">Schedule and run<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#schedule-and-run\"><i class=\"material-icons\">link</i></a></h2>\n<p>Architect runs builders asynchronously.\nTo invoke a builder, you schedule a task to be run when all configuration resolution is complete.</p>\n<p>The builder function is not executed until the scheduler returns a <code>BuilderRun</code> control object.\nThe CLI typically schedules tasks by calling the <code>BuilderContext.scheduleTarget()</code> function, and then resolves input options using the target definition in the <code>angular.json</code> file.</p>\n<p>Architect resolves input options for a given target by taking the default options object, then overwriting values from the configuration used (if any), then further overwriting values from the overrides object passed to <code>BuilderContext.scheduleTarget()</code>.\nFor the Angular CLI, the overrides object is built from command line arguments.</p>\n<p>Architect validates the resulting options values against the schema of the builder.\nIf inputs are valid, Architect creates the context and executes the builder.</p>\n<p>For more information see <a href=\"guide/workspace-config\">Workspace Configuration</a>.</p>\n<div class=\"alert is-helpful\">\n<p> You can also invoke a builder directly from another builder or test by calling <code>BuilderContext.scheduleBuilder()</code>.\nYou pass an <code>options</code> object directly to the method, and those option values are validated against the schema of the builder without further adjustment.</p>\n<p> Only the <code>BuilderContext.scheduleTarget()</code> method resolves the configuration and overrides through the <code>angular.json</code> file.</p>\n</div>\n<h3 id=\"default-architect-configuration\">Default architect configuration<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#default-architect-configuration\"><i class=\"material-icons\">link</i></a></h3>\n<p>Lets create a simple <code>angular.json</code> file that puts target configurations into context.</p>\n<p>We can publish the builder to npm (see <a href=\"guide/creating-libraries#publishing-your-library\">Publishing your Library</a>), and install it using the following command:</p>\n<code-example language=\"sh\">\n\nnpm install @example/command-runner\n\n</code-example>\n<p>If we create a new project with <code>ng new builder-test</code>, the generated <code>angular.json</code> file looks something like this, with only default builder configurations.</p>\n<code-example language=\"json\" header=\"angular.json\">\n\n{\n // ...\n \"projects\": {\n // ...\n \"builder-test\": {\n // ...\n \"architect\": {\n // ...\n \"build\": {\n \"builder\": \"@angular-devkit/build-angular:browser\",\n \"options\": {\n // ... more options...\n \"outputPath\": \"dist/builder-test\",\n \"index\": \"src/index.html\",\n \"main\": \"src/main.ts\",\n \"polyfills\": \"src/polyfills.ts\",\n \"tsConfig\": \"src/tsconfig.app.json\"\n },\n \"configurations\": {\n \"production\": {\n // ... more options...\n \"optimization\": true,\n \"aot\": true,\n \"buildOptimizer\": true\n }\n }\n }\n }\n }\n }\n // ...\n}\n\n</code-example>\n<h3 id=\"adding-a-target\">Adding a target<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#adding-a-target\"><i class=\"material-icons\">link</i></a></h3>\n<p>Let's add a new target that will run our builder to execute a particular command.\nThis target will tell the builder to run <code>touch</code> on a file, in order to update its modified date.</p>\n<p>We need to update the <code>angular.json</code> file to add a target for this builder to the \"architect\" section of our new project.</p>\n<ul>\n<li>\n<p>We'll add a new target section to the \"architect\" object for our project.</p>\n</li>\n<li>\n<p>The target named \"touch\" uses our builder, which we published to <code>@example/command-runner</code>. (See <a href=\"guide/creating-libraries#publishing-your-library\">Publishing your Library</a>)</p>\n</li>\n<li>\n<p>The options object provides default values for the two inputs that we defined; <code>command</code>, which is the Unix command to execute, and <code>args</code>, an array that contains the file to operate on.</p>\n</li>\n<li>\n<p>The configurations key is optional, we'll leave it out for now.</p>\n</li>\n</ul>\n<code-example language=\"json\" header=\"angular.json\">\n\n{\n \"projects\": {\n \"builder-test\": {\n \"architect\": {\n \"touch\": {\n \"builder\": \"@example/command-runner:command\",\n \"options\": {\n \"command\": \"touch\",\n \"args\": [\n \"src/main.ts\"\n ]\n }\n },\n \"build\": {\n \"builder\": \"@angular-devkit/build-angular:browser\",\n \"options\": {\n \"outputPath\": \"dist/builder-test\",\n \"index\": \"src/index.html\",\n \"main\": \"src/main.ts\",\n \"polyfills\": \"src/polyfills.ts\",\n \"tsConfig\": \"src/tsconfig.app.json\"\n },\n \"configurations\": {\n \"production\": {\n \"fileReplacements\": [\n {\n \"replace\": \"src/environments/environment.ts\",\n \"with\": \"src/environments/environment.prod.ts\"\n }\n ],\n \"optimization\": true,\n \"aot\": true,\n \"buildOptimizer\": true\n }\n }\n }\n }\n }\n }\n}\n\n</code-example>\n<h3 id=\"running-the-builder\">Running the builder<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#running-the-builder\"><i class=\"material-icons\">link</i></a></h3>\n<p>To run our builder with the new target's default configuration, use the following CLI command in a Linux shell.</p>\n<code-example language=\"sh\">\n\n ng run builder-test:touch\n\n</code-example>\n<p>This will run the <code>touch</code> command on the <code>src/main.ts</code> file.</p>\n<p>You can use command-line arguments to override the configured defaults.\nFor example, to run with a different <code>command</code> value, use the following CLI command.</p>\n<code-example language=\"sh\">\n\nng run builder-test:touch --command=ls\n\n</code-example>\n<p>This will call the <code>ls</code> command instead of the <code>touch</code> command.\nBecause we did not override the <em>args</em> option, it will list information about the <code>src/main.ts</code> file (the default value provided for the target).</p>\n<h2 id=\"testing-a-builder\">Testing a builder<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#testing-a-builder\"><i class=\"material-icons\">link</i></a></h2>\n<p>Use integration testing for your builder, so that you can use the Architect scheduler to create a context, as in this <a href=\"https://github.com/mgechev/cli-builders-demo\">example</a>.</p>\n<ul>\n<li>\n<p>In the builder source directory, we have created a new test file <code>my-builder.spec.ts</code>. The code creates new instances of <code>JsonSchemaRegistry</code> (for schema validation), <code>TestingArchitectHost</code> (an in-memory implementation of <code>ArchitectHost</code>), and <code>Architect</code>.</p>\n</li>\n<li>\n<p>We've added a <code>builders.json</code> file next to the builder's <a href=\"https://github.com/mgechev/cli-builders-demo/blob/master/command-builder/builders.json\"><code>package.json</code> file</a>, and modified the package file to point to it.</p>\n</li>\n</ul>\n<p>Heres an example of a test that runs the command builder.\nThe test uses the builder to run the <code>node --print 'foo'</code> command, then validates that the <code>logger</code> contains an entry for <code>foo</code>.</p>\n<code-example path=\"cli-builder/src/my-builder.spec.ts\" header=\"src/my-builder.spec.ts\">\nimport { Architect } from '@angular-devkit/architect';\nimport { TestingArchitectHost } from '@angular-devkit/architect/testing';\nimport { logging, schema } from '@angular-devkit/core';\n\ndescribe('Command Runner Builder', () => {\n let architect: Architect;\n let architectHost: TestingArchitectHost;\n\n beforeEach(async () => {\n const registry = new schema.CoreSchemaRegistry();\n registry.addPostTransform(schema.transforms.addUndefinedDefaults);\n\n // TestingArchitectHost() takes workspace and current directories.\n // Since we don't use those, both are the same in this case.\n architectHost = new TestingArchitectHost(__dirname, __dirname);\n architect = new Architect(architectHost, registry);\n\n // This will either take a Node package name, or a path to the directory\n // for the package.json file.\n await architectHost.addBuilderFromPackage('..');\n });\n\n it('can run node', async () => {\n // Create a logger that keeps an array of all messages that were logged.\n const logger = new logging.Logger('');\n const logs = [];\n logger.subscribe(ev => logs.push(ev.message));\n\n // A \"run\" can have <a href=\"api/forms/SelectMultipleControlValueAccessor\" class=\"code-anchor\">multiple</a> outputs, and contains progress information.\n const run = await architect.scheduleBuilder('@example/command-runner:command', {\n command: 'node',\n args: ['--print', '\\'foo\\''],\n }, { logger }); // We pass the logger for checking later.\n\n // The \"result\" member (of type BuilderOutput) is the next output.\n const output = await run.result;\n\n // Stop the builder from running. This stops Architect from keeping\n // the builder-associated states in memory, since builders keep waiting\n // to be scheduled.\n await run.stop();\n\n // Expect that foo was logged\n expect(logs).toContain('foo');\n });\n});\n\n</code-example>\n<div class=\"alert is-helpful\">\n<p> When running this test in your repo, you need the <a href=\"https://github.com/TypeStrong/ts-node\"><code>ts-node</code></a> package. You can avoid this by renaming <code>my-builder.spec.ts</code> to <code>my-builder.spec.js</code>.</p>\n</div>\n<h3 id=\"watch-mode\">Watch mode<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#watch-mode\"><i class=\"material-icons\">link</i></a></h3>\n<p>Architect expects builders to run once (by default) and return.\nThis behavior is not entirely compatible with a builder that watches for changes (like Webpack, for example).\nArchitect can support watch mode, but there are some things to look out for.</p>\n<ul>\n<li>\n<p>To be used with watch mode, a builder handler function should return an Observable. Architect subscribes to the Observable until it completes and might reuse it if the builder is scheduled again with the same arguments.</p>\n</li>\n<li>\n<p>The builder should always emit a <code>BuilderOutput</code> object after each execution. Once its been executed, it can enter a watch mode, to be triggered by an external event. If an event triggers it to restart, the builder should execute the <code>BuilderContext.reportRunning()</code> function to tell Architect that it is running again. This prevents Architect from stopping the builder if another run is scheduled.</p>\n</li>\n</ul>\n<p>When your builder calls <code>BuilderRun.stop()</code> to exit watch mode, Architect unsubscribes from the builders Observable and calls the builders teardown logic to clean up.\n(This behavior also allows for long running builds to be stopped and cleaned up.)</p>\n<p>In general, if your builder is watching an external event, you should separate your run into three phases.</p>\n<ol>\n<li>\n<p><strong>Running</strong>\nFor example, webpack compiles. This ends when webpack finishes and your builder emits a <code>BuilderOutput</code> object.</p>\n</li>\n<li>\n<p><strong>Watching</strong>\nBetween two runs, watch an external event stream. For example, webpack watches the file system for any changes. This ends when webpack restarts building, and <code>BuilderContext.reportRunning()</code> is called. This goes back to step 1.</p>\n</li>\n<li>\n<p><strong>Completion</strong>\nEither the task is fully completed (for example, webpack was supposed to run a number of times), or the builder run was stopped (using <code>BuilderRun.stop()</code>). Your teardown logic is executed, and Architect unsubscribes from your builders Observable.</p>\n</li>\n</ol>\n<h2 id=\"summary\">Summary<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/cli-builder#summary\"><i class=\"material-icons\">link</i></a></h2>\n<p>The CLI Builder API provides a new way of changing the behavior of the Angular CLI by using builders to execute custom logic.</p>\n<ul>\n<li>\n<p>Builders can be synchronous or asynchronous, execute once or watch for external events, and can schedule other builders or targets.</p>\n</li>\n<li>\n<p>Builders have option defaults specified in the <code>angular.json</code> configuration file, which can be overwritten by an alternate configuration for the target, and further overwritten by command line flags.</p>\n</li>\n<li>\n<p>We recommend that you use integration tests to test Architect builders. You can use unit tests to validate the logic that the builder executes.</p>\n</li>\n<li>\n<p>If your builder returns an Observable, it should clean up in the teardown logic of that Observable.</p>\n</li>\n</ul>\n\n \n</div>\n\n<!-- links to this doc:\n - cli/deploy\n - cli/lint\n - guide/architecture-next-steps\n - guide/deployment\n - guide/workspace-config\n-->\n<!-- links from this doc:\n - api/animations/browser\n - api/forms/SelectMultipleControlValueAccessor\n - cli/run\n - guide/cli-builder#adding-a-target\n - guide/cli-builder#angular-cli-builders\n - guide/cli-builder#builder-input\n - guide/cli-builder#builder-project-structure\n - guide/cli-builder#cli-builders\n - guide/cli-builder#creating-a-builder\n - guide/cli-builder#default-architect-configuration\n - guide/cli-builder#handling-output\n - guide/cli-builder#input-validation\n - guide/cli-builder#progress-and-status-reporting\n - guide/cli-builder#running-the-builder\n - guide/cli-builder#schedule-and-run\n - guide/cli-builder#summary\n - guide/cli-builder#target-configuration\n - guide/cli-builder#target-strings\n - guide/cli-builder#testing-a-builder\n - guide/cli-builder#watch-mode\n - guide/creating-libraries#publishing-your-library\n - guide/glossary#builder\n - guide/glossary#project\n - guide/glossary#schematic\n - guide/glossary#target\n - guide/workspace-config\n - http://json-schema.org/\n - https://docs.npmjs.com/files/package.json\n - https://electronjs.org/\n - https://github.com/TypeStrong/ts-node\n - https://github.com/angular/angular-cli/blob/ba21c855c0c8b778005df01d4851b5a2176edc6f/packages/angular_devkit/build_angular/src/tslint/index.ts#L107\n - https://github.com/angular/angular/edit/master/aio/content/guide/cli-builder.md?message=docs%3A%20describe%20your%20change...\n - https://github.com/mgechev/cli-builders-demo\n - https://github.com/mgechev/cli-builders-demo/blob/master/command-builder/builders.json\n - https://www.typescriptlang.org/docs/handbook/tsconfig-json.html\n-->"
}