fix(docs-infra): convert hard-coded cli-builder examples into a proper mini-app (#34362)
				
					
				
			Previously, the examples in the `cli-builder` guide were hard-coded. This made it impossible to test them and verify they are correct. This commit fixes this by converting them into a proper mini-app. In a subsequent commit, tests will be added to verify that the source code works as expected (and guard against regressions). Fixes #34314 PR Close #34362
This commit is contained in:
		
							parent
							
								
									6c8e322fa1
								
							
						
					
					
						commit
						15ae924035
					
				
							
								
								
									
										1
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							| @ -479,6 +479,7 @@ | |||||||
| 
 | 
 | ||||||
| /packages/compiler-cli/src/ngtools/**                           @angular/tools-cli @angular/framework-global-approvers | /packages/compiler-cli/src/ngtools/**                           @angular/tools-cli @angular/framework-global-approvers | ||||||
| /aio/content/guide/cli-builder.md                               @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | /aio/content/guide/cli-builder.md                               @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | ||||||
|  | /aio/content/examples/cli-builder/**                            @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | ||||||
| /aio/content/guide/ivy.md                                       @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | /aio/content/guide/ivy.md                                       @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | ||||||
| /aio/content/guide/web-worker.md                                @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | /aio/content/guide/web-worker.md                                @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										48
									
								
								aio/content/examples/cli-builder/src/my-builder.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								aio/content/examples/cli-builder/src/my-builder.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | // #docregion
 | ||||||
|  | import { Architect } from '@angular-devkit/architect'; | ||||||
|  | import { TestingArchitectHost } from '@angular-devkit/architect/testing'; | ||||||
|  | import { logging, schema } from '@angular-devkit/core'; | ||||||
|  | 
 | ||||||
|  | describe('Command Runner Builder', () => { | ||||||
|  |   let architect: Architect; | ||||||
|  |   let architectHost: TestingArchitectHost; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     const registry = new schema.CoreSchemaRegistry(); | ||||||
|  |     registry.addPostTransform(schema.transforms.addUndefinedDefaults); | ||||||
|  | 
 | ||||||
|  |     // TestingArchitectHost() takes workspace and current directories.
 | ||||||
|  |     // Since we don't use those, both are the same in this case.
 | ||||||
|  |     architectHost = new TestingArchitectHost(__dirname, __dirname); | ||||||
|  |     architect = new Architect(architectHost, registry); | ||||||
|  | 
 | ||||||
|  |     // This will either take a Node package name, or a path to the directory
 | ||||||
|  |     // for the package.json file.
 | ||||||
|  |     await architectHost.addBuilderFromPackage('..'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('can run node', async () => { | ||||||
|  |     // Create a logger that keeps an array of all messages that were logged.
 | ||||||
|  |     const logger = new logging.Logger(''); | ||||||
|  |     const logs = []; | ||||||
|  |     logger.subscribe(ev => logs.push(ev.message)); | ||||||
|  | 
 | ||||||
|  |     // A "run" can have multiple outputs, and contains progress information.
 | ||||||
|  |     const run = await architect.scheduleBuilder('@example/command-runner:command', { | ||||||
|  |       command: 'node', | ||||||
|  |       args: ['--print', '\'foo\''], | ||||||
|  |     }, { logger });  // We pass the logger for checking later.
 | ||||||
|  | 
 | ||||||
|  |     // The "result" member (of type BuilderOutput) is the next output.
 | ||||||
|  |     const output = await run.result; | ||||||
|  | 
 | ||||||
|  |     // Stop the builder from running. This stops Architect from keeping
 | ||||||
|  |     // the builder-associated states in memory, since builders keep waiting
 | ||||||
|  |     // to be scheduled.
 | ||||||
|  |     await run.stop(); | ||||||
|  | 
 | ||||||
|  |     // Expect that foo was logged
 | ||||||
|  |     expect(logs).toContain('foo'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | // #enddocregion
 | ||||||
							
								
								
									
										46
									
								
								aio/content/examples/cli-builder/src/my-builder.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								aio/content/examples/cli-builder/src/my-builder.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | // #docplaster
 | ||||||
|  | // #docregion builder, builder-skeleton, handling-output, progress-reporting
 | ||||||
|  | import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; | ||||||
|  | import { JsonObject } from '@angular-devkit/core'; | ||||||
|  | // #enddocregion builder-skeleton
 | ||||||
|  | import * as childProcess from 'child_process'; | ||||||
|  | // #docregion builder-skeleton
 | ||||||
|  | 
 | ||||||
|  | interface Options extends JsonObject { | ||||||
|  |   command: string; | ||||||
|  |   args: string[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default createBuilder(commandBuilder); | ||||||
|  | 
 | ||||||
|  | function commandBuilder( | ||||||
|  |   options: Options, | ||||||
|  |   context: BuilderContext, | ||||||
|  |   ): Promise<BuilderOutput> { | ||||||
|  |     // #enddocregion builder, builder-skeleton, handling-output
 | ||||||
|  |     // #docregion report-status
 | ||||||
|  |     context.reportStatus(`Executing "${options.command}"...`); | ||||||
|  |     // #docregion builder, handling-output
 | ||||||
|  |     const child = childProcess.spawn(options.command, options.args); | ||||||
|  |     // #enddocregion builder, report-status
 | ||||||
|  | 
 | ||||||
|  |     child.stdout.on('data', data => { | ||||||
|  |       context.logger.info(data.toString()); | ||||||
|  |     }); | ||||||
|  |     child.stderr.on('data', data => { | ||||||
|  |       context.logger.error(data.toString()); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // #docregion builder
 | ||||||
|  |     return new Promise(resolve => { | ||||||
|  |       // #enddocregion builder, handling-output
 | ||||||
|  |       context.reportStatus(`Done.`); | ||||||
|  |       // #docregion builder, handling-output
 | ||||||
|  |       child.on('close', code => { | ||||||
|  |         resolve({ success: code === 0 }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     // #docregion builder-skeleton
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // #enddocregion builder, builder-skeleton, handling-output, progress-reporting
 | ||||||
| @ -56,45 +56,23 @@ npm install @example/my-builder | |||||||
| 
 | 
 | ||||||
| ## Creating a builder | ## Creating a builder | ||||||
| 
 | 
 | ||||||
| As an example, let’s create a builder that executes a shell command. | As an example, let's create a builder that executes a shell command. | ||||||
| To create a builder, use the `createBuilder()` CLI Builder function, and return a `BuilderOutput` object. | To create a builder, use the `createBuilder()` CLI Builder function, and return a `Promise<BuilderOutput>` object. | ||||||
| 
 |  | ||||||
| <code-example language="typescript" header="/command/index.ts"> |  | ||||||
| import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; |  | ||||||
| 
 |  | ||||||
| export default createBuilder(_commandBuilder); |  | ||||||
| 
 |  | ||||||
| function _commandBuilder( |  | ||||||
|   options: JsonObject, |  | ||||||
|   context: BuilderContext, |  | ||||||
|   ): Promise<BuilderOutput> { |  | ||||||
|   ... |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
|  | <code-example  | ||||||
|  |   path="cli-builder/src/my-builder.ts"  | ||||||
|  |   header="src/my-builder.ts (builder skeleton)"  | ||||||
|  |   region="builder-skeleton"> | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| Now let’s add some logic to it. | Now let’s add some logic to it. | ||||||
| The following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish. | The following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish. | ||||||
| If the process is successful (returns a code of 0), it resolves the return value. | If the process is successful (returns a code of 0), it resolves the return value. | ||||||
| 
 | 
 | ||||||
| <code-example language="typescript" header="/command/index.ts"> | <code-example  | ||||||
| import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; |   path="cli-builder/src/my-builder.ts"  | ||||||
| import * as childProcess from 'child_process'; |   header="src/my-builder.ts (builder)"  | ||||||
| 
 |   region="builder"> | ||||||
| export default createBuilder(_commandBuilder); |  | ||||||
| 
 |  | ||||||
| function _commandBuilder( |  | ||||||
|   options: JsonObject, |  | ||||||
|   context: BuilderContext, |  | ||||||
| ): Promise<BuilderOutput> { |  | ||||||
|   const child = childProcess.spawn(options.command, options.args); |  | ||||||
|   return new Promise<BuilderOutput>(resolve => { |  | ||||||
|     child.on('close', code => { |  | ||||||
|       resolve({success: code === 0}); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| ### Handling output | ### Handling output | ||||||
| @ -105,31 +83,10 @@ This also allows the builder itself to be executed in a separate process, even i | |||||||
| 
 | 
 | ||||||
| We can retrieve a Logger instance from the context. | We can retrieve a Logger instance from the context. | ||||||
| 
 | 
 | ||||||
| <code-example language="typescript" header="/command/index.ts"> | <code-example  | ||||||
| import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; |   path="cli-builder/src/my-builder.ts"  | ||||||
| import * as childProcess from 'child_process'; |   header="src/my-builder.ts (handling output)"  | ||||||
| 
 |   region="handling-output"> | ||||||
| export default createBuilder(_commandBuilder); |  | ||||||
| 
 |  | ||||||
| function _commandBuilder( |  | ||||||
|   options: JsonObject, |  | ||||||
|   context: BuilderContext, |  | ||||||
| ): Promise<BuilderOutput> { |  | ||||||
|   const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'}); |  | ||||||
|   child.stdout.on('data', (data) => { |  | ||||||
|     context.logger.info(data.toString()); |  | ||||||
|   }); |  | ||||||
|   child.stderr.on('data', (data) => { |  | ||||||
|     context.logger.error(data.toString()); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return new Promise<BuilderOutput>(resolve => { |  | ||||||
|     child.on('close', code => { |  | ||||||
|       resolve({success: code === 0}); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| ### Progress and status reporting | ### Progress and status reporting | ||||||
| @ -147,34 +104,10 @@ Use the `BuilderContext.reportStatus()` method to generate a status string of an | |||||||
| (Note that there’s no guarantee that a long string will be shown entirely; it could be cut to fit the UI that displays it.) | (Note that there’s no guarantee that a long string will be shown entirely; it could be cut to fit the UI that displays it.) | ||||||
| Pass an empty string to remove the status. | Pass an empty string to remove the status. | ||||||
| 
 | 
 | ||||||
| <code-example language="typescript" header="/command/index.ts"> | <code-example  | ||||||
| import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; |   path="cli-builder/src/my-builder.ts"  | ||||||
| import * as childProcess from 'child_process'; |   header="src/my-builder.ts (progess reporting)"  | ||||||
| 
 |   region="progress-reporting"> | ||||||
| export default createBuilder(_commandBuilder); |  | ||||||
| 
 |  | ||||||
| function _commandBuilder( |  | ||||||
|   options: JsonObject, |  | ||||||
|   context: BuilderContext, |  | ||||||
| ): Promise<BuilderOutput> { |  | ||||||
|   context.reportStatus(`Executing "${options.command}"...`); |  | ||||||
|   const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'}); |  | ||||||
| 
 |  | ||||||
|   child.stdout.on('data', (data) => { |  | ||||||
|     context.logger.info(data.toString()); |  | ||||||
|   }); |  | ||||||
|   child.stderr.on('data', (data) => { |  | ||||||
|     context.logger.error(data.toString()); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   return new Promise<BuilderOutput>(resolve => { |  | ||||||
|     context.reportStatus(`Done.`); |  | ||||||
|     child.on('close', code => { |  | ||||||
|       resolve({success: code === 0}); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| ## Builder input | ## Builder input | ||||||
| @ -257,10 +190,10 @@ The first part of this is the package name (resolved using node resolution), and | |||||||
| 
 | 
 | ||||||
| Using one of our `options` is very straightforward, we did this in the previous section when we accessed `options.command`. | Using one of our `options` is very straightforward, we did this in the previous section when we accessed `options.command`. | ||||||
| 
 | 
 | ||||||
| <code-example language="typescript" header="/command/index.ts"> | <code-example  | ||||||
|     context.reportStatus(`Executing "${options.command}"...`); |   path="cli-builder/src/my-builder.ts"  | ||||||
|     const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); |   header="src/my-builder.ts (report status)"  | ||||||
| 
 |   region="report-status"> | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| ### Target configuration | ### Target configuration | ||||||
| @ -486,73 +419,21 @@ Because we did not override the *args* option, it will list information about th | |||||||
| 
 | 
 | ||||||
| Use integration testing for your builder, so that you can use the Architect scheduler to create a context, as in this [example](https://github.com/mgechev/cli-builders-demo). | Use integration testing for your builder, so that you can use the Architect scheduler to create a context, as in this [example](https://github.com/mgechev/cli-builders-demo). | ||||||
| 
 | 
 | ||||||
| * In the builder source directory, we have created a new test file `index.spec.ts`. The code creates new instances of `JsonSchemaRegistry` (for schema validation), `TestingArchitectHost` (an in-memory implementation of `ArchitectHost`), and `Architect`. | * In the builder source directory, we have created a new test file `my-builder.spec.ts`. The code creates new instances of `JsonSchemaRegistry` (for schema validation), `TestingArchitectHost` (an in-memory implementation of `ArchitectHost`), and `Architect`. | ||||||
| 
 | 
 | ||||||
| * We've added a `builders.json` file next to the builder's [`package.json` file](https://github.com/mgechev/cli-builders-demo/blob/master/command-builder/builders.json), and modified the package file to point to it. | * We've added a `builders.json` file next to the builder's [`package.json` file](https://github.com/mgechev/cli-builders-demo/blob/master/command-builder/builders.json), and modified the package file to point to it. | ||||||
| 
 | 
 | ||||||
| Here’s an example of a test that runs the command builder. | Here’s an example of a test that runs the command builder. | ||||||
| The test uses the builder to run the `ls` command, then validates that it ran successfully and listed the proper files. | The test uses the builder to run the `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`. | ||||||
| 
 |  | ||||||
| <code-example language="typescript" header="command/index_spec.ts"> |  | ||||||
| 
 |  | ||||||
| import { Architect } from '@angular-devkit/architect'; |  | ||||||
| import { TestingArchitectHost } from '@angular-devkit/architect/testing'; |  | ||||||
| // Our builder forwards the STDOUT of the command to the logger. |  | ||||||
| import { logging, schema } from '@angular-devkit/core'; |  | ||||||
| 
 |  | ||||||
| describe('Command Runner Builder', () => { |  | ||||||
|   let architect: Architect; |  | ||||||
|   let architectHost: TestingArchitectHost; |  | ||||||
| 
 |  | ||||||
|   beforeEach(async () => { |  | ||||||
|     const registry = new schema.CoreSchemaRegistry(); |  | ||||||
|     registry.addPostTransform(schema.transforms.addUndefinedDefaults); |  | ||||||
| 
 |  | ||||||
|     // TestingArchitectHost() takes workspace and current directories. |  | ||||||
|     // Since we don't use those, both are the same in this case. |  | ||||||
|     architectHost = new TestingArchitectHost(__dirname, __dirname); |  | ||||||
|     architect = new Architect(architectHost, registry); |  | ||||||
| 
 |  | ||||||
|     // This will either take a Node package name, or a path to the directory |  | ||||||
|     // for the package.json file. |  | ||||||
|     await architectHost.addBuilderFromPackage('..'); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // This might not work in Windows. |  | ||||||
|   it('can run ls', async () => { |  | ||||||
|     // Create a logger that keeps an array of all messages that were logged. |  | ||||||
|     const logger = new logging.Logger(''); |  | ||||||
|     const logs = []; |  | ||||||
|     logger.subscribe(ev => logs.push(ev.message)); |  | ||||||
| 
 |  | ||||||
|     // A "run" can have multiple outputs, and contains progress information. |  | ||||||
|     const run = await architect.scheduleBuilder('@example/command-runner:command', { |  | ||||||
|       command: 'ls', |  | ||||||
|       args: [__dirname], |  | ||||||
|     }, { logger });  // We pass the logger for checking later. |  | ||||||
| 
 |  | ||||||
|     // The "result" member (of type BuilderOutput) is the next output. |  | ||||||
|     const output = await run.result; |  | ||||||
| 
 |  | ||||||
|     // Stop the builder from running. This stops Architect from keeping |  | ||||||
|     // the builder-associated states in memory, since builders keep waiting |  | ||||||
|     // to be scheduled. |  | ||||||
|     await run.stop(); |  | ||||||
| 
 |  | ||||||
|     // Expect that it succeeded. |  | ||||||
|     expect(output.success).toBe(true); |  | ||||||
| 
 |  | ||||||
|     // Expect that this file was listed. It should be since we're running |  | ||||||
|     // `ls $__dirname`. |  | ||||||
|     expect(logs).toContain('index.spec.ts'); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
|  | <code-example  | ||||||
|  |   path="cli-builder/src/my-builder.spec.ts"  | ||||||
|  |   header="src/my-builder.spec.ts"> | ||||||
| </code-example> | </code-example> | ||||||
| 
 | 
 | ||||||
| <div class="alert is-helpful"> | <div class="alert is-helpful"> | ||||||
| 
 | 
 | ||||||
|    When running this test in your repo, you need the [`ts-node`](https://github.com/TypeStrong/ts-node) package. You can avoid this by renaming `index.spec.ts` to `index.spec.js`. |    When running this test in your repo, you need the [`ts-node`](https://github.com/TypeStrong/ts-node) package. You can avoid this by renaming `my-builder.spec.ts` to `my-builder.spec.js`. | ||||||
| 
 | 
 | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user