feat(misc): move run-commands executor to nx
This commit is contained in:
parent
a68120a35b
commit
e6738abc9f
@ -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"`.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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": {}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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": [
|
||||
{
|
||||
|
||||
@ -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`,
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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".`
|
||||
);
|
||||
}
|
||||
});
|
||||
328
packages/nx/src/executors/run-commands/run-commands.impl.ts
Normal file
328
packages/nx/src/executors/run-commands/run-commands.impl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
142
packages/nx/src/executors/run-commands/schema.json
Normal file
142
packages/nx/src/executors/run-commands/schema.json
Normal 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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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())
|
||||
: [],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user