Skip to content

Commit

Permalink
fix(docs-infra): convert hard-coded cli-builder examples into a pro…
Browse files Browse the repository at this point in the history
…per 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
  • Loading branch information
sonukapoor committed Dec 14, 2019
1 parent ad98702 commit dd9793f
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 148 deletions.
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -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

Expand Down Expand Up @@ -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


# ================================================
Expand Down
48 changes: 48 additions & 0 deletions 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
46 changes: 46 additions & 0 deletions 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<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
175 changes: 28 additions & 147 deletions aio/content/guide/cli-builder.md
Expand Up @@ -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.

<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> {
...
}
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<BuilderOutput>` object.

<code-example
path="cli-builder/src/my-builder.ts"
header="src/my-builder.ts (builder skeleton)"
region="builder-skeleton">
</code-example>

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.

<code-example language="typescript" header="/command/index.ts">
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
import * as childProcess from 'child_process';

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
path="cli-builder/src/my-builder.ts"
header="src/my-builder.ts (builder)"
region="builder">
</code-example>

### Handling output
Expand All @@ -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.

<code-example language="typescript" header="/command/index.ts">
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<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
path="cli-builder/src/my-builder.ts"
header="src/my-builder.ts (handling output)"
region="handling-output">
</code-example>

### Progress and status reporting
Expand All @@ -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.

<code-example language="typescript" header="/command/index.ts">
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<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
path="cli-builder/src/my-builder.ts"
header="src/my-builder.ts (progess reporting)"
region="progress-reporting">
</code-example>

## Builder input
Expand Down Expand Up @@ -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`.

<code-example language="typescript" header="/command/index.ts">
context.reportStatus(`Executing "${options.command}"...`);
const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' });

<code-example
path="cli-builder/src/my-builder.ts"
header="src/my-builder.ts (report status)"
region="report-status">
</code-example>

### Target configuration
Expand Down Expand Up @@ -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.

<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');
});
});
The test uses the builder to run the `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`.

<code-example
path="cli-builder/src/my-builder.spec.ts"
header="src/my-builder.spec.ts">
</code-example>

<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>

Expand Down

0 comments on commit dd9793f

Please sign in to comment.