From 5c49345c34975fcb6c32c47ec233a28d0154a17f Mon Sep 17 00:00:00 2001 From: Victor Savkin Date: Thu, 9 Jun 2022 14:21:29 -0400 Subject: [PATCH] feat(core): provide a way to handle get unparsed args in the executor --- docs/generated/packages/nx.json | 16 +- docs/generated/packages/workspace.json | 16 +- e2e/cli/src/{cli.test.ts => misc.test.ts} | 80 ------- e2e/cli/src/run.test.ts | 67 ++++++ e2e/workspace-core/src/run-commands.test.ts | 9 +- package.json | 4 +- packages/nx/src/command-line/affected.ts | 14 +- packages/nx/src/command-line/nx-commands.ts | 38 +++- packages/nx/src/command-line/run-many.ts | 4 +- .../run-commands/run-commands.impl.spec.ts | 208 ++++-------------- .../run-commands/run-commands.impl.ts | 24 +- .../nx/src/executors/run-commands/schema.json | 12 +- .../executors/run-script/run-script.impl.ts | 11 +- .../nx/src/executors/run-script/schema.json | 12 +- packages/nx/src/hasher/hasher.ts | 9 +- .../tasks-runner/create-task-graph.spec.ts | 72 ++++-- .../nx/src/tasks-runner/create-task-graph.ts | 4 +- ...mic-run-many-terminal-output-life-cycle.ts | 7 +- ...amic-run-one-terminal-output-life-cycle.ts | 5 +- .../life-cycles/formatting-utils.ts | 9 + ...tic-run-many-terminal-output-life-cycle.ts | 5 +- packages/nx/src/tasks-runner/run-command.ts | 9 +- packages/nx/src/tasks-runner/utils.spec.ts | 71 +----- packages/nx/src/tasks-runner/utils.ts | 43 +--- .../nx/src/utils/command-line-utils.spec.ts | 78 +++---- packages/nx/src/utils/command-line-utils.ts | 97 ++++++-- packages/nx/src/utils/params.spec.ts | 7 +- packages/nx/src/utils/params.ts | 26 ++- packages/workspace/index.ts | 2 - .../src/executors/run-commands/schema.json | 12 +- .../src/executors/run-script/schema.json | 12 +- 31 files changed, 454 insertions(+), 529 deletions(-) rename e2e/cli/src/{cli.test.ts => misc.test.ts} (80%) create mode 100644 e2e/cli/src/run.test.ts create mode 100644 packages/nx/src/tasks-runner/life-cycles/formatting-utils.ts diff --git a/docs/generated/packages/nx.json b/docs/generated/packages/nx.json index ca7f6d896e0a9..ad0e01f74a132 100644 --- a/docs/generated/packages/nx.json +++ b/docs/generated/packages/nx.json @@ -139,10 +139,16 @@ "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." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { "type": "string" }, + "$default": { "$source": "unparsed" } } }, "additionalProperties": true, - "required": [], + "required": ["__unparsed__"], "examplesFile": "`workspace.json`:\n\n```json\n//...\n\"frontend\": {\n \"targets\": {\n //...\n \"ls-project-root\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"ls apps/frontend/src\"\n }\n }\n }\n}\n```\n\n```bash\nnx run frontend:ls-project-root\n```\n\n##### Chaining commands, interpolating args and setting the cwd\n\nLet's say each of our workspace projects has some custom bash scripts in a `scripts` folder.\nWe want a simple way to create empty bash script files for a given project, that have the execute permissions already set.\n\nGiven that Nx knows our workspace structure, we should be able to give it a project and the name of our script, and it should take care of the rest.\n\nThe `commands` option accepts as many commands as you want. By default, they all run in parallel.\nYou can run them sequentially by setting `parallel: false`:\n\n```json\n\"create-script\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n \"mkdir -p scripts\",\n \"touch scripts/{args.name}.sh\",\n \"chmod +x scripts/{args.name}.sh\"\n ],\n \"cwd\": \"apps/frontend\",\n \"parallel\": false\n }\n}\n```\n\nBy setting the `cwd` option, each command will run in the `apps/frontend` folder.\n\nWe run the above with:\n\n```bash\nnx run frontend:create-script --args=\"--name=example\"\n```\n\nor simply with:\n\n```bash\nnx run frontend:create-script --name=example\n```\n\n##### Arguments forwarding\n\nWhen interpolation is not present in the command, all arguments are forwarded to the command by default.\n\nThis is useful when you need to pass raw argument strings to your command.\n\nFor example, when you run:\n\nnx run frontend:webpack --args=\"--config=example.config.js\"\n\n```json\n\"webpack\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"webpack\"\n }\n}\n```\n\nThe above command will execute: `webpack --config=example.config.js`\n\nThis functionality can be disabled by using `commands` and expanding each `command` into an object\nthat sets the `forwardAllArgs` option to `false` as shown below:\n\n```json\n\"webpack\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n {\n \"command\": \"webpack\",\n \"forwardAllArgs\": false\n }\n ]\n }\n}\n```\n\n##### Custom **done** conditions\n\nNormally, `run-commands` considers the commands done when all of them have finished running. If you don't need to wait until they're all done, you can set a special string that considers the commands finished the moment the string appears in `stdout` or `stderr`:\n\n```json\n\"finish-when-ready\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n \"sleep 5 && echo 'FINISHED'\",\n \"echo 'READY'\"\n ],\n \"readyWhen\": \"READY\",\n \"parallel\": true\n }\n}\n```\n\n```bash\nnx run frontend:finish-when-ready\n```\n\nThe above commands will finish immediately, instead of waiting for 5 seconds.\n\n##### Nx Affected\n\nThe true power of `run-commands` comes from the fact that it runs through `nx`, which knows about your project graph. So you can run **custom commands** only for the projects that have been affected by a change.\n\nWe can create some configurations to generate docs, and if run using `nx affected`, it will only generate documentation for the projects that have been changed:\n\n```bash\nnx affected --target=generate-docs\n```\n\n```json\n//...\n\"frontend\": {\n \"targets\": {\n //...\n \"generate-docs\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"npx compodoc -p apps/frontend/tsconfig.app.json\"\n }\n }\n }\n},\n\"api\": {\n \"targets\": {\n //...\n \"generate-docs\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"npx compodoc -p apps/api/tsconfig.app.json\"\n }\n }\n }\n}\n```\n" }, "description": "Run any custom commands with Nx.", @@ -163,10 +169,16 @@ "script": { "type": "string", "description": "An npm script name in the `package.json` file of the project (e.g., `build`)." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { "type": "string" }, + "$default": { "$source": "unparsed" } } }, "additionalProperties": true, - "required": ["script"], + "required": ["script", "__unparsed__"], "examplesFile": "`workspace.json`:\n\n```json\n\"frontend\": {\n \"root\": \"packages/frontend\",\n \"targets\": {\n \"build\": {\n \"executor\": \"nx:run-script\",\n \"options\": {\n \"script\": \"build-my-project\"\n }\n }\n }\n}\n```\n\n```bash\nnx run frontend:build\n```\n\nThe `build` target is going to run `npm run build-my-project` (or `yarn build-my-project`) in the `packages/frontend` directory.\n\n#### Caching Artifacts\n\nBy default, Nx is going to cache `dist/packages/frontend`, `packages/frontend/dist`, `packages/frontend/build`, `packages/frontend/public`. If your npm script writes files to other places, you can override the list of cached outputs as follows:\n\n```json\n\"frontend\": {\n \"root\": \"packages/frontend\",\n \"targets\": {\n \"build\": {\n \"executor\": \"nx:run-script\",\n \"outputs\": [\"packages/frontend/dist\", \"packaged/frontend/docs\"],\n \"options\": {\n \"script\": \"build-my-project\"\n }\n }\n }\n}\n```\n", "presets": [] }, diff --git a/docs/generated/packages/workspace.json b/docs/generated/packages/workspace.json index dbe57ad88ee13..d02b5b8c48226 100644 --- a/docs/generated/packages/workspace.json +++ b/docs/generated/packages/workspace.json @@ -823,10 +823,16 @@ "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." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { "type": "string" }, + "$default": { "$source": "unparsed" } } }, "additionalProperties": true, - "required": [], + "required": ["__unparsed__"], "examplesFile": "`workspace.json`:\n\n```json\n//...\n\"frontend\": {\n \"targets\": {\n //...\n \"ls-project-root\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"ls apps/frontend/src\"\n }\n }\n }\n}\n```\n\n```bash\nnx run frontend:ls-project-root\n```\n\n##### Chaining commands, interpolating args and setting the cwd\n\nLet's say each of our workspace projects has some custom bash scripts in a `scripts` folder.\nWe want a simple way to create empty bash script files for a given project, that have the execute permissions already set.\n\nGiven that Nx knows our workspace structure, we should be able to give it a project and the name of our script, and it should take care of the rest.\n\nThe `commands` option accepts as many commands as you want. By default, they all run in parallel.\nYou can run them sequentially by setting `parallel: false`:\n\n```json\n\"create-script\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n \"mkdir -p scripts\",\n \"touch scripts/{args.name}.sh\",\n \"chmod +x scripts/{args.name}.sh\"\n ],\n \"cwd\": \"apps/frontend\",\n \"parallel\": false\n }\n}\n```\n\nBy setting the `cwd` option, each command will run in the `apps/frontend` folder.\n\nWe run the above with:\n\n```bash\nnx run frontend:create-script --args=\"--name=example\"\n```\n\nor simply with:\n\n```bash\nnx run frontend:create-script --name=example\n```\n\n##### Arguments forwarding\n\nWhen interpolation is not present in the command, all arguments are forwarded to the command by default.\n\nThis is useful when you need to pass raw argument strings to your command.\n\nFor example, when you run:\n\nnx run frontend:webpack --args=\"--config=example.config.js\"\n\n```json\n\"webpack\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"webpack\"\n }\n}\n```\n\nThe above command will execute: `webpack --config=example.config.js`\n\nThis functionality can be disabled by using `commands` and expanding each `command` into an object\nthat sets the `forwardAllArgs` option to `false` as shown below:\n\n```json\n\"webpack\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n {\n \"command\": \"webpack\",\n \"forwardAllArgs\": false\n }\n ]\n }\n}\n```\n\n##### Custom **done** conditions\n\nNormally, `run-commands` considers the commands done when all of them have finished running. If you don't need to wait until they're all done, you can set a special string that considers the commands finished the moment the string appears in `stdout` or `stderr`:\n\n```json\n\"finish-when-ready\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"commands\": [\n \"sleep 5 && echo 'FINISHED'\",\n \"echo 'READY'\"\n ],\n \"readyWhen\": \"READY\",\n \"parallel\": true\n }\n}\n```\n\n```bash\nnx run frontend:finish-when-ready\n```\n\nThe above commands will finish immediately, instead of waiting for 5 seconds.\n\n##### Nx Affected\n\nThe true power of `run-commands` comes from the fact that it runs through `nx`, which knows about your project graph. So you can run **custom commands** only for the projects that have been affected by a change.\n\nWe can create some configurations to generate docs, and if run using `nx affected`, it will only generate documentation for the projects that have been changed:\n\n```bash\nnx affected --target=generate-docs\n```\n\n```json\n//...\n\"frontend\": {\n \"targets\": {\n //...\n \"generate-docs\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"npx compodoc -p apps/frontend/tsconfig.app.json\"\n }\n }\n }\n},\n\"api\": {\n \"targets\": {\n //...\n \"generate-docs\": {\n \"executor\": \"nx:run-commands\",\n \"options\": {\n \"command\": \"npx compodoc -p apps/api/tsconfig.app.json\"\n }\n }\n }\n}\n```\n" }, "description": "Run any custom commands with Nx.", @@ -870,10 +876,16 @@ "script": { "type": "string", "description": "An npm script name in the `package.json` file of the project (e.g., `build`)." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { "type": "string" }, + "$default": { "$source": "unparsed" } } }, "additionalProperties": true, - "required": ["script"], + "required": ["script", "__unparsed__"], "examplesFile": "`workspace.json`:\n\n```json\n\"frontend\": {\n \"root\": \"packages/frontend\",\n \"targets\": {\n \"build\": {\n \"executor\": \"nx:run-script\",\n \"options\": {\n \"script\": \"build-my-project\"\n }\n }\n }\n}\n```\n\n```bash\nnx run frontend:build\n```\n\nThe `build` target is going to run `npm run build-my-project` (or `yarn build-my-project`) in the `packages/frontend` directory.\n\n#### Caching Artifacts\n\nBy default, Nx is going to cache `dist/packages/frontend`, `packages/frontend/dist`, `packages/frontend/build`, `packages/frontend/public`. If your npm script writes files to other places, you can override the list of cached outputs as follows:\n\n```json\n\"frontend\": {\n \"root\": \"packages/frontend\",\n \"targets\": {\n \"build\": {\n \"executor\": \"nx:run-script\",\n \"outputs\": [\"packages/frontend/dist\", \"packaged/frontend/docs\"],\n \"options\": {\n \"script\": \"build-my-project\"\n }\n }\n }\n}\n```\n", "presets": [] }, diff --git a/e2e/cli/src/cli.test.ts b/e2e/cli/src/misc.test.ts similarity index 80% rename from e2e/cli/src/cli.test.ts rename to e2e/cli/src/misc.test.ts index d341db4ff4368..fb7643606af97 100644 --- a/e2e/cli/src/cli.test.ts +++ b/e2e/cli/src/misc.test.ts @@ -1,96 +1,16 @@ import { - getPublishedVersion, isNotWindows, newProject, readFile, readJson, runCLI, - runCLIAsync, runCommand, tmpProjPath, - uniq, updateFile, - updateProjectConfig, } from '@nrwl/e2e/utils'; import { renameSync } from 'fs'; import { packagesWeCareAbout } from 'nx/src/command-line/report'; -describe('Cli', () => { - beforeEach(() => newProject()); - - it('should execute long running tasks', async () => { - const myapp = uniq('myapp'); - runCLI(`generate @nrwl/web:app ${myapp}`); - updateProjectConfig(myapp, (c) => { - c.targets['counter'] = { - executor: '@nrwl/workspace:counter', - options: { - to: 2, - }, - }; - return c; - }); - - const success = runCLI(`counter ${myapp} --result=true`); - expect(success).toContain('0'); - expect(success).toContain('1'); - - expect(() => runCLI(`counter ${myapp} --result=false`)).toThrowError(); - }); - - it('should run npm scripts', async () => { - const mylib = uniq('mylib'); - runCLI(`generate @nrwl/node:lib ${mylib}`); - - updateProjectConfig(mylib, (j) => { - delete j.targets; - return j; - }); - - updateFile( - `libs/${mylib}/package.json`, - JSON.stringify({ - name: 'mylib1', - scripts: { 'echo:dev': `echo ECHOED` }, - }) - ); - - const { stdout } = await runCLIAsync(`echo:dev ${mylib} --a=123`, { - silent: true, - }); - expect(stdout).toMatch(/ECHOED "?--a=123"?/); - }, 1000000); - - it('should show help', async () => { - const myapp = uniq('myapp'); - runCLI(`generate @nrwl/web:app ${myapp}`); - - let mainHelp = runCLI(`--help`); - expect(mainHelp).toContain('Run a target for a project'); - expect(mainHelp).toContain('Run target for affected projects'); - - mainHelp = runCLI(`help`); - expect(mainHelp).toContain('Run a target for a project'); - expect(mainHelp).toContain('Run target for affected projects'); - - const genHelp = runCLI(`g @nrwl/web:app --help`); - expect(genHelp).toContain( - 'Find more information and examples at: https://nx.dev/packages/web/generators/application' - ); - - const buildHelp = runCLI(`build ${myapp} --help`); - expect(buildHelp).toContain( - 'Find more information and examples at: https://nx.dev/packages/web/executors/webpack' - ); - - const affectedHelp = runCLI(`affected --help`); - expect(affectedHelp).toContain('Run target for affected projects'); - - const version = runCLI(`--version`); - expect(version).toContain(getPublishedVersion()); // stub value - }, 120000); -}); - describe('report', () => { beforeEach(() => newProject()); diff --git a/e2e/cli/src/run.test.ts b/e2e/cli/src/run.test.ts new file mode 100644 index 0000000000000..14bf6ce4d2de9 --- /dev/null +++ b/e2e/cli/src/run.test.ts @@ -0,0 +1,67 @@ +import { + getPublishedVersion, + isNotWindows, + newProject, + readFile, + readJson, + runCLI, + runCLIAsync, + runCommand, + tmpProjPath, + uniq, + updateFile, + updateProjectConfig, +} from '@nrwl/e2e/utils'; +import { renameSync } from 'fs'; +import { packagesWeCareAbout } from 'nx/src/command-line/report'; + +// +describe('Running targets', () => { + beforeEach(() => newProject()); + + it('should execute long running tasks', async () => { + const myapp = uniq('myapp'); + runCLI(`generate @nrwl/web:app ${myapp}`); + updateProjectConfig(myapp, (c) => { + c.targets['counter'] = { + executor: '@nrwl/workspace:counter', + options: { + to: 2, + }, + }; + return c; + }); + + const success = runCLI(`counter ${myapp} --result=true`); + expect(success).toContain('0'); + expect(success).toContain('1'); + + expect(() => runCLI(`counter ${myapp} --result=false`)).toThrowError(); + }); + + it('should run npm scripts', async () => { + const mylib = uniq('mylib'); + runCLI(`generate @nrwl/node:lib ${mylib}`); + + updateProjectConfig(mylib, (j) => { + delete j.targets; + return j; + }); + + updateFile( + `libs/${mylib}/package.json`, + JSON.stringify({ + name: 'mylib1', + scripts: { 'echo:dev': `echo ECHOED` }, + }) + ); + + const { stdout } = await runCLIAsync( + `echo:dev ${mylib} -- positional --a=123 --no-b`, + { + silent: true, + } + ); + expect(stdout).toMatch(/ECHOED positional --a=123 --no-b/); + }, 1000000); +}); diff --git a/e2e/workspace-core/src/run-commands.test.ts b/e2e/workspace-core/src/run-commands.test.ts index 9429f98710681..26bebe1b8589b 100644 --- a/e2e/workspace-core/src/run-commands.test.ts +++ b/e2e/workspace-core/src/run-commands.test.ts @@ -53,20 +53,15 @@ describe('Run Commands', () => { config.targets.echo = { executor: '@nrwl/workspace:run-commands', options: { - command: 'echo', + command: 'echo --var1={args.var1}', var1: 'a', - var2: 'b', - 'var-hyphen': 'c', - varCamelCase: 'd', }, }; return config; }); const result = runCLI(`run ${mylib}:echo`, { silent: true }); - expect(result).toContain( - '--var1=a --var2=b --var-hyphen=c --varCamelCase=d' - ); + expect(result).toContain('--var1=a'); }, 120000); it('should interpolate provided arguments', async () => { diff --git a/package.json b/package.json index b409c7b6ea143..f3fddafe08925 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "local-registry": "./scripts/local-registry.sh", "documentation": "ts-node -P scripts/tsconfig.scripts.json ./scripts/documentation/documentation.ts && yarn check-documentation-map", "submit-plugin": "node ./scripts/submit-plugin.js", - "prepare": "is-ci || husky install" + "prepare": "is-ci || husky install", + "echo": "echo 1234" }, "devDependencies": { "@angular-devkit/architect": "~0.1400.0", @@ -305,4 +306,3 @@ "minimist": "^1.2.6" } } - diff --git a/packages/nx/src/command-line/affected.ts b/packages/nx/src/command-line/affected.ts index c54de49ab4225..253d291acb234 100644 --- a/packages/nx/src/command-line/affected.ts +++ b/packages/nx/src/command-line/affected.ts @@ -21,7 +21,7 @@ import { TargetDependencyConfig } from 'nx/src/config/workspace-json-project-jso export async function affected( command: 'apps' | 'libs' | 'graph' | 'print-affected' | 'affected', - parsedArgs: yargs.Arguments & RawNxArgs, + args: { [k: string]: any }, extraTargetDependencies: Record< string, (TargetDependencyConfig | string)[] @@ -30,10 +30,10 @@ export async function affected( performance.mark('command-execution-begins'); const env = readEnvironment(); const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides( - parsedArgs, + args, 'affected', { - printWarnings: command !== 'print-affected' && !parsedArgs.plain, + printWarnings: command !== 'print-affected' && !args.plain, }, env.nxJson ); @@ -49,7 +49,7 @@ export async function affected( const apps = projects .filter((p) => p.type === 'app') .map((p) => p.name); - if (parsedArgs.plain) { + if (args.plain) { console.log(apps.join(' ')); } else { if (apps.length) { @@ -69,7 +69,7 @@ export async function affected( const libs = projects .filter((p) => p.type === 'lib') .map((p) => p.name); - if (parsedArgs.plain) { + if (args.plain) { console.log(libs.join(' ')); } else { if (libs.length) { @@ -87,7 +87,7 @@ export async function affected( case 'graph': const projectNames = projects.map((p) => p.name); - await generateGraph(parsedArgs as any, projectNames); + await generateGraph(args as any, projectNames); break; case 'print-affected': @@ -128,7 +128,7 @@ export async function affected( } } } catch (e) { - printError(e, parsedArgs.verbose); + printError(e, args.verbose); process.exit(1); } } diff --git a/packages/nx/src/command-line/nx-commands.ts b/packages/nx/src/command-line/nx-commands.ts index 1aa7a5fc4865f..1e8949d7d21d9 100644 --- a/packages/nx/src/command-line/nx-commands.ts +++ b/packages/nx/src/command-line/nx-commands.ts @@ -8,7 +8,7 @@ import { workspaceRoot } from '../utils/workspace-root'; import { getPackageManagerCommand } from '../utils/package-manager'; import { writeJsonFile } from '../utils/fileutils'; -// Ensure that the output takes up the available width of the terminal +// Ensure that the output takes up the available width of the terminal. yargs.wrap(yargs.terminalWidth()); export const parserConfiguration: Partial = { @@ -50,7 +50,7 @@ export const commandsObject = yargs You can skip the use of Nx cache by using the --skip-nx-cache option.`, builder: (yargs) => withRunOneOptions(yargs), handler: async (args) => - (await import('./run-one')).runOne(process.cwd(), { ...args }), + (await import('./run-one')).runOne(process.cwd(), withOverrides(args)), }) .command({ command: 'run-many', @@ -62,7 +62,8 @@ export const commandsObject = yargs ), 'run-many' ), - handler: async (args) => (await import('./run-many')).runMany({ ...args }), + handler: async (args) => + (await import('./run-many')).runMany(withOverrides(args)), }) .command({ command: 'affected', @@ -75,7 +76,7 @@ export const commandsObject = yargs 'affected' ), handler: async (args) => - (await import('./affected')).affected('affected', { ...args }), + (await import('./affected')).affected('affected', withOverrides(args)), }) .command({ command: 'affected:test', @@ -87,7 +88,7 @@ export const commandsObject = yargs ), handler: async (args) => (await import('./affected')).affected('affected', { - ...args, + ...withOverrides(args), target: 'test', }), }) @@ -101,7 +102,7 @@ export const commandsObject = yargs ), handler: async (args) => (await import('./affected')).affected('affected', { - ...args, + ...withOverrides(args), target: 'build', }), }) @@ -115,7 +116,7 @@ export const commandsObject = yargs ), handler: async (args) => (await import('./affected')).affected('affected', { - ...args, + ...withOverrides(args), target: 'lint', }), }) @@ -129,7 +130,7 @@ export const commandsObject = yargs ), handler: async (args) => (await import('./affected')).affected('affected', { - ...args, + ...withOverrides(args), target: 'e2e', }), }) @@ -185,9 +186,10 @@ export const commandsObject = yargs 'print-affected' ), handler: async (args) => - (await import('./affected')).affected('print-affected', { - ...args, - }), + (await import('./affected')).affected( + 'print-affected', + withOverrides(args) + ), }) .command({ command: 'daemon', @@ -540,6 +542,7 @@ function withDepGraphOptions(yargs: yargs.Argv): yargs.Argv { type: 'array', coerce: parseCSV, }) + .option('groupByFolder', { describe: 'Group projects by folder in the project graph', type: 'boolean', @@ -564,6 +567,19 @@ function withDepGraphOptions(yargs: yargs.Argv): yargs.Argv { }); } +function withOverrides(args: any): any { + const split = process.argv.indexOf('--'); + if (split > -1) { + const overrides = process.argv.slice(split + 1); + delete args._; + return { ...args, __overrides__: overrides }; + } else { + args['__positional_overrides__'] = args._.slice(1); + delete args._; + return args; + } +} + function withParallelOption(yargs: yargs.Argv): yargs.Argv { return yargs.option('parallel', { describe: 'Max number of parallel processes [default is 3]', diff --git a/packages/nx/src/command-line/run-many.ts b/packages/nx/src/command-line/run-many.ts index f1043228ed28c..ff4d90e8ac183 100644 --- a/packages/nx/src/command-line/run-many.ts +++ b/packages/nx/src/command-line/run-many.ts @@ -12,7 +12,7 @@ import { readEnvironment } from './read-environment'; import { TargetDependencyConfig } from '../config/workspace-json-project-json'; export async function runMany( - parsedArgs: yargs.Arguments & RawNxArgs, + args: { [k: string]: any }, extraTargetDependencies: Record< string, (TargetDependencyConfig | string)[] @@ -21,7 +21,7 @@ export async function runMany( performance.mark('command-execution-begins'); const env = readEnvironment(); const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides( - parsedArgs, + args, 'run-many', { printWarnings: true }, env.nxJson diff --git a/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts b/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts index 8c32103722d49..e962b263a05f9 100644 --- a/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts +++ b/packages/nx/src/executors/run-commands/run-commands.impl.spec.ts @@ -1,8 +1,12 @@ import { readFileSync, unlinkSync, writeFileSync } from 'fs'; import { relative } from 'path'; import { dirSync, fileSync } from 'tmp'; -import runCommands, { LARGE_BUFFER } from './run-commands.impl'; +import runCommands, { + interpolateArgsIntoCommand, + LARGE_BUFFER, +} from './run-commands.impl'; import { env } from 'npm-run-path'; + const { devDependencies: { nx: version }, } = require('package.json'); @@ -10,6 +14,7 @@ const { function normalize(p: string) { return p.startsWith('/private') ? p.substring(8) : p; } + function readFile(f: string) { return readFileSync(f).toString().replace(/\s/g, ''); } @@ -21,17 +26,14 @@ describe('Run Commands', () => { jest.clearAllMocks(); }); - it('should run one command', async () => { - const f = fileSync().name; - const result = await runCommands({ command: `echo 1 >> ${f}` }, context); - expect(result).toEqual(expect.objectContaining({ success: true })); - expect(readFile(f)).toEqual('1'); - }); - it('should interpolate provided --args', async () => { const f = fileSync().name; const result = await runCommands( - { command: `echo {args.key} >> ${f}`, args: '--key=123' }, + { + command: `echo {args.key} >> ${f}`, + args: '--key=123', + __unparsed__: [], + }, context ); expect(result).toEqual(expect.objectContaining({ success: true })); @@ -44,6 +46,7 @@ describe('Run Commands', () => { { command: `echo {args.key} >> ${f}`, key: 123, + __unparsed__: [], }, context ); @@ -51,170 +54,13 @@ describe('Run Commands', () => { expect(readFile(f)).toEqual('123'); }); - it('should add all args to the command if no interpolation in the command', async () => { - const exec = jest.spyOn(require('child_process'), 'execSync'); - - await runCommands( - { - command: `echo`, - a: 123, - b: 456, - }, - context - ); - expect(exec).toHaveBeenCalledWith(`echo --a=123 --b=456`, { - stdio: ['inherit', 'inherit', 'inherit'], - cwd: undefined, - env: { - ...process.env, - ...env(), - }, - maxBuffer: LARGE_BUFFER, - }); - }); - - it('should add args containing spaces to in the command', async () => { - const exec = jest.spyOn(require('child_process'), 'execSync'); - - await runCommands( - { - command: `echo`, - a: 123, - b: '4 5 6', - c: '4 "5" 6', - }, - context - ); - expect(exec).toHaveBeenCalledWith( - `echo --a=123 --b="4 5 6" --c="4 \"5\" 6"`, - { - stdio: ['inherit', 'inherit', 'inherit'], - cwd: undefined, - env: { - ...process.env, - ...env(), - }, - maxBuffer: LARGE_BUFFER, - } - ); - }); - - it('should forward args by default when using commands (plural)', async () => { - const exec = jest.spyOn(require('child_process'), 'exec'); - - await runCommands( - { - commands: [{ command: 'echo' }, { command: 'echo foo' }], - parallel: true, - a: 123, - b: 456, - }, - context - ); - - expect(exec).toHaveBeenCalledTimes(2); - expect(exec).toHaveBeenNthCalledWith(1, 'echo --a=123 --b=456', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - expect(exec).toHaveBeenNthCalledWith(2, 'echo foo --a=123 --b=456', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - }); - - it('should forward args when forwardAllArgs is set to true', async () => { - const exec = jest.spyOn(require('child_process'), 'exec'); - - await runCommands( - { - commands: [ - { command: 'echo', forwardAllArgs: true }, - { command: 'echo foo', forwardAllArgs: true }, - ], - parallel: true, - a: 123, - b: 456, - }, - context - ); - - expect(exec).toHaveBeenCalledTimes(2); - expect(exec).toHaveBeenNthCalledWith(1, 'echo --a=123 --b=456', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - expect(exec).toHaveBeenNthCalledWith(2, 'echo foo --a=123 --b=456', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - }); - - it('should not forward args when forwardAllArgs is set to false', async () => { - const exec = jest.spyOn(require('child_process'), 'exec'); - - await runCommands( - { - commands: [ - { command: 'echo', forwardAllArgs: false }, - { command: 'echo foo', forwardAllArgs: false }, - ], - parallel: true, - a: 123, - b: 456, - }, - context - ); - - expect(exec).toHaveBeenCalledTimes(2); - expect(exec).toHaveBeenNthCalledWith(1, 'echo', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - expect(exec).toHaveBeenNthCalledWith(2, 'echo foo', { - maxBuffer: LARGE_BUFFER, - env: { - ...process.env, - ...env(), - }, - }); - }); - - it('should throw when invalid args', async () => { - try { - await runCommands( - { - command: `echo {args.key}`, - args: 'key=value', - }, - context - ); - } catch (e) { - expect(e.message).toEqual('Invalid args: key=value'); - } - }); - it('should run commands serially', async () => { const f = fileSync().name; const result = await runCommands( { commands: [`sleep 0.2 && echo 1 >> ${f}`, `echo 2 >> ${f}`], parallel: false, + __unparsed__: [], }, context ); @@ -235,6 +81,7 @@ describe('Run Commands', () => { }, ], parallel: true, + __unparsed__: [], }, context ); @@ -252,6 +99,7 @@ describe('Run Commands', () => { commands: [{ command: 'echo foo' }, { command: 'echo bar' }], parallel: false, readyWhen: 'READY', + __unparsed__: [], }, context ); @@ -270,7 +118,9 @@ describe('Run Commands', () => { commands: [`echo READY && sleep 0.1 && echo 1 >> ${f}`, `echo foo`], parallel: true, readyWhen: 'READY', + __unparsed__: [], }, + context ); expect(result).toEqual(expect.objectContaining({ success: true })); @@ -290,6 +140,7 @@ describe('Run Commands', () => { { commands: [`echo 1 >> ${f} && exit 1`, `echo 2 >> ${f}`], parallel: false, + __unparsed__: [], }, context ); @@ -298,6 +149,18 @@ describe('Run Commands', () => { expect(readFile(f)).toEqual('1'); }); + describe('interpolateArgsIntoCommand', () => { + it('should add all unparsed args when forwardAllArgs is true', () => { + expect( + interpolateArgsIntoCommand( + 'echo', + { __unparsed__: ['one', '-a=b'] } as any, + true + ) + ).toEqual('echo one -a=b'); + }); + }); + describe('--color', () => { it('should not set FORCE_COLOR=true', async () => { const exec = jest.spyOn(require('child_process'), 'exec'); @@ -305,6 +168,7 @@ describe('Run Commands', () => { { commands: [`echo 'Hello World'`, `echo 'Hello Universe'`], parallel: true, + __unparsed__: [], }, context ); @@ -333,6 +197,7 @@ describe('Run Commands', () => { commands: [`echo 'Hello World'`, `echo 'Hello Universe'`], parallel: true, color: true, + __unparsed__: [], }, context ); @@ -363,6 +228,7 @@ describe('Run Commands', () => { ], parallel: true, cwd: process.cwd(), + __unparsed__: [], }, { root } as any ); @@ -382,7 +248,9 @@ describe('Run Commands', () => { }, ], parallel: true, + __unparsed__: [], }, + { root } as any ); @@ -405,6 +273,7 @@ describe('Run Commands', () => { ], cwd, parallel: true, + __unparsed__: [], }, { root } as any ); @@ -427,6 +296,7 @@ describe('Run Commands', () => { ], cwd: childFolder, parallel: true, + __unparsed__: [], }, { root } as any ); @@ -459,6 +329,7 @@ describe('Run Commands', () => { command: `echo $NRWL_SITE >> ${f}`, }, ], + __unparsed__: [], }, context ); @@ -479,6 +350,7 @@ describe('Run Commands', () => { }, ], envFile: devEnv, + __unparsed__: [], }, context ); @@ -498,7 +370,9 @@ describe('Run Commands', () => { }, ], envFile: '/somePath/.fakeEnv', + __unparsed__: [], }, + context ); fail('should not reach'); diff --git a/packages/nx/src/executors/run-commands/run-commands.impl.ts b/packages/nx/src/executors/run-commands/run-commands.impl.ts index 5556da19de6f9..aed60308944be 100644 --- a/packages/nx/src/executors/run-commands/run-commands.impl.ts +++ b/packages/nx/src/executors/run-commands/run-commands.impl.ts @@ -46,6 +46,7 @@ export interface RunCommandsOptions extends Json { args?: string; envFile?: string; outputPath?: string; + __unparsed__: string[]; } const propKeys = [ @@ -161,9 +162,9 @@ function normalizeOptions( ); } (options as NormalizedRunCommandsOptions).commands.forEach((c) => { - c.command = transformCommand( + c.command = interpolateArgsIntoCommand( c.command, - (options as NormalizedRunCommandsOptions).parsedArgs, + options as NormalizedRunCommandsOptions, c.forwardAllArgs ?? true ); }); @@ -282,23 +283,18 @@ function processEnv(color: boolean) { return env; } -function transformCommand( +export function interpolateArgsIntoCommand( command: string, - args: { [key: string]: string }, + opts: NormalizedRunCommandsOptions, forwardAllArgs: boolean ) { if (command.indexOf('{args.') > -1) { const regex = /{args\.([^}]+)}/g; - return command.replace(regex, (_, group: string) => args[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}`; + return command.replace(regex, (_, group: string) => opts.parsedArgs[group]); + } else if (forwardAllArgs) { + return `${command}${ + opts.__unparsed__.length > 0 ? ' ' + opts.__unparsed__.join(' ') : '' + }`; } else { return command; } diff --git a/packages/nx/src/executors/run-commands/schema.json b/packages/nx/src/executors/run-commands/schema.json index a7164ad5a2e7f..53c294e23952a 100644 --- a/packages/nx/src/executors/run-commands/schema.json +++ b/packages/nx/src/executors/run-commands/schema.json @@ -124,9 +124,19 @@ "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." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "unparsed" + } } }, "additionalProperties": true, - "required": [], + "required": ["__unparsed__"], "examplesFile": "../../../docs/run-commands-examples.md" } diff --git a/packages/nx/src/executors/run-script/run-script.impl.ts b/packages/nx/src/executors/run-script/run-script.impl.ts index ecd60c6e93c58..d0ebc3f91ba4d 100644 --- a/packages/nx/src/executors/run-script/run-script.impl.ts +++ b/packages/nx/src/executors/run-script/run-script.impl.ts @@ -5,6 +5,7 @@ import * as path from 'path'; export interface RunScriptOptions { script: string; + __unparsed__: string[]; } export default async function ( @@ -12,16 +13,8 @@ export default async function ( context: ExecutorContext ) { const pm = getPackageManagerCommand(); - const script = options.script; - delete options.script; - - const args = []; - Object.keys(options).forEach((r) => { - args.push(`--${r}=${options[r]}`); - }); - try { - execSync(pm.run(script, args.join(' ')), { + execSync(pm.run(options.script, options.__unparsed__.join(' ')), { stdio: ['inherit', 'inherit', 'inherit'], cwd: path.join( context.root, diff --git a/packages/nx/src/executors/run-script/schema.json b/packages/nx/src/executors/run-script/schema.json index 443f57138c1e0..dd940ac16f491 100644 --- a/packages/nx/src/executors/run-script/schema.json +++ b/packages/nx/src/executors/run-script/schema.json @@ -8,9 +8,19 @@ "script": { "type": "string", "description": "An npm script name in the `package.json` file of the project (e.g., `build`)." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "unparsed" + } } }, "additionalProperties": true, - "required": ["script"], + "required": ["script", "__unparsed__"], "examplesFile": "../../../docs/run-script-examples.md" } diff --git a/packages/nx/src/hasher/hasher.ts b/packages/nx/src/hasher/hasher.ts index b3496cde2a816..51698d74aa3b2 100644 --- a/packages/nx/src/hasher/hasher.ts +++ b/packages/nx/src/hasher/hasher.ts @@ -119,11 +119,18 @@ export class Hasher { } hashCommand(task: Task) { + const overrides = { ...task.overrides }; + delete overrides['__overrides_unparsed__']; + const sortedOverrides = {}; + for (let k of Object.keys(overrides).sort()) { + sortedOverrides[k] = overrides[k]; + } + return this.hashing.hashArray([ task.target.project ?? '', task.target.target ?? '', task.target.configuration ?? '', - JSON.stringify(task.overrides), + JSON.stringify(sortedOverrides), ]); } diff --git a/packages/nx/src/tasks-runner/create-task-graph.spec.ts b/packages/nx/src/tasks-runner/create-task-graph.spec.ts index a33953c4fa2bd..4bcf1b79035a7 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.spec.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.spec.ts @@ -174,7 +174,9 @@ describe('createTaskGraph', () => { ['app1'], ['build'], 'development', - {} + { + __overrides_unparsed__: [], + } ); // prebuild should also be in here expect(taskGraph).toEqual({ @@ -186,7 +188,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'app1:prebuild': { @@ -195,7 +199,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'prebuild', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'lib1:build': { @@ -204,7 +210,9 @@ describe('createTaskGraph', () => { project: 'lib1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'lib1-root', }, }, @@ -288,7 +296,9 @@ describe('createTaskGraph', () => { ['app1'], ['build'], 'development', - {} + { + __overrides_unparsed__: [], + } ); // prebuild should also be in here expect(taskGraph).toEqual({ @@ -300,7 +310,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'lib1:build': { @@ -309,7 +321,9 @@ describe('createTaskGraph', () => { project: 'lib1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'lib1-root', }, 'lib2:build': { @@ -318,7 +332,9 @@ describe('createTaskGraph', () => { project: 'lib2', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'lib2-root', }, 'lib3:build': { @@ -327,7 +343,9 @@ describe('createTaskGraph', () => { project: 'lib3', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'lib3-root', }, }, @@ -369,7 +387,9 @@ describe('createTaskGraph', () => { ['app1'], ['build'], 'development', - {} + { + __overrides_unparsed__: [], + } ); // prebuild should also be in here expect(taskGraph).toEqual({ @@ -381,7 +401,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'app1:test': { @@ -390,7 +412,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'test', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, }, @@ -454,7 +478,9 @@ describe('createTaskGraph', () => { ['app1'], ['build'], 'development', - {} + { + __overrides_unparsed__: [], + } ); expect(taskGraph).toEqual({ roots: [], @@ -465,7 +491,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'app3:build': { @@ -474,7 +502,9 @@ describe('createTaskGraph', () => { project: 'app3', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app3-root', }, }, @@ -535,7 +565,9 @@ describe('createTaskGraph', () => { ['app1'], ['build'], 'development', - {} + { + __overrides_unparsed__: [], + } ); expect(taskGraph).toEqual({ roots: ['app3:build'], @@ -546,7 +578,9 @@ describe('createTaskGraph', () => { project: 'app1', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app1-root', }, 'app3:build': { @@ -555,7 +589,9 @@ describe('createTaskGraph', () => { project: 'app3', target: 'build', }, - overrides: {}, + overrides: { + __overrides_unparsed__: [], + }, projectRoot: 'app3-root', }, }, diff --git a/packages/nx/src/tasks-runner/create-task-graph.ts b/packages/nx/src/tasks-runner/create-task-graph.ts index 8f2b59014c67a..b569045de2b3f 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.ts @@ -103,7 +103,7 @@ export class ProcessTasks { depProject, dependencyConfig.target, resolvedConfiguration, - {} + { __overrides_unparsed__: [] } ); this.tasks[depTargetId] = newTask; this.dependencies[depTargetId] = []; @@ -139,7 +139,7 @@ export class ProcessTasks { selfProject, dependencyConfig.target, resolvedConfiguration, - {} + { __overrides_unparsed__: [] } ); this.tasks[selfTaskId] = newTask; this.dependencies[selfTaskId] = []; diff --git a/packages/nx/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts index 642b653ee99ff..4ff79eb8d6516 100644 --- a/packages/nx/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts @@ -7,6 +7,7 @@ import type { LifeCycle } from '../life-cycle'; import type { TaskStatus } from '../tasks-runner'; import { Task } from '../../config/task-graph'; import { prettyTime } from './pretty-time'; +import { formatFlags } from './formatting-utils'; /** * The following function is responsible for creating a life cycle with dynamic @@ -273,7 +274,7 @@ export async function createRunManyDynamicOutputRenderer({ ); Object.entries(overrides) .map(([flag, value]) => - output.dim.cyan(`${leftPadding} --${flag}=${value}`) + output.dim.cyan(formatFlags(leftPadding, flag, value)) ) .forEach((arg) => taskOverridesRows.push(arg)); } @@ -335,7 +336,7 @@ export async function createRunManyDynamicOutputRenderer({ ); Object.entries(overrides) .map(([flag, value]) => - output.dim.green(`${leftPadding} --${flag}=${value}`) + output.dim.green(formatFlags(leftPadding, flag, value)) ) .forEach((arg) => taskOverridesRows.push(arg)); } @@ -374,7 +375,7 @@ export async function createRunManyDynamicOutputRenderer({ ); Object.entries(overrides) .map(([flag, value]) => - output.dim.red(`${leftPadding} --${flag}=${value}`) + output.dim.red(formatFlags(leftPadding, flag, value)) ) .forEach((arg) => taskOverridesRows.push(arg)); } diff --git a/packages/nx/src/tasks-runner/life-cycles/dynamic-run-one-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/dynamic-run-one-terminal-output-life-cycle.ts index 58cdc88e403e8..9912e35b75de8 100644 --- a/packages/nx/src/tasks-runner/life-cycles/dynamic-run-one-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/dynamic-run-one-terminal-output-life-cycle.ts @@ -6,6 +6,7 @@ import { output } from '../../utils/output'; import type { LifeCycle } from '../life-cycle'; import { prettyTime } from './pretty-time'; import { Task } from '../../config/task-graph'; +import { formatFlags } from './formatting-utils'; /** * As tasks are completed the overall state moves from: @@ -288,7 +289,7 @@ export async function createRunOneDynamicOutputRenderer({ ); Object.entries(overrides) .map(([flag, value]) => - output.dim.green(`${leftPadding} --${flag}=${value}`) + output.dim.green(formatFlags(leftPadding, flag, value)) ) .forEach((arg) => taskOverridesLines.push(arg)); } @@ -329,7 +330,7 @@ export async function createRunOneDynamicOutputRenderer({ ); Object.entries(overrides) .map(([flag, value]) => - output.dim.red(`${leftPadding} --${flag}=${value}`) + output.dim.red(formatFlags(leftPadding, flag, value)) ) .forEach((arg) => taskOverridesLines.push(arg)); } diff --git a/packages/nx/src/tasks-runner/life-cycles/formatting-utils.ts b/packages/nx/src/tasks-runner/life-cycles/formatting-utils.ts new file mode 100644 index 0000000000000..11b34133cc2f3 --- /dev/null +++ b/packages/nx/src/tasks-runner/life-cycles/formatting-utils.ts @@ -0,0 +1,9 @@ +export function formatFlags( + leftPadding: string, + flag: string, + value: any +): string { + return flag == '_' + ? `${leftPadding} ${(value as string[]).join(' ')}` + : `${leftPadding} --${flag}=${value}`; +} diff --git a/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts index 53aaeca73d188..4b261ae27af3e 100644 --- a/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts @@ -3,6 +3,7 @@ import { TaskStatus } from '../tasks-runner'; import { getPrintableCommandArgsForTask } from '../utils'; import type { LifeCycle } from '../life-cycle'; import { Task } from '../../config/task-graph'; +import { formatFlags } from './formatting-utils'; /** * The following life cycle's outputs are static, meaning no previous content @@ -42,9 +43,9 @@ export class StaticRunManyTerminalOutputLifeCycle implements LifeCycle { ); if (Object.keys(this.taskOverrides).length > 0) { bodyLines.push(''); - bodyLines.push(`${output.dim('With flags:')}`); + bodyLines.push(`${output.dim('With additional flags:')}`); Object.entries(this.taskOverrides) - .map(([flag, value]) => ` --${flag}=${value}`) + .map(([flag, value]) => formatFlags('', flag, value)) .forEach((arg) => bodyLines.push(arg)); } diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 99acf236e4e2d..4c8e792018793 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -34,13 +34,16 @@ async function getTerminalOutputLifeCycle( process.env.NX_VERBOSE_LOGGING !== 'true' && process.env.NX_TASKS_RUNNER_DYNAMIC_OUTPUT !== 'false'; + const overridesWithoutHidden = { ...overrides }; + delete overridesWithoutHidden['__overrides_unparsed__']; + if (isRunOne) { if (useDynamicOutput) { return await createRunOneDynamicOutputRenderer({ initiatingProject, tasks, args: nxArgs, - overrides, + overrides: overridesWithoutHidden, }); } return { @@ -58,7 +61,7 @@ async function getTerminalOutputLifeCycle( projectNames, tasks, args: nxArgs, - overrides, + overrides: overridesWithoutHidden, }); } else { return { @@ -66,7 +69,7 @@ async function getTerminalOutputLifeCycle( projectNames, tasks, nxArgs, - overrides + overridesWithoutHidden ), renderIsDone: Promise.resolve(), }; diff --git a/packages/nx/src/tasks-runner/utils.spec.ts b/packages/nx/src/tasks-runner/utils.spec.ts index d49a9c31c0e69..7cfe13d9fe7b4 100644 --- a/packages/nx/src/tasks-runner/utils.spec.ts +++ b/packages/nx/src/tasks-runner/utils.spec.ts @@ -1,4 +1,4 @@ -import { getOutputsForTargetAndConfiguration, unparse } from './utils'; +import { getOutputsForTargetAndConfiguration } from './utils'; import { ProjectGraphProjectNode } from '../config/project-graph'; describe('utils', () => { @@ -241,73 +241,4 @@ describe('utils', () => { }); }); }); - - describe('unparse', () => { - it('should unparse options whose values are primitives', () => { - const options = { - boolean1: false, - boolean2: true, - number: 4, - string: 'foo', - 'empty-string': '', - ignore: null, - }; - - expect(unparse(options)).toEqual([ - '--no-boolean1', - '--boolean2', - '--number=4', - '--string=foo', - '--empty-string=', - ]); - }); - - it('should unparse options whose values are arrays', () => { - const options = { - array1: [1, 2], - array2: [3, 4], - }; - - expect(unparse(options)).toEqual([ - '--array1=1', - '--array1=2', - '--array2=3', - '--array2=4', - ]); - }); - - it('should unparse options whose values are objects', () => { - const options = { - foo: { - x: 'x', - y: 'y', - w: [1, 2], - z: [3, 4], - }, - }; - - expect(unparse(options)).toEqual([ - '--foo.x=x', - '--foo.y=y', - '--foo.w=1', - '--foo.w=2', - '--foo.z=3', - '--foo.z=4', - ]); - }); - - it('should quote string values with space(s)', () => { - const options = { - string1: 'one', - string2: 'one two', - string3: 'one two three', - }; - - expect(unparse(options)).toEqual([ - '--string1=one', - '--string2="one two"', - '--string3="one two three"', - ]); - }); - }); }); diff --git a/packages/nx/src/tasks-runner/utils.ts b/packages/nx/src/tasks-runner/utils.ts index 989c914f6abf3..85c5507e866b9 100644 --- a/packages/nx/src/tasks-runner/utils.ts +++ b/packages/nx/src/tasks-runner/utils.ts @@ -119,47 +119,6 @@ export function getOutputsForTargetAndConfiguration( } } -export function unparse(options: Object): string[] { - const unparsed = []; - for (const key of Object.keys(options)) { - const value = options[key]; - unparseOption(key, value, unparsed); - } - - return unparsed; -} - -function unparseOption(key: string, value: any, unparsed: string[]) { - if (value === true) { - unparsed.push(`--${key}`); - } else if (value === false) { - unparsed.push(`--no-${key}`); - } else if (Array.isArray(value)) { - value.forEach((item) => unparseOption(key, item, unparsed)); - } else if (Object.prototype.toString.call(value) === '[object Object]') { - const flattened = flatten(value, { safe: true }); - for (const flattenedKey in flattened) { - unparseOption( - `${key}.${flattenedKey}`, - flattened[flattenedKey], - unparsed - ); - } - } else if ( - typeof value === 'string' && - stringShouldBeWrappedIntoQuotes(value) - ) { - const sanitized = value.replace(/"/g, String.raw`\"`); - unparsed.push(`--${key}="${sanitized}"`); - } else if (value != null) { - unparsed.push(`--${key}=${value}`); - } -} - -function stringShouldBeWrappedIntoQuotes(str: string) { - return str.includes(' ') || str.includes('{') || str.includes('"'); -} - export function interpolate(template: string, data: any): string { return template.replace(/{([\s\S]+?)}/g, (match: string) => { let value = data; @@ -260,7 +219,7 @@ export function getCliPath() { } export function getPrintableCommandArgsForTask(task: Task) { - const args: string[] = unparse(task.overrides || {}); + const args: string[] = task.overrides['__overrides_unparsed__']; const target = task.target.target.includes(':') ? `"${task.target.target}"` diff --git a/packages/nx/src/utils/command-line-utils.spec.ts b/packages/nx/src/utils/command-line-utils.spec.ts index 8b158190e2436..9252f1c4673b1 100644 --- a/packages/nx/src/utils/command-line-utils.spec.ts +++ b/packages/nx/src/utils/command-line-utils.spec.ts @@ -80,7 +80,7 @@ describe('splitArgs', () => { splitArgsIntoNxArgsAndOverrides( { notNxArg: true, - _: ['--override'], + _: ['affecteda', '--override'], $0: '', }, 'affected', @@ -99,7 +99,6 @@ describe('splitArgs', () => { { files: [''], notNxArg: true, - _: ['--override'], $0: '', }, 'affected', @@ -107,35 +106,45 @@ describe('splitArgs', () => { {} as any ).overrides ).toEqual({ + __overrides_unparsed__: ['--notNxArg=true'], notNxArg: true, - override: true, }); }); - it('should set base and head in the affected mode', () => { - const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides( - { - notNxArg: true, - _: ['affected', '--name', 'bob', 'sha1', 'sha2', '--override'], - $0: '', - }, - 'affected', - {} as any, - {} as any - ); - - expect(nxArgs).toEqual({ - base: 'sha1', - head: 'sha2', - skipNxCache: false, - }); - expect(overrides).toEqual({ - notNxArg: true, - override: true, - name: 'bob', + it('should only use explicitly provided overrides', () => { + expect( + splitArgsIntoNxArgsAndOverrides( + { + files: [''], + notNxArg: true, + _: ['explicit'], + $0: '', + }, + 'affected', + {} as any, + {} as any + ).overrides + ).toEqual({ + __overrides_unparsed__: ['explicit'], + _: ['explicit'], }); }); + it('should throw when base and head are set as positional args', () => { + expect(() => + splitArgsIntoNxArgsAndOverrides( + { + notNxArg: true, + __positional_overrides__: ['sha1', 'sha2'], + $0: '', + }, + 'affected', + {} as any, + {} as any + ) + ).toThrow(); + }); + it('should set base and head based on environment variables in affected mode, if they are not provided directly on the command', () => { const originalNxBase = process.env.NX_BASE; process.env.NX_BASE = 'envVarSha1'; @@ -200,27 +209,6 @@ describe('splitArgs', () => { process.env.NX_HEAD = originalNxHead; }); - it('should not set base and head in the run-one mode', () => { - const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides( - { - notNxArg: true, - _: ['--exclude=file'], - $0: '', - }, - 'run-one', - {} as any, - {} as any - ); - - expect(nxArgs).toEqual({ - skipNxCache: false, - }); - expect(overrides).toEqual({ - notNxArg: true, - exclude: 'file', - }); - }); - describe('--parallel', () => { it('should be a number', () => { const parallel = splitArgsIntoNxArgsAndOverrides( diff --git a/packages/nx/src/utils/command-line-utils.ts b/packages/nx/src/utils/command-line-utils.ts index acf1cd235c722..41946af2c3ca5 100644 --- a/packages/nx/src/utils/command-line-utils.ts +++ b/packages/nx/src/utils/command-line-utils.ts @@ -2,13 +2,11 @@ import * as yargsParser from 'yargs-parser'; import * as yargs from 'yargs'; import { TEN_MEGABYTES } from '../project-graph/file-utils'; import { output } from './output'; -import { NxAffectedConfig, NxJsonConfiguration } from '../config/nx-json'; +import { NxJsonConfiguration } from '../config/nx-json'; import { execSync } from 'child_process'; -import { - readAllWorkspaceConfiguration, - readNxJson, -} from '../config/configuration'; +import { readAllWorkspaceConfiguration } from '../config/configuration'; +// export function names(name: string): { name: string; className: string; @@ -136,35 +134,70 @@ export interface NxArgs { const ignoreArgs = ['$0', '_']; export function splitArgsIntoNxArgsAndOverrides( - args: yargs.Arguments, + args: { [k: string]: any }, mode: 'run-one' | 'run-many' | 'affected' | 'print-affected', options = { printWarnings: true }, nxJson: NxJsonConfiguration ): { nxArgs: NxArgs; overrides: yargs.Arguments } { + if (!args.__overrides__ && args._) { + // required for backwards compatibility + args.__overrides__ = args._; + delete args._; + } + const nxSpecific = mode === 'run-one' ? runOne : mode === 'run-many' ? runMany : runAffected; + let explicitOverrides; + if (args.__overrides__) { + explicitOverrides = yargsParser(args.__overrides__ as string[], { + configuration: { + 'camel-case-expansion': false, + 'dot-notation': false, + }, + }); + if (!explicitOverrides._ || explicitOverrides._.length === 0) { + delete explicitOverrides._; + } + } + const overridesFromMainArgs = {} as any; + if (args['__positional_overrides__']) { + overridesFromMainArgs['_'] = args['__positional_overrides__']; + } const nxArgs: RawNxArgs = {}; - const overrides = yargsParser(args._ as string[], { - configuration: { - 'camel-case-expansion': false, - 'dot-notation': false, - }, - }); - // This removes the overrides from the nxArgs._ - args._ = overrides._; - - delete overrides._; - Object.entries(args).forEach(([key, value]) => { const camelCased = names(key).propertyName; if (nxSpecific.includes(camelCased) || camelCased.startsWith('nx')) { if (value !== undefined) nxArgs[camelCased] = value; - } else if (!ignoreArgs.includes(key)) { - overrides[key] = value; + } else if ( + !ignoreArgs.includes(key) && + key !== '__positional_overrides__' && + key !== '__overrides__' + ) { + overridesFromMainArgs[key] = value; } }); + let overrides; + if (explicitOverrides) { + overrides = explicitOverrides; + overrides['__overrides_unparsed__'] = args.__overrides__; + if (Object.keys(overridesFromMainArgs).length > 0) { + const s = Object.keys(overridesFromMainArgs).join(', '); + output.warn({ + title: `Nx didn't recognize the following args: ${s}`, + bodyLines: [ + "When using '--' all executor args have to be defined after '--'.", + ], + }); + } + } else { + overrides = overridesFromMainArgs; + overrides['__overrides_unparsed__'] = serializeArgsIntoCommandLine( + overridesFromMainArgs + ); + } + if (mode === 'run-many') { if (!nxArgs.projects) { nxArgs.projects = []; @@ -210,10 +243,12 @@ export function splitArgsIntoNxArgsAndOverrides( !nxArgs.base && !nxArgs.head && !nxArgs.all && - args._.length >= 3 + overridesFromMainArgs._ && + overridesFromMainArgs._.length >= 2 ) { - nxArgs.base = args._[1] as string; - nxArgs.head = args._[2] as string; + throw new Error( + `Nx no longer supports using positional arguments for base and head. Please use --base and --head instead.` + ); } // Allow setting base and head via environment variables (lower priority then direct command arguments) @@ -316,6 +351,8 @@ function getUncommittedFiles(): string[] { return parseGitOutput(`git diff --name-only --relative HEAD .`); } +``; + function getUntrackedFiles(): string[] { return parseGitOutput(`git ls-files --others --exclude-standard`); } @@ -352,3 +389,19 @@ export function getProjectRoots(projectNames: string[]): string[] { const { projects } = readAllWorkspaceConfiguration(); return projectNames.map((name) => projects[name].root); } + +export function serializeArgsIntoCommandLine(args: { + [k: string]: any; +}): string[] { + const r = args['_'] ? [...args['_']] : []; + Object.keys(args).forEach((a) => { + if (a !== '_') { + r.push( + typeof args[a] === 'string' && args[a].includes(' ') + ? `--${a}="${args[a].replace(/"/g, '"')}"` + : `--${a}=${args[a]}` + ); + } + }); + return r; +} diff --git a/packages/nx/src/utils/params.spec.ts b/packages/nx/src/utils/params.spec.ts index 55aa5d45c15bb..f36128497ff7b 100644 --- a/packages/nx/src/utils/params.spec.ts +++ b/packages/nx/src/utils/params.spec.ts @@ -647,7 +647,7 @@ describe('params', () => { describe('convertSmartDefaultsIntoNamedParams', () => { it('should use argv', () => { - const params = {}; + const params = { _: ['argv-value', 'unused'] }; convertSmartDefaultsIntoNamedParams( params, { @@ -661,12 +661,11 @@ describe('params', () => { }, }, }, - ['argv-value'], null, null ); - expect(params).toEqual({ a: 'argv-value' }); + expect(params).toEqual({ a: 'argv-value', _: ['unused'] }); }); it('should use projectName', () => { @@ -683,7 +682,6 @@ describe('params', () => { }, }, }, - [], 'myProject', null ); @@ -704,7 +702,6 @@ describe('params', () => { }, }, }, - [], null, './somepath' ); diff --git a/packages/nx/src/utils/params.ts b/packages/nx/src/utils/params.ts index 931dc7c554433..c6ae2df13addf 100644 --- a/packages/nx/src/utils/params.ts +++ b/packages/nx/src/utils/params.ts @@ -26,7 +26,10 @@ type PropertyDescription = { | string[] | { [key: string]: string | number | boolean | string[] }; $ref?: string; - $default?: { $source: 'argv'; index: number } | { $source: 'projectName' }; + $default?: + | { $source: 'argv'; index: number } + | { $source: 'projectName' } + | { $source: 'unparsed' }; additionalProperties?: boolean; 'x-prompt'?: | string @@ -541,7 +544,6 @@ export function combineOptionsForExecutor( convertSmartDefaultsIntoNamedParams( combined, schema, - (commandLineOpts['_'] as string[]) || [], defaultProjectName, relativeCwd ); @@ -579,7 +581,6 @@ export async function combineOptionsForGenerator( convertSmartDefaultsIntoNamedParams( combined, schema, - (commandLineOpts['_'] as string[]) || [], defaultProjectName, relativeCwd ); @@ -615,10 +616,11 @@ export function warnDeprecations( export function convertSmartDefaultsIntoNamedParams( opts: { [k: string]: any }, schema: Schema, - argv: string[], defaultProjectName: string | null, relativeCwd: string | null ) { + const argv = opts['_'] || []; + const usedPositionalArgs = {}; Object.entries(schema.properties).forEach(([k, v]) => { if ( opts[k] === undefined && @@ -626,7 +628,10 @@ export function convertSmartDefaultsIntoNamedParams( v.$default.$source === 'argv' && argv[v.$default.index] ) { + usedPositionalArgs[v.$default.index] = true; opts[k] = coerceType(v, argv[v.$default.index]); + } else if (v.$default !== undefined && v.$default.$source === 'unparsed') { + opts[k] = opts['__overrides_unparsed__']; } else if ( opts[k] === undefined && v.$default !== undefined && @@ -643,7 +648,18 @@ export function convertSmartDefaultsIntoNamedParams( opts[k] = relativeCwd.replace(/\\/g, '/'); } }); - delete opts['_']; + const leftOverPositionalArgs = []; + for (let i = 0; i < argv.length; ++i) { + if (!usedPositionalArgs[i]) { + leftOverPositionalArgs.push(argv[i]); + } + } + if (leftOverPositionalArgs.length === 0) { + delete opts['_']; + } else { + opts['_'] = leftOverPositionalArgs; + } + delete opts['__overrides_unparsed__']; } function getGeneratorDefaults( diff --git a/packages/workspace/index.ts b/packages/workspace/index.ts index ccc7ff6ace152..1eb2794da672c 100644 --- a/packages/workspace/index.ts +++ b/packages/workspace/index.ts @@ -55,8 +55,6 @@ export { serializeTarget, } from './src/utils/cli-config-utils'; -export { unparse } from 'nx/src/tasks-runner/utils'; - export { getWorkspace, updateWorkspace, diff --git a/packages/workspace/src/executors/run-commands/schema.json b/packages/workspace/src/executors/run-commands/schema.json index a7164ad5a2e7f..53c294e23952a 100644 --- a/packages/workspace/src/executors/run-commands/schema.json +++ b/packages/workspace/src/executors/run-commands/schema.json @@ -124,9 +124,19 @@ "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." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "unparsed" + } } }, "additionalProperties": true, - "required": [], + "required": ["__unparsed__"], "examplesFile": "../../../docs/run-commands-examples.md" } diff --git a/packages/workspace/src/executors/run-script/schema.json b/packages/workspace/src/executors/run-script/schema.json index 443f57138c1e0..dd940ac16f491 100644 --- a/packages/workspace/src/executors/run-script/schema.json +++ b/packages/workspace/src/executors/run-script/schema.json @@ -8,9 +8,19 @@ "script": { "type": "string", "description": "An npm script name in the `package.json` file of the project (e.g., `build`)." + }, + "__unparsed__": { + "hidden": true, + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "unparsed" + } } }, "additionalProperties": true, - "required": ["script"], + "required": ["script", "__unparsed__"], "examplesFile": "../../../docs/run-script-examples.md" }