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
|
/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/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/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
|
/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/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/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.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
|
## Creating a builder
|
||||||
|
|
||||||
As an example, let’s create a builder that executes a shell command.
|
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.
|
To create a builder, use the `createBuilder()` CLI Builder function, and return a `Promise<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> {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<code-example
|
||||||
|
path="cli-builder/src/my-builder.ts"
|
||||||
|
header="src/my-builder.ts (builder skeleton)"
|
||||||
|
region="builder-skeleton">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
Now let’s add some logic to it.
|
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.
|
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.
|
If the process is successful (returns a code of 0), it resolves the return value.
|
||||||
|
|
||||||
<code-example language="typescript" header="/command/index.ts">
|
<code-example
|
||||||
import { BuilderOutput, createBuilder } from '@angular-devkit/architect';
|
path="cli-builder/src/my-builder.ts"
|
||||||
import * as childProcess from 'child_process';
|
header="src/my-builder.ts (builder)"
|
||||||
|
region="builder">
|
||||||
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>
|
</code-example>
|
||||||
|
|
||||||
### Handling output
|
### 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.
|
We can retrieve a Logger instance from the context.
|
||||||
|
|
||||||
<code-example language="typescript" header="/command/index.ts">
|
<code-example
|
||||||
import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect';
|
path="cli-builder/src/my-builder.ts"
|
||||||
import * as childProcess from 'child_process';
|
header="src/my-builder.ts (handling output)"
|
||||||
|
region="handling-output">
|
||||||
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>
|
</code-example>
|
||||||
|
|
||||||
### Progress and status reporting
|
### 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.)
|
(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.
|
Pass an empty string to remove the status.
|
||||||
|
|
||||||
<code-example language="typescript" header="/command/index.ts">
|
<code-example
|
||||||
import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect';
|
path="cli-builder/src/my-builder.ts"
|
||||||
import * as childProcess from 'child_process';
|
header="src/my-builder.ts (progess reporting)"
|
||||||
|
region="progress-reporting">
|
||||||
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>
|
</code-example>
|
||||||
|
|
||||||
## Builder input
|
## 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`.
|
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">
|
<code-example
|
||||||
context.reportStatus(`Executing "${options.command}"...`);
|
path="cli-builder/src/my-builder.ts"
|
||||||
const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' });
|
header="src/my-builder.ts (report status)"
|
||||||
|
region="report-status">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
### Target configuration
|
### 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).
|
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.
|
* 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.
|
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.
|
The test uses the builder to run the `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`.
|
||||||
|
|
||||||
<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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
<code-example
|
||||||
|
path="cli-builder/src/my-builder.spec.ts"
|
||||||
|
header="src/my-builder.spec.ts">
|
||||||
</code-example>
|
</code-example>
|
||||||
|
|
||||||
<div class="alert is-helpful">
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue