From dd9793f928c5bc5a16e6bef08d612ae4124a9b31 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Wed, 11 Dec 2019 21:31:45 -0500 Subject: [PATCH] fix(docs-infra): convert hard-coded `cli-builder` examples into a proper mini-app 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 --- .github/CODEOWNERS | 3 +- .../cli-builder/src/my-builder.spec.ts | 48 +++++ .../examples/cli-builder/src/my-builder.ts | 46 +++++ aio/content/guide/cli-builder.md | 175 +++--------------- 4 files changed, 124 insertions(+), 148 deletions(-) create mode 100644 aio/content/examples/cli-builder/src/my-builder.spec.ts create mode 100644 aio/content/examples/cli-builder/src/my-builder.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 32b8ff541fb03..75a0203789334 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -480,6 +480,7 @@ /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/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/web-worker.md @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes @@ -901,7 +902,7 @@ testing/** @angular/fw-test /aio/content/guide/migration-module-with-providers.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/guide/updating-to-version-9.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/guide/ivy-compatibility.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes -/aio/content/guide/ivy-compatibility-examples.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes +/aio/content/guide/ivy-compatibility-examples.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes # ================================================ diff --git a/aio/content/examples/cli-builder/src/my-builder.spec.ts b/aio/content/examples/cli-builder/src/my-builder.spec.ts new file mode 100644 index 0000000000000..27753d66b366f --- /dev/null +++ b/aio/content/examples/cli-builder/src/my-builder.spec.ts @@ -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 diff --git a/aio/content/examples/cli-builder/src/my-builder.ts b/aio/content/examples/cli-builder/src/my-builder.ts new file mode 100644 index 0000000000000..308fd8ffe512d --- /dev/null +++ b/aio/content/examples/cli-builder/src/my-builder.ts @@ -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 { + // #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 diff --git a/aio/content/guide/cli-builder.md b/aio/content/guide/cli-builder.md index 8319358f6abbc..56770045834c1 100644 --- a/aio/content/guide/cli-builder.md +++ b/aio/content/guide/cli-builder.md @@ -56,45 +56,23 @@ npm install @example/my-builder ## Creating a builder -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. - - -import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; - -export default createBuilder(_commandBuilder); - -function _commandBuilder( - options: JsonObject, - context: BuilderContext, - ): Promise { - ... -} +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 `Promise` object. + 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. If the process is successful (returns a code of 0), it resolves the return value. - -import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; -import * as childProcess from 'child_process'; - -export default createBuilder(_commandBuilder); - -function _commandBuilder( - options: JsonObject, - context: BuilderContext, -): Promise { - const child = childProcess.spawn(options.command, options.args); - return new Promise(resolve => { - child.on('close', code => { - resolve({success: code === 0}); - }); - }); -} - + ### 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. - -import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; -import * as childProcess from 'child_process'; - -export default createBuilder(_commandBuilder); - -function _commandBuilder( - options: JsonObject, - context: BuilderContext, -): Promise { - 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(resolve => { - child.on('close', code => { - resolve({success: code === 0}); - }); - }); -} - + ### 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.) Pass an empty string to remove the status. - -import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; -import * as childProcess from 'child_process'; - -export default createBuilder(_commandBuilder); - -function _commandBuilder( - options: JsonObject, - context: BuilderContext, -): Promise { - 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(resolve => { - context.reportStatus(`Done.`); - child.on('close', code => { - resolve({success: code === 0}); - }); - }); -} - + ## 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`. - - context.reportStatus(`Executing "${options.command}"...`); - const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); - + ### 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). -* 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. 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. - - - -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'); - }); -}); +The test uses the builder to run the `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`. +
- 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`.