Skip to content

Commit 15ae924

Browse files
sonukapoormatsko
authored andcommitted
fix(docs-infra): convert hard-coded cli-builder examples into a proper mini-app (angular#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 angular#34314 PR Close angular#34362
1 parent 6c8e322 commit 15ae924

4 files changed

Lines changed: 124 additions & 148 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@
479479

480480
/packages/compiler-cli/src/ngtools/** @angular/tools-cli @angular/framework-global-approvers
481481
/aio/content/guide/cli-builder.md @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
482+
/aio/content/examples/cli-builder/** @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
482483
/aio/content/guide/ivy.md @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
483484
/aio/content/guide/web-worker.md @angular/tools-cli @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
484485

@@ -900,7 +901,7 @@ testing/** @angular/fw-test
900901
/aio/content/guide/migration-module-with-providers.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
901902
/aio/content/guide/updating-to-version-9.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
902903
/aio/content/guide/ivy-compatibility.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
903-
/aio/content/guide/ivy-compatibility-examples.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
904+
/aio/content/guide/ivy-compatibility-examples.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes
904905

905906

906907
# ================================================
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// #docregion
2+
import { Architect } from '@angular-devkit/architect';
3+
import { TestingArchitectHost } from '@angular-devkit/architect/testing';
4+
import { logging, schema } from '@angular-devkit/core';
5+
6+
describe('Command Runner Builder', () => {
7+
let architect: Architect;
8+
let architectHost: TestingArchitectHost;
9+
10+
beforeEach(async () => {
11+
const registry = new schema.CoreSchemaRegistry();
12+
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
13+
14+
// TestingArchitectHost() takes workspace and current directories.
15+
// Since we don't use those, both are the same in this case.
16+
architectHost = new TestingArchitectHost(__dirname, __dirname);
17+
architect = new Architect(architectHost, registry);
18+
19+
// This will either take a Node package name, or a path to the directory
20+
// for the package.json file.
21+
await architectHost.addBuilderFromPackage('..');
22+
});
23+
24+
it('can run node', async () => {
25+
// Create a logger that keeps an array of all messages that were logged.
26+
const logger = new logging.Logger('');
27+
const logs = [];
28+
logger.subscribe(ev => logs.push(ev.message));
29+
30+
// A "run" can have multiple outputs, and contains progress information.
31+
const run = await architect.scheduleBuilder('@example/command-runner:command', {
32+
command: 'node',
33+
args: ['--print', '\'foo\''],
34+
}, { logger }); // We pass the logger for checking later.
35+
36+
// The "result" member (of type BuilderOutput) is the next output.
37+
const output = await run.result;
38+
39+
// Stop the builder from running. This stops Architect from keeping
40+
// the builder-associated states in memory, since builders keep waiting
41+
// to be scheduled.
42+
await run.stop();
43+
44+
// Expect that foo was logged
45+
expect(logs).toContain('foo');
46+
});
47+
});
48+
// #enddocregion
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// #docplaster
2+
// #docregion builder, builder-skeleton, handling-output, progress-reporting
3+
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
4+
import { JsonObject } from '@angular-devkit/core';
5+
// #enddocregion builder-skeleton
6+
import * as childProcess from 'child_process';
7+
// #docregion builder-skeleton
8+
9+
interface Options extends JsonObject {
10+
command: string;
11+
args: string[];
12+
}
13+
14+
export default createBuilder(commandBuilder);
15+
16+
function commandBuilder(
17+
options: Options,
18+
context: BuilderContext,
19+
): Promise<BuilderOutput> {
20+
// #enddocregion builder, builder-skeleton, handling-output
21+
// #docregion report-status
22+
context.reportStatus(`Executing "${options.command}"...`);
23+
// #docregion builder, handling-output
24+
const child = childProcess.spawn(options.command, options.args);
25+
// #enddocregion builder, report-status
26+
27+
child.stdout.on('data', data => {
28+
context.logger.info(data.toString());
29+
});
30+
child.stderr.on('data', data => {
31+
context.logger.error(data.toString());
32+
});
33+
34+
// #docregion builder
35+
return new Promise(resolve => {
36+
// #enddocregion builder, handling-output
37+
context.reportStatus(`Done.`);
38+
// #docregion builder, handling-output
39+
child.on('close', code => {
40+
resolve({ success: code === 0 });
41+
});
42+
});
43+
// #docregion builder-skeleton
44+
}
45+
46+
// #enddocregion builder, builder-skeleton, handling-output, progress-reporting

aio/content/guide/cli-builder.md

Lines changed: 28 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -56,45 +56,23 @@ npm install @example/my-builder
5656

5757
## Creating a builder
5858

59-
As an example, let’s create a builder that executes a shell command.
60-
To create a builder, use the `createBuilder()` CLI Builder function, and return a `BuilderOutput` object.
61-
62-
<code-example language="typescript" header="/command/index.ts">
63-
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
64-
65-
export default createBuilder(_commandBuilder);
66-
67-
function _commandBuilder(
68-
options: JsonObject,
69-
context: BuilderContext,
70-
): Promise<BuilderOutput> {
71-
...
72-
}
59+
As an example, let's create a builder that executes a shell command.
60+
To create a builder, use the `createBuilder()` CLI Builder function, and return a `Promise<BuilderOutput>` object.
7361

62+
<code-example
63+
path="cli-builder/src/my-builder.ts"
64+
header="src/my-builder.ts (builder skeleton)"
65+
region="builder-skeleton">
7466
</code-example>
7567

7668
Now let’s add some logic to it.
7769
The following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish.
7870
If the process is successful (returns a code of 0), it resolves the return value.
7971

80-
<code-example language="typescript" header="/command/index.ts">
81-
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
82-
import * as childProcess from 'child_process';
83-
84-
export default createBuilder(_commandBuilder);
85-
86-
function _commandBuilder(
87-
options: JsonObject,
88-
context: BuilderContext,
89-
): Promise<BuilderOutput> {
90-
const child = childProcess.spawn(options.command, options.args);
91-
return new Promise<BuilderOutput>(resolve => {
92-
child.on('close', code => {
93-
resolve({success: code === 0});
94-
});
95-
});
96-
}
97-
72+
<code-example
73+
path="cli-builder/src/my-builder.ts"
74+
header="src/my-builder.ts (builder)"
75+
region="builder">
9876
</code-example>
9977

10078
### Handling output
@@ -105,31 +83,10 @@ This also allows the builder itself to be executed in a separate process, even i
10583

10684
We can retrieve a Logger instance from the context.
10785

108-
<code-example language="typescript" header="/command/index.ts">
109-
import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect';
110-
import * as childProcess from 'child_process';
111-
112-
export default createBuilder(_commandBuilder);
113-
114-
function _commandBuilder(
115-
options: JsonObject,
116-
context: BuilderContext,
117-
): Promise<BuilderOutput> {
118-
const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'});
119-
child.stdout.on('data', (data) => {
120-
context.logger.info(data.toString());
121-
});
122-
child.stderr.on('data', (data) => {
123-
context.logger.error(data.toString());
124-
});
125-
126-
return new Promise<BuilderOutput>(resolve => {
127-
child.on('close', code => {
128-
resolve({success: code === 0});
129-
});
130-
});
131-
}
132-
86+
<code-example
87+
path="cli-builder/src/my-builder.ts"
88+
header="src/my-builder.ts (handling output)"
89+
region="handling-output">
13390
</code-example>
13491

13592
### Progress and status reporting
@@ -147,34 +104,10 @@ Use the `BuilderContext.reportStatus()` method to generate a status string of an
147104
(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.)
148105
Pass an empty string to remove the status.
149106

150-
<code-example language="typescript" header="/command/index.ts">
151-
import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect';
152-
import * as childProcess from 'child_process';
153-
154-
export default createBuilder(_commandBuilder);
155-
156-
function _commandBuilder(
157-
options: JsonObject,
158-
context: BuilderContext,
159-
): Promise<BuilderOutput> {
160-
context.reportStatus(`Executing "${options.command}"...`);
161-
const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'});
162-
163-
child.stdout.on('data', (data) => {
164-
context.logger.info(data.toString());
165-
});
166-
child.stderr.on('data', (data) => {
167-
context.logger.error(data.toString());
168-
});
169-
170-
return new Promise<BuilderOutput>(resolve => {
171-
context.reportStatus(`Done.`);
172-
child.on('close', code => {
173-
resolve({success: code === 0});
174-
});
175-
});
176-
}
177-
107+
<code-example
108+
path="cli-builder/src/my-builder.ts"
109+
header="src/my-builder.ts (progess reporting)"
110+
region="progress-reporting">
178111
</code-example>
179112

180113
## Builder input
@@ -257,10 +190,10 @@ The first part of this is the package name (resolved using node resolution), and
257190

258191
Using one of our `options` is very straightforward, we did this in the previous section when we accessed `options.command`.
259192

260-
<code-example language="typescript" header="/command/index.ts">
261-
context.reportStatus(`Executing "${options.command}"...`);
262-
const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' });
263-
193+
<code-example
194+
path="cli-builder/src/my-builder.ts"
195+
header="src/my-builder.ts (report status)"
196+
region="report-status">
264197
</code-example>
265198

266199
### Target configuration
@@ -486,73 +419,21 @@ Because we did not override the *args* option, it will list information about th
486419

487420
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).
488421

489-
* 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`.
422+
* 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`.
490423

491424
* 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.
492425

493426
Here’s an example of a test that runs the command builder.
494-
The test uses the builder to run the `ls` command, then validates that it ran successfully and listed the proper files.
495-
496-
<code-example language="typescript" header="command/index_spec.ts">
497-
498-
import { Architect } from '@angular-devkit/architect';
499-
import { TestingArchitectHost } from '@angular-devkit/architect/testing';
500-
// Our builder forwards the STDOUT of the command to the logger.
501-
import { logging, schema } from '@angular-devkit/core';
502-
503-
describe('Command Runner Builder', () => {
504-
let architect: Architect;
505-
let architectHost: TestingArchitectHost;
506-
507-
beforeEach(async () => {
508-
const registry = new schema.CoreSchemaRegistry();
509-
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
510-
511-
// TestingArchitectHost() takes workspace and current directories.
512-
// Since we don't use those, both are the same in this case.
513-
architectHost = new TestingArchitectHost(__dirname, __dirname);
514-
architect = new Architect(architectHost, registry);
515-
516-
// This will either take a Node package name, or a path to the directory
517-
// for the package.json file.
518-
await architectHost.addBuilderFromPackage('..');
519-
});
520-
521-
// This might not work in Windows.
522-
it('can run ls', async () => {
523-
// Create a logger that keeps an array of all messages that were logged.
524-
const logger = new logging.Logger('');
525-
const logs = [];
526-
logger.subscribe(ev => logs.push(ev.message));
527-
528-
// A "run" can have multiple outputs, and contains progress information.
529-
const run = await architect.scheduleBuilder('@example/command-runner:command', {
530-
command: 'ls',
531-
args: [__dirname],
532-
}, { logger }); // We pass the logger for checking later.
533-
534-
// The "result" member (of type BuilderOutput) is the next output.
535-
const output = await run.result;
536-
537-
// Stop the builder from running. This stops Architect from keeping
538-
// the builder-associated states in memory, since builders keep waiting
539-
// to be scheduled.
540-
await run.stop();
541-
542-
// Expect that it succeeded.
543-
expect(output.success).toBe(true);
544-
545-
// Expect that this file was listed. It should be since we're running
546-
// `ls $__dirname`.
547-
expect(logs).toContain('index.spec.ts');
548-
});
549-
});
427+
The test uses the builder to run the `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`.
550428

429+
<code-example
430+
path="cli-builder/src/my-builder.spec.ts"
431+
header="src/my-builder.spec.ts">
551432
</code-example>
552433

553434
<div class="alert is-helpful">
554435

555-
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`.
436+
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`.
556437

557438
</div>
558439

0 commit comments

Comments
 (0)