feat(misc): move run-commands executor to nx

This commit is contained in:
Victor Savkin 2022-05-03 13:52:06 -04:00
parent a68120a35b
commit e6738abc9f
23 changed files with 563 additions and 333 deletions

View File

@ -77,6 +77,9 @@ sure that `mylib`'s dependencies are built as well. This doesn't mean Nx is goin
artifacts are already in the right place, Nx will do nothing. If they aren't in the right place, but they are available
in the cache, Nx will retrieve them from the cache.
Depending on another target of the same project is very common. That's why we provide some syntax sugar, so
`"dependsOn": [{"target": "build", "projects": "self"}]` can be shortened to `"dependsOn": ["build"]`.
Another common scenario is for a target to depend on another target of the same project. For instance, `dependsOn` of
the `test` target tells Nx that before it can test `mylib` it needs to make sure that `mylib` is built, which will
result in `mylib`'s dependencies being built as well.
@ -97,7 +100,8 @@ You can annotate your projects with `tags` as follows:
}
```
You can [configure lint rules using these tags](/structure/monorepo-tags) to, for instance, ensure that libraries belonging to `myteam` are not depended on by libraries belong to `theirteam`.
You can [configure lint rules using these tags](/structure/monorepo-tags) to, for instance, ensure that libraries
belonging to `myteam` are not depended on by libraries belong to `theirteam`.
### implicitDependencies
@ -140,7 +144,9 @@ project, add the following to its `package.json`:
### workspace json
The `workspace.json` file in the root directory is optional. It's used if you want to list the projects in your workspace explicitly instead of Nx scanning the file tree for all `project.json` and `package.json` files that match the globs specified in the `workspaces` property of the root `package.json`.
The `workspace.json` file in the root directory is optional. It's used if you want to list the projects in your
workspace explicitly instead of Nx scanning the file tree for all `project.json` and `package.json` files that match the
globs specified in the `workspaces` property of the root `package.json`.
```json
{
@ -324,7 +330,8 @@ pass `--buildable=true` when creating new libraries.
> A task is an invocation of a target.
Tasks runners are invoked when you run `nx test`, `nx build`, `nx run-many`, `nx affected`, and so on. The tasks runner
named "default" is used by default. Specify a different one like this `nx run-many --target=build --all --runner=another`.
named "default" is used by default. Specify a different one like
this `nx run-many --target=build --all --runner=another`.
Tasks runners can accept different options. The following are the options supported
by `"nx/tasks-runners/default"` and `"@nrwl/nx-cloud"`.

View File

@ -177,6 +177,9 @@ sure that `mylib`'s dependencies are built as well. This doesn't mean Nx is goin
artifacts are already in the right place, Nx will do nothing. If they aren't in the right place, but they are available
in the cache, Nx will retrieve them from the cache.
Depending on another target of the same project is very common. That's why we provide some syntax sugar, so
`"dependsOn": [{"target": "build", "projects": "self"}]` can be shortened to `"dependsOn": ["build"]`.
Another common scenario is for a target to depend on another target of the same project. For instance, `dependsOn` of
the `test` target tells Nx that before it can test `mylib` it needs to make sure that `mylib` is built, which will
result in `mylib`'s dependencies being built as well.

View File

@ -27,14 +27,14 @@ Create a `project.json` file for your Go CLI.
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "go build -o='../../dist/packages/cli/' ./src/ascii.go",
"cwd": "packages/cli"
}
},
"serve": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "go run ./src/ascii.go",
"cwd": "packages/cli"
@ -68,7 +68,7 @@ All of these reasons are matters of preference. After this tutorial, you should
- `root`, `sourceRoot` and `application` are properties that help Nx know more about your project.
- `targets` is similar to the `scripts` property in `package.json`.
- Just as in `package.json`, `build` and `serve` can be any string you pick.
- The `executor` is the code that runs the target. In this case, [`@nrwl/workspace:run-commands`](https://nx.dev/workspace/run-commands-executor) launches a terminal process to execute whatever command you pass in.
- The `executor` is the code that runs the target. In this case, [`run-commands`](https://nx.dev/packages/nx/executors/run-commands) launches a terminal process to execute whatever command you pass in.
- `options` contains whatever configuration properties the executor needs to run.
## Create the CLI

View File

@ -139,14 +139,14 @@ For the cli project, you add the implicit dependencies in the `project.json` fil
"implicitDependencies": ["ascii"],
"targets": {
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "go build -o='../../dist/packages/cli/' ./src/ascii.go",
"cwd": "packages/cli"
}
},
"serve": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "go run ./src/ascii.go",
"cwd": "packages/cli"

View File

@ -45,7 +45,7 @@ for workspace-specific settings (like the [Nx Cloud token](/using-nx/caching#dis
If you want to load variables from `env` files other than the ones listed above:
1. Use the [env-cmd](https://www.npmjs.com/package/env-cmd) package: `env-cmd -f .qa.env nx serve`
2. Use the `envFile` option of the [run-commands](/workspace/run-commands-executor#envfile) builder and execute your command inside of the builder
2. Use the `envFile` option of the [run-commands](/packages/nx/executors/run-commands#envfile) builder and execute your command inside of the builder
### Ad-hoc variables

View File

@ -261,7 +261,7 @@ dist/apps
Now, we can add a simple deploy command to simulate deploying this folder to production.
```bash
nx g @nrwl/workspace:run-commands \
nx g nx:run-commands \
deploy \
--project=shell \
--command="rm -rf production && mkdir production && cp -r dist/apps/shell/* production && cp -r dist/apps/{shop,cart,about} production && http-server -p 3000 -a localhost production"

View File

@ -168,16 +168,11 @@ Generating a library with `--publishable` flag does several things extra on top
"targets": {
"build": {},
"publish": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs publish-me {args.ver} {args.tag}"
},
"dependsOn": [
{
"projects": "self",
"target": "build"
}
]
"dependsOn": ["build"]
},
"lint": {},
"test": {}
@ -186,7 +181,7 @@ Generating a library with `--publishable` flag does several things extra on top
}
```
The `publish` target invokes the generated `publish.mjs` script using [`@nrwl/workspace:run-commands`](/executors/run-commands-builder) executor. The script does the following:
The `publish` target invokes the generated `publish.mjs` script using [`nx:run-commands`](/executors/run-commands-builder) executor. The script does the following:
- Validate the `ver` argument against a simple [SemVer](https://semver.org/) RegExp.
- Validate the `name` of the project (eg: `publish-me`) against the workspace existing projects.
@ -214,16 +209,11 @@ Let's set up our `hello-tsc` library to be publishable as well but this time, we
"targets": {
"build": {},
"publish": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs hello-tsc {args.ver} {args.tag}",
},
"dependsOn": [
{
"projects": "self",
"target": "build"
}
]
"dependsOn": ["build"]
},
"lint": {},
"test": {}

View File

@ -464,7 +464,7 @@ Next, you need to configure your project to build the theme when you build the l
"defaultConfiguration": "production"
},
"build-lib": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": ["dist/libs/lib1"],
"configurations": {
"production": {
@ -483,7 +483,7 @@ Next, you need to configure your project to build the theme when you build the l
"defaultConfiguration": "production"
},
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": ["dist/libs/lib1"],
"configurations": {
"production": {

View File

@ -172,7 +172,7 @@ You should consider implementing them as Nx tasks which should be a quick transi
Your use-case may also be covered by one of our community plugins. Plugin authors are able to extend the functionality of Nx through our plugin API.
[Learn more about the `run-commands` builder](/workspace/run-commands-executor)
[Learn more about the `run-commands` builder](/packages/nx/executors/run-commands)
[Learn more about caching](/using-nx/caching)

View File

@ -305,7 +305,7 @@ You need to point your `build` and `serve` tasks at this gulp build process. Typ
```json
...
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
{
@ -315,7 +315,7 @@ You need to point your `build` and `serve` tasks at this gulp build process. Typ
}
},
"serve": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
{

View File

@ -235,7 +235,7 @@ export default async function (tree: Tree, options: PresetGeneratorSchema) {
sourceRoot: `${normalizedOptions.projectRoot}/src`,
targets: {
exec: {
executor: '@nrwl/workspace:run-commands',
executor: 'nx:run-commands',
options: {
command: `node ${projectRoot}/src/index.js`,
},

View File

@ -27,7 +27,7 @@ For each project for which you want to enable `make`, add a target in its `proje
// ...
"targets": {
"make": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
{
@ -40,7 +40,7 @@ For each project for which you want to enable `make`, add a target in its `proje
}
```
For more information, see the [run-commands api doc](/workspace/run-commands-executor).
For more information, see the [run-commands api doc](/packages/nx/executors/run-commands).
##### 3. Trigger the executor from the terminal

View File

@ -43,8 +43,8 @@ Like when moving projects, some steps are often missed when removing projects. T
## Running custom commands
Executors provide an optimized way of running targets but unfortunately, not every target has an executor written for it. The [`@nrwl/workspace:run-commands`](/workspace/run-commands-executor) executor is an executor that runs any command or multiple commands in the shell. This can be useful when integrating with other tools which do not have an executor provided. There is also a generator to help configure this executor.
Executors provide an optimized way of running targets but unfortunately, not every target has an executor written for it. The [`nx:run-commands`](/packages/nx/executors/run-commands) executor is an executor that runs any command or multiple commands in the shell. This can be useful when integrating with other tools which do not have an executor provided. There is also a generator to help configure this executor.
Running `nx g @nrwl/workspace:run-commands printhello --project my-feature-lib --command 'echo hello'` will create a `my-feature-lib:printhello` target that executes `echo hello` in the shell.
Running `nx g nx:run-commands printhello --project my-feature-lib --command 'echo hello'` will create a `my-feature-lib:printhello` target that executes `echo hello` in the shell.
> See more about [`@nrwl/workspace:run-commands`](/workspace/run-commands-executor)
> See more about [`nx:run-commands`](/packages/nx/executors/run-commands)

View File

@ -128,7 +128,7 @@ describe('Run Commands', () => {
fail('Should error if process errors');
} catch (e) {
expect(e.stderr.toString()).toContain(
'Something went wrong in @nrwl/run-commands - Command failed: exit 1'
'Something went wrong in run-commands - Command failed: exit 1'
);
}
});

View File

@ -127,7 +127,14 @@ describe('run-one', () => {
it('should be able to include deps using target dependencies', () => {
const originalWorkspace = readProjectConfig(myapp);
updateProjectConfig(myapp, (config) => {
config.targets.prep = {
executor: "nx:run-commands",
options: {
command: "echo PREP"
}
};
config.targets.build.dependsOn = [
"prep",
{
target: 'build',
projects: 'dependencies',
@ -138,11 +145,12 @@ describe('run-one', () => {
const output = runCLI(`build ${myapp}`);
expect(output).toContain(
`NX Running target build for project ${myapp} and 2 task(s) it depends on`
`NX Running target build for project ${myapp} and 3 task(s) it depends on`
);
expect(output).toContain(myapp);
expect(output).toContain(mylib1);
expect(output).toContain(mylib2);
expect(output).toContain("PREP");
updateProjectConfig(myapp, () => originalWorkspace);
}, 10000);

View File

@ -4,7 +4,7 @@ import { dirSync, fileSync } from 'tmp';
import runCommands, { LARGE_BUFFER } from './run-commands.impl';
import { env } from 'npm-run-path';
const {
devDependencies: { '@nrwl/workspace': version },
devDependencies: { nx: version },
} = require('package.json');
function normalize(p: string) {
@ -14,7 +14,7 @@ function readFile(f: string) {
return readFileSync(f).toString().replace(/\s/g, '');
}
describe('Command Runner Builder', () => {
describe('Run Commands', () => {
const context = {} as any;
beforeEach(() => {
@ -258,7 +258,7 @@ describe('Command Runner Builder', () => {
fail('should throw');
} catch (e) {
expect(e.message).toEqual(
`ERROR: Bad executor config for @nrwl/run-commands - "readyWhen" can only be used when "parallel=true".`
`ERROR: Bad executor config for run-commands - "readyWhen" can only be used when "parallel=true".`
);
}
});

View File

@ -0,0 +1,328 @@
import { exec, execSync } from 'child_process';
import * as path from 'path';
import * as yargsParser from 'yargs-parser';
import { env as appendLocalEnv } from 'npm-run-path';
import { ExecutorContext } from '../../config/misc-interfaces';
import * as chalk from 'chalk';
export const LARGE_BUFFER = 1024 * 1000000;
async function loadEnvVars(path?: string) {
if (path) {
const result = (await import('dotenv')).config({ path });
if (result.error) {
throw result.error;
}
} else {
try {
(await import('dotenv')).config();
} catch {}
}
}
export type Json = { [k: string]: any };
export interface RunCommandsOptions extends Json {
command?: string;
commands?: (
| {
command: string;
forwardAllArgs?: boolean;
/**
* description was added to allow users to document their commands inline,
* it is not intended to be used as part of the execution of the command.
*/
description?: string;
prefix?: string;
color?: string;
bgColor?: string;
}
| string
)[];
color?: boolean;
parallel?: boolean;
readyWhen?: string;
cwd?: string;
args?: string;
envFile?: string;
outputPath?: string;
}
const propKeys = [
'command',
'commands',
'color',
'parallel',
'readyWhen',
'cwd',
'args',
'envFile',
'outputPath',
];
export interface NormalizedRunCommandsOptions extends RunCommandsOptions {
commands: {
command: string;
forwardAllArgs?: boolean;
}[];
parsedArgs: { [k: string]: any };
}
export default async function (
options: RunCommandsOptions,
context: ExecutorContext
): Promise<{ success: boolean }> {
await loadEnvVars(options.envFile);
const normalized = normalizeOptions(options);
if (options.readyWhen && !options.parallel) {
throw new Error(
'ERROR: Bad executor config for run-commands - "readyWhen" can only be used when "parallel=true".'
);
}
if (
options.commands.find((c: any) => c.prefix || c.color || c.bgColor) &&
!options.parallel
) {
throw new Error(
'ERROR: Bad executor config for run-commands - "prefix", "color" and "bgColor" can only be set when "parallel=true".'
);
}
try {
const success = options.parallel
? await runInParallel(normalized, context)
: await runSerially(normalized, context);
return { success };
} catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.error(e);
}
throw new Error(
`ERROR: Something went wrong in run-commands - ${e.message}`
);
}
}
async function runInParallel(
options: NormalizedRunCommandsOptions,
context: ExecutorContext
) {
const procs = options.commands.map((c) =>
createProcess(
c,
options.readyWhen,
options.color,
calculateCwd(options.cwd, context)
).then((result) => ({
result,
command: c.command,
}))
);
if (options.readyWhen) {
const r = await Promise.race(procs);
if (!r.result) {
process.stderr.write(
`Warning: run-commands command "${r.command}" exited with non-zero status code`
);
return false;
} else {
return true;
}
} else {
const r = await Promise.all(procs);
const failed = r.filter((v) => !v.result);
if (failed.length > 0) {
failed.forEach((f) => {
process.stderr.write(
`Warning: run-commands command "${f.command}" exited with non-zero status code`
);
});
return false;
} else {
return true;
}
}
}
function normalizeOptions(
options: RunCommandsOptions
): NormalizedRunCommandsOptions {
options.parsedArgs = parseArgs(options);
if (options.command) {
options.commands = [{ command: options.command }];
options.parallel = !!options.readyWhen;
} else {
options.commands = options.commands.map((c) =>
typeof c === 'string' ? { command: c } : c
);
}
(options as NormalizedRunCommandsOptions).commands.forEach((c) => {
c.command = transformCommand(
c.command,
(options as NormalizedRunCommandsOptions).parsedArgs,
c.forwardAllArgs ?? true
);
});
return options as any;
}
async function runSerially(
options: NormalizedRunCommandsOptions,
context: ExecutorContext
) {
for (const c of options.commands) {
createSyncProcess(
c.command,
options.color,
calculateCwd(options.cwd, context)
);
}
return true;
}
function createProcess(
commandConfig: {
command: string;
color?: string;
bgColor?: string;
prefix?: string;
},
readyWhen: string,
color: boolean,
cwd: string
): Promise<boolean> {
return new Promise((res) => {
const childProcess = exec(commandConfig.command, {
maxBuffer: LARGE_BUFFER,
env: processEnv(color),
cwd,
});
/**
* Ensure the child process is killed when the parent exits
*/
const processExitListener = () => childProcess.kill();
process.on('exit', processExitListener);
process.on('SIGTERM', processExitListener);
childProcess.stdout.on('data', (data) => {
process.stdout.write(addColorAndPrefix(data, commandConfig));
if (readyWhen && data.toString().indexOf(readyWhen) > -1) {
res(true);
}
});
childProcess.stderr.on('data', (err) => {
process.stderr.write(addColorAndPrefix(err, commandConfig));
if (readyWhen && err.toString().indexOf(readyWhen) > -1) {
res(true);
}
});
childProcess.on('exit', (code) => {
if (!readyWhen) {
res(code === 0);
}
});
});
}
function addColorAndPrefix(
out: string,
config: {
prefix?: string;
color?: string;
bgColor?: string;
}
) {
if (config.prefix) {
out = out
.split('\n')
.map((l) =>
l.trim().length > 0 ? `${chalk.bold(config.prefix)} ${l}` : l
)
.join('\n');
}
if (config.color && chalk[config.color]) {
out = chalk[config.color](out);
}
if (config.bgColor && chalk[config.bgColor]) {
out = chalk[config.bgColor](out);
}
return out;
}
function createSyncProcess(command: string, color: boolean, cwd: string) {
execSync(command, {
env: processEnv(color),
stdio: ['inherit', 'inherit', 'inherit'],
maxBuffer: LARGE_BUFFER,
cwd,
});
}
function calculateCwd(
cwd: string | undefined,
context: ExecutorContext
): string {
if (!cwd) return context.root;
if (path.isAbsolute(cwd)) return cwd;
return path.join(context.root, cwd);
}
function processEnv(color: boolean) {
const env = {
...process.env,
...appendLocalEnv(),
};
if (color) {
env.FORCE_COLOR = `${color}`;
}
return env;
}
function transformCommand(
command: string,
args: { [key: string]: string },
forwardAllArgs: boolean
) {
if (command.indexOf('{args.') > -1) {
const regex = /{args\.([^}]+)}/g;
return command.replace(regex, (_, group: string) => args[camelCase(group)]);
} else if (Object.keys(args).length > 0 && forwardAllArgs) {
const stringifiedArgs = Object.keys(args)
.map((a) =>
typeof args[a] === 'string' && args[a].includes(' ')
? `--${a}="${args[a].replace(/"/g, '"')}"`
: `--${a}=${args[a]}`
)
.join(' ');
return `${command} ${stringifiedArgs}`;
} else {
return command;
}
}
function parseArgs(options: RunCommandsOptions) {
const args = options.args;
if (!args) {
const unknownOptionsTreatedAsArgs = Object.keys(options)
.filter((p) => propKeys.indexOf(p) === -1)
.reduce((m, c) => ((m[c] = options[c]), m), {});
return unknownOptionsTreatedAsArgs;
}
return yargsParser(args.replace(/(^"|"$)/g, ''), {
configuration: { 'camel-case-expansion': true },
});
}
function camelCase(input) {
if (input.indexOf('-') > 1) {
return input
.toLowerCase()
.replace(/-(.)/g, (match, group1) => group1.toUpperCase());
} else {
return input;
}
}

View File

@ -0,0 +1,142 @@
{
"title": "Run Commands",
"description": "Run any custom commands with Nx.",
"type": "object",
"cli": "nx",
"outputCapture": "pipe",
"presets": [
{
"name": "Arguments forwarding",
"keys": [
"commands"
]
},
{
"name": "Custom done conditions",
"keys": [
"commands",
"readyWhen"
]
},
{
"name": "Setting the cwd",
"keys": [
"commands",
"cwd"
]
}
],
"properties": {
"commands": {
"type": "array",
"description": "Commands to run in child process.",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command to run in child process."
},
"forwardAllArgs": {
"type": "boolean",
"description": "Whether arguments should be forwarded when interpolation is not present."
},
"prefix": {
"type": "string",
"description": "Prefix in front of every line out of the output"
},
"color": {
"type": "string",
"description": "Color of the output",
"enum": [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white"
]
},
"bgColor": {
"type": "string",
"description": "Background color of the output",
"enum": [
"bgBlack",
"bgRed",
"bgGreen",
"bgYellow",
"bgBlue",
"bgMagenta",
"bgCyan",
"bgWhite"
]
},
"description": {
"type": "string",
"description": "An optional description useful for inline documentation purposes. It is not used as part of the execution of the command."
}
},
"additionalProperties": false,
"required": [
"command"
]
},
{
"type": "string"
}
]
}
},
"command": {
"type": "string",
"description": "Command to run in child process."
},
"parallel": {
"type": "boolean",
"description": "Run commands in parallel.",
"default": true
},
"readyWhen": {
"type": "string",
"description": "String to appear in `stdout` or `stderr` that indicates that the task is done. When running multiple commands, this option can only be used when `parallel` is set to `true`. If not specified, the task is done when all the child processes complete."
},
"args": {
"type": "string",
"description": "Extra arguments. You can pass them as follows: nx run project:target --args='--wait=100'. You can then use {args.wait} syntax to interpolate them in the workspace config file. See example [above](#chaining-commands-interpolating-args-and-setting-the-cwd)"
},
"envFile": {
"type": "string",
"description": "You may specify a custom .env file path."
},
"color": {
"type": "boolean",
"description": "Use colors when showing output of command.",
"default": false
},
"outputPath": {
"description": "Allows you to specify where the build artifacts are stored. This allows Nx Cloud to pick them up correctly, in the case that the build artifacts are placed somewhere other than the top level dist folder.",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"cwd": {
"type": "string",
"description": "Current working directory of the commands. If it's not specified the commands will run in the workspace root, if a relative path is specified the commands will run in that path relative to the workspace root and if it's an absolute path the commands will run in that path."
}
},
"additionalProperties": true,
"required": [],
"examplesFile": "../../../docs/run-commands-examples.md"
}

View File

@ -6,7 +6,7 @@
"targets": {
//...
"ls-project-root": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "ls apps/frontend/src"
}
@ -31,7 +31,7 @@ You can run them sequentially by setting `parallel: false`:
```json
"create-script": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"mkdir -p scripts",
@ -70,7 +70,7 @@ nx run frontend:webpack --args="--config=example.config.js"
```json
"webpack": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "webpack"
}
@ -84,7 +84,7 @@ that sets the `forwardAllArgs` option to `false` as shown below:
```json
"webpack": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
{
@ -102,7 +102,7 @@ Normally, `run-commands` considers the commands done when all of them have finis
```json
"finish-when-ready": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"sleep 5 && echo 'FINISHED'",
@ -136,7 +136,7 @@ nx affected --target=generate-docs
"targets": {
//...
"generate-docs": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "npx compodoc -p apps/frontend/tsconfig.app.json"
}
@ -147,7 +147,7 @@ nx affected --target=generate-docs
"targets": {
//...
"generate-docs": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"command": "npx compodoc -p apps/api/tsconfig.app.json"
}

View File

@ -1,285 +1,5 @@
import { ExecutorContext } from '@nrwl/devkit';
import { exec, execSync } from 'child_process';
import * as path from 'path';
import * as yargsParser from 'yargs-parser';
import { env as appendLocalEnv } from 'npm-run-path';
import runCommands from 'nx/src/executors/run-commands/run-commands.impl';
export const LARGE_BUFFER = 1024 * 1000000;
export { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl';
async function loadEnvVars(path?: string) {
if (path) {
const result = (await import('dotenv')).config({ path });
if (result.error) {
throw result.error;
}
} else {
try {
(await import('dotenv')).config();
} catch {}
}
}
export type Json = { [k: string]: any };
export interface RunCommandsBuilderOptions extends Json {
command?: string;
commands?: (
| {
command: string;
forwardAllArgs?: boolean;
/**
* description was added to allow users to document their commands inline,
* it is not intended to be used as part of the execution of the command.
*/
description?: string;
}
| string
)[];
color?: boolean;
parallel?: boolean;
readyWhen?: string;
cwd?: string;
args?: string;
envFile?: string;
outputPath?: string;
}
const propKeys = [
'command',
'commands',
'color',
'parallel',
'readyWhen',
'cwd',
'args',
'envFile',
'outputPath',
];
export interface NormalizedRunCommandsBuilderOptions
extends RunCommandsBuilderOptions {
commands: {
command: string;
forwardAllArgs?: boolean;
}[];
parsedArgs: { [k: string]: any };
}
export default async function (
options: RunCommandsBuilderOptions,
context: ExecutorContext
): Promise<{ success: boolean }> {
await loadEnvVars(options.envFile);
const normalized = normalizeOptions(options);
if (options.readyWhen && !options.parallel) {
throw new Error(
'ERROR: Bad executor config for @nrwl/run-commands - "readyWhen" can only be used when "parallel=true".'
);
}
try {
const success = options.parallel
? await runInParallel(normalized, context)
: await runSerially(normalized, context);
return { success };
} catch (e) {
if (process.env.NX_VERBOSE_LOGGING === 'true') {
console.error(e);
}
throw new Error(
`ERROR: Something went wrong in @nrwl/run-commands - ${e.message}`
);
}
}
async function runInParallel(
options: NormalizedRunCommandsBuilderOptions,
context: ExecutorContext
) {
const procs = options.commands.map((c) =>
createProcess(
c.command,
options.readyWhen,
options.color,
calculateCwd(options.cwd, context)
).then((result) => ({
result,
command: c.command,
}))
);
if (options.readyWhen) {
const r = await Promise.race(procs);
if (!r.result) {
process.stderr.write(
`Warning: @nrwl/run-commands command "${r.command}" exited with non-zero status code`
);
return false;
} else {
return true;
}
} else {
const r = await Promise.all(procs);
const failed = r.filter((v) => !v.result);
if (failed.length > 0) {
failed.forEach((f) => {
process.stderr.write(
`Warning: @nrwl/run-commands command "${f.command}" exited with non-zero status code`
);
});
return false;
} else {
return true;
}
}
}
function normalizeOptions(
options: RunCommandsBuilderOptions
): NormalizedRunCommandsBuilderOptions {
options.parsedArgs = parseArgs(options);
if (options.command) {
options.commands = [{ command: options.command }];
options.parallel = !!options.readyWhen;
} else {
options.commands = options.commands.map((c) =>
typeof c === 'string' ? { command: c } : c
);
}
(options as NormalizedRunCommandsBuilderOptions).commands.forEach((c) => {
c.command = transformCommand(
c.command,
(options as NormalizedRunCommandsBuilderOptions).parsedArgs,
c.forwardAllArgs ?? true
);
});
return options as any;
}
async function runSerially(
options: NormalizedRunCommandsBuilderOptions,
context: ExecutorContext
) {
for (const c of options.commands) {
createSyncProcess(
c.command,
options.color,
calculateCwd(options.cwd, context)
);
}
return true;
}
function createProcess(
command: string,
readyWhen: string,
color: boolean,
cwd: string
): Promise<boolean> {
return new Promise((res) => {
const childProcess = exec(command, {
maxBuffer: LARGE_BUFFER,
env: processEnv(color),
cwd,
});
/**
* Ensure the child process is killed when the parent exits
*/
const processExitListener = () => childProcess.kill();
process.on('exit', processExitListener);
process.on('SIGTERM', processExitListener);
childProcess.stdout.on('data', (data) => {
process.stdout.write(data);
if (readyWhen && data.toString().indexOf(readyWhen) > -1) {
res(true);
}
});
childProcess.stderr.on('data', (err) => {
process.stderr.write(err);
if (readyWhen && err.toString().indexOf(readyWhen) > -1) {
res(true);
}
});
childProcess.on('exit', (code) => {
if (!readyWhen) {
res(code === 0);
}
});
});
}
function createSyncProcess(command: string, color: boolean, cwd: string) {
execSync(command, {
env: processEnv(color),
stdio: ['inherit', 'inherit', 'inherit'],
maxBuffer: LARGE_BUFFER,
cwd,
});
}
function calculateCwd(
cwd: string | undefined,
context: ExecutorContext
): string {
if (!cwd) return context.root;
if (path.isAbsolute(cwd)) return cwd;
return path.join(context.root, cwd);
}
function processEnv(color: boolean) {
const env = {
...process.env,
...appendLocalEnv(),
};
if (color) {
env.FORCE_COLOR = `${color}`;
}
return env;
}
function transformCommand(
command: string,
args: { [key: string]: string },
forwardAllArgs: boolean
) {
if (command.indexOf('{args.') > -1) {
const regex = /{args\.([^}]+)}/g;
return command.replace(regex, (_, group: string) => args[camelCase(group)]);
} else if (Object.keys(args).length > 0 && forwardAllArgs) {
const stringifiedArgs = Object.keys(args)
.map((a) =>
typeof args[a] === 'string' && args[a].includes(' ')
? `--${a}="${args[a].replace(/"/g, '"')}"`
: `--${a}=${args[a]}`
)
.join(' ');
return `${command} ${stringifiedArgs}`;
} else {
return command;
}
}
function parseArgs(options: RunCommandsBuilderOptions) {
const args = options.args;
if (!args) {
const unknownOptionsTreatedAsArgs = Object.keys(options)
.filter((p) => propKeys.indexOf(p) === -1)
.reduce((m, c) => ((m[c] = options[c]), m), {});
return unknownOptionsTreatedAsArgs;
}
return yargsParser(args.replace(/(^"|"$)/g, ''), {
configuration: { 'camel-case-expansion': true },
});
}
function camelCase(input) {
if (input.indexOf('-') > 1) {
return input
.toLowerCase()
.replace(/-(.)/g, (match, group1) => group1.toUpperCase());
} else {
return input;
}
}
export default runCommands;

View File

@ -35,6 +35,38 @@
"type": "boolean",
"description": "Whether arguments should be forwarded when interpolation is not present."
},
"prefix": {
"type": "string",
"description": "Prefix in front of every line out of the output"
},
"color": {
"type": "string",
"description": "Color of the output",
"enum": [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white"
]
},
"bgColor": {
"type": "string",
"description": "Background color of the output",
"enum": [
"bgBlack",
"bgRed",
"bgGreen",
"bgYellow",
"bgBlue",
"bgMagenta",
"bgCyan",
"bgWhite"
]
},
"description": {
"type": "string",
"description": "An optional description useful for inline documentation purposes. It is not used as part of the execution of the command."

View File

@ -23,7 +23,7 @@ describe('run-commands', () => {
const customTarget = JSON.parse(tree.read('workspace.json').toString())
.projects['lib'].architect['custom'];
expect(customTarget).toEqual({
builder: '@nrwl/workspace:run-commands',
builder: 'nx:run-commands',
outputs: ['/dist/a', '/dist/b', '/dist/c'],
options: {
command: 'echo 1',

View File

@ -11,7 +11,7 @@ export async function runCommandsGenerator(host: Tree, schema: Schema) {
const project = readProjectConfiguration(host, schema.project);
project.targets = project.targets || {};
project.targets[schema.name] = {
executor: '@nrwl/workspace:run-commands',
executor: 'nx:run-commands',
outputs: schema.outputs
? schema.outputs.split(',').map((s) => s.trim())
: [],