cleanup(core): rework task graph creation to handle edge cases better

This commit is contained in:
Victor Savkin 2022-05-30 14:14:01 -04:00
parent 37c529d3bb
commit 912e81957f
26 changed files with 1078 additions and 2265 deletions

View File

@ -89,6 +89,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -131,6 +131,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -89,6 +89,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -107,6 +107,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -75,6 +75,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -75,6 +75,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -101,6 +101,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -71,6 +71,14 @@ Default: false
Stop command execution after the first failed task
### nx-ignore-cycles
Type: boolean
Default: false
Ignore cycles in the task graph
### ~~only-failed~~
Type: boolean

View File

@ -931,6 +931,7 @@ stored in the daemon process. To reset both run: `nx reset`.
| `context.nxJson` | [`NxJsonConfiguration`](../../devkit/index#nxjsonconfiguration)<`string`[] \| `"*"`\> |
| `context.projectGraph` | [`ProjectGraph`](../../devkit/index#projectgraph)<`any`\> |
| `context.target?` | `string` |
| `context.taskGraph?` | [`TaskGraph`](../../devkit/index#taskgraph) |
#### Returns

View File

@ -39,49 +39,49 @@
"name": "run-many",
"id": "run-many",
"file": "generated/cli/run-many",
"content": "---\ntitle: 'run-many - CLI command'\ndescription: 'Run target for multiple listed projects'\n---\n\n# run-many\n\nRun target for multiple listed projects\n\n## Usage\n\n```bash\nnx run-many\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nTest all projects:\n\n```bash\nnx run-many --target=test --all\n```\n\nTest proj1 and proj2:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2\n```\n\nTest proj1 and proj2 in parallel:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2 --parallel=2\n```\n\n## Options\n\n### all\n\nType: boolean\n\nRun the target on all projects in the workspace\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nOnly run the target on projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### projects\n\nType: string\n\nProjects to run (comma delimited)\n\n### runner\n\nType: string\n\nOverride the tasks runner in `nx.json`\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'run-many - CLI command'\ndescription: 'Run target for multiple listed projects'\n---\n\n# run-many\n\nRun target for multiple listed projects\n\n## Usage\n\n```bash\nnx run-many\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nTest all projects:\n\n```bash\nnx run-many --target=test --all\n```\n\nTest proj1 and proj2:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2\n```\n\nTest proj1 and proj2 in parallel:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2 --parallel=2\n```\n\n## Options\n\n### all\n\nType: boolean\n\nRun the target on all projects in the workspace\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nOnly run the target on projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### projects\n\nType: string\n\nProjects to run (comma delimited)\n\n### runner\n\nType: string\n\nOverride the tasks runner in `nx.json`\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "affected",
"id": "affected",
"file": "generated/cli/affected",
"content": "---\ntitle: 'affected - CLI command'\ndescription: 'Run target for affected projects'\n---\n\n# affected\n\nRun target for affected projects\n\n## Usage\n\n```bash\nnx affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nRun custom target for all affected projects:\n\n```bash\nnx affected --target=custom-target\n```\n\nRun tests in parallel:\n\n```bash\nnx affected --target=test --parallel=5\n```\n\nRun the test target for all projects:\n\n```bash\nnx affected --target=test --all\n```\n\nRun tests for all the projects affected by changing the index.ts file:\n\n```bash\nnx affected --target=test --files=libs/mylib/src/index.ts\n```\n\nRun tests for all the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected --target=test --base=main --head=HEAD\n```\n\nRun tests for all the projects affected by the last commit on main:\n\n```bash\nnx affected --target=test --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'affected - CLI command'\ndescription: 'Run target for affected projects'\n---\n\n# affected\n\nRun target for affected projects\n\n## Usage\n\n```bash\nnx affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nRun custom target for all affected projects:\n\n```bash\nnx affected --target=custom-target\n```\n\nRun tests in parallel:\n\n```bash\nnx affected --target=test --parallel=5\n```\n\nRun the test target for all projects:\n\n```bash\nnx affected --target=test --all\n```\n\nRun tests for all the projects affected by changing the index.ts file:\n\n```bash\nnx affected --target=test --files=libs/mylib/src/index.ts\n```\n\nRun tests for all the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected --target=test --base=main --head=HEAD\n```\n\nRun tests for all the projects affected by the last commit on main:\n\n```bash\nnx affected --target=test --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "affected:graph",
"id": "affected-dep-graph",
"file": "generated/cli/affected-graph",
"content": "---\ntitle: 'affected:graph - CLI command'\ndescription: 'Graph dependencies affected by changes'\n---\n\n# affected:graph\n\nGraph dependencies affected by changes\n\n## Usage\n\n```bash\nnx affected:graph\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by changing the index.ts file:\n\n```bash\nnx affected:graph --files=libs/mylib/src/index.ts\n```\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD\n```\n\nSave the project graph of the workspace in a json file, and highlight the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD --file=output.json\n```\n\nGenerate a static website with project graph data in an html file, highlighting the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD --file=output.html\n```\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by the last commit on main:\n\n```bash\nnx affected:graph --base=main~1 --head=main\n```\n\nOpen the project graph of the workspace in the browser, highlight the projects affected, but exclude project-one and project-two:\n\n```bash\nnx affected:graph --exclude=project-one,project-two\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### file\n\nType: string\n\nOutput file (e.g. --file=output.json or --file=dep-graph.html)\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### focus\n\nType: string\n\nUse to show the project graph for a particular project and every node that is either an ancestor or a descendant.\n\n### groupByFolder\n\nType: boolean\n\nGroup projects by folder in the project graph\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### host\n\nType: string\n\nBind the project graph server to a specific ip address.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### open\n\nType: boolean\n\nDefault: true\n\nOpen the project graph in the browser.\n\n### port\n\nType: number\n\nBind the project graph server to a specific port.\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n\n### watch\n\nType: boolean\n\nDefault: false\n\nWatch for changes to project graph and update in-browser\n"
"content": "---\ntitle: 'affected:graph - CLI command'\ndescription: 'Graph dependencies affected by changes'\n---\n\n# affected:graph\n\nGraph dependencies affected by changes\n\n## Usage\n\n```bash\nnx affected:graph\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by changing the index.ts file:\n\n```bash\nnx affected:graph --files=libs/mylib/src/index.ts\n```\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD\n```\n\nSave the project graph of the workspace in a json file, and highlight the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD --file=output.json\n```\n\nGenerate a static website with project graph data in an html file, highlighting the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:graph --base=main --head=HEAD --file=output.html\n```\n\nOpen the project graph of the workspace in the browser, and highlight the projects affected by the last commit on main:\n\n```bash\nnx affected:graph --base=main~1 --head=main\n```\n\nOpen the project graph of the workspace in the browser, highlight the projects affected, but exclude project-one and project-two:\n\n```bash\nnx affected:graph --exclude=project-one,project-two\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### file\n\nType: string\n\nOutput file (e.g. --file=output.json or --file=dep-graph.html)\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### focus\n\nType: string\n\nUse to show the project graph for a particular project and every node that is either an ancestor or a descendant.\n\n### groupByFolder\n\nType: boolean\n\nGroup projects by folder in the project graph\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### host\n\nType: string\n\nBind the project graph server to a specific ip address.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### open\n\nType: boolean\n\nDefault: true\n\nOpen the project graph in the browser.\n\n### port\n\nType: number\n\nBind the project graph server to a specific port.\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n\n### watch\n\nType: boolean\n\nDefault: false\n\nWatch for changes to project graph and update in-browser\n"
},
{
"name": "affected:apps",
"id": "affected-apps",
"file": "generated/cli/affected-apps",
"content": "---\ntitle: 'affected:apps - CLI command'\ndescription: 'Print applications affected by changes'\n---\n\n# affected:apps\n\nPrint applications affected by changes\n\n## Usage\n\n```bash\nnx affected:apps\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint the names of all the apps affected by changing the index.ts file:\n\n```bash\nnx affected:apps --files=libs/mylib/src/index.ts\n```\n\nPrint the names of all the apps affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:apps --base=main --head=HEAD\n```\n\nPrint the names of all the apps affected by the last commit on main:\n\n```bash\nnx affected:apps --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### plain\n\nProduces a plain output for affected:apps and affected:libs\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'affected:apps - CLI command'\ndescription: 'Print applications affected by changes'\n---\n\n# affected:apps\n\nPrint applications affected by changes\n\n## Usage\n\n```bash\nnx affected:apps\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint the names of all the apps affected by changing the index.ts file:\n\n```bash\nnx affected:apps --files=libs/mylib/src/index.ts\n```\n\nPrint the names of all the apps affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:apps --base=main --head=HEAD\n```\n\nPrint the names of all the apps affected by the last commit on main:\n\n```bash\nnx affected:apps --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### plain\n\nProduces a plain output for affected:apps and affected:libs\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "affected:libs",
"id": "affected-libs",
"file": "generated/cli/affected-libs",
"content": "---\ntitle: 'affected:libs - CLI command'\ndescription: 'Print libraries affected by changes'\n---\n\n# affected:libs\n\nPrint libraries affected by changes\n\n## Usage\n\n```bash\nnx affected:libs\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint the names of all the libs affected by changing the index.ts file:\n\n```bash\nnx affected:libs --files=libs/mylib/src/index.ts\n```\n\nPrint the names of all the libs affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:libs --base=main --head=HEAD\n```\n\nPrint the names of all the libs affected by the last commit on main:\n\n```bash\nnx affected:libs --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### plain\n\nProduces a plain output for affected:apps and affected:libs\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'affected:libs - CLI command'\ndescription: 'Print libraries affected by changes'\n---\n\n# affected:libs\n\nPrint libraries affected by changes\n\n## Usage\n\n```bash\nnx affected:libs\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint the names of all the libs affected by changing the index.ts file:\n\n```bash\nnx affected:libs --files=libs/mylib/src/index.ts\n```\n\nPrint the names of all the libs affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected:libs --base=main --head=HEAD\n```\n\nPrint the names of all the libs affected by the last commit on main:\n\n```bash\nnx affected:libs --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### plain\n\nProduces a plain output for affected:apps and affected:libs\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "print-affected",
"id": "print-affected",
"file": "generated/cli/print-affected",
"content": "---\ntitle: 'print-affected - CLI command'\ndescription: 'Prints information about the projects and targets affected by changes'\n---\n\n# print-affected\n\nPrints information about the projects and targets affected by changes\n\n## Usage\n\n```bash\nnx print-affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint information about affected projects and the project graph:\n\n```bash\nnx print-affected\n```\n\nPrint information about the projects affected by the changes between main and HEAD (e.g,. PR):\n\n```bash\nnx print-affected --base=main --head=HEAD\n```\n\nPrints information about the affected projects and a list of tasks to test them:\n\n```bash\nnx print-affected --target=test\n```\n\nPrints the projects property from the print-affected output:\n\n```bash\nnx print-affected --target=build --select=projects\n```\n\nPrints the tasks.target.project property from the print-affected output:\n\n```bash\nnx print-affected --target=build --select=tasks.target.project\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### select\n\nType: string\n\nSelect the subset of the returned json document (e.g., --selected=projects)\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'print-affected - CLI command'\ndescription: 'Prints information about the projects and targets affected by changes'\n---\n\n# print-affected\n\nPrints information about the projects and targets affected by changes\n\n## Usage\n\n```bash\nnx print-affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nPrint information about affected projects and the project graph:\n\n```bash\nnx print-affected\n```\n\nPrint information about the projects affected by the changes between main and HEAD (e.g,. PR):\n\n```bash\nnx print-affected --base=main --head=HEAD\n```\n\nPrints information about the affected projects and a list of tasks to test them:\n\n```bash\nnx print-affected --target=test\n```\n\nPrints the projects property from the print-affected output:\n\n```bash\nnx print-affected --target=build --select=projects\n```\n\nPrints the tasks.target.project property from the print-affected output:\n\n```bash\nnx print-affected --target=build --select=tasks.target.project\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### select\n\nType: string\n\nSelect the subset of the returned json document (e.g., --selected=projects)\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "format:check",
"id": "format-check",
"file": "generated/cli/format-check",
"content": "---\ntitle: 'format:check - CLI command'\ndescription: 'Check for un-formatted files'\n---\n\n# format:check\n\nCheck for un-formatted files\n\n## Usage\n\n```bash\nnx format:check\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### libs-and-apps\n\nType: boolean\n\nFormat only libraries and applications files.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### projects\n\nType: array\n\nProjects to format (comma delimited)\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'format:check - CLI command'\ndescription: 'Check for un-formatted files'\n---\n\n# format:check\n\nCheck for un-formatted files\n\n## Usage\n\n```bash\nnx format:check\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### libs-and-apps\n\nType: boolean\n\nFormat only libraries and applications files.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### projects\n\nType: array\n\nProjects to format (comma delimited)\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "format:write",
"id": "format-write",
"file": "generated/cli/format-write",
"content": "---\ntitle: 'format:write - CLI command'\ndescription: 'Overwrite un-formatted files'\n---\n\n# format:write\n\nOverwrite un-formatted files\n\n## Usage\n\n```bash\nnx format:write\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### libs-and-apps\n\nType: boolean\n\nFormat only libraries and applications files.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### projects\n\nType: array\n\nProjects to format (comma delimited)\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
"content": "---\ntitle: 'format:write - CLI command'\ndescription: 'Overwrite un-formatted files'\n---\n\n# format:write\n\nOverwrite un-formatted files\n\n## Usage\n\n```bash\nnx format:write\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### libs-and-apps\n\nType: boolean\n\nFormat only libraries and applications files.\n\n### nx-bail\n\nType: boolean\n\nDefault: false\n\nStop command execution after the first failed task\n\n### nx-ignore-cycles\n\nType: boolean\n\nDefault: false\n\nIgnore cycles in the task graph\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### projects\n\nType: array\n\nProjects to format (comma delimited)\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n"
},
{
"name": "migrate",

File diff suppressed because one or more lines are too long

View File

@ -96,13 +96,6 @@ describe('run-one', () => {
);
}, 10000);
it('should error for invalid configurations', () => {
const myapp = uniq('app');
runCLI(`generate @nrwl/react:app ${myapp}`);
// configuration has to be valid for the initiating project
expect(() => runCLI(`build ${myapp} -c=invalid`)).toThrow();
}, 10000);
describe('target dependencies', () => {
let myapp;
let mylib1;

View File

@ -434,6 +434,11 @@ function withAffectedOptions(yargs: yargs.Argv): yargs.Argv {
type: 'boolean',
default: false,
})
.option('nx-ignore-cycles', {
describe: 'Ignore cycles in the task graph',
type: 'boolean',
default: false,
})
.conflicts({
files: ['uncommitted', 'untracked', 'base', 'head', 'all'],
untracked: ['uncommitted', 'files', 'base', 'head', 'all'],
@ -499,6 +504,11 @@ function withRunManyOptions(yargs: yargs.Argv): yargs.Argv {
type: 'boolean',
default: false,
})
.option('nx-ignore-cycles', {
describe: 'Ignore cycles in the task graph',
type: 'boolean',
default: false,
})
.conflicts({
all: 'projects',
});
@ -654,6 +664,11 @@ function withRunOneOptions(yargs: yargs.Argv) {
describe: 'Stop command execution after the first failed task',
type: 'boolean',
default: false,
})
.option('nx-ignore-cycles', {
describe: 'Ignore cycles in the task graph',
type: 'boolean',
default: false,
});
if (executorShouldShowHelp) {

View File

@ -1,10 +1,10 @@
import { createTask } from '../tasks-runner/run-command';
import { getCommandAsString, getOutputs } from '../tasks-runner/utils';
import * as yargs from 'yargs';
import type { NxArgs } from '../utils/command-line-utils';
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
import { Task } from '../config/task-graph';
import { Environment } from './read-environment';
import { ProcessTasks } from 'nx/src/tasks-runner/create-task-graph';
export async function printAffected(
affectedProjectsWithTargetAndConfig: ProjectGraphProjectNode[],
@ -40,14 +40,21 @@ async function createTasks(
overrides: yargs.Arguments
) {
const tasks: Task[] = affectedProjectsWithTargetAndConfig.map(
(affectedProject) =>
createTask({
project: affectedProject,
target: nxArgs.target,
configuration: nxArgs.configuration,
overrides,
errorIfCannotFindConfiguration: false,
})
(affectedProject) => {
const p = new ProcessTasks({}, projectGraph);
const resolvedConfiguration = p.resolveConfiguration(
affectedProject,
nxArgs.target,
nxArgs.configuration
);
return p.createTask(
p.getId(affectedProject.name, nxArgs.target, resolvedConfiguration),
affectedProject,
nxArgs.target,
resolvedConfiguration,
overrides
);
}
);
return tasks.map((task, index) => ({

View File

@ -1,397 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TaskGraphCreator (tasks with dependency configurations) should create a task graph (builds depend on builds of dependencies even with intermediate projects) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common2:build",
],
"common2:build": Array [],
},
"roots": Array [
"common2:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects) 2 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common3:build",
],
"common3:build": Array [],
},
"roots": Array [
"common3:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"common3:build": Object {
"id": "common3:build",
"overrides": Object {},
"projectRoot": "common3",
"target": Object {
"configuration": undefined,
"project": "common3",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create task graph (builds depend on build of dependencies and prebuild of self) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common1:build",
"app1:prebuild",
],
"app1:prebuild": Array [],
"app2:build": Array [
"common2:build",
"app2:prebuild",
],
"app2:prebuild": Array [],
"common1:build": Array [],
"common2:build": Array [],
},
"roots": Array [
"common1:build",
"app1:prebuild",
"common2:build",
"app2:prebuild",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"app1:prebuild": Object {
"id": "app1:prebuild",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "prebuild",
},
},
"app2:build": Object {
"id": "app2:build",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "build",
},
},
"app2:prebuild": Object {
"id": "app2:prebuild",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "prebuild",
},
},
"common1:build": Object {
"id": "common1:build",
"overrides": Object {},
"projectRoot": "common1-root",
"target": Object {
"configuration": undefined,
"project": "common1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create task graph (builds depend on build of dependencies) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common1:build",
],
"app2:build": Array [
"common2:build",
],
"common1:build": Array [],
"common2:build": Array [],
},
"roots": Array [
"common1:build",
"common2:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"app2:build": Object {
"id": "app2:build",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "build",
},
},
"common1:build": Object {
"id": "common1:build",
"overrides": Object {},
"projectRoot": "common1-root",
"target": Object {
"configuration": undefined,
"project": "common1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create task graph (builds depend on build of dependencies, builds depend on prebuilds) 1`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common1:build",
"app1:prebuild",
],
"app1:prebuild": Array [
"common1:build",
],
"app2:build": Array [
"common2:build",
"app2:prebuild",
],
"app2:prebuild": Array [
"common2:build",
],
"common1:build": Array [],
"common2:build": Array [],
},
"roots": Array [
"common1:build",
"common2:build",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"app1:prebuild": Object {
"id": "app1:prebuild",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "prebuild",
},
},
"app2:build": Object {
"id": "app2:build",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "build",
},
},
"app2:prebuild": Object {
"id": "app2:prebuild",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "prebuild",
},
},
"common1:build": Object {
"id": "common1:build",
"overrides": Object {},
"projectRoot": "common1-root",
"target": Object {
"configuration": undefined,
"project": "common1",
"target": "build",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
},
}
`;
exports[`TaskGraphCreator (tasks with dependency configurations) should create task graph (builds depend on build of dependencies, builds depend on prebuilds) 2`] = `
Object {
"dependencies": Object {
"app1:build": Array [
"common1:build",
],
"app2:build": Array [
"common2:build",
],
"common1:build": Array [
"common1:prebuild",
],
"common1:prebuild": Array [],
"common2:build": Array [
"common2:prebuild",
],
"common2:prebuild": Array [],
},
"roots": Array [
"common1:prebuild",
"common2:prebuild",
],
"tasks": Object {
"app1:build": Object {
"id": "app1:build",
"overrides": Object {},
"projectRoot": "app1-root",
"target": Object {
"configuration": undefined,
"project": "app1",
"target": "build",
},
},
"app2:build": Object {
"id": "app2:build",
"overrides": Object {},
"projectRoot": "app2-root",
"target": Object {
"configuration": undefined,
"project": "app2",
"target": "build",
},
},
"common1:build": Object {
"id": "common1:build",
"overrides": Object {},
"projectRoot": "common1-root",
"target": Object {
"configuration": undefined,
"project": "common1",
"target": "build",
},
},
"common1:prebuild": Object {
"id": "common1:prebuild",
"overrides": Object {},
"projectRoot": "common1-root",
"target": Object {
"configuration": undefined,
"project": "common1",
"target": "prebuild",
},
},
"common2:build": Object {
"id": "common2:build",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "build",
},
},
"common2:prebuild": Object {
"id": "common2:prebuild",
"overrides": Object {},
"projectRoot": "common2-root",
"target": Object {
"configuration": undefined,
"project": "common2",
"target": "prebuild",
},
},
},
}
`;

View File

@ -0,0 +1,568 @@
import { ProjectGraph } from '../config/project-graph';
import { createTaskGraph } from 'nx/src/tasks-runner/create-task-graph';
describe('createTaskGraph', () => {
let projectGraph: ProjectGraph;
beforeEach(() => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
prebuild: {},
build: {
dependsOn: [
{
projects: 'dependencies',
target: 'build',
},
{
projects: 'self',
target: 'prebuild',
},
],
},
test: {},
serve: {},
},
},
},
lib1: {
name: 'lib1',
type: 'lib',
data: {
root: 'lib1-root',
files: [],
targets: {
build: {},
test: {},
},
},
},
},
dependencies: {
app1: [{ source: 'app1', target: 'lib1', type: 'static' }],
lib1: [],
},
};
});
it('should return an empty task for an empty project graph', () => {
const tasks = createTaskGraph(
{ nodes: {}, dependencies: {} },
{},
[],
['test'],
'development',
{}
);
expect(tasks).toEqual({
roots: [],
tasks: {},
dependencies: {},
});
});
it('should return a task per project with the given target', () => {
const oneTask = createTaskGraph(
projectGraph,
{},
['app1'],
['test'],
'development',
{
a: 123,
}
);
expect(oneTask).toEqual({
roots: ['app1:test'],
tasks: {
'app1:test': {
id: 'app1:test',
target: {
project: 'app1',
target: 'test',
},
overrides: { a: 123 },
projectRoot: 'app1-root',
},
},
dependencies: {
'app1:test': [],
},
});
const twoTasks = createTaskGraph(
projectGraph,
{},
['app1', 'lib1'],
['test'],
'development',
{
a: 123,
}
);
expect(twoTasks).toEqual({
roots: ['app1:test', 'lib1:test'],
tasks: {
'app1:test': {
id: 'app1:test',
target: {
project: 'app1',
target: 'test',
},
overrides: { a: 123 },
projectRoot: 'app1-root',
},
'lib1:test': {
id: 'lib1:test',
target: {
project: 'lib1',
target: 'test',
},
overrides: { a: 123 },
projectRoot: 'lib1-root',
},
},
dependencies: {
'app1:test': [],
'lib1:test': [],
},
});
});
it('should interpolate overrides', () => {
const oneTask = createTaskGraph(
projectGraph,
{},
['app1'],
['test'],
'development',
{
a: '--value={project.root}',
}
);
expect(oneTask).toEqual({
roots: ['app1:test'],
tasks: {
'app1:test': {
id: 'app1:test',
target: {
project: 'app1',
target: 'test',
},
overrides: { a: '--value=app1-root' },
projectRoot: 'app1-root',
},
},
dependencies: {
'app1:test': [],
},
});
});
it('should create graphs with dependencies', () => {
const taskGraph = createTaskGraph(
projectGraph,
{},
['app1'],
['build'],
'development',
{}
);
// prebuild should also be in here
expect(taskGraph).toEqual({
roots: ['lib1:build', 'app1:prebuild'],
tasks: {
'app1:build': {
id: 'app1:build',
target: {
project: 'app1',
target: 'build',
},
overrides: {},
projectRoot: 'app1-root',
},
'app1:prebuild': {
id: 'app1:prebuild',
target: {
project: 'app1',
target: 'prebuild',
},
overrides: {},
projectRoot: 'app1-root',
},
'lib1:build': {
id: 'lib1:build',
target: {
project: 'lib1',
target: 'build',
},
overrides: {},
projectRoot: 'lib1-root',
},
},
dependencies: {
'app1:build': ['lib1:build', 'app1:prebuild'],
'app1:prebuild': [],
'lib1:build': [],
},
});
});
it('should handle diamond shape dependencies', () => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
build: {},
},
},
},
lib1: {
name: 'lib1',
type: 'lib',
data: {
root: 'lib1-root',
files: [],
targets: {
build: {},
},
},
},
lib2: {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
},
lib3: {
name: 'lib3',
type: 'lib',
data: {
root: 'lib3-root',
files: [],
targets: {
build: {},
},
},
},
},
dependencies: {
app1: [
{ source: 'app1', target: 'lib1', type: 'static' },
{ source: 'app1', target: 'lib2', type: 'static' },
],
lib1: [{ source: 'lib1', target: 'lib3', type: 'static' }],
lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }],
lib3: [],
},
};
const taskGraph = createTaskGraph(
projectGraph,
{
build: [
{
projects: 'dependencies',
target: 'build',
},
],
},
['app1'],
['build'],
'development',
{}
);
// prebuild should also be in here
expect(taskGraph).toEqual({
roots: ['lib3:build'],
tasks: {
'app1:build': {
id: 'app1:build',
target: {
project: 'app1',
target: 'build',
},
overrides: {},
projectRoot: 'app1-root',
},
'lib1:build': {
id: 'lib1:build',
target: {
project: 'lib1',
target: 'build',
},
overrides: {},
projectRoot: 'lib1-root',
},
'lib2:build': {
id: 'lib2:build',
target: {
project: 'lib2',
target: 'build',
},
overrides: {},
projectRoot: 'lib2-root',
},
'lib3:build': {
id: 'lib3:build',
target: {
project: 'lib3',
target: 'build',
},
overrides: {},
projectRoot: 'lib3-root',
},
},
dependencies: {
'app1:build': ['lib1:build', 'lib2:build'],
'lib1:build': ['lib3:build'],
'lib2:build': ['lib3:build'],
'lib3:build': [],
},
});
});
it('should handle cycles within the same project', () => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
build: {
dependsOn: [{ target: 'test', projects: 'self' }],
},
test: {
dependsOn: [{ target: 'build', projects: 'self' }],
},
},
},
},
},
dependencies: {},
};
const taskGraph = createTaskGraph(
projectGraph,
{},
['app1'],
['build'],
'development',
{}
);
// prebuild should also be in here
expect(taskGraph).toEqual({
roots: [],
tasks: {
'app1:build': {
id: 'app1:build',
target: {
project: 'app1',
target: 'build',
},
overrides: {},
projectRoot: 'app1-root',
},
'app1:test': {
id: 'app1:test',
target: {
project: 'app1',
target: 'test',
},
overrides: {},
projectRoot: 'app1-root',
},
},
dependencies: {
'app1:build': ['app1:test'],
'app1:test': ['app1:build'],
},
});
});
it('should handle cycles between projects (app1:build <-> app2 <-> app3:build)', () => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
build: {},
},
},
},
app2: {
name: 'app2',
type: 'app',
data: {
root: 'app2-root',
files: [],
targets: {},
},
},
app3: {
name: 'app3',
type: 'app',
data: {
root: 'app3-root',
files: [],
targets: {
build: {},
},
},
},
},
dependencies: {
app1: [{ source: 'app1', target: 'app2', type: 'static' }],
app2: [
{ source: 'app2', target: 'app1', type: 'static' },
{ source: 'app2', target: 'app3', type: 'static' },
],
app3: [{ source: 'app3', target: 'app2', type: 'static' }],
},
};
const taskGraph = createTaskGraph(
projectGraph,
{
build: [{ target: 'build', projects: 'dependencies' }],
},
['app1'],
['build'],
'development',
{}
);
expect(taskGraph).toEqual({
roots: [],
tasks: {
'app1:build': {
id: 'app1:build',
target: {
project: 'app1',
target: 'build',
},
overrides: {},
projectRoot: 'app1-root',
},
'app3:build': {
id: 'app3:build',
target: {
project: 'app3',
target: 'build',
},
overrides: {},
projectRoot: 'app3-root',
},
},
dependencies: {
'app1:build': ['app3:build'],
'app3:build': ['app1:build'],
},
});
});
it('should handle cycles between projects that do not create cycles between tasks (app1:build -> app2 <-> app3:build)``', () => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
build: {},
},
},
},
app2: {
name: 'app2',
type: 'app',
data: {
root: 'app2-root',
files: [],
targets: {},
},
},
app3: {
name: 'app3',
type: 'app',
data: {
root: 'app3-root',
files: [],
targets: {
build: {},
},
},
},
},
dependencies: {
app1: [{ source: 'app1', target: 'app2', type: 'static' }],
app2: [{ source: 'app2', target: 'app3', type: 'static' }],
app3: [{ source: 'app3', target: 'app2', type: 'static' }],
},
};
const taskGraph = createTaskGraph(
projectGraph,
{
build: [{ target: 'build', projects: 'dependencies' }],
},
['app1'],
['build'],
'development',
{}
);
expect(taskGraph).toEqual({
roots: ['app3:build'],
tasks: {
'app1:build': {
id: 'app1:build',
target: {
project: 'app1',
target: 'build',
},
overrides: {},
projectRoot: 'app1-root',
},
'app3:build': {
id: 'app3:build',
target: {
project: 'app3',
target: 'build',
},
overrides: {},
projectRoot: 'app3-root',
},
},
dependencies: {
'app1:build': ['app3:build'],
'app3:build': [],
},
});
});
});

View File

@ -0,0 +1,241 @@
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
import { TargetDependencyConfig } from '../config/workspace-json-project-json';
import { getDependencyConfigs } from './utils';
import {
projectHasTarget,
projectHasTargetAndConfiguration,
} from '../utils/project-graph-utils';
import { Task, TaskGraph } from '../config/task-graph';
export class ProcessTasks {
private readonly seen = new Set<string>();
readonly tasks: { [id: string]: Task } = {};
readonly dependencies: { [k: string]: string[] } = {};
constructor(
private readonly defaultDependencyConfigs: Record<
string,
(TargetDependencyConfig | string)[]
>,
private readonly projectGraph: ProjectGraph
) {}
processTasks(
projectNames: string[],
targets: string[],
configuration: string,
overrides: Object
) {
for (const projectName of projectNames) {
for (const target of targets) {
const resolvedConfiguration = this.resolveConfiguration(
this.projectGraph.nodes[projectName],
target,
configuration
);
const id = this.getId(projectName, target, resolvedConfiguration);
debugger;
const task = this.createTask(
id,
this.projectGraph.nodes[projectName],
target,
resolvedConfiguration,
overrides
);
this.tasks[task.id] = task;
this.dependencies[task.id] = [];
}
}
for (const taskId of Object.keys(this.tasks)) {
const task = this.tasks[taskId];
this.processTask(task, task.target.project, configuration);
}
return Object.keys(this.dependencies).filter(
(d) => this.dependencies[d].length === 0
);
}
processTask(
task: Task,
projectUsedToDeriveDependencies: string,
configuration: string
) {
const seenKey = `${task.id}-${projectUsedToDeriveDependencies}`;
if (this.seen.has(seenKey)) {
return;
}
this.seen.add(seenKey);
const dependencyConfigs = getDependencyConfigs(
{ project: task.target.project, target: task.target.target },
this.defaultDependencyConfigs,
this.projectGraph
);
for (const dependencyConfig of dependencyConfigs) {
if (dependencyConfig.projects === 'dependencies') {
for (const dep of this.projectGraph.dependencies[
projectUsedToDeriveDependencies
]) {
const depProject = this.projectGraph.nodes[
dep.target
] as ProjectGraphProjectNode;
// this is to handle external dependencies
if (!depProject) continue;
if (projectHasTarget(depProject, dependencyConfig.target)) {
const resolvedConfiguration = this.resolveConfiguration(
depProject,
dependencyConfig.target,
configuration
);
const depTargetId = this.getId(
depProject.name,
dependencyConfig.target,
resolvedConfiguration
);
if (task.id !== depTargetId) {
this.dependencies[task.id].push(depTargetId);
}
if (!this.tasks[depTargetId]) {
const newTask = this.createTask(
depTargetId,
depProject,
dependencyConfig.target,
resolvedConfiguration,
{}
);
this.tasks[depTargetId] = newTask;
this.dependencies[depTargetId] = [];
this.processTask(newTask, newTask.target.project, configuration);
}
} else {
this.processTask(task, depProject.name, configuration);
}
}
} else {
const selfProject = this.projectGraph.nodes[
task.target.project
] as ProjectGraphProjectNode;
if (projectHasTarget(selfProject, dependencyConfig.target)) {
const resolvedConfiguration = this.resolveConfiguration(
selfProject,
dependencyConfig.target,
configuration
);
const selfTaskId = this.getId(
selfProject.name,
dependencyConfig.target,
resolvedConfiguration
);
if (task.id !== selfTaskId) {
this.dependencies[task.id].push(selfTaskId);
}
if (!this.tasks[selfTaskId]) {
const newTask = this.createTask(
selfTaskId,
selfProject,
dependencyConfig.target,
resolvedConfiguration,
{}
);
this.tasks[selfTaskId] = newTask;
this.dependencies[selfTaskId] = [];
this.processTask(newTask, newTask.target.project, configuration);
}
}
}
}
}
createTask(
id: string,
project: ProjectGraphProjectNode,
target: string,
resolvedConfiguration: string | undefined,
overrides: Object
): Task {
const qualifiedTarget = {
project: project.name,
target,
configuration: resolvedConfiguration,
};
return {
id,
target: qualifiedTarget,
projectRoot: project.data.root,
overrides: interpolateOverrides(overrides, project.name, project.data),
};
}
resolveConfiguration(
project: ProjectGraphProjectNode,
target: string,
configuration: string | undefined
) {
configuration ??= project.data.targets?.[target]?.defaultConfiguration;
return projectHasTargetAndConfiguration(project, target, configuration)
? configuration
: undefined;
}
getId(
project: string,
target: string,
configuration: string | undefined
): string {
let id = `${project}:${target}`;
if (configuration) {
id += `:${configuration}`;
}
return id;
}
}
export function createTaskGraph(
projectGraph: ProjectGraph,
defaultDependencyConfigs: Record<
string,
(TargetDependencyConfig | string)[]
> = {},
projectNames: string[],
targets: string[],
configuration: string | undefined,
overrides: Object
): TaskGraph {
const p = new ProcessTasks(defaultDependencyConfigs, projectGraph);
const roots = p.processTasks(projectNames, targets, configuration, overrides);
return {
roots,
tasks: p.tasks,
dependencies: p.dependencies,
};
}
function interpolateOverrides<T = any>(
args: T,
projectName: string,
projectMetadata: any
): T {
const interpolatedArgs: T = { ...args };
Object.entries(interpolatedArgs).forEach(([name, value]) => {
if (typeof value === 'string') {
const regex = /{project\.([^}]+)}/g;
interpolatedArgs[name] = value.replace(regex, (_, group: string) => {
if (group.includes('.')) {
throw new Error('Only top-level properties can be interpolated');
}
if (group === 'name') {
return projectName;
}
return projectMetadata[group];
});
}
});
return interpolatedArgs;
}

View File

@ -1,12 +1,11 @@
import { TasksRunner, TaskStatus } from './tasks-runner';
import { TaskOrchestrator } from './task-orchestrator';
import { performance } from 'perf_hooks';
import { TaskGraphCreator } from './task-graph-creator';
import { Hasher } from '../hasher/hasher';
import { LifeCycle } from './life-cycle';
import { ProjectGraph } from '../config/project-graph';
import { NxJsonConfiguration } from '../config/nx-json';
import { Task } from '../config/task-graph';
import { Task, TaskGraph } from '../config/task-graph';
import { NxArgs } from '../utils/command-line-utils';
export interface RemoteCache {
@ -37,6 +36,7 @@ export const defaultTasksRunner: TasksRunner<
projectGraph: ProjectGraph;
nxJson: NxJsonConfiguration;
nxArgs: NxArgs;
taskGraph: TaskGraph;
}
): Promise<{ [id: string]: TaskStatus }> => {
if (
@ -72,17 +72,10 @@ async function runAllTasks(
projectGraph: ProjectGraph;
nxJson: NxJsonConfiguration;
nxArgs: NxArgs;
taskGraph: TaskGraph;
}
): Promise<{ [id: string]: TaskStatus }> {
const defaultTargetDependencies = context.nxJson.targetDependencies ?? {};
const taskGraphCreator = new TaskGraphCreator(
context.projectGraph,
defaultTargetDependencies
);
const taskGraph = taskGraphCreator.createTaskGraph(tasks);
// TODO: vsavkin: remove this after Nx 16
performance.mark('task-graph-created');
performance.measure('nx-prep-work', 'init-local', 'task-graph-created');
@ -98,7 +91,7 @@ async function runAllTasks(
hasher,
context.initiatingProject,
context.projectGraph,
taskGraph,
context.taskGraph,
options,
context.nxArgs?.nxBail
);

View File

@ -1,858 +1,7 @@
import { TasksRunner } from './tasks-runner';
import defaultTaskRunner from './default-tasks-runner';
import { createTasksForProjectToRun, getRunner } from './run-command';
import { DependencyType, ProjectGraph } from '../config/project-graph';
import { getRunner } from './run-command';
import { NxJsonConfiguration } from '../config/nx-json';
describe('createTasksForProjectToRun', () => {
let projectGraph: ProjectGraph;
beforeEach(() => {
projectGraph = {
nodes: {
app1: {
name: 'app1',
type: 'app',
data: {
root: 'app1-root',
files: [],
targets: {
prebuild: {},
build: {},
serve: {},
},
},
},
lib1: {
name: 'lib1',
type: 'lib',
data: {
root: 'lib1-root',
files: [],
targets: {
build: {},
},
},
},
},
dependencies: {
app1: [],
lib1: [],
},
};
});
it('should create the task for the project and target passed', () => {
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'serve',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
expect(tasks).toEqual([
{
id: 'app1:serve',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'serve',
},
},
]);
});
it('should create the task for multiple projects passed', () => {
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.lib1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
expect(tasks).toEqual([
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
]);
});
it('should create the tasks for multiple projects passed with configuration', () => {
projectGraph.nodes.app1.data.targets.build.configurations =
projectGraph.nodes.lib1.data.targets.build.configurations = {
production: {},
};
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.lib1],
{
target: 'build',
configuration: 'production',
overrides: {},
},
projectGraph,
null
);
expect(tasks).toEqual([
{
id: 'app1:build:production',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: 'production',
project: 'app1',
target: 'build',
},
},
{
id: 'lib1:build:production',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: 'production',
project: 'lib1',
target: 'build',
},
},
]);
});
it('should create the tasks for multiple projects passed with configuration and fallback to default configuration', () => {
projectGraph.nodes.app1.data.targets.build.configurations = {
production: {},
};
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.lib1],
{
target: 'build',
configuration: 'production',
overrides: {},
},
projectGraph,
null
);
expect(tasks).toEqual([
{
id: 'app1:build:production',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: 'production',
project: 'app1',
target: 'build',
},
},
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
project: 'lib1',
target: 'build',
},
},
]);
});
it('should create tasks for self dependencies', () => {
projectGraph.nodes.app1.data.targets.serve.dependsOn = [
{
target: 'build',
projects: 'self',
},
];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'serve',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
{
id: 'app1:serve',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'serve',
},
},
]);
});
it('should create tasks for target dependencies', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
]);
});
it('should create tasks for multiple sets of dependencies', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'prebuild',
projects: 'self',
},
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'app1:prebuild',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'prebuild',
},
},
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
]);
});
it('should create tasks for multiple sets of dependencies for multiple targets', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'prebuild',
projects: 'dependencies',
},
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
projectGraph.nodes.lib1.data.targets.prebuild = {};
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'lib1:prebuild',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'prebuild',
},
},
{
id: 'lib1:build',
overrides: {},
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
{
id: 'app1:build',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
]);
});
it('should include dependencies of projects without the same target', () => {
// App 1 depends on builds of its dependencies
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
// Lib 1 does not have build but depends on Lib 2
delete projectGraph.nodes.lib1.data.targets.build;
projectGraph.dependencies.lib1.push({
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
});
// Lib 2 has a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
projectGraph.dependencies.lib2 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toContainEqual({
id: 'app1:build',
target: { project: 'app1', target: 'build' },
projectRoot: 'app1-root',
overrides: {},
});
expect(tasks).toContainEqual({
id: 'lib2:build',
target: { project: 'lib2', target: 'build' },
projectRoot: 'lib2-root',
overrides: {},
});
});
it('should handle APP1 <=> LIB1(no build) => LIB2', () => {
// APP1 <=> LIB1 => LIB2
// App 1 depends on builds of its dependencies
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
// Lib 1 does not have build but depends on Lib 2
delete projectGraph.nodes.lib1.data.targets.build;
projectGraph.dependencies.lib1.push(
{
type: DependencyType.static,
source: 'lib1',
target: 'app1',
},
{
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
}
);
// Lib 2 has a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
projectGraph.dependencies.lib2 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toContainEqual({
id: 'app1:build',
target: { project: 'app1', target: 'build' },
projectRoot: 'app1-root',
overrides: {},
});
expect(tasks).toContainEqual({
id: 'lib2:build',
target: { project: 'lib2', target: 'build' },
projectRoot: 'lib2-root',
overrides: {},
});
});
it('should error when APP1 <=> LIB1(no build) <=> LIB2', () => {
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
// Define Lib 2 with a build
projectGraph.nodes.lib2 = {
name: 'lib2',
type: 'lib',
data: {
root: 'lib2-root',
files: [],
targets: {
build: {},
},
},
};
// Lib 1 does not have "build"
delete projectGraph.nodes.lib1.data.targets.build;
// APP1 <=> LIB1 <=> LIB2
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.nodes.lib2.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
// App 1 depends on Lib 1
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
projectGraph.dependencies.lib1.push(
{
type: DependencyType.static,
source: 'lib1',
target: 'app1',
},
{
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
}
);
projectGraph.dependencies.lib2 = [
{
type: DependencyType.static,
source: 'lib2',
target: 'lib1',
},
];
try {
createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
// Technically, this could work but it creates a lot of problems in the implementation,
// so instead we error saying that the circular dependency cannot be handled.
// xit('should handle APP1 => LIB1(no build) <=> LIB2', () => {
// });
it('should throw an error for an invalid target', () => {
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
try {
createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'invalid',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
it('should throw an error for an invalid configuration for the initiating project', () => {
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
try {
createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'serve',
configuration: 'invalid',
overrides: {},
},
projectGraph,
'app1'
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
it('should throw an error for circular dependencies', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.nodes.lib1.data.targets.build.dependsOn = [
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.dependencies.app1.push({
type: DependencyType.static,
source: 'app1',
target: 'lib1',
});
projectGraph.dependencies.lib1.push({
type: DependencyType.static,
source: 'lib1',
target: 'app1',
});
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
try {
createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
it('should throw an error for depending on a non-existent target of itself', () => {
projectGraph.nodes.app1.data.targets.serve.dependsOn = [
{
target: 'non-existent',
projects: 'self',
},
];
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
try {
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'serve',
configuration: undefined,
overrides: {},
},
projectGraph,
projectGraph.nodes.app1.name
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
it('should throw an error for circular dependencies in tasks', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'serve',
projects: 'self',
},
];
projectGraph.nodes.app1.data.targets.serve.dependsOn = [
{
target: 'build',
projects: 'self',
},
];
jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error();
});
try {
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
fail();
} catch (e) {
expect(process.exit).toHaveBeenCalledWith(1);
}
});
it('should forward overrides to tasks with the same target executor', () => {
projectGraph.nodes.app1.data.targets.build.dependsOn = [
{
target: 'prebuild',
projects: 'self',
},
{
target: 'build',
projects: 'dependencies',
},
];
projectGraph.nodes.app1.data.targets.build.executor = 'executor1';
projectGraph.nodes.lib1.data.targets.build.executor = 'executor1';
projectGraph.nodes.app1.data.targets.prebuild.executor = 'executor2';
projectGraph.nodes.lib2 = {
data: {
root: 'lib2-root',
files: [],
targets: { build: { executor: 'executor2' } },
},
name: 'lib2',
type: 'lib',
};
projectGraph.dependencies.lib2 = [];
projectGraph.dependencies.app1.push(
{
type: DependencyType.static,
source: 'app1',
target: 'lib1',
},
{
type: DependencyType.static,
source: 'app1',
target: 'lib2',
}
);
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: { myFlag: 'flag value' },
},
projectGraph,
projectGraph.nodes.app1.name
);
expect(tasks).toEqual([
{
id: 'app1:prebuild',
overrides: {},
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'prebuild',
},
},
{
id: 'lib1:build',
overrides: { myFlag: 'flag value' },
projectRoot: 'lib1-root',
target: {
configuration: undefined,
project: 'lib1',
target: 'build',
},
},
{
id: 'lib2:build',
overrides: {},
projectRoot: 'lib2-root',
target: {
configuration: undefined,
project: 'lib2',
target: 'build',
},
},
{
id: 'app1:build',
overrides: { myFlag: 'flag value' },
projectRoot: 'app1-root',
target: {
configuration: undefined,
project: 'app1',
target: 'build',
},
},
]);
});
});
describe('getRunner', () => {
let nxJson: NxJsonConfiguration;
let mockRunner: TasksRunner;
@ -865,18 +14,6 @@ describe('getRunner', () => {
mockRunner = jest.fn();
});
it('gets a default runner when runner is not defined in the nx json', () => {
const { tasksRunner, runnerOptions } = getRunner({}, nxJson);
expect(tasksRunner).toEqual(defaultTaskRunner);
});
it('gets a default runner when default options are not configured', () => {
const { tasksRunner, runnerOptions } = getRunner({}, nxJson);
expect(tasksRunner).toEqual(defaultTaskRunner);
});
it('gets a custom task runner', () => {
jest.mock('custom-runner', () => mockRunner, {
virtual: true,

View File

@ -3,12 +3,8 @@ import { join } from 'path';
import { workspaceRoot } from '../utils/app-root';
import { NxArgs } from '../utils/command-line-utils';
import { isRelativePath } from '../utils/fileutils';
import {
projectHasTarget,
projectHasTargetAndConfiguration,
} from '../utils/project-graph-utils';
import { output } from '../utils/output';
import { getDependencyConfigs, shouldStreamOutput } from './utils';
import { shouldStreamOutput } from './utils';
import { CompositeLifeCycle, LifeCycle } from './life-cycle';
import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle';
import { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle';
@ -20,10 +16,8 @@ import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
import { NxJsonConfiguration } from '../config/nx-json';
import { Task } from '../config/task-graph';
import {
ProjectConfiguration,
TargetDependencyConfig,
} from '../config/workspace-json-project-json';
import { createTaskGraph } from './create-task-graph';
import { findCycle, makeAcyclic } from './task-graph-utils';
async function getTerminalOutputLifeCycle(
initiatingProject: string,
@ -90,19 +84,34 @@ export async function runCommand(
const { tasksRunner, runnerOptions } = getRunner(nxArgs, nxJson);
const defaultDependencyConfigs = nxJson.targetDependencies;
const tasks = createTasksForProjectToRun(
projectsToRun,
{
target: nxArgs.target,
configuration: nxArgs.configuration,
overrides,
},
const projectNames = projectsToRun.map((t) => t.name);
const taskGraph = createTaskGraph(
projectGraph,
initiatingProject,
defaultDependencyConfigs
defaultDependencyConfigs,
projectNames,
[nxArgs.target],
nxArgs.configuration,
overrides
);
const projectNames = projectsToRun.map((t) => t.name);
const cycle = findCycle(taskGraph);
if (cycle) {
if (nxArgs.nxIgnoreCycles) {
output.warn({
title: `The task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
makeAcyclic(taskGraph);
} else {
output.error({
title: `Could not execute command because the task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
process.exit(1);
}
}
const tasks = Object.values(taskGraph.tasks);
if (nxArgs.outputStyle == 'stream') {
process.env.NX_STREAM_OUTPUT = 'true';
process.env.NX_PREFIX_OUTPUT = 'true';
@ -138,6 +147,7 @@ export async function runCommand(
projectGraph,
nxJson,
nxArgs,
taskGraph,
}
);
@ -195,45 +205,6 @@ async function anyFailuresInObservable(obs: any) {
});
}
interface TaskParams {
project: ProjectGraphProjectNode;
target: string;
configuration: string;
overrides: Object;
errorIfCannotFindConfiguration: boolean;
}
export function createTasksForProjectToRun(
projectsToRun: ProjectGraphProjectNode[],
params: Omit<TaskParams, 'project' | 'errorIfCannotFindConfiguration'>,
projectGraph: ProjectGraph,
initiatingProject: string | null,
defaultDependencyConfigs: Record<
string,
(TargetDependencyConfig | string)[]
> = {}
) {
const tasksMap: Map<string, Task> = new Map<string, Task>();
const seenSet = new Set<string>();
for (const project of projectsToRun) {
addTasksForProjectTarget(
{
project,
...params,
errorIfCannotFindConfiguration: project.name === initiatingProject,
},
defaultDependencyConfigs,
projectGraph,
project.data.targets?.[params.target]?.executor,
tasksMap,
[],
seenSet
);
}
return Array.from(tasksMap.values());
}
function shouldUseDynamicLifeCycle(
tasks: Task[],
options: any,
@ -247,260 +218,6 @@ function shouldUseDynamicLifeCycle(
return noForwarding;
}
function addTasksForProjectTarget(
{
project,
target,
configuration,
overrides,
errorIfCannotFindConfiguration,
}: TaskParams,
defaultDependencyConfigs: Record<
string,
(TargetDependencyConfig | string)[]
> = {},
projectGraph: ProjectGraph,
originalTargetExecutor: string,
tasksMap: Map<string, Task>,
path: { targetIdentifier: string; hasTarget: boolean }[],
seenSet: Set<string>
) {
const task = createTask({
project,
target,
configuration,
overrides:
project.data.targets?.[target]?.executor === originalTargetExecutor
? overrides
: {},
errorIfCannotFindConfiguration,
});
const dependencyConfigs = getDependencyConfigs(
{ project: project.name, target },
defaultDependencyConfigs,
projectGraph
);
if (dependencyConfigs) {
for (const dependencyConfig of dependencyConfigs) {
addTasksForProjectDependencyConfig(
project,
{
target,
configuration,
overrides,
},
dependencyConfig,
defaultDependencyConfigs,
projectGraph,
originalTargetExecutor,
tasksMap,
path,
seenSet
);
}
}
tasksMap.set(task.id, task);
}
export function createTask({
project,
target,
configuration,
overrides,
errorIfCannotFindConfiguration,
}: TaskParams): Task {
if (!projectHasTarget(project, target)) {
output.error({
title: `Cannot find target '${target}' for project '${project.name}'`,
});
process.exit(1);
}
configuration ??= project.data.targets?.[target]?.defaultConfiguration;
const config = projectHasTargetAndConfiguration(
project,
target,
configuration
)
? configuration
: undefined;
if (errorIfCannotFindConfiguration && configuration && !config) {
output.error({
title: `Cannot find configuration '${configuration}' for project '${project.name}:${target}'`,
});
process.exit(1);
}
const qualifiedTarget = {
project: project.name,
target,
configuration: config,
};
return {
id: getId(qualifiedTarget),
target: qualifiedTarget,
projectRoot: project.data.root,
overrides: interpolateOverrides(overrides, project.name, project.data),
};
}
function addTasksForProjectDependencyConfig(
project: ProjectGraphProjectNode<ProjectConfiguration>,
{
target,
configuration,
overrides,
}: Pick<TaskParams, 'target' | 'configuration' | 'overrides'>,
dependencyConfig: TargetDependencyConfig,
defaultDependencyConfigs: Record<string, (TargetDependencyConfig | string)[]>,
projectGraph: ProjectGraph,
originalTargetExecutor: string,
tasksMap: Map<string, Task>,
path: { targetIdentifier: string; hasTarget: boolean }[],
seenSet: Set<string>
) {
const targetIdentifier = getId({
project: project.name,
target,
configuration,
});
const pathFragment = {
targetIdentifier,
hasTarget: projectHasTarget(project, target),
};
const newPath = [...path, pathFragment];
seenSet.add(targetIdentifier);
if (tasksMap.has(targetIdentifier)) {
return;
}
if (dependencyConfig.projects === 'dependencies') {
const dependencies = projectGraph.dependencies[project.name];
if (dependencies) {
for (const dep of dependencies) {
const depProject = projectGraph.nodes[
dep.target
] as ProjectGraphProjectNode;
if (
depProject &&
projectHasTarget(depProject, dependencyConfig.target)
) {
const depTargetId = getId({
project: depProject.name,
target: dependencyConfig.target,
configuration: configuration,
});
exitOnCircularDep(newPath, depTargetId);
if (seenSet.has(depTargetId)) {
continue;
}
addTasksForProjectTarget(
{
project: depProject,
target: dependencyConfig.target,
configuration,
overrides,
errorIfCannotFindConfiguration: false,
},
defaultDependencyConfigs,
projectGraph,
originalTargetExecutor,
tasksMap,
newPath,
seenSet
);
} else {
if (!depProject) {
continue;
}
const depTargetId = getId({
project: depProject.name,
target: dependencyConfig.target,
configuration: configuration,
});
exitOnCircularDep(newPath, depTargetId);
if (seenSet.has(depTargetId)) {
continue;
}
addTasksForProjectDependencyConfig(
depProject,
{ target, configuration, overrides },
dependencyConfig,
defaultDependencyConfigs,
projectGraph,
originalTargetExecutor,
tasksMap,
newPath,
seenSet
);
}
}
}
} else if (projectHasTarget(project, dependencyConfig.target)) {
addTasksForProjectTarget(
{
project,
target: dependencyConfig.target,
configuration,
overrides,
errorIfCannotFindConfiguration: false,
},
defaultDependencyConfigs,
projectGraph,
originalTargetExecutor,
tasksMap,
newPath,
seenSet
);
}
}
function exitOnCircularDep(
path: { targetIdentifier: string; hasTarget: boolean }[],
targetIdentifier: string
) {
if (
path.length > 0 &&
path[path.length - 1].hasTarget &&
path.filter((p) => p.targetIdentifier === targetIdentifier).length > 0
) {
const identifiers = path.map((p) => p.targetIdentifier);
output.error({
title: `Could not execute ${identifiers[0]} because it has a circular dependency`,
bodyLines: [`${[...identifiers, targetIdentifier].join(' --> ')}`],
});
process.exit(1);
}
}
function getId({
project,
target,
configuration,
}: {
project: string;
target: string;
configuration?: string;
}): string {
let id = `${project}:${target}`;
if (configuration) {
id += `:${configuration}`;
}
return id;
}
export function getRunner(
nxArgs: NxArgs,
nxJson: NxJsonConfiguration
@ -509,27 +226,7 @@ export function getRunner(
runnerOptions: any;
} {
let runner = nxArgs.runner;
//TODO: vsavkin remove in Nx 12
if (!nxJson.tasksRunnerOptions) {
const t = require('./default-tasks-runner');
return {
tasksRunner: t.defaultTasksRunner,
runnerOptions: nxArgs,
};
}
//TODO: vsavkin remove in Nx 12
if (!runner && !nxJson.tasksRunnerOptions.default) {
const t = require('./default-tasks-runner');
return {
tasksRunner: t.defaultTasksRunner,
runnerOptions: nxArgs,
};
}
runner = runner || 'default';
if (nxJson.tasksRunnerOptions[runner]) {
let modulePath: string = nxJson.tasksRunnerOptions[runner].runner;
@ -562,27 +259,3 @@ export function getRunner(
process.exit(1);
}
}
function interpolateOverrides<T = any>(
args: T,
projectName: string,
projectMetadata: any
): T {
const interpolatedArgs: T = { ...args };
Object.entries(interpolatedArgs).forEach(([name, value]) => {
if (typeof value === 'string') {
const regex = /{project\.([^}]+)}/g;
interpolatedArgs[name] = value.replace(regex, (_, group: string) => {
if (group.includes('.')) {
throw new Error('Only top-level properties can be interpolated');
}
if (group === 'name') {
return projectName;
}
return projectMetadata[group];
});
}
});
return interpolatedArgs;
}

View File

@ -1,484 +0,0 @@
import { DependencyType, ProjectGraph } from '../config/project-graph';
import { createTasksForProjectToRun } from './run-command';
import { TaskGraphCreator } from './task-graph-creator';
describe('TaskGraphCreator', () => {
it('should return empty for an empty array', () => {
const empty = new TaskGraphCreator(
{
nodes: {},
dependencies: {},
},
{}
).createTaskGraph([]);
expect(empty).toEqual({
roots: [],
tasks: {},
dependencies: {},
});
});
it('should order tasks based on project dependencies', () => {
const taskGraph = new TaskGraphCreator(
{
nodes: {
child1: { type: 'lib' },
child2: { type: 'lib' },
parent: { type: 'lib' },
grandparent: { type: 'lib' },
} as any,
dependencies: {
child1: [],
child2: [],
parent: [
{
source: 'parent',
target: 'child1',
type: DependencyType.static,
},
{
source: 'parent',
target: 'child2',
type: DependencyType.static,
},
],
grandparent: [
{
source: 'grandparent',
target: 'parent',
type: DependencyType.static,
},
],
},
},
{
build: [
{
target: 'build',
projects: 'dependencies',
},
],
}
).createTaskGraph([
{
id: 'parent:build',
target: { project: 'parent', target: 'build' },
},
{
id: 'child1:build',
target: { project: 'child1', target: 'build' },
},
{
id: 'child2:build',
target: { project: 'child2', target: 'build' },
},
{
id: 'grandparent:build',
target: { project: 'grandparent', target: 'build' },
},
] as any);
expect(taskGraph.roots).toEqual(['child1:build', 'child2:build']);
expect(Object.keys(taskGraph.tasks)).toEqual([
'parent:build',
'child1:build',
'child2:build',
'grandparent:build',
]);
expect(taskGraph.dependencies).toEqual({
'child1:build': [],
'child2:build': [],
'grandparent:build': ['parent:build'],
'parent:build': ['child1:build', 'child2:build'],
});
});
it('should support custom targets that require strict ordering', () => {
const taskGraph = new TaskGraphCreator(
{
nodes: {
child1: { type: 'lib', data: { targets: { custom: {} } } },
parent: { type: 'lib', data: { targets: { custom: {} } } },
} as any,
dependencies: {
child1: [],
parent: [
{
source: 'parent',
target: 'child1',
type: DependencyType.static,
},
],
},
},
{
custom: [
{
target: 'custom',
projects: 'dependencies',
},
],
}
).createTaskGraph([
{
id: 'parent:custom',
target: { project: 'parent', target: 'custom' },
},
{
id: 'child1:custom',
target: { project: 'child1', target: 'custom' },
},
] as any);
expect(taskGraph.roots).toEqual(['child1:custom']);
expect(Object.keys(taskGraph.tasks)).toEqual([
'parent:custom',
'child1:custom',
]);
expect(taskGraph.dependencies).toEqual({
'child1:custom': [],
'parent:custom': ['child1:custom'],
});
const noDeps = new TaskGraphCreator(
{
nodes: {
child1: { type: 'lib', data: { targets: { custom: {} } } },
parent: { type: 'lib', data: { targets: { custom: {} } } },
} as any,
dependencies: {
child1: [],
parent: [
{
source: 'parent',
target: 'child1',
type: DependencyType.static,
},
],
},
},
{}
).createTaskGraph([
{
id: 'parent:custom',
target: { project: 'parent', target: 'custom' },
},
{
id: 'child1:custom',
target: { project: 'child1', target: 'custom' },
},
] as any);
expect(noDeps.roots).toEqual(['parent:custom', 'child1:custom']);
});
describe('(tasks with dependency configurations)', () => {
let projectGraph: ProjectGraph;
beforeEach(() => {
projectGraph = {
nodes: {
app1: {
type: 'app',
name: 'app1',
data: {
targets: {
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
},
},
root: 'app1-root',
files: [],
},
},
app2: {
type: 'app',
name: 'app2',
data: {
targets: {
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
},
},
root: 'app2-root',
files: [],
},
},
common1: {
type: 'lib',
name: 'common1',
data: {
targets: {
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
},
},
root: 'common1-root',
files: [],
},
},
common2: {
type: 'lib',
name: 'common2',
data: {
targets: {
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
},
},
root: 'common2-root',
files: [],
},
},
},
dependencies: {
app1: [
{
source: 'app1',
target: 'common1',
type: DependencyType.static,
},
],
app2: [
{
source: 'app2',
target: 'common2',
type: DependencyType.static,
},
],
common1: [],
common2: [],
},
};
});
it('should create task graph (builds depend on build of dependencies)', () => {
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.app2],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create a task graph (builds depend on builds of dependencies even with intermediate projects)', () => {
delete projectGraph.nodes.common1.data.targets.build;
projectGraph.dependencies.common1.push({
type: DependencyType.static,
source: 'common1',
target: 'common2',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create a task graph (builds depend on builds of dependencies with intermediate projects and circular dependencies between projects) 2', () => {
delete projectGraph.nodes.common1.data.targets.build;
projectGraph.dependencies.common1.push({
type: DependencyType.static,
source: 'common1',
target: 'common2',
});
delete projectGraph.nodes.common2.data.targets.build;
projectGraph.dependencies.common2.push({
type: DependencyType.static,
source: 'common2',
target: 'common3',
});
projectGraph.nodes.common3 = {
name: 'common3',
type: 'lib',
data: {
root: 'common3',
targets: {
build: {},
},
},
};
projectGraph.dependencies.common3 = [];
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create task graph (builds depend on build of dependencies and prebuild of self)', () => {
projectGraph.nodes.app1.data.targets.prebuild = {};
projectGraph.nodes.app2.data.targets.prebuild = {};
projectGraph.nodes.app1.data.targets.build.dependsOn.push({
projects: 'self',
target: 'prebuild',
});
projectGraph.nodes.app2.data.targets.build.dependsOn.push({
projects: 'self',
target: 'prebuild',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.app2],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create task graph (builds depend on build of dependencies, builds depend on prebuilds)', () => {
projectGraph.nodes.app1.data.targets.prebuild = {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
};
projectGraph.nodes.app2.data.targets.prebuild = {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
};
projectGraph.nodes.app1.data.targets.build.dependsOn.push({
projects: 'self',
target: 'prebuild',
});
projectGraph.nodes.app2.data.targets.build.dependsOn.push({
projects: 'self',
target: 'prebuild',
});
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.app2],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
it('should create task graph (builds depend on build of dependencies, builds depend on prebuilds)', () => {
projectGraph.nodes.common1.data.targets =
projectGraph.nodes.common2.data.targets = {
prebuild: {},
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
{
target: 'prebuild',
projects: 'self',
},
],
},
};
projectGraph.nodes.app1.data.targets =
projectGraph.nodes.app2.data.targets = {
build: {
dependsOn: [
{
target: 'build',
projects: 'dependencies',
},
],
},
};
const tasks = createTasksForProjectToRun(
[projectGraph.nodes.app1, projectGraph.nodes.app2],
{
target: 'build',
configuration: undefined,
overrides: {},
},
projectGraph,
null
);
const taskGraph = new TaskGraphCreator(projectGraph, {}).createTaskGraph(
tasks
);
expect(taskGraph).toMatchSnapshot();
});
});
});

View File

@ -1,126 +0,0 @@
import { ProjectGraph } from '../config/project-graph';
import { Task, TaskGraph } from '../config/task-graph';
import { getDependencyConfigs } from './utils';
import { TargetDependencyConfig } from '../config/workspace-json-project-json';
export class TaskGraphCreator {
constructor(
private readonly projectGraph: ProjectGraph,
private readonly defaultTargetDependencies: Record<
string,
(TargetDependencyConfig | string)[]
>
) {}
createTaskGraph(tasks: Task[]): TaskGraph {
const graph: TaskGraph = {
roots: [],
tasks: {},
dependencies: {},
};
for (const task of tasks) {
this.addTaskToGraph(task, graph);
const dependencyConfigs = getDependencyConfigs(
task.target,
this.defaultTargetDependencies,
this.projectGraph
);
if (!dependencyConfigs) {
continue;
}
this.addTaskDependencies(task, dependencyConfigs, tasks, graph);
}
graph.roots = Object.keys(graph.dependencies).filter(
(k) => graph.dependencies[k].length === 0
);
return graph;
}
private addTaskDependencies(
task: Task,
dependencyConfigs: TargetDependencyConfig[],
tasks: Task[],
graph: TaskGraph
) {
for (const dependencyConfig of dependencyConfigs) {
if (dependencyConfig.projects === 'self') {
for (const t of tasks) {
if (
t.target.project === task.target.project &&
t.target.target === dependencyConfig.target
) {
graph.dependencies[task.id].push(t.id);
}
}
} else if (dependencyConfig.projects === 'dependencies') {
const seen = new Set<string>();
this.addDependencies(
task.target.project,
dependencyConfig.target,
tasks,
graph,
task.id,
seen
);
}
}
}
private addDependencies(
project: string,
target: string,
tasks: Task[],
graph: TaskGraph,
taskId: string,
seen: Set<string>
) {
seen.add(project);
const dependencies = this.projectGraph.dependencies[project];
if (dependencies) {
const projectDependencies = dependencies.map(
(dependency) => dependency.target
);
for (const projectDependency of projectDependencies) {
if (seen.has(projectDependency)) {
continue;
}
const dependency = this.findTask(
{ project: projectDependency, target },
tasks
);
if (dependency) {
graph.dependencies[taskId].push(dependency.id);
} else {
this.addDependencies(
projectDependency,
target,
tasks,
graph,
taskId,
seen
);
}
}
}
}
private findTask(
{ project, target }: { project: string; target: string },
tasks: Task[]
): Task {
return tasks.find(
(t) => t.target.project === project && t.target.target === target
);
}
private addTaskToGraph(task: Task, graph: TaskGraph) {
graph.tasks[task.id] = task;
graph.dependencies[task.id] = [];
}
}

View File

@ -0,0 +1,60 @@
import { findCycle, makeAcyclic } from './task-graph-utils';
describe('task graph utils', () => {
describe('findCycles', () => {
it('should return a cycle if it is there', () => {
expect(
findCycle({
dependencies: {
a: ['b', 'c'],
b: ['d'],
c: ['e'],
d: [],
e: ['q', 'a'],
q: [],
},
} as any)
).toEqual(['a', 'c', 'e', 'a']);
});
it('should return null when no cycle', () => {
expect(
findCycle({
dependencies: {
a: ['b', 'c'],
b: ['d'],
c: ['e'],
d: [],
e: ['q'],
q: [],
},
} as any)
).toEqual(null);
});
});
describe('makeAcyclic', () => {
it('should remove cycles when they are there', () => {
const graph = {
roots: ['d'],
dependencies: {
a: ['b', 'c'],
b: ['d'],
c: ['e'],
d: [],
e: ['a'],
},
} as any;
makeAcyclic(graph);
expect(graph.dependencies).toEqual({
a: ['b', 'c'],
b: ['d'],
c: ['e'],
d: [],
e: [],
});
expect(graph.roots).toEqual(['d', 'e']);
});
});
});

View File

@ -0,0 +1,65 @@
import { TaskGraph } from '../config/task-graph';
function _findCycle(
taskGraph: TaskGraph,
taskId: string,
visited: { [taskId: string]: boolean },
path: string[]
) {
if (visited[taskId]) return null;
visited[taskId] = true;
for (const d of taskGraph.dependencies[taskId]) {
if (path.includes(d)) return [...path, d];
const cycle = _findCycle(taskGraph, d, visited, [...path, d]);
if (cycle) return cycle;
}
return null;
}
export function findCycle(taskGraph: TaskGraph): string[] | null {
const visited = {};
for (const t of Object.keys(taskGraph.dependencies)) {
visited[t] = false;
}
for (const t of Object.keys(taskGraph.dependencies)) {
const cycle = _findCycle(taskGraph, t, visited, [t]);
if (cycle) return cycle;
}
return null;
}
function _makeAcyclic(
taskGraph: TaskGraph,
taskId: string,
visited: { [taskId: string]: boolean },
path: string[]
) {
if (visited[taskId]) return;
visited[taskId] = true;
const deps = taskGraph.dependencies[taskId];
for (const d of [...deps]) {
if (path.includes(d)) {
deps.splice(deps.indexOf(d), 1);
} else {
_makeAcyclic(taskGraph, d, visited, [...path, d]);
}
}
return null;
}
export function makeAcyclic(taskGraph: TaskGraph): void {
const visited = {};
for (const t of Object.keys(taskGraph.dependencies)) {
visited[t] = false;
}
for (const t of Object.keys(taskGraph.dependencies)) {
_makeAcyclic(taskGraph, t, visited, [t]);
}
taskGraph.roots = Object.keys(taskGraph.dependencies).filter(
(t) => taskGraph.dependencies[t].length === 0
);
}

View File

@ -1,6 +1,6 @@
import { NxJsonConfiguration } from '../config/nx-json';
import { ProjectGraph } from '../config/project-graph';
import { Task } from '../config/task-graph';
import { Task, TaskGraph } from '../config/task-graph';
import { NxArgs } from '../utils/command-line-utils';
export type TaskStatus =
@ -24,5 +24,6 @@ export type TasksRunner<T = unknown> = (
projectGraph: ProjectGraph;
nxJson: NxJsonConfiguration;
nxArgs: NxArgs;
taskGraph?: TaskGraph;
}
) => any | Promise<{ [id: string]: TaskStatus }>;

View File

@ -83,6 +83,7 @@ const runOne: string[] = [
'scan',
'outputStyle',
'nxBail',
'nxIgnoreCycles',
];
const runMany: string[] = [...runOne, 'projects', 'all'];
@ -127,6 +128,7 @@ export interface NxArgs {
outputStyle?: string;
scan?: boolean;
nxBail?: boolean;
nxIgnoreCycles?: boolean;
}
const ignoreArgs = ['$0', '_'];