diff --git a/aio/content/examples/cli-builder/src/my-builder.spec.ts b/aio/content/examples/cli-builder/src/my-builder.spec.ts index 27753d66b36..ab6a074e383 100644 --- a/aio/content/examples/cli-builder/src/my-builder.spec.ts +++ b/aio/content/examples/cli-builder/src/my-builder.spec.ts @@ -1,9 +1,10 @@ // #docregion import { Architect } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; -import { logging, schema } from '@angular-devkit/core'; +import { schema } from '@angular-devkit/core'; +import { promises as fs } from 'fs'; -describe('Command Runner Builder', () => { +describe('Copy File Builder', () => { let architect: Architect; let architectHost: TestingArchitectHost; @@ -21,17 +22,12 @@ describe('Command Runner Builder', () => { 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)); - + it('can copy files', async () => { // 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. + const run = await architect.scheduleBuilder('@example/copy-file:copy', { + source: 'package.json', + destination: 'package-copy.json', + }); // The "result" member (of type BuilderOutput) is the next output. const output = await run.result; @@ -41,8 +37,10 @@ describe('Command Runner Builder', () => { // to be scheduled. await run.stop(); - // Expect that foo was logged - expect(logs).toContain('foo'); + // Expect that the copied file is the same as its source. + const sourceContent = await fs.readFile('package.json', 'utf8'); + const destinationContent = await fs.readFile('package-copy.json', 'utf8'); + expect(destinationContent).toBe(sourceContent); }); }); // #enddocregion diff --git a/aio/content/examples/cli-builder/src/my-builder.ts b/aio/content/examples/cli-builder/src/my-builder.ts index 308fd8ffe51..b7603078d47 100644 --- a/aio/content/examples/cli-builder/src/my-builder.ts +++ b/aio/content/examples/cli-builder/src/my-builder.ts @@ -3,44 +3,42 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { JsonObject } from '@angular-devkit/core'; // #enddocregion builder-skeleton -import * as childProcess from 'child_process'; +import { promises as fs } from 'fs'; // #docregion builder-skeleton interface Options extends JsonObject { - command: string; - args: string[]; + source: string; + destination: string; } -export default createBuilder(commandBuilder); +export default createBuilder(copyFileBuilder); -function commandBuilder( +async function copyFileBuilder( options: Options, context: BuilderContext, - ): Promise { - // #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()); - }); - +): Promise { + // #enddocregion builder, builder-skeleton, handling-output + // #docregion report-status + context.reportStatus(`Copying ${options.source} to ${options.destination}.`); + // #docregion builder, handling-output + try { + await fs.copyFile(options.source, options.destination); + } catch (err) { + // #enddocregion builder + context.logger.error('Failed to copy file.'); // #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 + return { + success: false, + error: err.message, + }; + } + + // #enddocregion builder, handling-output + context.reportStatus('Done.'); + // #docregion builder, handling-output + return { success: true }; + // #enddocregion report-status + // #docregion builder-skeleton } // #enddocregion builder, builder-skeleton, handling-output, progress-reporting diff --git a/aio/content/guide/cli-builder.md b/aio/content/guide/cli-builder.md index 1c374dba1b5..76ee813b006 100644 --- a/aio/content/guide/cli-builder.md +++ b/aio/content/guide/cli-builder.md @@ -56,7 +56,7 @@ npm install @example/my-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 copies a file. To create a builder, use the `createBuilder()` CLI Builder function, and return a `Promise` object. -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. +Now let’s add some logic to it. The following code retrieves the source and destination file paths +from user options and copies the file from the source to the destination (leveraging the +[Promise version of the built-in NodeJS `copyFile()` function](https://nodejs.org/api/fs.html#fs_fspromises_copyfile_src_dest_mode)). +If the copy operation fails, it returns an error with a message about the underlying problem. + { "$schema": "http://json-schema.org/schema", "type": "object", "properties": { - "command": { + "source": { "type": "string" }, - "args": { - "type": "array", - "items": { - "type": "string" - } + "destination": { + "type": "string" } } } @@ -159,10 +160,10 @@ Create a file named `builders.json` that looks like this: { "builders": { - "command": { - "implementation": "./command", - "schema": "./command/schema.json", - "description": "Runs any command line in the operating system." + "copy": { + "implementation": "./dist/my-builder.js", + "schema": "./src/schema.json", + "description": "Copies a file." } } } @@ -174,21 +175,22 @@ In the `package.json` file, add a `builders` key that tells the Architect tool w { - "name": "@example/command-runner", + "name": "@example/copy-file", "version": "1.0.0", - "description": "Builder for Command Runner", + "description": "Builder for copying files", "builders": "builders.json", - "devDependencies": { - "@angular-devkit/architect": "^1.0.0" + "dependencies": { + "@angular-devkit/architect": "~0.1200.0", + "@angular-devkit/core": "^12.0.0" } } -The official name of our builder is now ` @example/command-runner:command`. +The official name of our builder is now ` @example/copy-file:copy`. The first part of this is the package name (resolved using node resolution), and the second part is the builder name (resolved using the `builders.json` file). -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.source` and `options.destination`. -npm install @example/command-runner +npm install @example/copy-file @@ -333,18 +335,18 @@ If we create a new project with `ng new builder-test`, the generated `angular.js ### Adding a target -Let's add a new target that will run our builder to execute a particular command. -This target will tell the builder to run `touch` on a file, in order to update its modified date. +Let's add a new target that will run our builder to copy a file. +This target will tell the builder to copy the `package.json` file. We need to update the `angular.json` file to add a target for this builder to the "architect" section of our new project. * We'll add a new target section to the "architect" object for our project. -* The target named "touch" uses our builder, which we published to `@example/command-runner`. (See [Publishing your Library](guide/creating-libraries#publishing-your-library).) +* The target named "copy-package" uses our builder, which we published to `@example/copy-file`. (See [Publishing your Library](guide/creating-libraries#publishing-your-library)) -* The options object provides default values for the two inputs that we defined; `command`, which is the Unix command to execute, and `args`, an array that contains the file to operate on. +* The options object provides default values for the two inputs that we defined; `source`, which is the existing file we are copying, and `destination`, the path we want to copy to. -* The configurations key is optional, we'll leave it out for now. +* The `configurations` key is optional, we'll leave it out for now. @@ -352,13 +354,11 @@ We need to update the `angular.json` file to add a target for this builder to th "projects": { "builder-test": { "architect": { - "touch": { - "builder": "@example/command-runner:command", + "copy-package": { + "builder": "@example/copy-file:copy", "options": { - "command": "touch", - "args": [ - "src/main.ts" - ] + "source": "package.json", + "destination": "package-copy.json" } }, "build": { @@ -393,27 +393,27 @@ We need to update the `angular.json` file to add a target for this builder to th ### Running the builder -To run our builder with the new target's default configuration, use the following CLI command in a Linux shell. +To run our builder with the new target's default configuration, use the following CLI command. - ng run builder-test:touch +ng run builder-test:copy-package -This will run the `touch` command on the `src/main.ts` file. +This will copy the `package.json` file to `package-copy.json`. You can use command-line arguments to override the configured defaults. -For example, to run with a different `command` value, use the following CLI command. +For example, to run with a different `destination` value, use the following CLI command. -ng run builder-test:touch --command=ls +ng run builder-test:copy-package --destination=package-other.json -This will call the `ls` command instead of the `touch` command. -Because we did not override the *args* option, it will list information about the `src/main.ts` file (the default value provided for the target). +This will copy the file to `package-other.json` instead of `package-copy.json`. +Because we did not override the *source* option, it will copy from the `package.json` file (the default value provided for the target). ## Testing a builder @@ -421,10 +421,10 @@ Use integration testing for your builder, so that you can use the Architect sche * 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, 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 `node --print 'foo'` command, then validates that the `logger` contains an entry for `foo`. +Here’s an example of a test that runs the copy file builder. +The test uses the builder to copy the `package.json` file and validates that the copied file's contents are the same as the source.