Skip to content

Commit

Permalink
feat(builder): implement run-commands builder
Browse files Browse the repository at this point in the history
  • Loading branch information
vsavkin committed Jan 15, 2019
1 parent ffd9d93 commit 8f3226d
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 2 deletions.
5 changes: 5 additions & 0 deletions packages/builders/src/builders.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"class": "./cypress/cypress.builder",
"schema": "./cypress/schema.json",
"description": "Run Cypress e2e tests"
},
"command-runner": {
"class": "./command-runner/command-runner.builder",
"schema": "./command-runner/schema.json",
"description": "Run a command in a child process"
}
}
}
141 changes: 141 additions & 0 deletions packages/builders/src/run-commands/run-commands.builder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { normalize } from '@angular-devkit/core';
import * as path from 'path';
import RunCommandsBuilder from './run-commands.builder';
import { fileSync } from 'tmp';
import { readFileSync } from 'fs';

describe('Command Runner Builder', () => {
let builder: RunCommandsBuilder;

beforeEach(() => {
builder = new RunCommandsBuilder();
});

it('should error when no commands are given', async () => {
const root = normalize('/root');
try {
const result = await builder
.run({
root,
builder: '@nrwl/run-commands',
projectType: 'application',
options: {} as any
})
.toPromise();
fail('should throw');
} catch (e) {
expect(e).toEqual(
`ERROR: Bad builder config for @nrwl/run-command - "commands" option is required`
);
}
});

it('should error when no command is given', async () => {
const root = normalize('/root');
try {
const result = await builder
.run({
root,
builder: '@nrwl/run-commands',
projectType: 'application',
options: {
commands: [{}] as any
}
})
.toPromise();
fail('should throw');
} catch (e) {
expect(e).toEqual(
`ERROR: Bad builder config for @nrwl/run-command - "command" option is required`
);
}
});

it('should run commands serially', async () => {
const root = normalize('/root');
const f = fileSync().name;
const result = await builder
.run({
root,
builder: '@nrwl/run-commands',
projectType: 'application',
options: {
commands: [
{
command: `sleep 0.2 && echo 1 >> ${f}`
},
{
command: `sleep 0.1 && echo 2 >> ${f}`
}
]
}
})
.toPromise();

expect(result).toEqual({ success: true });
expect(
readFileSync(f)
.toString()
.replace(/\s/g, '')
).toEqual('12');
});

it('should run commands in parallel', async () => {
const root = normalize('/root');
const f = fileSync().name;
const result = await builder
.run({
root,
builder: '@nrwl/run-commands',
projectType: 'application',
options: {
commands: [
{
command: `sleep 0.2 && echo 1 >> ${f}`
},
{
command: `sleep 0.1 && echo 2 >> ${f}`
}
],
parallel: true
}
})
.toPromise();

expect(result).toEqual({ success: true });
expect(
readFileSync(f)
.toString()
.replace(/\s/g, '')
).toEqual('21');
});

it('should stop execution when a command fails', async () => {
const root = normalize('/root');
const f = fileSync().name;
const result = await builder
.run({
root,
builder: '@nrwl/run-commands',
projectType: 'application',
options: {
commands: [
{
command: `echo 1 >> ${f} && exit 1`
},
{
command: `echo 2 >> ${f}`
}
]
}
})
.toPromise();

expect(result).toEqual({ success: false });
expect(
readFileSync(f)
.toString()
.replace(/\s/g, '')
).toEqual('1');
});
});
108 changes: 108 additions & 0 deletions packages/builders/src/run-commands/run-commands.builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
Builder,
BuilderConfiguration,
BuildEvent
} from '@angular-devkit/architect';

import { Observable } from 'rxjs';
import { exec } from 'child_process';

export interface RunCommandsBuilderOptions {
commands: { command: string }[];
parallel?: boolean;
}

export default class RunCommandsBuilder
implements Builder<RunCommandsBuilderOptions> {
run(
config: BuilderConfiguration<RunCommandsBuilderOptions>
): Observable<BuildEvent> {
return Observable.create(async observer => {
if (!config || !config.options || !config.options.commands) {
observer.error(
'ERROR: Bad builder config for @nrwl/run-command - "commands" option is required'
);
return;
}

if (config.options.commands.some(c => !c.command)) {
observer.error(
'ERROR: Bad builder config for @nrwl/run-command - "command" option is required'
);
return;
}

try {
const success = config.options.parallel
? await this.runInParallel(config)
: await this.runSerially(config);
observer.next({ success });
observer.complete();
} catch (e) {
observer.error(
`ERROR: Something went wrong in @nrwl/run-command - ${e.message}`
);
}
});
}

private async runInParallel(
config: BuilderConfiguration<RunCommandsBuilderOptions>
) {
const r = await Promise.all(
config.options.commands.map(c =>
this.createProcess(c.command).then(result => ({
result,
command: c.command
}))
)
);
const failed = r.filter(v => !v.result);
if (failed.length > 0) {
failed.forEach(f => {
process.stderr.write(
`Warning: @nrwl/run-command command "${
f.command
}" exited with non-zero status code`
);
});
return false;
} else {
return true;
}
}

private async runSerially(
config: BuilderConfiguration<RunCommandsBuilderOptions>
) {
const failedCommand = await config.options.commands.reduce<
Promise<string | null>
>(async (m, c) => {
if ((await m) === null) {
const success = await this.createProcess(c.command);
return !success ? c.command : null;
} else {
return m;
}
}, Promise.resolve(null));

if (failedCommand) {
process.stderr.write(
`Warning: @nrwl/run-command command "${failedCommand}" exited with non-zero status code`
);
return false;
}
return true;
}

private createProcess(command: string): Promise<boolean> {
return new Promise(res => {
const childProcess = exec(command, {});
childProcess.stdout.on('data', data => process.stdout.write(data));
childProcess.stderr.on('data', err => process.stderr.write(err));
childProcess.on('close', code => {
res(code === 0);
});
});
}
}
27 changes: 27 additions & 0 deletions packages/builders/src/run-commands/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"title": "Run Commands",
"description": "Run Commands",
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command to run in child process"
}
},
"additionalProperties": false,
"required": ["command"]
}
},
"parallel": {
"type": "boolean",
"description": "Run commands in parallel",
"default": false
}
},
"required": ["command"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

module.exports = (on, config) => {
module.exports = (on: any, config: any) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};
3 changes: 2 additions & 1 deletion scripts/build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/bash
#!/usr/bin/env bash

npx ng-packagr -p packages/nx/ng-package.json

rm -rf build
Expand Down

0 comments on commit 8f3226d

Please sign in to comment.