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
|
@ -479,6 +479,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
|
||||
|
||||
|
@ -900,7 +901,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
|
||||
|
||||
|
||||
# ================================================
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue