Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
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
Comment thread
gkalpak marked this conversation as resolved.
Resolved

Comment thread
gkalpak marked this conversation as resolved.
Resolved

# ================================================
Expand Down
48 changes: 48 additions & 0 deletions aio/content/examples/cli-builder/src/my-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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